Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/openchs-android/android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
AsyncStorage_useNextStorage=true
VisionCamera_enableCodeScanner=true
29 changes: 29 additions & 0 deletions packages/openchs-android/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions packages/openchs-android/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
"react-native-vector-icons": "9.2.0",
"react-native-video": "5.2.1",
"react-native-video-player": "0.12.0",
"react-native-vision-camera": "^4.7.1",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Blocking: react-native-vision-camera@^4.7.1 is incompatible with react-native 0.72.8

VisionCamera 4.3.2+ requires React Native ≥ 0.73 due to React Native framework API changes. This repo is on RN 0.72.8, so 4.7.1 will not build. Options:

  • Upgrade RN to ≥ 0.73 (preferably 0.74+) and update Android build to compileSdk 34 (and AGP 8.x), or
  • Use a VisionCamera version that supports RN 0.72 (≤ 4.3.1, or v3.x), validating Code Scanner availability/perf on your target devices.

Release notes documenting the RN ≥ 0.73 requirement: “react-native-vision-camera 4.3.2 now requires react-native 0.73 or above.” (download.ddeec.com)

Apply one of these diffs:

A) If upgrading RN later, temporarily pin a compatible VC to unblock this PR’s feature work:

-    "react-native-vision-camera": "^4.7.1",
+    "react-native-vision-camera": "4.3.1",

B) If you plan to upgrade RN now, leave ^4.7.1 but also raise Android compileSdk to 34 and verify AGP/Kotlin; coordinate that change in a separate PR to keep scope clean.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"react-native-vision-camera": "^4.7.1",
"react-native-vision-camera": "4.3.1",
🤖 Prompt for AI Agents
In packages/openchs-android/package.json around line 100,
react-native-vision-camera is pinned to ^4.7.1 which requires React Native ≥0.73
and will not build with the repo's RN 0.72.8; either (A) temporarily
downgrade/pin the dependency to a version compatible with RN 0.72 (e.g., 4.3.1
or a 3.x release) to unblock this PR, or (B) if you intend to upgrade RN now,
keep ^4.7.1 but perform the RN upgrade in a separate PR that also raises Android
compileSdk to 34 and updates AGP/Kotlin as needed, then verify build and scanner
functionality.

"react-native-webview": "11.23.0",
"react-native-zip-archive": "6.0.8",
"realm": "11.10.2",
Expand All @@ -112,6 +113,7 @@
"@babel/plugin-proposal-decorators": "7.18.9",
"@babel/plugin-proposal-object-rest-spread": "7.18.9",
"@babel/runtime": "^7.20.0",
"@react-native-community/cli": "latest",
"babel-jest": "^29.2.1",
"bugsnag-sourcemaps": "1.3.0",
"chai": "4.3.6",
Expand All @@ -126,8 +128,7 @@
"patch-package": "6.5.0",
"react-addons-test-utils": "15.6.2",
"react-dom": "18.2.0",
"react-test-renderer": "18.2.0",
"@react-native-community/cli": "latest"
"react-test-renderer": "18.2.0"
},
"engines": {
"node": ">=16"
Expand Down
9 changes: 9 additions & 0 deletions packages/openchs-android/src/views/common/Observations.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ class Observations extends AbstractComponent {
)
} else if (Concept.dataType.PhoneNumber === renderType) {
return this.renderPhoneNumber(observationModel.getValueWrapper());
} else if (Concept.dataType.QR === renderType) {
return this.renderQRValue(displayable.displayValue);
}
return this.renderObservationText(isAbnormal, displayable.displayValue);
}
Expand All @@ -213,6 +215,13 @@ class Observations extends AbstractComponent {
</View>
}

renderQRValue(qrValue) {
return <View style={[this.styles.observationPhoneNumberContainer, this.styles.observationColumn]}>
<Text style={this.styles.observationPhoneNumber}>{qrValue}</Text>
<MCIIcon name="qrcode" style={[{color: Colors.AccentColor}, this.styles.iconStyle]}/>
</View>
}

