diff --git a/App.tsx b/App.tsx index 457cc90..f2dc5ca 100644 --- a/App.tsx +++ b/App.tsx @@ -5,14 +5,14 @@ * @format */ -import React, { useEffect, useState } from 'react'; -import { SafeAreaView, ScrollView, StatusBar, StyleSheet } from 'react-native'; +import React, {useEffect, useState} from 'react'; +import {SafeAreaView, ScrollView, StatusBar, StyleSheet} from 'react-native'; -import { getFormTemplate } from './formstr/formstr'; -import { Colors } from 'react-native/Libraries/NewAppScreen'; -import { PrescriptionCreator } from './components/PrescriptionCreator'; +import {getFormTemplate} from './formstr/formstr'; +import {Colors} from 'react-native/Libraries/NewAppScreen'; +import {PrescriptionCreator} from './components/PrescriptionCreator'; import 'react-native-url-polyfill/auto'; -import PolyfillCrypto from 'react-native-webview-crypto' +import PolyfillCrypto from 'react-native-webview-crypto'; function App(): React.JSX.Element { const backgroundStyle = { @@ -27,12 +27,11 @@ function App(): React.JSX.Element { if (!form) { let form = await getFormTemplate( 'eb3df1f89653475f0bcbd22da35f8d2f126db8a68a88a7abedc53535c76c39b4', - ) - setForm(form); + ); } }; fetchForm(); - }, [form]); + }, [form, setForm]); return ( @@ -44,7 +43,7 @@ function App(): React.JSX.Element { - + ); diff --git a/components/Inputs/Inputs.tsx b/components/Inputs/Inputs.tsx index 63b38f5..88b1208 100644 --- a/components/Inputs/Inputs.tsx +++ b/components/Inputs/Inputs.tsx @@ -1,7 +1,15 @@ -import { DatePicker, DatePickerView, InputItem, List, Text, TextareaItem, View } from "@ant-design/react-native"; -import { V1AnswerSettings, AnswerTypes } from "@formstr/sdk/dist/interfaces"; -import { useState } from "react"; -import RNPickerSelect from "react-native-picker-select" +import { + DatePicker, + DatePickerView, + InputItem, + List, + Text, + TextareaItem, + View, +} from '@ant-design/react-native'; +import {V1AnswerSettings, AnswerTypes} from '@formstr/sdk/dist/interfaces'; +import {useState} from 'react'; +import RNPickerSelect from 'react-native-picker-select'; interface InputFillerProps { answerType: AnswerTypes; @@ -16,39 +24,38 @@ export const InputFiller: React.FC = ({ onChange, defaultValue, }) => { - - const [inputValue, setInputValue] = useState(""); - const handleInputChange = ( - e: any - ) => { - console.log("E is", e) + const [inputValue, setInputValue] = useState(''); + const handleInputChange = (e: any) => { + console.log('E is', e); setInputValue(e); - onChange(e) + onChange(e); }; const handleValueChange = (value: string) => { if (!value) return; - setInputValue(value) + setInputValue(value); onChange(value); }; const getInput = ( answerType: AnswerTypes, - answerSettings: V1AnswerSettings + answerSettings: V1AnswerSettings, ) => { - const dropdownItems = (answerSettings.choices || []).map((choice) => { + const dropdownItems = (answerSettings.choices || []).map(choice => { return { - label: choice.label, value: choice.choiceId, key: choice.choiceId - }}) - const INPUT_TYPE_COMPONENT_MAP: { [key in AnswerTypes]?: JSX.Element } = { + label: choice.label, + value: choice.choiceId, + key: choice.choiceId, + }; + }); + const INPUT_TYPE_COMPONENT_MAP: {[key in AnswerTypes]?: JSX.Element} = { [AnswerTypes.label]: <>, [AnswerTypes.shortText]: ( - + placeholderTextColor="#aaaaaa"> ), [AnswerTypes.paragraph]: ( = ({ placeholderTextColor="#aaaaaa" onChange={handleInputChange} autoHeight - style={{ paddingVertical: 5 }} + style={{paddingVertical: 5}} /> ), - [AnswerTypes.number]: ( - // - - ), - [AnswerTypes.radioButton]: ( - // - - ), - [AnswerTypes.checkboxes]: ( - // - - ), + [AnswerTypes.number]: , + [AnswerTypes.radioButton]: , + [AnswerTypes.checkboxes]: , [AnswerTypes.dropdown]: ( - {inputValue ? answerSettings.choices?.filter((choice) => { return choice.choiceId === inputValue})[0].label : "Select an option"} + + + {inputValue + ? answerSettings.choices?.filter(choice => { + return choice.choiceId === inputValue; + })[0].label + : 'Select an option'} + + ), [AnswerTypes.date]: ( - + ), [AnswerTypes.time]: ( - + ), }; diff --git a/components/PrescriptionCreator/AddressForm.tsx b/components/PrescriptionCreator/AddressForm.tsx new file mode 100644 index 0000000..6fab350 --- /dev/null +++ b/components/PrescriptionCreator/AddressForm.tsx @@ -0,0 +1,51 @@ +import {Text, TextInput, View} from 'react-native'; +import {Section} from './Section'; +import {styles, TextTheme} from './styles'; +import {useState} from 'react'; +import DatePicker from 'react-native-date-picker'; +import {Button} from '@ant-design/react-native'; + +interface AddressForm { + address_line_1?: string; + city?: string; + state_province?: string; + postal_code?: string; + country_code?: string; +} + +interface AddressFormProps { + nestedFormCallback: (tag: string, form: Object) => void; +} + +export const AddressForm: React.FC = ({ + nestedFormCallback, +}) => { + const [form, setForm] = useState({}); + const [openDate, setOpenDate] = useState(false); + + const handleTextChange = (tag: keyof AddressForm, text: string) => { + let newForm = {...form}; + newForm[tag] = text; + setForm(newForm); + nestedFormCallback('Address', newForm); + }; + + return ( +
+ + + Name + + handleTextChange('address_line_1', text) + } + /> + + +
+ ); +}; diff --git a/components/PrescriptionCreator/PatientForm.tsx b/components/PrescriptionCreator/PatientForm.tsx new file mode 100644 index 0000000..964d8ca --- /dev/null +++ b/components/PrescriptionCreator/PatientForm.tsx @@ -0,0 +1,80 @@ +import {Text, TextInput, View} from 'react-native'; +import {Section} from './Section'; +import {styles, TextTheme} from './styles'; +import {useState} from 'react'; +import DatePicker from 'react-native-date-picker'; +import {Button} from '@ant-design/react-native'; + +interface PatientForm { + name?: string; + date_of_birth?: string; +} + +interface PatientFormProps { + nestedFormCallback: (tag: string, form: Object) => void; +} + +export const PatientForm: React.FC = ({ + nestedFormCallback, +}) => { + const [form, setForm] = useState({}); + const [openDate, setOpenDate] = useState(false); + + const handleTextChange = (tag: 'name' | 'date_of_birth', text: string) => { + let newForm = {...form}; + newForm[tag] = text; + setForm(newForm); + nestedFormCallback('patient', {human_patient: newForm}); + }; + + return ( +
+ + + Name + handleTextChange('name', text)} + /> + + + Date of Birth + {form.date_of_birth ? ( + + {form.date_of_birth} + + + ) : ( + + )} + setOpenDate(false)} + onConfirm={(date: Date) => { + handleTextChange('date_of_birth', date.toDateString()); + setOpenDate(false); + }} + /> + + +
+ ); +}; diff --git a/components/PrescriptionCreator/Section.tsx b/components/PrescriptionCreator/Section.tsx new file mode 100644 index 0000000..3e37c37 --- /dev/null +++ b/components/PrescriptionCreator/Section.tsx @@ -0,0 +1,17 @@ +import {PropsWithChildren} from 'react'; +import {Dimensions, StyleSheet, Text, View} from 'react-native'; +import {Colors} from 'react-native/Libraries/NewAppScreen'; +import {styles} from './styles'; + +type SectionProps = PropsWithChildren<{ + title: string; +}>; + +export function Section({children, title}: SectionProps): React.JSX.Element { + return ( + + {title} + {children} + + ); +} diff --git a/components/PrescriptionCreator/index.tsx b/components/PrescriptionCreator/index.tsx index 2753caa..848957a 100644 --- a/components/PrescriptionCreator/index.tsx +++ b/components/PrescriptionCreator/index.tsx @@ -1,192 +1,213 @@ -import { Alert, Appearance, Dimensions, Image, StyleSheet, Text, View } from 'react-native'; -import { Colors } from 'react-native/Libraries/NewAppScreen'; -import { PropsWithChildren, useEffect, useState } from 'react'; -import { Button, Card, Modal } from '@ant-design/react-native'; -import { V1Field } from '@formstr/sdk/dist/interfaces'; -import { InputFiller } from '../Inputs/Inputs'; +import {Alert, Appearance, Dimensions, Image, Text, View} from 'react-native'; +import {Colors} from 'react-native/Libraries/NewAppScreen'; +import {PropsWithChildren, useEffect, useState} from 'react'; +import {Button, Card, Modal} from '@ant-design/react-native'; // import { SendPrescription } from './sendPrescription'; -import { Dropdown } from 'react-native-element-dropdown'; -import { SimplePool, UnsignedEvent, finalizeEvent, generateSecretKey, getPublicKey, nip04, nip19 } from 'nostr-tools'; +import {Dropdown} from 'react-native-element-dropdown'; +import { + SimplePool, + UnsignedEvent, + finalizeEvent, + generateSecretKey, + getPublicKey, + nip04, + nip19, +} from 'nostr-tools'; import EncryptedStorage from 'react-native-encrypted-storage'; -import { ImportNsec } from './ImportNsec'; -import { json2xml } from 'xml-js'; +import {ImportNsec} from './ImportNsec'; +import {json2xml} from 'xml-js'; +import {Section} from './Section'; +import {PatientForm} from './PatientForm'; +import {AddressForm} from './AddressForm'; + +/* + Patient + - Name + - Date Of Birth + + Address + - Address Line 1 + - City + - StateProvince + - Postal Code + - Country Code + + Medicine + - Name + - Dosage Form + - Strength + - Quantity + - Re-fills + - Directions + ` + */ function OBJtoXML(obj: any) { var xml = ''; for (var prop in obj) { - xml += "<" + prop + ">"; - if(Array.isArray(obj[prop])) { - for (var array of obj[prop]) { + xml += '<' + prop + '>'; + if (Array.isArray(obj[prop])) { + for (var array of obj[prop]) { + // A real botch fix here + xml += ''; + xml += '<' + prop + '>'; - // A real botch fix here - xml += ""; - xml += "<" + prop + ">"; - - xml += OBJtoXML(new Object(array)); - } - } else if (typeof obj[prop] == "object") { - xml += OBJtoXML(new Object(obj[prop])); - } else { - xml += obj[prop]; + xml += OBJtoXML(new Object(array)); } - xml += ""; + } else if (typeof obj[prop] == 'object') { + xml += OBJtoXML(new Object(obj[prop])); + } else { + xml += obj[prop]; + } + xml += ''; } - var xml = xml.replace(/<\/?[0-9]{1,}>/g,''); - return xml + var xml = xml.replace(/<\/?[0-9]{1,}>/g, ''); + return xml; } -type SectionProps = PropsWithChildren<{ - title: string; -}>; - const colorScheme = Appearance.getColorScheme(); const backgroundStyle = { backgroundColor: Colors.darker, }; -const styles = StyleSheet.create({ - sectionContainer: { - marginTop: 32, - paddingHorizontal: 24, - width: Dimensions.get('window').width - 80, - }, - sectionTitle: { - fontSize: 24, - fontWeight: '600', - }, - sectionDescription: { - marginTop: 8, - fontSize: 18, - fontWeight: '400', - }, - highlight: { - fontWeight: '500', - }, -}); - - const width = Dimensions.get('window').width; //full width -const height = Dimensions.get('window').height +const height = Dimensions.get('window').height; -function Section({ children, title }: SectionProps): React.JSX.Element { - return ( - - - {title} - - - {children} - - - ); -} const locationData = [ - { label: 'Pharmacy A', value: 'A', npub: 'npub1tea09rtjeuzgk4gjajzry37wuyv7h02d4zw38cpadcrkg5yt0qhqncr7km', relays: ["wss://relay.damus.io"] }, - { label: 'Pharmacy B', value: 'B', npub: 'npub1tea09rtjeuzgk4gjajzry37wuyv7h02d4zw38cpadcrkg5yt0qhqncr7km', relays: ["wss://relay.primal.net"] }, - { label: 'Pharmacy C', value: 'C', npub: 'npub1tea09rtjeuzgk4gjajzry37wuyv7h02d4zw38cpadcrkg5yt0qhqncr7km', relays: ["wss://relay.hllo.live"] }, - { label: 'Pharmacy D', value: 'D', npub: 'npub1tea09rtjeuzgk4gjajzry37wuyv7h02d4zw38cpadcrkg5yt0qhqncr7km', relays: ["wss://nos.lol", "wss://relay.damus.io"] } -] + { + label: 'Pharmacy A', + value: 'A', + npub: 'npub1tea09rtjeuzgk4gjajzry37wuyv7h02d4zw38cpadcrkg5yt0qhqncr7km', + relays: ['wss://relay.damus.io'], + }, + { + label: 'Pharmacy B', + value: 'B', + npub: 'npub1tea09rtjeuzgk4gjajzry37wuyv7h02d4zw38cpadcrkg5yt0qhqncr7km', + relays: ['wss://relay.primal.net'], + }, + { + label: 'Pharmacy C', + value: 'C', + npub: 'npub1tea09rtjeuzgk4gjajzry37wuyv7h02d4zw38cpadcrkg5yt0qhqncr7km', + relays: ['wss://relay.hllo.live'], + }, + { + label: 'Pharmacy D', + value: 'D', + npub: 'npub1tea09rtjeuzgk4gjajzry37wuyv7h02d4zw38cpadcrkg5yt0qhqncr7km', + relays: ['wss://nos.lol', 'wss://relay.damus.io'], + }, +]; -const locationDummyData = [ - { label: 'Pharmacy A', value: 'A' } -] +const locationDummyData = [{label: 'Pharmacy A', value: 'A'}]; -export const PrescriptionCreator = ({ form }: { form: any }) => { - if (form === null) return Loading... +export const PrescriptionCreator = () => { const [showImportNsec, setShowImportNsec] = useState(false); - const [loggedInNpub, setLoggedInNpub] = useState("") - const [selectedPharmacyId, setSelectedPharmacyId] = useState(locationData[0].npub); + const [loggedInNpub, setLoggedInNpub] = useState(''); + const [selectedPharmacyId, setSelectedPharmacyId] = useState( + locationData[0].npub, + ); const [selectedPharmacyRelays, setSelectedPharmacyRelays] = useState([]); - const [finalJSON, setFinalJson] = useState({}) + const [finalJSON, setFinalJson] = useState({}); useEffect(() => { async function initialize() { let doctorCredentials = null; try { - doctorCredentials = await EncryptedStorage.getItem("user_credentials"); - if(!doctorCredentials) { - setShowImportNsec(true) + doctorCredentials = await EncryptedStorage.getItem('user_credentials'); + if (!doctorCredentials) { + setShowImportNsec(true); + } else { + setLoggedInNpub( + nip19.npubEncode( + getPublicKey(nip19.decode(doctorCredentials).data as Uint8Array), + ), + ); } - else { - setLoggedInNpub(nip19.npubEncode(getPublicKey(nip19.decode(doctorCredentials).data as Uint8Array))) - } - } - catch(e) { - console.log("Error getting credentials", e) + } catch (e) { + console.log('Error getting credentials', e); } } - initialize() - }, []) + initialize(); + }, []); const renderItem = (item: any) => { - return - {item.label} - - Npub: {item.npub} - Relays: {item.relays.join(', ')} + return ( + + {item.label} + + + Npub: {item.npub} + + Relays: {item.relays.join(', ')} + - - } + ); + }; - const handleFormItemChange = (questionId: string, value: string) => { - console.log("Filling", questionId, value) - setFinalJson({...finalJSON, [questionId]: value}) - } + const nestedFormCallback = ( + xmlTag: string, + value: Object | Array | string, + ) => { + console.log('Filling', xmlTag, value); + setFinalJson({...finalJSON, [xmlTag]: value}); + }; const handleLocationChange = (item: any) => { - setSelectedPharmacyId(item.npub) - setSelectedPharmacyRelays(item.relays) - } + setSelectedPharmacyId(item.npub); + setSelectedPharmacyRelays(item.relays); + }; const handleImportNsec = (nsec: string) => { - EncryptedStorage.setItem("user_credentials", nsec) - if(nsec.startsWith('nsec1') && nsec.length !== 63) { - Alert.alert("not a valid nsec!") + EncryptedStorage.setItem('user_credentials', nsec); + if (nsec.startsWith('nsec1') && nsec.length !== 63) { + Alert.alert('not a valid nsec!'); return; } - setLoggedInNpub(nip19.npubEncode(getPublicKey(nip19.decode(nsec).data as Uint8Array))) - setShowImportNsec(false) - } + setLoggedInNpub( + nip19.npubEncode(getPublicKey(nip19.decode(nsec).data as Uint8Array)), + ); + setShowImportNsec(false); + }; const handleButtonPress = () => { - console.log("Final JSON is", finalJSON) - const xml = OBJtoXML({form: finalJSON }) - console.log("XML is...", xml, typeof xml ) + console.log('Final JSON is', finalJSON); + const xml = OBJtoXML({prescription: finalJSON}); + console.log('XML is...', xml, typeof xml); sendPrescription(xml); - } + }; const sendPrescription = async (xml: string) => { - console.log("Will generate IDs") - const sk = nip19.decode(await EncryptedStorage.getItem("user_credentials") as `nsec1${string}`).data as Uint8Array - const pk = getPublicKey(sk) - const pharmacyId = nip19.decode(selectedPharmacyId).data as string - console.log("Got ids") + console.log('Will generate IDs'); + const sk = nip19.decode( + (await EncryptedStorage.getItem('user_credentials')) as `nsec1${string}`, + ).data as Uint8Array; + const pk = getPublicKey(sk); + const pharmacyId = nip19.decode(selectedPharmacyId).data as string; + console.log('Got ids'); + console.log('content is ', xml); const baseKind4Event: UnsignedEvent = { kind: 4, - tags: [["p", pharmacyId]], - content: await nip04.encrypt(sk, pharmacyId, `This is a test prescription from PeerScribe ${xml}`), + tags: [['p', pharmacyId]], + content: await nip04.encrypt(sk, pharmacyId, `${xml}`), created_at: Math.floor(Date.now() / 1000), - pubkey: pk - } - const finalEvent = finalizeEvent(baseKind4Event, sk) - const pool = new SimplePool() - console.log("publishing event") - await Promise.any(pool.publish(selectedPharmacyRelays, finalEvent)) - console.log("Event Published") - Alert.alert("Prescription Sent to the pharmacy!") - } + pubkey: pk, + }; + const finalEvent = finalizeEvent(baseKind4Event, sk); + const pool = new SimplePool(); + console.log('publishing event'); + await Promise.any(pool.publish(selectedPharmacyRelays, finalEvent)); + console.log('Event Published'); + Alert.alert('Prescription Sent to the pharmacy!'); + }; return ( { width: Dimensions.get('window').width, }} source={{ - uri: form.settings.titleImageUrl, + uri: 'https://www.studentdoctor.net/wp-content/uploads/2018/08/20180815_prescription.png', }} />
From the practice of {loggedInNpub} - +
- - + +
- - {form.fields.map((field: V1Field) => { - return ( - - - {field.question} - - {/* - {field.answerType} - */} - handleFormItemChange(field.questionId, answer)} /> - - ); - })} - + + + +
- { setShowImportNsec(false)}} onPress={handleImportNsec}/> - + { + setShowImportNsec(false); + }} + onPress={handleImportNsec} + />
); }; diff --git a/components/PrescriptionCreator/styles.ts b/components/PrescriptionCreator/styles.ts new file mode 100644 index 0000000..cdea331 --- /dev/null +++ b/components/PrescriptionCreator/styles.ts @@ -0,0 +1,39 @@ +import {Dimensions, StyleSheet} from 'react-native'; +import {Colors} from 'react-native/Libraries/NewAppScreen'; + +export const styles = StyleSheet.create({ + input: { + height: 40, + margin: 12, + borderWidth: 1, + borderBottomColor: 'white', + padding: 10, + color: Colors.white, + }, + sectionContainer: { + marginTop: 32, + paddingHorizontal: 24, + width: Dimensions.get('window').width - 80, + color: Colors.white, + }, + sectionTitle: { + fontSize: 24, + fontWeight: '600', + color: Colors.white, + }, + sectionDescription: { + marginTop: 8, + fontSize: 18, + fontWeight: '400', + color: Colors.white, + }, + highlight: { + fontWeight: '500', + }, +}); + +export const TextTheme = [ + { + color: Colors.white, + }, +]; diff --git a/package.json b/package.json index 4c2c6fd..a672fcc 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@ant-design/react-native": "^5.1.0", "@formstr/sdk": "^0.0.4-alpha", + "@react-native-community/datetimepicker": "^8.0.1", "@react-native-community/segmented-control": "^2.2.2", "@react-native-community/slider": "^4.5.0", "@react-native-picker/picker": "^2.6.1", @@ -21,6 +22,7 @@ "react": "18.2.0", "react-native": "0.73.4", "react-native-crypto": "^2.2.0", + "react-native-date-picker": "^5.0.3", "react-native-element-dropdown": "^2.10.4", "react-native-encrypted-storage": "^4.0.3", "react-native-get-random-values": "^1.11.0", diff --git a/yarn.lock b/yarn.lock index c82eae6..8246dac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1775,6 +1775,13 @@ prompts "^2.4.2" semver "^7.5.2" +"@react-native-community/datetimepicker@^8.0.1": + version "8.0.1" + resolved "https://registry.yarnpkg.com/@react-native-community/datetimepicker/-/datetimepicker-8.0.1.tgz#047f27566fb21b5095fa54f558bffd8ab6472b46" + integrity sha512-4BO0t3geMNNw9cIIm9p9FNUzwMXexdzD4pAH0AaUAycs3BS71HLrX8jHbrI7nzq/+8O7cLAXn5Gudte+YpTV8Q== + dependencies: + invariant "^2.2.4" + "@react-native-community/segmented-control@^2.2.2": version "2.2.2" resolved "https://registry.yarnpkg.com/@react-native-community/segmented-control/-/segmented-control-2.2.2.tgz#4014256819ab8f40f6bc3a3929ff14a9d149cf04" @@ -6587,6 +6594,11 @@ react-native-crypto@^2.2.0: public-encrypt "^4.0.0" randomfill "^1.0.3" +react-native-date-picker@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/react-native-date-picker/-/react-native-date-picker-5.0.3.tgz#8fc5a3e2dc62ad689cb54f7a1a8083db269212ae" + integrity sha512-pA/HcnB7RBac/CigQcqvM95JiD4c7mN5DPie3c/LFBol55ntvpxyxxr5ixAonB5PYLTjop21EIajAHKAP7/JeQ== + react-native-element-dropdown@^2.10.4: version "2.10.4" resolved "https://registry.yarnpkg.com/react-native-element-dropdown/-/react-native-element-dropdown-2.10.4.tgz#58d8a5e38d2a3fb74498c195bd775d9e1536e6bd"