diff --git a/src/api/tokenomics/startMiningSession.ts b/src/api/tokenomics/startMiningSession.ts index 0e7a4493c..164483465 100644 --- a/src/api/tokenomics/startMiningSession.ts +++ b/src/api/tokenomics/startMiningSession.ts @@ -1,16 +1,20 @@ // SPDX-License-Identifier: ice License 1.0 import {post} from '@api/client'; -import {MiningSummary} from '@api/tokenomics/types'; +import {FaceAuthKycNumber, MiningSummary} from '@api/tokenomics/types'; interface Params { userId: string; resurrect?: boolean | null; + skipKYCStep?: FaceAuthKycNumber | null; } -export function startMiningSession({userId, resurrect}: Params) { - return post<{resurrect?: boolean | null}, MiningSummary | null>( - `/tokenomics/${userId}/mining-sessions`, - {resurrect}, - ); +export function startMiningSession({userId, resurrect, skipKYCStep}: Params) { + return post< + {resurrect?: boolean | null; skipKYCSteps?: number[] | undefined}, + MiningSummary | null + >(`/tokenomics/${userId}/mining-sessions`, { + resurrect, + skipKYCSteps: skipKYCStep ? [skipKYCStep] : undefined, + }); } diff --git a/src/api/tokenomics/types.ts b/src/api/tokenomics/types.ts index 1e728116c..e3a7432a0 100644 --- a/src/api/tokenomics/types.ts +++ b/src/api/tokenomics/types.ts @@ -71,3 +71,7 @@ export type BalanceHistoryPoint = { balance: BalanceDiff; timeSeries?: BalanceHistoryPoint[]; }; + +export type SELFIE_KYC_STEP = 1; +export type EMOTIONS_KYC_STEP = 2; +export type FaceAuthKycNumber = SELFIE_KYC_STEP | EMOTIONS_KYC_STEP; diff --git a/src/constants/faceRecognition.ts b/src/constants/faceRecognition.ts index 39338575d..667a7ee1f 100644 --- a/src/constants/faceRecognition.ts +++ b/src/constants/faceRecognition.ts @@ -1,9 +1,14 @@ // SPDX-License-Identifier: ice License 1.0 import {degreesToRadians} from '@utils/units'; +import {VideoQuality} from 'expo-camera'; +import {Platform} from 'react-native'; export const FACE_RECOGNITION_PICTURE_SIZE = 224; export const VIDEO_DURATION_SEC = 5; export const DEVICE_Y_ALLOWED_ROTATION_RADIANS = degreesToRadians(60); + +export const VIDEO_QUALITY = + VideoQuality[Platform.OS === 'ios' ? '720p' : '480p']; diff --git a/src/navigation/Main.tsx b/src/navigation/Main.tsx index a953ee9da..4373ad606 100644 --- a/src/navigation/Main.tsx +++ b/src/navigation/Main.tsx @@ -2,6 +2,7 @@ import {BadgeType} from '@api/achievements/types'; import {NotificationDeliveryChannel} from '@api/notifications/types'; +import {FaceAuthKycNumber} from '@api/tokenomics/types'; import {Country} from '@constants/countries'; import {commonStyles} from '@constants/styles'; import {ViewMeasurementsResult} from '@ice/react-native'; @@ -102,7 +103,10 @@ export type MainStackParamList = { targetCircleSize?: number; descriptionOffset?: number; }; - FaceRecognition: undefined; + FaceRecognition: { + kycSteps: FaceAuthKycNumber[]; + kycStepBlocked?: FaceAuthKycNumber; + }; Staking: undefined; CreativeIceLibrary: undefined; ImageView: { diff --git a/src/screens/FaceRecognitionFlow/EmotionsAuthCameraFeed/components/EmotionsSentStep/index.tsx b/src/screens/FaceRecognitionFlow/EmotionsAuthCameraFeed/components/EmotionsSentStep/index.tsx index 5b17ca0a6..e4bac6862 100644 --- a/src/screens/FaceRecognitionFlow/EmotionsAuthCameraFeed/components/EmotionsSentStep/index.tsx +++ b/src/screens/FaceRecognitionFlow/EmotionsAuthCameraFeed/components/EmotionsSentStep/index.tsx @@ -28,6 +28,7 @@ export function EmotionsSentStep({onGatherMoreEmotions}: Props) { const onFaceAuthSuccess = () => { dispatch(TokenomicsActions.START_MINING_SESSION.START.create()); navigation.goBack(); + dispatch(FaceRecognitionActions.RESET_EMOTIONS_AUTH_STATUS.STATE.create()); }; const onBanned = () => { diff --git a/src/screens/FaceRecognitionFlow/EmotionsAuthCameraFeed/components/GatherEmotionsStep/hooks/useGetVideoDimensions.ts b/src/screens/FaceRecognitionFlow/EmotionsAuthCameraFeed/components/GatherEmotionsStep/hooks/useGetVideoDimensions.ts new file mode 100644 index 000000000..5324f2ef3 --- /dev/null +++ b/src/screens/FaceRecognitionFlow/EmotionsAuthCameraFeed/components/GatherEmotionsStep/hooks/useGetVideoDimensions.ts @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: ice License 1.0 + +import {getVideoDimensionsWithFFmpeg, VideoDimensions} from '@utils/ffmpeg'; +import {useCallback, useRef} from 'react'; + +export function useGetVideoDimensions() { + const videoDimensionsRef = useRef(null); + + return useCallback(async (videoUri: string) => { + if (!videoDimensionsRef.current) { + videoDimensionsRef.current = await getVideoDimensionsWithFFmpeg(videoUri); + } + return videoDimensionsRef.current; + }, []); +} diff --git a/src/screens/FaceRecognitionFlow/EmotionsAuthCameraFeed/components/GatherEmotionsStep/index.tsx b/src/screens/FaceRecognitionFlow/EmotionsAuthCameraFeed/components/GatherEmotionsStep/index.tsx index 03fe20cd0..5fb6a1cc7 100644 --- a/src/screens/FaceRecognitionFlow/EmotionsAuthCameraFeed/components/GatherEmotionsStep/index.tsx +++ b/src/screens/FaceRecognitionFlow/EmotionsAuthCameraFeed/components/GatherEmotionsStep/index.tsx @@ -2,7 +2,7 @@ import {AuthEmotion} from '@api/faceRecognition/types'; import {COLORS} from '@constants/colors'; -import {VIDEO_DURATION_SEC} from '@constants/faceRecognition'; +import {VIDEO_DURATION_SEC, VIDEO_QUALITY} from '@constants/faceRecognition'; import {commonStyles} from '@constants/styles'; import {Header} from '@navigation/components/Header'; import {CameraFeed} from '@screens/FaceRecognitionFlow/components/CameraFeed/CameraFeed'; @@ -10,9 +10,9 @@ import {DeviceAngleWarning} from '@screens/FaceRecognitionFlow/components/Device import {isSmallDevice} from '@screens/FaceRecognitionFlow/constants'; import {EmotionCard} from '@screens/FaceRecognitionFlow/EmotionsAuthCameraFeed/components/GatherEmotionsStep/components/EmotionCard'; import {StartButton} from '@screens/FaceRecognitionFlow/EmotionsAuthCameraFeed/components/GatherEmotionsStep/components/StartButton'; +import {useGetVideoDimensions} from '@screens/FaceRecognitionFlow/EmotionsAuthCameraFeed/components/GatherEmotionsStep/hooks/useGetVideoDimensions'; import {useIsDeviceAngleAllowed} from '@screens/FaceRecognitionFlow/hooks/useIsDeviceAngleAllowed'; import {useMaxHeightStyle} from '@screens/FaceRecognitionFlow/hooks/useMaxHeightStyle'; -import {getPictureCropStartY} from '@screens/FaceRecognitionFlow/utils'; import {dayjs} from '@services/dayjs'; import {FaceRecognitionActions} from '@store/modules/FaceRecognition/actions'; import { @@ -23,11 +23,10 @@ import { } from '@store/modules/FaceRecognition/selectors'; import {isEmotionsAuthFinalised} from '@store/modules/FaceRecognition/utils'; import {t} from '@translations/i18n'; -import {getVideoDimensionsWithFFmpeg} from '@utils/ffmpeg'; import {Duration} from 'dayjs/plugin/duration'; -import {Camera, VideoQuality} from 'expo-camera'; +import {Camera} from 'expo-camera'; import React, {useCallback, useEffect, useRef, useState} from 'react'; -import {BackHandler, Platform, StyleSheet, View} from 'react-native'; +import {BackHandler, StyleSheet, View} from 'react-native'; import {useDispatch, useSelector} from 'react-redux'; import {rem, wait} from 'rn-units'; @@ -72,6 +71,8 @@ export function GatherEmotionsStep({ null, ); + const getVideoDimensions = useGetVideoDimensions(); + useEffect(() => { if (isAllRecorded && !isVideoRecording && started) { onAllEmotionsGathered(); @@ -115,7 +116,7 @@ export function GatherEmotionsStep({ const video = await cameraRef.current .recordAsync({ maxDuration: 5, - quality: VideoQuality[Platform.OS === 'ios' ? '720p' : '480p'], + quality: VIDEO_QUALITY, mute: true, }) .catch(() => { @@ -129,19 +130,15 @@ export function GatherEmotionsStep({ if (toAbort) { return; } - // You now have the video object which contains the URI to the video file - const {width, height} = await getVideoDimensionsWithFFmpeg(video.uri); + const {width, height} = await getVideoDimensions(video.uri); if (toAbort) { return; } dispatch( FaceRecognitionActions.EMOTIONS_AUTH.START.create({ videoUri: video.uri, - cropStartY: getPictureCropStartY({ - pictureWidth: width, - pictureHeight: height, - }), videoWidth: width, + videoHeight: height, }), ); } @@ -159,6 +156,7 @@ export function GatherEmotionsStep({ session, isCameraReady, started, + getVideoDimensions, ]); useEffect(() => { diff --git a/src/screens/FaceRecognitionFlow/FaceAuthCameraFeed/components/PictureSentStep/index.tsx b/src/screens/FaceRecognitionFlow/FaceAuthCameraFeed/components/PictureSentStep/index.tsx index fddcf5b40..b1edbfaed 100644 --- a/src/screens/FaceRecognitionFlow/FaceAuthCameraFeed/components/PictureSentStep/index.tsx +++ b/src/screens/FaceRecognitionFlow/FaceAuthCameraFeed/components/PictureSentStep/index.tsx @@ -42,6 +42,7 @@ export function PictureSentStep({ navigation.goBack(); }; const onFaceAuthTryLater = () => { + dispatch(FaceRecognitionActions.RESET_FACE_AUTH_STATUS.STATE.create()); navigation.goBack(); }; diff --git a/src/screens/FaceRecognitionFlow/FaceAuthCameraFeed/components/SendOrRetakeStep/index.tsx b/src/screens/FaceRecognitionFlow/FaceAuthCameraFeed/components/SendOrRetakeStep/index.tsx index 5a29b6c1b..8ffee9199 100644 --- a/src/screens/FaceRecognitionFlow/FaceAuthCameraFeed/components/SendOrRetakeStep/index.tsx +++ b/src/screens/FaceRecognitionFlow/FaceAuthCameraFeed/components/SendOrRetakeStep/index.tsx @@ -8,7 +8,6 @@ import {cameraStyles} from '@screens/FaceRecognitionFlow/components/CameraFeed/C import {FaceAuthOverlay} from '@screens/FaceRecognitionFlow/components/FaceAuthOverlay'; import {isSmallDevice} from '@screens/FaceRecognitionFlow/constants'; import {useMaxHeightStyle} from '@screens/FaceRecognitionFlow/hooks/useMaxHeightStyle'; -import {getPictureCropStartY} from '@screens/FaceRecognitionFlow/utils'; import {FaceRecognitionActions} from '@store/modules/FaceRecognition/actions'; import {cameraRatioSelector} from '@store/modules/FaceRecognition/selectors'; import {RestartIcon} from '@svg/RestartIcon'; @@ -37,10 +36,7 @@ export function SendOrRetakeStep({ FaceRecognitionActions.FACE_AUTH.START.create({ pictureUri: picture.uri, pictureWidth: picture.width, - cropStartY: getPictureCropStartY({ - pictureWidth: picture.width, - pictureHeight: picture.height, - }), + pictureHeight: picture.height, }), ); onPictureSent(); diff --git a/src/screens/FaceRecognitionFlow/FaceAuthCameraFeed/index.tsx b/src/screens/FaceRecognitionFlow/FaceAuthCameraFeed/index.tsx index 8d1c9026b..92e4f2c8d 100644 --- a/src/screens/FaceRecognitionFlow/FaceAuthCameraFeed/index.tsx +++ b/src/screens/FaceRecognitionFlow/FaceAuthCameraFeed/index.tsx @@ -14,10 +14,10 @@ import {View} from 'react-native'; type FaceAuthPhase = 'TAKE_SELFIE' | 'SEND_OR_RETAKE' | 'SENT'; type Props = { - updateKycStepPassed: () => void; + onFaceAuthSuccess: () => void; }; -export function FaceAuthCameraFeed({updateKycStepPassed}: Props) { +export function FaceAuthCameraFeed({onFaceAuthSuccess}: Props) { const [faceAuthPhase, setFaceAuthPhase] = useState('TAKE_SELFIE'); @@ -59,7 +59,7 @@ export function FaceAuthCameraFeed({updateKycStepPassed}: Props) { ) : null} diff --git a/src/screens/FaceRecognitionFlow/index.tsx b/src/screens/FaceRecognitionFlow/index.tsx index 61729079d..3021ec909 100644 --- a/src/screens/FaceRecognitionFlow/index.tsx +++ b/src/screens/FaceRecognitionFlow/index.tsx @@ -1,61 +1,31 @@ // SPDX-License-Identifier: ice License 1.0 +import {FaceAuthKycNumber} from '@api/tokenomics/types'; import {COLORS} from '@constants/colors'; import {commonStyles} from '@constants/styles'; import {Header} from '@navigation/components/Header'; import {useFocusStatusBar} from '@navigation/hooks/useFocusStatusBar'; -import {useNavigation} from '@react-navigation/native'; +import {MainStackParamList} from '@navigation/Main'; +import {RouteProp, useNavigation, useRoute} from '@react-navigation/native'; import {StatusOverlay} from '@screens/FaceRecognitionFlow/components/StatusOverlay'; import {EmotionsAuthCameraFeed} from '@screens/FaceRecognitionFlow/EmotionsAuthCameraFeed'; import {FaceAuthCameraFeed} from '@screens/FaceRecognitionFlow/FaceAuthCameraFeed'; import {FaceAuthUserConsent} from '@screens/FaceRecognitionFlow/FaceAuthUserConsent'; -import {dayjs} from '@services/dayjs'; -import {unsafeUserSelector} from '@store/modules/Account/selectors'; import { emotionsAuthStatusSelector, faceAuthStatusSelector, } from '@store/modules/FaceRecognition/selectors'; +import {TokenomicsActions} from '@store/modules/Tokenomics/actions'; import {t} from '@translations/i18n'; -import React, {useEffect, useState} from 'react'; +import React, {useState} from 'react'; import {StyleSheet, View} from 'react-native'; -import {useSelector} from 'react-redux'; +import {useDispatch, useSelector} from 'react-redux'; type FaceRecognitionPhase = 'USER_CONSENT' | 'FACE_AUTH' | 'EMOTIONS_AUTH'; -function renderContent({ - faceRecognitionPhase, - setFaceRecognitionPhase, -}: { - faceRecognitionPhase: FaceRecognitionPhase; - setFaceRecognitionPhase: (phase: FaceRecognitionPhase) => void; -}) { - switch (faceRecognitionPhase) { - case 'USER_CONSENT': { - return ( - setFaceRecognitionPhase('FACE_AUTH')} - /> - ); - } - case 'FACE_AUTH': { - return ( - setFaceRecognitionPhase('EMOTIONS_AUTH')} - /> - ); - } - case 'EMOTIONS_AUTH': { - return ; - } - } -} - -function kycStepToFaceRecognitionPhase(kycStepPassed?: number) { - if (!kycStepPassed) { - return 'USER_CONSENT'; - } +function kycStepToFaceRecognitionPhase(kycStepPassed: FaceAuthKycNumber) { switch (kycStepPassed) { - case 0: + case 1: return 'USER_CONSENT'; default: return 'EMOTIONS_AUTH'; @@ -63,34 +33,33 @@ function kycStepToFaceRecognitionPhase(kycStepPassed?: number) { } export function FaceRecognition() { + const { + params: {kycSteps, kycStepBlocked}, + } = useRoute>(); useFocusStatusBar({style: 'dark-content'}); const navigation = useNavigation(); - const user = useSelector(unsafeUserSelector); + const dispatch = useDispatch(); const faceAuthStatus = useSelector(faceAuthStatusSelector); const emotionsAuthStatus = useSelector(emotionsAuthStatusSelector); const isBanned = faceAuthStatus === 'BANNED' || emotionsAuthStatus === 'BANNED' || - (user.kycStepBlocked && !user.kycStepPassed); + !!kycStepBlocked; const [faceRecognitionPhase, setFaceRecognitionPhase] = useState(() => - kycStepToFaceRecognitionPhase(user.kycStepPassed), + kycStepToFaceRecognitionPhase(kycSteps[0]), ); - useEffect(() => { - if (user.kycStepPassed === 2) { - const step2Timestamp = user?.repeatableKYCSteps?.['2']; - if (step2Timestamp && dayjs(step2Timestamp).valueOf() < Date.now()) { - setFaceRecognitionPhase(kycStepToFaceRecognitionPhase(1)); - } - const step1Timestamp = user?.repeatableKYCSteps?.['1']; - if (step1Timestamp && dayjs(step1Timestamp).valueOf() < Date.now()) { - setFaceRecognitionPhase(kycStepToFaceRecognitionPhase(0)); - } + const onFaceAuthSuccess = () => { + if (kycSteps.length > 1) { + setFaceRecognitionPhase('EMOTIONS_AUTH'); + } else { + dispatch(TokenomicsActions.START_MINING_SESSION.START.create()); + navigation.goBack(); } - }, [user.kycStepPassed, user?.repeatableKYCSteps]); + }; return ( @@ -111,7 +80,19 @@ export function FaceRecognition() { /> ) : ( - renderContent({faceRecognitionPhase, setFaceRecognitionPhase}) + <> + {faceRecognitionPhase === 'USER_CONSENT' ? ( + setFaceRecognitionPhase('FACE_AUTH')} + /> + ) : null} + {faceRecognitionPhase === 'FACE_AUTH' ? ( + + ) : null} + {faceRecognitionPhase === 'EMOTIONS_AUTH' ? ( + + ) : null} + )} ); diff --git a/src/screens/FaceRecognitionFlow/utils.ts b/src/screens/FaceRecognitionFlow/utils.ts deleted file mode 100644 index f975446fe..000000000 --- a/src/screens/FaceRecognitionFlow/utils.ts +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: ice License 1.0 - -import {windowWidth} from '@constants/styles'; -import { - FACE_CONTAINER_ASPECT_RATIO, - FACE_CONTAINER_PADDING_TOP, - FACE_CONTAINER_WIDTH, -} from '@screens/FaceRecognitionFlow/components/FaceAuthOverlay'; -import {screenHeight} from 'rn-units'; - -export function getPictureCropStartY({ - pictureWidth, - pictureHeight, -}: { - pictureWidth: number; - pictureHeight: number; -}) { - const windowToPhotoAspectRatio = windowWidth / pictureWidth; - const cameraHeight = (pictureHeight / pictureWidth) * windowWidth; - const topOffset = (screenHeight - cameraHeight) / 2; - const ovalCenterY = - FACE_CONTAINER_PADDING_TOP + - FACE_CONTAINER_WIDTH / FACE_CONTAINER_ASPECT_RATIO / 2 - - topOffset; - return Math.max(0, ovalCenterY / windowToPhotoAspectRatio - pictureWidth / 2); -} diff --git a/src/store/modules/FaceRecognition/actions/index.ts b/src/store/modules/FaceRecognition/actions/index.ts index a4fef631d..7d994c69a 100644 --- a/src/store/modules/FaceRecognition/actions/index.ts +++ b/src/store/modules/FaceRecognition/actions/index.ts @@ -11,8 +11,8 @@ import {createAction} from '@store/utils/actions/createAction'; const FACE_AUTH = createAction('FACE_AUTH', { START: (payload: { pictureUri: string; - cropStartY: number; pictureWidth: number; + pictureHeight: number; }) => payload, SUCCESS: true, FAILURE: (payload: {status: FaceAuthStatus}) => payload, @@ -20,19 +20,15 @@ const FACE_AUTH = createAction('FACE_AUTH', { const FETCH_EMOTIONS_FOR_AUTH = createAction('FETCH_EMOTIONS_FOR_AUTH', { START: true, - SUCCESS: (payload: { - emotions: AuthEmotion[]; - sessionId: string; - sessionExpiredAt: number; - }) => payload, + SUCCESS: (payload: {emotions: AuthEmotion[]; sessionId: string}) => payload, FAILURE: (payload: {status: EmotionsAuthStatus}) => payload, }); const EMOTIONS_AUTH = createAction('EMOTIONS_AUTH', { START: (payload: { videoUri: string; - cropStartY: number; videoWidth: number; + videoHeight: number; }) => payload, NEED_MORE_EMOTIONS: (payload: {emotions: AuthEmotion[]}) => payload, SUCCESS: true, @@ -47,13 +43,6 @@ const RESET_EMOTIONS_AUTH_STATUS = createAction('RESET_EMOTIONS_AUTH_STATUS', { STATE: true, }); -const RESET_EMOTIONS_SUCCESS_AUTH_STATUS = createAction( - 'RESET_EMOTIONS_SUCCESS_AUTH_STATUS', - { - STATE: true, - }, -); - const SET_CAMERA_RATIO = createAction('SET_CAMERA_RATIO', { STATE: (payload: {cameraRatio: CameraRatio}) => payload, }); @@ -64,6 +53,5 @@ export const FaceRecognitionActions = Object.freeze({ FETCH_EMOTIONS_FOR_AUTH, RESET_FACE_AUTH_STATUS, RESET_EMOTIONS_AUTH_STATUS, - RESET_EMOTIONS_SUCCESS_AUTH_STATUS, SET_CAMERA_RATIO, }); diff --git a/src/store/modules/FaceRecognition/reducer/index.ts b/src/store/modules/FaceRecognition/reducer/index.ts index 179a8a39b..5bd712219 100644 --- a/src/store/modules/FaceRecognition/reducer/index.ts +++ b/src/store/modules/FaceRecognition/reducer/index.ts @@ -19,7 +19,6 @@ export interface State { sessionId: string | null; emotions: AuthEmotion[]; nextEmotionIndex: number; - sessionExpiredAt: number | null; activeRequests: number; cameraRatio: CameraRatio; @@ -37,7 +36,6 @@ type Actions = ReturnType< | typeof FaceRecognitionActions.EMOTIONS_AUTH.FAILURE.create | typeof FaceRecognitionActions.RESET_FACE_AUTH_STATUS.STATE.create | typeof FaceRecognitionActions.RESET_EMOTIONS_AUTH_STATUS.STATE.create - | typeof FaceRecognitionActions.RESET_EMOTIONS_SUCCESS_AUTH_STATUS.STATE.create | typeof FaceRecognitionActions.SET_CAMERA_RATIO.STATE.create | typeof AccountActions.SIGN_OUT.SUCCESS.create >; @@ -48,7 +46,6 @@ const INITIAL_STATE: State = { sessionId: null, emotions: [], nextEmotionIndex: 0, - sessionExpiredAt: null, activeRequests: 0, cameraRatio: '16:9', }; @@ -59,7 +56,6 @@ function reducer(state = INITIAL_STATE, action: Actions): State { draft.emotions = []; draft.sessionId = null; draft.nextEmotionIndex = 0; - draft.sessionExpiredAt = null; draft.activeRequests = 0; }; switch (action.type) { @@ -78,7 +74,6 @@ function reducer(state = INITIAL_STATE, action: Actions): State { } draft.emotions = action.payload.emotions; draft.sessionId = action.payload.sessionId; - draft.sessionExpiredAt = action.payload.sessionExpiredAt; break; case FaceRecognitionActions.FETCH_EMOTIONS_FOR_AUTH.FAILURE.type: draft.emotionsAuthStatus = action.payload.status; @@ -112,12 +107,6 @@ function reducer(state = INITIAL_STATE, action: Actions): State { draft.emotionsAuthStatus = null; resetSession(); break; - case FaceRecognitionActions.RESET_EMOTIONS_SUCCESS_AUTH_STATUS.STATE.type: - if (draft.emotionsAuthStatus === 'SUCCESS') { - draft.emotionsAuthStatus = null; - resetSession(); - } - break; case FaceRecognitionActions.SET_CAMERA_RATIO.STATE.type: draft.cameraRatio = action.payload.cameraRatio; break; @@ -131,13 +120,7 @@ export const faceRecognitionReducer = persistReducer( { key: 'faceRecognition', storage: AsyncStorage, - whitelist: [ - 'sessionId', - 'emotions', - 'nextEmotionIndex', - 'sessionExpiredAt', - 'cameraRatio', - ], + whitelist: ['sessionId', 'emotions', 'nextEmotionIndex', 'cameraRatio'], }, reducer, ); diff --git a/src/store/modules/FaceRecognition/sagas/fetchEmotionsForAuth.ts b/src/store/modules/FaceRecognition/sagas/fetchEmotionsForAuth.ts index fe46177a3..9918de920 100644 --- a/src/store/modules/FaceRecognition/sagas/fetchEmotionsForAuth.ts +++ b/src/store/modules/FaceRecognition/sagas/fetchEmotionsForAuth.ts @@ -2,7 +2,6 @@ import {is5xxApiError, isApiError} from '@api/client'; import {Api} from '@api/index'; -import {dayjs} from '@services/dayjs'; import {userIdSelector} from '@store/modules/Account/selectors'; import {FaceRecognitionActions} from '@store/modules/FaceRecognition/actions'; import {showError} from '@utils/errors'; @@ -22,7 +21,6 @@ export function* fetchEmotionsForAuthSaga() { FaceRecognitionActions.FETCH_EMOTIONS_FOR_AUTH.SUCCESS.create({ emotions: response.emotions, sessionId: response.sessionId, - sessionExpiredAt: dayjs(response.sessionExpiredAt).valueOf(), }), ); } catch (error: unknown) { diff --git a/src/store/modules/FaceRecognition/sagas/initEmotionsAuth.ts b/src/store/modules/FaceRecognition/sagas/initEmotionsAuth.ts index f9957d385..964b07d94 100644 --- a/src/store/modules/FaceRecognition/sagas/initEmotionsAuth.ts +++ b/src/store/modules/FaceRecognition/sagas/initEmotionsAuth.ts @@ -7,14 +7,13 @@ import {userIdSelector} from '@store/modules/Account/selectors'; import {FaceRecognitionActions} from '@store/modules/FaceRecognition/actions'; import { emotionsAuthEmotionsSelector, - emotionsAuthSessionExpiredAtSelector, emotionsAuthSessionSelector, emotionsAuthStatusSelector, } from '@store/modules/FaceRecognition/selectors'; import {isEmotionsAuthFinalised} from '@store/modules/FaceRecognition/utils'; import {shallowCompare} from '@utils/array'; import {showError} from '@utils/errors'; -import {extractFramesWithFFmpeg} from '@utils/ffmpeg'; +import {extractFramesWithFFmpeg, getPictureCropStartY} from '@utils/ffmpeg'; import {call, put, SagaReturnType, select, spawn} from 'redux-saga/effects'; type Actions = ReturnType< @@ -23,7 +22,7 @@ type Actions = ReturnType< export function* initEmotionsAuthSaga(action: Actions) { try { - const {videoUri, cropStartY, videoWidth} = action.payload; + const {videoUri, videoWidth, videoHeight} = action.payload; const sessionId: ReturnType = yield select(emotionsAuthSessionSelector); const emotions: ReturnType = @@ -31,21 +30,11 @@ export function* initEmotionsAuthSaga(action: Actions) { const userId: ReturnType = yield select( userIdSelector, ); - const sessionExpiredAt: ReturnType< - typeof emotionsAuthSessionExpiredAtSelector - > = yield select(emotionsAuthSessionExpiredAtSelector); - const isSessionExpired = sessionExpiredAt - ? Date.now() >= sessionExpiredAt - : false; - if (isSessionExpired) { - yield put( - FaceRecognitionActions.EMOTIONS_AUTH.FAILURE.create({ - status: 'SESSION_EXPIRED', - }), - ); - return; - } + const cropStartY: SagaReturnType = yield call( + getPictureCropStartY, + {pictureWidth: videoWidth, pictureHeight: videoHeight}, + ); const frames: SagaReturnType = yield call( extractFramesWithFFmpeg, { diff --git a/src/store/modules/FaceRecognition/sagas/initFaceAuth.ts b/src/store/modules/FaceRecognition/sagas/initFaceAuth.ts index 2f8e05d02..f3a501631 100644 --- a/src/store/modules/FaceRecognition/sagas/initFaceAuth.ts +++ b/src/store/modules/FaceRecognition/sagas/initFaceAuth.ts @@ -6,7 +6,7 @@ import {FACE_RECOGNITION_PICTURE_SIZE} from '@constants/faceRecognition'; import {userIdSelector} from '@store/modules/Account/selectors'; import {FaceRecognitionActions} from '@store/modules/FaceRecognition/actions'; import {showError} from '@utils/errors'; -import {cropAndResizeWithFFmpeg} from '@utils/ffmpeg'; +import {cropAndResizeWithFFmpeg, getPictureCropStartY} from '@utils/ffmpeg'; import {getFilenameFromPath} from '@utils/file'; import {cacheDirectory} from 'expo-file-system'; import {call, put, SagaReturnType, select, spawn} from 'redux-saga/effects'; @@ -15,7 +15,12 @@ type Actions = ReturnType; export function* initFaceAuthSaga(action: Actions) { try { - const {pictureUri, cropStartY, pictureWidth} = action.payload; + const {pictureUri, pictureWidth, pictureHeight} = action.payload; + + const cropStartY: SagaReturnType = yield call( + getPictureCropStartY, + {pictureWidth, pictureHeight}, + ); const croppedPictureUri: SagaReturnType = yield call(cropAndResizeWithFFmpeg, { diff --git a/src/store/modules/FaceRecognition/selectors/index.ts b/src/store/modules/FaceRecognition/selectors/index.ts index 0c52266fe..254a409e8 100644 --- a/src/store/modules/FaceRecognition/selectors/index.ts +++ b/src/store/modules/FaceRecognition/selectors/index.ts @@ -15,8 +15,5 @@ export const emotionsAuthEmotionsSelector = (state: RootState) => state.faceRecognition.emotions; export const emotionsAuthNextEmotionIndexSelector = (state: RootState) => state.faceRecognition.nextEmotionIndex; -export const emotionsAuthSessionExpiredAtSelector = (state: RootState) => - state.faceRecognition.sessionExpiredAt; - export const cameraRatioSelector = (state: RootState) => state.faceRecognition.cameraRatio; diff --git a/src/store/modules/Tokenomics/actions/index.ts b/src/store/modules/Tokenomics/actions/index.ts index 062b102ff..a957322e1 100644 --- a/src/store/modules/Tokenomics/actions/index.ts +++ b/src/store/modules/Tokenomics/actions/index.ts @@ -3,6 +3,7 @@ import { BalanceHistoryPoint, BalanceSummary, + FaceAuthKycNumber, MiningSummary, PreStakingSummary, RankingSummary, @@ -54,6 +55,7 @@ const START_MINING_SESSION = createAction('START_MINING_SESSION', { START: (params?: { resurrect?: boolean; tapToMineActionType?: 'Extended' | 'Default'; + skipKYCStep?: FaceAuthKycNumber; }) => params, SUCCESS: (miningSummary: MiningSummary | null) => ({miningSummary}), FAILED: (errorMessage: string) => ({errorMessage}), diff --git a/src/store/modules/Tokenomics/sagas/startMiningSession.ts b/src/store/modules/Tokenomics/sagas/startMiningSession.ts index cea282dd2..ea419e32d 100644 --- a/src/store/modules/Tokenomics/sagas/startMiningSession.ts +++ b/src/store/modules/Tokenomics/sagas/startMiningSession.ts @@ -10,19 +10,15 @@ import {loadLocalAudio} from '@services/audio'; import {dayjs} from '@services/dayjs'; import {AccountActions} from '@store/modules/Account/actions'; import { - authConfigSelector, firstMiningDateSelector, unsafeUserSelector, userIdSelector, } from '@store/modules/Account/selectors'; import {AnalyticsActions} from '@store/modules/Analytics/actions'; import {AnalyticsEventLogger} from '@store/modules/Analytics/constants'; -import {FaceRecognitionActions} from '@store/modules/FaceRecognition/actions'; -import {emotionsAuthStatusSelector} from '@store/modules/FaceRecognition/selectors'; import {TokenomicsActions} from '@store/modules/Tokenomics/actions'; import { isMiningActiveSelector, - miningStartedSelector, tapToMineActionTypeSelector, } from '@store/modules/Tokenomics/selectors'; import {openConfirmResurrect} from '@store/modules/Tokenomics/utils/openConfirmResurrect'; @@ -44,29 +40,9 @@ export function* startMiningSessionSaga( typeof TokenomicsActions.START_MINING_SESSION.START.create >, ) { - const emotionsAuthStatus: ReturnType = - yield select(emotionsAuthStatusSelector); - const authConfig: ReturnType = yield select( - authConfigSelector, - ); const user: ReturnType = yield select( unsafeUserSelector, ); - const miningStarted: ReturnType = yield select( - miningStartedSelector, - ); - if ( - emotionsAuthStatus !== 'SUCCESS' && - authConfig?.['face-auth']?.enabled && - !!miningStarted // allowing to mine 1st time without face recognition - ) { - yield removeScreenByName('Tooltip'); - navigate({ - name: 'FaceRecognition', - params: undefined, - }); - return; - } const tapToMineActionType: ReturnType = yield select(tapToMineActionTypeSelector); @@ -77,14 +53,11 @@ export function* startMiningSessionSaga( > = yield call(Api.tokenomics.startMiningSession, { userId: user.id, resurrect: action.payload?.resurrect, + skipKYCStep: action.payload?.skipKYCStep, }); yield put( TokenomicsActions.START_MINING_SESSION.SUCCESS.create(miningSummary), ); - // Reset success emotions auth status here so on next tap to mine a user would have to face auth again - yield put( - FaceRecognitionActions.RESET_EMOTIONS_SUCCESS_AUTH_STATUS.STATE.create(), - ); yield call(setFirstMiningDate, user); @@ -125,6 +98,33 @@ export function* startMiningSessionSaga( ? errorData.duringTheLastXSeconds : 0, }); + } else if (isApiError(error, 409, 'KYC_STEPS_REQUIRED')) { + const errorData = error?.response?.data?.data; + if (errorData && Array.isArray(errorData.kycSteps)) { + if (errorData.kycSteps.includes(1) || errorData.kycSteps.includes(2)) { + yield removeScreenByName('Tooltip').catch(); + navigate({ + name: 'FaceRecognition', + params: {kycSteps: errorData.kycSteps}, + }); + return; + } + } + } else if (isApiError(error, 403, 'MINING_DISABLED')) { + const errorData = error?.response?.data?.data; + if (errorData && typeof errorData.kycStepBlocked === 'number') { + if (errorData.kycStepBlocked === 1 || errorData.kycStepBlocked === 2) { + yield removeScreenByName('Tooltip').catch(); + navigate({ + name: 'FaceRecognition', + params: { + kycSteps: [errorData.kycStepBlocked], + kycStepBlocked: errorData.kycStepBlocked, + }, + }); + return; + } + } } else { yield spawn(showError, error); } diff --git a/src/utils/ffmpeg.ts b/src/utils/ffmpeg.ts index 418b1d516..97d47f3a2 100644 --- a/src/utils/ffmpeg.ts +++ b/src/utils/ffmpeg.ts @@ -1,9 +1,34 @@ // SPDX-License-Identifier: ice License 1.0 +import {VIDEO_QUALITY} from '@constants/faceRecognition'; +import {windowWidth} from '@constants/styles'; +import { + FACE_CONTAINER_ASPECT_RATIO, + FACE_CONTAINER_PADDING_TOP, + FACE_CONTAINER_WIDTH, +} from '@screens/FaceRecognitionFlow/components/FaceAuthOverlay'; import {logError} from '@services/logging'; import {getFilenameFromPathWithoutExtension} from '@utils/file'; import {cacheDirectory} from 'expo-file-system'; import {FFmpegKit} from 'ffmpeg-kit-react-native'; +import {screenHeight} from 'rn-units'; + +export function getPictureCropStartY({ + pictureWidth, + pictureHeight, +}: { + pictureWidth: number; + pictureHeight: number; +}) { + const windowToPhotoAspectRatio = windowWidth / pictureWidth; + const cameraHeight = (pictureHeight / pictureWidth) * windowWidth; + const topOffset = (screenHeight - cameraHeight) / 2; + const ovalCenterY = + FACE_CONTAINER_PADDING_TOP + + FACE_CONTAINER_WIDTH / FACE_CONTAINER_ASPECT_RATIO / 2 - + topOffset; + return Math.max(0, ovalCenterY / windowToPhotoAspectRatio - pictureWidth / 2); +} export async function cropAndResizeWithFFmpeg({ inputUri, @@ -82,11 +107,23 @@ export async function extractFramesWithFFmpeg({ } } -export async function getVideoDimensionsWithFFmpeg(videoUri: string) { - let output; +export type VideoDimensions = {width: number; height: number}; + +export function qualityToDimensions(quality: '720p' | '480p'): VideoDimensions { + switch (quality) { + case '720p': + return {width: 720, height: 1280}; + default: + return {width: 480, height: 720}; + } +} + +export async function getVideoDimensionsWithFFmpeg( + videoUri: string, +): Promise { try { const session = await FFmpegKit.execute(`-i ${videoUri}`); - output = await session.getOutput(); + const output = await session.getOutput(); // Use regex to extract video dimensions from FFmpeg output const regex = /, (\d+)x(\d+),/; @@ -96,11 +133,7 @@ export async function getVideoDimensionsWithFFmpeg(videoUri: string) { const height = parseInt(match[1], 10); const width = parseInt(match[2], 10); return {width, height}; - } else { - throw new Error('Failed to extract video dimensions.'); } - } catch (error) { - logError(error, {output}); - throw error; - } + } catch {} + return qualityToDimensions(VIDEO_QUALITY); }