diff --git a/android/build.gradle b/android/build.gradle index ed5a568..fec09e4 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,4 +1,5 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. +def REACT_NATIVE_VERSION = new File(['node', '--print',"JSON.parse(require('fs').readFileSync(require.resolve('react-native/package.json'), 'utf-8')).version"].execute(null, rootDir).text.trim()) buildscript { ext { @@ -34,4 +35,10 @@ allprojects { jcenter() maven { url 'https://www.jitpack.io' } } + configurations.all { + resolutionStrategy { + // Remove this override in 0.65+, as a proper fix is included in react-native itself. + force "com.facebook.react:react-native:" + REACT_NATIVE_VERSION + } + } } diff --git a/package.json b/package.json index 4afb30f..93a88bd 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,14 @@ "react-native-redash": "14.2.4", "react-native-safe-area-context": "3.1.4", "react-native-screens": "2.7.0", + "react-native-select-dropdown": "3.0.1", "react-native-splash-screen": "3.2.0", "react-native-vector-icons": "6.6.0", "react-redux": "7.2.0", "redux": "4.0.5", "redux-persist": "5.10.0", - "typesafe-actions": "4.4.2" + "typesafe-actions": "4.4.2", + "yup": "0.32.11" }, "devDependencies": { "@babel/core": "^7.8.4", diff --git a/src/AddMedication.tsx b/src/AddMedication.tsx new file mode 100644 index 0000000..4daa2c1 --- /dev/null +++ b/src/AddMedication.tsx @@ -0,0 +1,357 @@ +import React, {useState, useEffect} from 'react' +import {StyleSheet, View, Text, Modal} from 'react-native' +import {useDispatch, useSelector} from 'react-redux' +import { + SecondaryBlueButton, + PrimaryBlueButton, + TextInput, + H2, + Select, + SelectItem, + selectItemMapper, +} from '~/Components' +import type {AddMedicationRouteProps, RootNavigation} from '~/Root' +import {addMedication} from '~/Store/Actions' +import { + MedicationFormData, + MedicationProps, + MedicationStrength, + UnitVariants, + AdministrationVariants, + IMedicationWithId, +} from '~/Store/User' +import {validateAddMedicationForm} from '~/Utils' +import {medicationsSelector} from '~/Store/Selectors' + +const styles = StyleSheet.create({ + container: { + paddingTop: 24, + paddingHorizontal: 24, + }, + divider: { + borderWidth: 0.5, + borderColor: '#ccc', // TODO Replace with global color variable + marginBottom: 10, // TODO Replace with global variable for margins + marginTop: 10, + }, + centeredView: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + marginTop: 22, + }, + modalView: { + margin: 20, + backgroundColor: 'white', + borderRadius: 20, + padding: 35, + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 5, + }, + modalText: { + marginBottom: 15, + textAlign: 'center', + }, + leftOfTwoColumns: { + paddingRight: 5, + width: '50%', + }, + rightOfTwoColumns: { + paddingLeft: 5, + width: '50%', + }, +}) + +type AddMedicationProps = { + navigation: RootNavigation + route: AddMedicationRouteProps +} + +// Make all Medicine props optional, including the nested strength +type AddMedicineForm = Partial< + Omit & { + [MedicationProps.STRENGTH]: Partial + } +> + +enum BarcodeUserTestStatus { + SHOW_TEST = 'SHOW_TEST', + SHOW_MODAL_INFO = 'SHOW_MODAL_INFO', + HIDE_TEST = 'HIDE_TEST', +} + +export const AddMedication: React.FC = ({navigation}) => { + const dispatch = useDispatch() + const medications = useSelector(medicationsSelector) + const [form, setForm] = useState({}) + const [isFormSubmitted, setIsFormSubmitted] = useState(false) + const [isAddFromPreviousModalVisible, setIsAddFromPreviousModalVisible] = useState(false) + const [addFromPreviousMedication, setAddFromPreviousMedication] = useState< + IMedicationWithId | undefined + >(undefined) + const [formErrors, setFormErrors] = useState([]) + const [barcodeTestStatus, setBarcodeTestStatus] = useState( + BarcodeUserTestStatus.SHOW_TEST + ) + + type Timer = ReturnType + + const setFormProp = (prop: MedicationProps) => { + return (value: T) => { + setForm({ + ...form, + [prop]: value, + }) + } + } + + const setBrandName = setFormProp(MedicationProps.BRAND_NAME) + const setSubstance = setFormProp(MedicationProps.SUBSTANCE) + const setAdministration = setFormProp(MedicationProps.ADMINISTRATION) + const setIntakeInterval = setFormProp(MedicationProps.INTERVAL) + const setStrength = (strengthProp: 'value' | 'unit') => { + return (value: string) => { + const newStrength: Partial = { + ...form.strength, + [strengthProp]: value, + } + + setForm({ + ...form, + strength: newStrength, + }) + } + } + + const isBarcodeTestVisible = + barcodeTestStatus === BarcodeUserTestStatus.SHOW_TEST || + barcodeTestStatus === BarcodeUserTestStatus.SHOW_MODAL_INFO + + const isBarcodeModalVisible = barcodeTestStatus === BarcodeUserTestStatus.SHOW_MODAL_INFO + + const handleBarcodeScanClick = () => { + // TODO This is where we should register the user interaction + // via GA or similar tools to see if there's interest for this + // function before building it + setBarcodeTestStatus(BarcodeUserTestStatus.SHOW_MODAL_INFO) + } + + const handleBarcodeInfoModalClose = () => setBarcodeTestStatus(BarcodeUserTestStatus.HIDE_TEST) + + // Just for the sake of this demo, we reset the barcode status + // after 6s so we can try the button again + useEffect(() => { + let timer: Timer + if (barcodeTestStatus === BarcodeUserTestStatus.HIDE_TEST) { + timer = setTimeout(() => setBarcodeTestStatus(BarcodeUserTestStatus.SHOW_TEST), 6 * 1000) + return () => { + clearTimeout(timer) + } + } + }, [barcodeTestStatus]) + + const handleSubmitForm = () => { + let validationResult + try { + validationResult = validateAddMedicationForm(form as MedicationFormData) + } catch (error: any) { + setFormErrors(error.errors) + return + } + // We pass `validationResult`, instead of `form`, since yup will type cast for us. + dispatch(addMedication(validationResult as MedicationFormData)) + setIsFormSubmitted(true) + } + + useEffect(() => { + let timer: Timer + if (formErrors.length > 0) { + timer = setTimeout(() => setFormErrors([]), 5000) + return () => { + clearTimeout(timer) + } + } + }, [formErrors]) + + const handleCloseThankYouModal = () => { + setIsFormSubmitted(false) + navigation.goBack() + } + + const handleSubmitAddFromPrevious = () => { + if (addFromPreviousMedication) { + // TODO We dont copy the administration/strength.unit props since + // I haven't figured out how to make the select box controlled + const {brandName, substance, strength, interval} = addFromPreviousMedication + setForm({ + brandName, + substance, + strength: { + value: strength.value, + }, + interval, + }) + } + setIsAddFromPreviousModalVisible(false) + } + + const unitData: SelectItem[] = Object.keys(UnitVariants).map(selectItemMapper) + const administrationData: SelectItem[] = Object.keys(AdministrationVariants).map(selectItemMapper) + + return ( + +

Quick add

+ {isBarcodeTestVisible ? ( + + ) : null} + + setIsAddFromPreviousModalVisible(true)} + /> + +

Medication data

+ + + Brand name + + + + Substance + + + + Strength + + + + + + setAdministration(selectedItem.value)} + buttonTextAfterSelection={(selectedItem: SelectItem) => selectedItem.label} + rowTextForSelection={(item: SelectItem) => item.label} + /> + + + {/* These errors messages are not very user friendly/readable at the moment */} + {formErrors.length > 0 ? ( + {formErrors.map(error => error)} + ) : null} + + + + + Your medicine has been added. Have a nice day! + + + + + + + + + Thank you for showing interest in our barcode scanner! We have registered your + interest and if enough people want it - we will make it happen. + + + + + + + setIsAddFromPreviousModalVisible(false)}> + + + {medications.length > 0 ? ( + <> +