diff --git a/app.config.js b/app.config.js index 554ab17fb..75248d028 100644 --- a/app.config.js +++ b/app.config.js @@ -280,6 +280,6 @@ export default { palette, }, ...app, - ...eas.build.development.env, + ...eas.build.local.env, }, }; diff --git a/eas.json b/eas.json index 8c015d7c8..33641b005 100644 --- a/eas.json +++ b/eas.json @@ -64,10 +64,10 @@ "distribution": "internal", "env": { "ENV": "local", - "IRA_DOMAIN": "localhost:3000", - "API_DOMAIN": "api.dev.monk.ai/v1", + "IRA_DOMAIN": "localhost:5000", + "API_DOMAIN": "localhost:5000/v1", "AUTH_AUDIENCE": "https://api.monk.ai/v1/", - "AUTH_CLIENT_ID": "ZH7GK6zgjyVDiHN0A6kY98PBWVeJfKvX", + "AUTH_CLIENT_ID": "rq1PDCY20CYLlW0TqDUH8zAvSjyscUjf", "AUTH_DOMAIN": "idp.dev.monk.ai", "SENTRY_DSN": "https://b883345bef184d588d038ed18a563170@sentry.dev.monk.ai/3", "PDF_REPORT_CUSTOMER": "monk_QSBtYXJ0aW5pLiBTaGFrZW4sIG5vdCBzdGlycmVkLgo=", diff --git a/package.json b/package.json index 8c89b6695..eeff792f9 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ "react-webcam": "^7.0.0", "screenfull": "^6.0.1", "sentry-expo": "^4.0.0", + "socket.io-client": "^4.6.1", "webpack": "5.0.0", "webrtc-adapter": "^8.1.1", "xmldom": "^0.6.0", diff --git a/src/config/corejs.js b/src/config/corejs.js index b1d0ca503..bf5eb58fd 100644 --- a/src/config/corejs.js +++ b/src/config/corejs.js @@ -2,7 +2,7 @@ import Constants from 'expo-constants'; import monk from '@monkvision/corejs'; const axiosConfig = { - baseURL: `https://${Constants.manifest.extra.API_DOMAIN}`, + baseURL: `http://${Constants.manifest.extra.API_DOMAIN}`, headers: { 'Access-Control-Allow-Origin': '*' }, }; diff --git a/src/context/socket.js b/src/context/socket.js new file mode 100644 index 000000000..9b36e7f5b --- /dev/null +++ b/src/context/socket.js @@ -0,0 +1,81 @@ +import React, { createContext, useContext, useCallback, useMemo, useState, useEffect } from 'react'; +import { io } from 'socket.io-client'; + +/** + * Creates a socket.io client that connects to the server + * Make sure to enable Cross-Origin Resource Sharing (CORS) on the server + * The options parameter is optional and can be used to configure the connection + */ +const socket = io('http://localhost:5000', {}); // Replace url with your server URL + +// React context for web socket data +const SocketContext = createContext({ + socketID: null, + onSocketEvent: () => {}, + emitSocketEvent: () => {}, +}); + +function SocketProvider({ children }) { + const [socketID, setSocketID] = useState(null); + + const onSocketEvent = useCallback((event, callback, off = true) => { + if (socket.connected) { + socket.on(event, (data) => { + if (off) { + socket.off(event); + } + callback(data); + }); + } + }, []); + + const emitSocketEvent = useCallback((event, args = {}, callback = () => {}) => { + if (socket.connected) { + socket.emit(event, args, () => { + console.log('[Socket] - emit event', event); + callback(); + }); + } + }, []); + + useEffect(() => { + socket.on('connect', () => { + console.log('[Socket] - connected', socket); + console.log('[Socket] - socket id', socket.id); + setSocketID(socket.id); + socket.on('post_inspection', (data) => { + console.log('[Socket] - listen post_inspection event from server', data); + }); + }); + socket.on('disconnect', (reason) => { + console.log('[Socket] - disconnected due to', reason); + }); + socket.on('connect_error', () => { + console.log('[Socket] - has a connection error'); + setTimeout(() => { + socket.connect(); + }, 1000); + }); + }, []); + + const value = useMemo(() => ( + { + socketID, + onSocketEvent, + emitSocketEvent, + } + ), [socketID]); + + return ( + + {children} + + ); +} + +const useWebSocket = () => useContext(SocketContext); + +export { + useWebSocket, + SocketProvider, +}; diff --git a/src/main.js b/src/main.js index ab4fade78..e91580151 100644 --- a/src/main.js +++ b/src/main.js @@ -4,10 +4,12 @@ import { registerRootComponent } from 'expo'; import Constants from 'expo-constants'; import { Platform } from 'react-native'; import * as Sentry from 'sentry-expo'; +import { MonitoringProvider } from '@monkvision/corejs'; + import { name, version } from '@package/json'; import App from 'components/App'; +import { SocketProvider } from './context/socket'; import './i18n'; -import { MonitoringProvider } from '@monkvision/corejs'; const config = { dsn: Constants.manifest.extra.SENTRY_DSN, @@ -20,7 +22,14 @@ const config = { if (Platform.OS === 'web') { const container = document.getElementById('root'); - render(, container); + render( + + + + + , + container, + ); } else { registerRootComponent(Sentry.Native.wrap(App)); } diff --git a/src/screens/InspectionCreate/index.js b/src/screens/InspectionCreate/index.js index 644d78f3f..6e58d1d62 100644 --- a/src/screens/InspectionCreate/index.js +++ b/src/screens/InspectionCreate/index.js @@ -15,6 +15,7 @@ import * as names from 'screens/names'; import useAuth from 'hooks/useAuth'; import useSignIn from 'hooks/useSignIn'; import useCreateInspection from './useCreateInspection'; +import { useWebSocket } from '../../context/socket'; const styles = StyleSheet.create({ root: { @@ -43,6 +44,7 @@ export default function InspectionCreate() { const { errorHandler } = useMonitoring(); const { t } = useTranslation(); const { colors, loaderDotsColors } = useTheme(); + const { socketID } = useWebSocket(); const route = useRoute(); @@ -63,7 +65,7 @@ export default function InspectionCreate() { }, }); - const createInspection = useCreateInspection({ ...vehicle, vin }); + const createInspection = useCreateInspection({ ...vehicle, vin, socketID }); const handleCreate = useCallback(async () => { if (isEmpty(inspectionId) && isAuthenticated && createInspection.state.count < 1) { utils.log(['[Click] Inspection task chosen: ', selected]); diff --git a/src/screens/InspectionCreate/useCreateInspection/index.js b/src/screens/InspectionCreate/useCreateInspection/index.js index 3723d6f11..41c9fecad 100644 --- a/src/screens/InspectionCreate/useCreateInspection/index.js +++ b/src/screens/InspectionCreate/useCreateInspection/index.js @@ -4,20 +4,22 @@ import useAuth from 'hooks/useAuth'; import { useCallback, useState } from 'react'; import { useDispatch } from 'react-redux'; -export default function useCreateInspection(vehicle) { +export default function useCreateInspection({ socketID, ...vehicle }) { const dispatch = useDispatch(); const { isAuthenticated } = useAuth(); const [inspectionId, setInspectionId] = useState(); const axiosRequest = useCallback(async () => { - const taskOptions = { status: monk.types.ProgressStatusUpdate.NOT_STARTED }; + const taskOptions = { + status: monk.types.ProgressStatusUpdate.NOT_STARTED, + }; const tasks = { wheelAnalysis: { ...taskOptions, useLongshots: true }, damageDetection: taskOptions, ...(vehicle?.vin ? {} : { imagesOcr: taskOptions }), }; - return monk.entity.inspection.createOne({ tasks, vehicle }); + return monk.entity.inspection.createOne({ tasks, vehicle, websocket_id: socketID }); }, []); const handleRequestSuccess = useCallback(({ entities, result }) => { diff --git a/src/screens/Landing/index.js b/src/screens/Landing/index.js index 0b9344076..925d98ecd 100644 --- a/src/screens/Landing/index.js +++ b/src/screens/Landing/index.js @@ -1,8 +1,8 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import monk, { useMonitoring } from '@monkvision/corejs'; -import { useInterval, utils } from '@monkvision/toolkit'; +import { utils } from '@monkvision/toolkit'; import { Container } from '@monkvision/ui'; -import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native'; +import { useNavigation, useRoute } from '@react-navigation/native'; import Inspection from 'components/Inspection'; import Modal from 'components/Modal'; import ExpoConstants from 'expo-constants'; @@ -27,6 +27,7 @@ import useUpdateOneTask from './useUpdateOneTask'; import useVinModal from './useVinModal'; import VehicleType from './VehicleType'; import useUpdateInspectionVehicle from './useUpdateInspectionVehicle'; +import { useWebSocket } from '../../context/socket'; const ICON_BY_STATUS = { NOT_STARTED: 'chevron-right', @@ -42,8 +43,10 @@ export default function Landing() { const { errorHandler } = useMonitoring(); const { t, i18n } = useTranslation(); const { setShowTranslatedMessage, Notice } = useSnackbar(true); + const { onSocketEvent, emitSocketEvent } = useWebSocket(); const [vehicleType, setVehicleType] = useState(''); + const [currentPercentage, setCurrentPercentage] = useState(0); const isPortrait = useMediaQuery({ query: '(orientation: portrait)' }); const route = useRoute(); @@ -103,7 +106,6 @@ export default function Landing() { const isVin = value === 'vinNumber'; const vinOption = ExpoConstants.manifest.extra.options.find((option) => option.value === 'vinNumber'); if (isVin && vinOption?.mode.includes('manually')) { vinOptionsRef.current?.open(); return; } - const shouldSignIn = !isAuthenticated; const to = shouldSignIn ? names.SIGN_IN : names.INSPECTION_CREATE; navigation.navigate(to, { selectedMod: value, inspectionId, vehicle: { vehicleType } }); @@ -162,9 +164,12 @@ export default function Landing() { onPress={handlePress} disabled={disabled} /> + { + item.taskName === 'damage_detection' && + } ); - }, [handleListItemPress, inspection]); + }, [handleListItemPress, currentPercentage, inspection]); const start = useCallback(() => { if (inspectionId && getInspection.state.loading !== true) { @@ -172,14 +177,34 @@ export default function Landing() { errorHandler(err); }); } - }, [inspectionId, getInspection]); - - const intervalId = useInterval(start, 1000); + }, [inspectionId]); - useFocusEffect(useCallback(() => { - start(); - return () => clearInterval(intervalId); - }, [navigation, start, intervalId])); + useEffect(() => { + console.log('[Landing page] - [Use Effect]'); + if (inspectionId) { + // Listen websocket server event to get the updated progress for damage_detection task + emitSocketEvent('join', {"room": inspectionId}) + onSocketEvent('task_progress_update', (data) => { + console.log('[Socket] - [task_progress_update]', data); + console.log('[Socket] - [task_progress_update]', inspectionId); + if (data.task_name === 'damage_detection') { + console.log('[Socket] - [task_progress_update] in the if!'); + console.log('[Socket] - [task_progress_update]', data.progress); + setCurrentPercentage(parseFloat(data.progress) * 100); + } + }, false); + + // Listen websocket server event to get the updated status for each task + onSocketEvent('update_task_status', (data) => { + console.log('[Socket] - [update_task_status]', data); + console.log('[Socket] - [update_task_status]', inspectionId); + if (data.inspection_id === inspectionId) { + console.log('[Socket] - [update_task_status] in the if!'); + start(); + } + }, false); + } + }, [inspectionId]); useEffect(() => { if (inspectionId && !allTasksAreCompleted) { diff --git a/src/screens/Landing/styles.js b/src/screens/Landing/styles.js index bffa36c0e..a379d5db4 100644 --- a/src/screens/Landing/styles.js +++ b/src/screens/Landing/styles.js @@ -97,4 +97,9 @@ export default StyleSheet.create({ textAlignRight: { alignItems: 'flex-end', }, + progress: { + backgroundColor: '#305ebf', + height: 3, + transition: 'width ease .4s', + }, }); diff --git a/src/screens/Landing/useGetPdfReport/index.js b/src/screens/Landing/useGetPdfReport/index.js index 7c4643a05..8a1b64d51 100644 --- a/src/screens/Landing/useGetPdfReport/index.js +++ b/src/screens/Landing/useGetPdfReport/index.js @@ -5,6 +5,8 @@ import { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Platform } from 'react-native'; +import { useWebSocket } from '../../../context/socket'; + const webDownload = (url, inspectionId) => { const link = document.createElement('a'); link.href = url; @@ -28,6 +30,7 @@ export default function useGetPdfReport(inspectionId, onError) { const [reportUrl, setReportUrl] = useState(null); const [loading, setLoading] = useState(false); const { i18n } = useTranslation(); + const { onSocketEvent } = useWebSocket(); const requestPdfPayload = useMemo(() => ({ pricing: false, @@ -55,17 +58,12 @@ export default function useGetPdfReport(inspectionId, onError) { const preparePdf = useCallback( async () => { setLoading(true); - await requestPdfReport(); - let done = false; - while (!done) { + // Send/Listen an event from server + onSocketEvent('ready_inspection_pdf_url', async () => { try { - // eslint-disable-next-line no-await-in-loop - await timeout(2000); - // eslint-disable-next-line no-await-in-loop const res = await getPdfUrl(); if (res.axiosResponse?.data?.pdfUrl) { setReportUrl(res.axiosResponse.data.pdfUrl); - done = true; setLoading(false); } } catch (err) { @@ -74,7 +72,9 @@ export default function useGetPdfReport(inspectionId, onError) { if (onError) { onError(err); } } } - } + }); + // api call for pdf report + await requestPdfReport(); }, [inspectionId, requestPdfReport, getPdfUrl, setReportUrl, setLoading], ); diff --git a/yarn.lock b/yarn.lock index b604adf99..d698856dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3738,6 +3738,11 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@socket.io/component-emitter@~3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" + integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== + "@svgr/babel-plugin-add-jsx-attribute@^6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.0.0.tgz#bd6d1ff32a31b82b601e73672a789cc41e84fe18" @@ -7046,7 +7051,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6. dependencies: ms "2.0.0" -debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: +debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -7605,6 +7610,22 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" +engine.io-client@~6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.4.0.tgz#88cd3082609ca86d7d3c12f0e746d12db4f47c91" + integrity sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + engine.io-parser "~5.0.3" + ws "~8.11.0" + xmlhttprequest-ssl "~2.0.0" + +engine.io-parser@~5.0.3: + version "5.0.6" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.6.tgz#7811244af173e157295dec9b2718dfe42a64ef45" + integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw== + enhanced-resolve@^4.1.0: version "4.5.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz#2f3cfd84dbe3b487f18f2db2ef1e064a571ca5ec" @@ -16455,6 +16476,24 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" +socket.io-client@^4.6.1: + version "4.6.1" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.6.1.tgz#80d97d5eb0feca448a0fb6d69a7b222d3d547eab" + integrity sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.4.0" + socket.io-parser "~4.2.1" + +socket.io-parser@~4.2.1: + version "4.2.2" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.2.tgz#1dd384019e25b7a3d374877f492ab34f2ad0d206" + integrity sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + sockjs-client@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.4.0.tgz#c9f2568e19c8fd8173b4997ea3420e0bb306c7d5" @@ -18404,6 +18443,11 @@ ws@^7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== +ws@~8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== + x-path@^0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/x-path/-/x-path-0.0.2.tgz#294d076bb97a7706cc070bbb2a6fd8c54df67b12" @@ -18494,6 +18538,11 @@ xmldom@^0.6.0: resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.6.0.tgz#43a96ecb8beece991cef382c08397d82d4d0c46f" integrity sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg== +xmlhttprequest-ssl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" + integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== + xregexp@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"