renderSubject(subject) {
if(!subject.entityObject) {
return this.renderObservationText(true, subject.displayValue);
Expand Down
9 changes: 9 additions & 0 deletions packages/openchs-android/src/views/form/FormElementGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import RepeatableFormElement from "./formElement/RepeatableFormElement";
import SingleSelectEncounterFormElement from "./formElement/SingleSelectEncounterFormElement";
import MultiSelectEncounterFormElement from "./formElement/MultiSelectEncounterFormElement";
import MediaV2FormElement from "./formElement/MediaV2FormElement";
import QRFormElement from "./formElement/QRFormElement";
import Colors from "../primitives/Colors";

class FormElementGroup extends AbstractComponent {
Expand Down Expand Up @@ -351,6 +352,14 @@ class FormElementGroup extends AbstractComponent {
observationHolder={this.props.observationHolder}
subjectUUID={this.props.subjectUUID}
/>, uniqueKey, formElement.uuid === erroredUUID);
} else if (formElement.concept.datatype === Concept.dataType.QR) {
return this.wrap(<QRFormElement
key={uniqueKey}
element={formElement}
actionName={this.props.actions["PRIMITIVE_VALUE_CHANGE"]}
value={this.getSelectedAnswer(formElement.concept, new PrimitiveValue())}
validationResult={validationResult}
/>, uniqueKey, formElement.uuid === erroredUUID);
}
})
}
Expand Down
138 changes: 138 additions & 0 deletions packages/openchs-android/src/views/form/formElement/QRFormElement.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import {Text, TouchableNativeFeedback, View, Modal, StyleSheet} from "react-native";
import PropTypes from 'prop-types';
import React from "react";
import AbstractFormElement from "./AbstractFormElement";
import ValidationErrorMessage from "../ValidationErrorMessage";
import Colors from "../../primitives/Colors";
import Fonts from "../../primitives/Fonts";
import FormElementLabelWithDocumentation from "../../common/FormElementLabelWithDocumentation";
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import QRScanner from "./QRScanner";
import _ from "lodash";

