diff --git a/App.tsx b/App.tsx index 52577dd4..31fe4d2c 100644 --- a/App.tsx +++ b/App.tsx @@ -9,19 +9,14 @@ */ import React from 'react'; -import {SafeAreaView, Text, View} from 'react-native'; -import data from './src/data/templateData.json'; -import userData from './src/data/userData.json'; -import RenderingEngine from './src/components/formRenderingEngine'; import {Provider} from 'react-redux'; import store, {persistor} from './src/store/store'; import {PersistGate} from 'redux-persist/integration/react'; import {NavigationContainer} from '@react-navigation/native'; -import {createNativeStackNavigator} from '@react-navigation/native-stack'; -import Widget from './src/components/form'; import {navigationRef} from './src/components/utlis/navigationUtlis'; import FullScreenLoader from 'RN-UI-LIB/src/components/FullScreenLoader'; +import ProtectedRouter from './ProtectedRouter'; function HomeScreen() { return ( @@ -36,32 +31,13 @@ function HomeScreen() { const Stack = createNativeStackNavigator(); const App = () => { - // const [hasBeenManuplated, setHasBeenManuplated] = useState(false); return ( } persistor={persistor}> - - null, - }} - /> - {Object.keys(data.widgets).map(key => ( - null, - }} - /> - ))} - + diff --git a/ProtectedRouter.tsx b/ProtectedRouter.tsx new file mode 100644 index 00000000..ce14f53e --- /dev/null +++ b/ProtectedRouter.tsx @@ -0,0 +1,86 @@ +import {createNativeStackNavigator} from '@react-navigation/native-stack'; +import React from 'react'; +import {View} from 'react-native'; +import {SafeAreaView} from 'react-native-safe-area-context'; +import {useSelector} from 'react-redux'; +import Widget from './src/components/form'; +import RenderingEngine from './src/components/formRenderingEngine'; +import Login from './src/components/login'; +import OtpInput from './src/components/login/OtpInput'; +import {RootState} from './src/store/store'; +import data from './src/data/templateData.json'; +import userData from './src/data/userData.json'; +import {getUniqueId} from 'react-native-device-info'; +import {useAppDispatch} from './src/hooks'; +import {setAuthData} from './src/reducer/commonSlice'; + +const Stack = createNativeStackNavigator(); + +const LoginScreen = () => ; + +const OTPScreen = () => ; + +const HomeScreen = () => ( + + + + + +); + +const ProtectedRouter = () => { + const {isLoggedIn, deviceId} = useSelector( + (state: RootState) => state.common.userData, + ); + + const dispatch = useAppDispatch(); + + if (!deviceId) { + getUniqueId().then(id => dispatch(setAuthData({deviceId: id}))); + } + + return ( + + {isLoggedIn ? ( + <> + null, + }} + /> + {Object.keys(data.widgets).map(key => ( + null, + }} + /> + ))} + + ) : ( + <> + null, + }} + /> + null, + }} + /> + + )} + + ); +}; + +export default ProtectedRouter; diff --git a/babel.config.js b/babel.config.js index 80ce241f..dcbb6807 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,3 +1,16 @@ module.exports = { presets: ['module:metro-react-native-babel-preset'], + plugins: [ + [ + require.resolve('babel-plugin-module-resolver'), + { + cwd: 'babelrc', + extensions: ['.ts', '.tsx', '.js', '.ios.js', '.android.js'], + alias: { + '@cuteapp': './app' + } + } + ], + 'jest-hoist' + ], }; diff --git a/package.json b/package.json index 21686e06..2cc0c538 100644 --- a/package.json +++ b/package.json @@ -13,18 +13,20 @@ }, "dependencies": { "@react-native-async-storage/async-storage": "1.17.11", - "@react-native-firebase/app": "^16.4.6", - "@react-native-firebase/database": "^16.4.6", - "@react-navigation/native": "^6.0.16", - "@react-navigation/native-stack": "^6.9.4", + "@react-native-firebase/app": "16.4.6", + "@react-native-firebase/database": "16.4.6", + "@react-navigation/native": "6.0.16", + "@react-navigation/native-stack": "6.9.4", "@reduxjs/toolkit": "1.9.1", + "axios": "1.2.1", "react": "18.1.0", "react-hook-form": "7.40.0", "react-native": "0.70.6", + "react-native-device-info": "10.3.0", "react-native-image-picker": "4.10.2", "react-native-pager-view": "6.1.2", - "react-native-safe-area-context": "^4.4.1", - "react-native-screens": "^3.18.2", + "react-native-safe-area-context": "4.4.1", + "react-native-screens": "3.18.2", "react-native-svg": "13.6.0", "react-native-tab-view": "3.3.2", "react-native-toast-message": "2.1.5", @@ -44,6 +46,7 @@ "@typescript-eslint/eslint-plugin": "5.45.0", "@typescript-eslint/parser": "5.37.0", "babel-jest": "26.6.3", + "babel-plugin-module-resolver": "^4.1.0", "eslint": "8.28.0", "eslint-config-airbnb-typescript": "17.0.0", "eslint-config-prettier": "8.5.0", diff --git a/src/action/authActions.ts b/src/action/authActions.ts new file mode 100644 index 00000000..46b40dc7 --- /dev/null +++ b/src/action/authActions.ts @@ -0,0 +1,69 @@ +import {setAuthData} from '../reducer/commonSlice'; +import axiosInstance, {ApiKeys, getApiUrl} from '../components/utlis/apiHelper'; +import { + setFormLoading, + setGetOTPError, + setShowOTPScreen, + setVerifyOTPError, + setVerifyOTPSuccess, +} from '../components/login/loginSlice'; +import {Dispatch} from '@reduxjs/toolkit'; +import {navigateToScreen} from '../components/utlis/navigationUtlis'; + +export interface GenerateOTPPayload { + phoneNumber: string; +} + +export interface VerifyOTPPayload { + otp: string; + otpToken: string; +} + +export const generateOTP = + ({phoneNumber}: GenerateOTPPayload, isResendOTP?: boolean) => + (dispatch: Dispatch) => { + const url = getApiUrl(ApiKeys.GENERATE_OTP); + dispatch(setFormLoading(true)); + axiosInstance + .post(url, {phoneNumber: Number(phoneNumber)}) + .then(response => { + if (response.status === 200) { + if (response?.data?.data?.otpToken) { + dispatch( + setShowOTPScreen({ + phoneNumber, + otpToken: response?.data?.data?.otpToken, + }), + ); + !isResendOTP && navigateToScreen('OTP'); + } + } + }) + .catch(err => { + if (err.response.status === 404) { + dispatch( + setGetOTPError('Enter a registered mobile number'), + ); + } else { + dispatch(setGetOTPError(err.response?.data?.message)); + } + }); + }; + +export const verifyOTP = + ({otp, otpToken}: VerifyOTPPayload) => + (dispatch: Dispatch) => { + const url = getApiUrl(ApiKeys.VERIFY_OTP); + + dispatch(setFormLoading(true)); + axiosInstance + .post(url, {otp, otpToken}, {headers: {donotHandleError: true}}) + .then((response: any) => { + const {sessionToken} = response?.data?.data; + dispatch(setAuthData({sessionToken, isLoggedIn: true})); + dispatch(setVerifyOTPSuccess('OTP verified')); + }) + .catch(err => { + dispatch(setVerifyOTPError('Invalid OTP, try again')); + }); + }; diff --git a/src/components/countdown/index.tsx b/src/components/countdown/index.tsx new file mode 100644 index 00000000..68b98b2e --- /dev/null +++ b/src/components/countdown/index.tsx @@ -0,0 +1,44 @@ +import {useEffect, useRef, useState} from 'react'; + +interface CountdownProps { + startFrom: number; + stopAt?: number; + interval?: number; + decrementBy?: number; + step?: (current: number) => void; + onComplete?: () => void; +} + +export const Countdown = (props: CountdownProps) => { + const { + startFrom = 0, + stopAt = 0, + interval = 1000, + decrementBy = 1, + step, + onComplete, + } = props; + const [ticker, setTicker] = useState(startFrom); + const intervalRef = useRef(); + useEffect(() => { + const tick = () => { + const newTickerValue = ticker - decrementBy; + + if (newTickerValue < stopAt) { + clearTimeout(intervalRef.current); + onComplete && onComplete(); + return; + } + + step && step(newTickerValue); + setTicker(newTickerValue); + }; + + clearTimeout(intervalRef.current); + intervalRef.current = setTimeout(tick, interval); + + return () => clearTimeout(intervalRef.current); + }, [ticker]); + + return <>{ticker}; +}; diff --git a/src/components/form/QuestionRenderingEngine.tsx b/src/components/form/QuestionRenderingEngine.tsx index b01d81fa..f419052f 100644 --- a/src/components/form/QuestionRenderingEngine.tsx +++ b/src/components/form/QuestionRenderingEngine.tsx @@ -9,12 +9,12 @@ import TextArea from './components/TextArea'; import TextInput from './components/TextInput'; const Component = { - TextInput: TextInput, - TextArea: TextArea, - RadioButton: RadioButton, - ImageUpload: ImageUpload, - Checkbox: Checkbox, - Rating: Rating, + TextInput, + TextArea, + RadioButton, + ImageUpload, + Checkbox, + Rating, }; interface IQuestionRenderingEngine { diff --git a/src/components/login/OtpInput.tsx b/src/components/login/OtpInput.tsx new file mode 100644 index 00000000..64adbca7 --- /dev/null +++ b/src/components/login/OtpInput.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import {Controller, useForm} from 'react-hook-form'; +import {StyleSheet} from 'react-native'; +import {SafeAreaView} from 'react-native-safe-area-context'; +import {useSelector} from 'react-redux'; +import Button from '../../../RN-UI-LIB/src/components/Button'; +import Heading from '../../../RN-UI-LIB/src/components/Heading'; +import Text from '../../../RN-UI-LIB/src/components/Text'; +import TextInput from '../../../RN-UI-LIB/src/components/TextInput'; +import BackArrowIcon from '../../../RN-UI-LIB/src/Icons/BackArrowIcon'; +import {GenericStyles} from '../../../RN-UI-LIB/src/styles'; +import {COLORS} from '../../../RN-UI-LIB/src/styles/colors'; +import { + verifyOTP, + VerifyOTPPayload, +} from '../../action/authActions'; +import {useAppDispatch} from '../../hooks'; +import {RootState} from '../../store/store'; +import {navigateToScreen} from '../utlis/navigationUtlis'; +import OtpText from './OtpText'; + +interface IOtpForm { + otp: string; +} + +const OtpInput = () => { + const { + handleSubmit, + control, + formState: {errors, isValid}, + } = useForm(); + + const {phoneNumber, otpToken, verifyOTPError, isLoading} = useSelector( + (state: RootState) => state.loginInfo, + ); + + const dispatch = useAppDispatch(); + + const handleVerifyOTP = (data: IOtpForm) => { + const payload: VerifyOTPPayload = { + otp: data.otp, + otpToken, + }; + dispatch(verifyOTP(payload)); + }; + + const handleBackClick = () => { + navigateToScreen('Login'); + }; + + return ( + + + Enter OTP + + OTP sent to {phoneNumber}{' '} + + Change + + + ( + onChange(value)} + value={value} + maxLength={4} + /> + )} + name="otp" + rules={{required: true, minLength: 4}} + /> + +