class QRFormElement extends AbstractFormElement {
static propTypes = {
element: PropTypes.object.isRequired,
actionName: PropTypes.string.isRequired,
value: PropTypes.object,
validationResult: PropTypes.object
};

constructor(props, context) {
super(props, context);
this.state = {
showQRScanner: false
};
}

displayValue() {
const value = this.props.value.getValue();
return _.isNil(value) || value === ''
? this.I18n.t('tapToScanQR')
: value;
}

openQRScanner() {
this.setState({ showQRScanner: true });
}

onQRRead(qrValue) {
this.setState({ showQRScanner: false });
if (qrValue) {
this.notifyChange(qrValue);
}
}
Comment on lines +39 to +44
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Normalize scanned value; avoid dropping valid “0” and whitespace-only scans

Handle strings safely and ignore empty-after-trim.

-    onQRRead(qrValue) {
-        this.setState({ showQRScanner: false });
-        if (qrValue) {
-            this.notifyChange(qrValue);
-        }
-    }
+    onQRRead(qrValue) {
+        this.setState({ showQRScanner: false });
+        const normalized = _.isString(qrValue) ? qrValue.trim() : qrValue;
+        if (!_.isNil(normalized) && normalized !== '') {
+            this.notifyChange(normalized);
+        }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onQRRead(qrValue) {
this.setState({ showQRScanner: false });
if (qrValue) {
this.notifyChange(qrValue);
}
}
onQRRead(qrValue) {
this.setState({ showQRScanner: false });
const normalized = _.isString(qrValue) ? qrValue.trim() : qrValue;
if (!_.isNil(normalized) && normalized !== '') {
this.notifyChange(normalized);
}
}
🤖 Prompt for AI Agents
In packages/openchs-android/src/views/form/formElement/QRFormElement.js around
lines 39 to 44, the QR handler currently closes the scanner and directly passes
qrValue which can drop valid values like "0" or pass whitespace-only strings;
normalize by converting qrValue to a string, trimming whitespace, and only call
notifyChange when the trimmed string is non-empty (ensure you treat
null/undefined separately so "0" is preserved), then set showQRScanner false as
before.


notifyChange(value) {
this.props.value.answer = value;
this.dispatchAction(this.props.actionName, {
formElement: this.props.element,
parentFormElement: this.props.parentElement,
questionGroupIndex: this.props.questionGroupIndex,
value: value
});
}

removeValue() {
this.notifyChange(null);
}

renderRemoveButton() {
const hasValue = !_.isNil(this.props.value.getValue()) && this.props.value.getValue() !== '';
if (hasValue) {
return (
<TouchableNativeFeedback onPress={() => this.removeValue()}
background={TouchableNativeFeedback.SelectableBackgroundBorderless()}
useForeground>
<Icon name="backspace"
style={{marginLeft: 8, fontSize: 20, color: Colors.AccentColor}}/>
</TouchableNativeFeedback>
);
}
return null;
}

renderFormElement() {
const hasValue = !_.isNil(this.props.value.getValue()) && this.props.value.getValue() !== '';

return (
<View>
<FormElementLabelWithDocumentation element={this.props.element}/>
<View style={{flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center'}}>
<TouchableNativeFeedback onPress={() => this.openQRScanner()}
background={TouchableNativeFeedback.SelectableBackground()}>
<View style={styles.qrInputContainer}>
<Icon name="qrcode-scan"
style={[styles.qrIcon, {color: hasValue ? Colors.AccentColor : Colors.InputBorderNormal}]}/>
<Text style={[
styles.qrText,
{
color: _.isNil(this.props.validationResult) ?
(hasValue ? Colors.InputNormal : Colors.InputBorderNormal) :
Colors.ValidationError,
fontStyle: hasValue ? 'normal' : 'italic'
}
]}>
{this.displayValue()}
</Text>
</View>
</TouchableNativeFeedback>
{this.renderRemoveButton()}
</View>
<ValidationErrorMessage validationResult={this.props.validationResult}/>

<Modal
visible={this.state.showQRScanner}
animationType="slide"
onRequestClose={() => this.onQRRead(null)}>
<QRScanner onRead={(qrValue) => this.onQRRead(qrValue)} />
</Modal>
</View>
Comment on lines +104 to +110
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use i18n strings inside QRScanner

QRScanner hardcodes English (“Scan QR Code”, instruction, error, “Close”). Wire the new translation keys via props or context.

I can send a small patch to QRScanner.js replacing literals with this.I18n.t('scanQRCodeTitle' | 'pointCameraAtQR' | 'cameraPermissionError') and reuse existing “closeModal” key. Want me to draft it?

🤖 Prompt for AI Agents
In packages/openchs-android/src/views/form/formElement/QRFormElement.js around
lines 104 to 110, QRScanner is used but currently relies on hardcoded English
strings; pass localized strings into QRScanner via props (e.g., title,
instruction, errorMessage, closeLabel) by calling I18n.t with the existing keys
('scanQRCodeTitle', 'pointCameraAtQR', 'cameraPermissionError', and reuse
'closeModal'), and update the QRScanner invocation to supply those props so the
component renders translated text instead of literals.

);
}
}

const styles = StyleSheet.create({
qrInputContainer: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderColor: Colors.InputBorderNormal,
borderRadius: 4,
paddingHorizontal: 12,
paddingVertical: 8,
backgroundColor: Colors.GreyContentBackground,
flex: 1,
minHeight: 44
},
qrIcon: {
fontSize: 20,
marginRight: 8
},
qrText: {
fontSize: Fonts.Large,
flex: 1
}
});

export default QRFormElement;
Loading