Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion configs/e2e/native_dependencies.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"react-native-system-navigation-bar": "2.6.3",
"react-native-video": "6.10.0",
"@react-native-async-storage/async-storage": "2.0.0",
"react-native-camera": "3.40.0",
"react-native-vision-camera": "4.7.1",
"react-native-view-shot": "4.0.3",
"react-native-blob-util": "0.21.2",
"react-native-file-viewer": "2.1.5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

- We migrated to react-native-vision-camera from react-native-camera.

## [4.1.0] - 2024-12-3

### Changed
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "barcode-scanner-native",
"widgetName": "BarcodeScanner",
"version": "4.1.0",
"version": "4.2.0",
"license": "Apache-2.0",
"repository": {
"type": "git",
Expand All @@ -21,9 +21,8 @@
"dependencies": {
"@mendix/piw-native-utils-internal": "*",
"@mendix/piw-utils-internal": "*",
"deprecated-react-native-prop-types": "^4.0.0",
"react-native-barcode-mask": "^1.2.4",
"react-native-camera": "3.40.0"
"react-native-vision-camera": "4.7.1"
},
"devDependencies": {
"@mendix/pluggable-widgets-tools": "~10.0.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { flattenStyles } from "@mendix/piw-native-utils-internal";
import { ValueStatus } from "mendix";
import { Component, createElement } from "react";
import { createElement, ReactElement, useCallback, useMemo, useRef } from "react";
import { View } from "react-native";
import { RNCamera } from "react-native-camera";
import { Camera, useCodeScanner, Code, useCameraDevice } from "react-native-vision-camera";
import BarcodeMask from "react-native-barcode-mask";

import { BarcodeScannerProps } from "../typings/BarcodeScannerProps";
Expand All @@ -11,55 +11,68 @@ import { executeAction } from "@mendix/piw-utils-internal";

export type Props = BarcodeScannerProps<BarcodeScannerStyle>;

export class BarcodeScanner extends Component<Props> {
private readonly styles = flattenStyles(defaultBarcodeScannerStyle, this.props.style);
private readonly onBarCodeReadHandler = throttle(this.onBarCodeRead.bind(this), 2000);
export function BarcodeScanner(props: Props): ReactElement {
const device = useCameraDevice("back");

render(): JSX.Element {
return (
<View style={this.styles.container}>
<RNCamera
testID={this.props.name}
style={{ flex: 1, justifyContent: "center", alignItems: "center" }}
captureAudio={false}
onBarCodeRead={this.onBarCodeReadHandler}
>
{this.props.showMask && (
<BarcodeMask
edgeColor={this.styles.mask.color}
width={this.styles.mask.width}
height={this.styles.mask.height}
backgroundColor={this.styles.mask.backgroundColor}
showAnimatedLine={this.props.showAnimatedLine}
/>
)}
</RNCamera>
</View>
);
}
const styles = useMemo(() => flattenStyles(defaultBarcodeScannerStyle, props.style), [props.style]);

private onBarCodeRead(event: { data: string }): void {
if (this.props.barcode.status !== ValueStatus.Available || !event.data) {
return;
}
// Ref to track the lock state
const isLockedRef = useRef(false);

if (event.data !== this.props.barcode.value) {
this.props.barcode.setValue(event.data);
}
const onCodeScanned = useCallback(
(codes: Code[]) => {
// Block if still in cooldown
if (isLockedRef.current) {
return;
}

executeAction(this.props.onDetect);
}
}
if (props.barcode.status !== ValueStatus.Available || codes.length === 0 || !codes[0].value) {
return;
}

const { value } = codes[0];
if (value !== props.barcode.value) {
props.barcode.setValue(value);
}

executeAction(props.onDetect);

export function throttle<F extends (...params: any[]) => void>(fn: F, threshold: number): F {
let wait = false;
return function invokeFn(this: any, ...args: any[]) {
if (!wait) {
fn(...args);
wait = true;
// Lock further scans for 2 seconds
isLockedRef.current = true;
setTimeout(() => {
wait = false;
}, threshold);
}
} as F;
isLockedRef.current = false;
}, 2000);
},
[props.barcode, props.onDetect]
);

const codeScanner = useCodeScanner({
codeTypes: ["ean-13", "qr", "aztec", "codabar", "code-128", "data-matrix"],
onCodeScanned
});

return (
<View style={styles.container}>
{device && (
<Camera
testID={props.name}
style={{ flex: 1, justifyContent: "center", alignItems: "center" }}
audio={false}
isActive
device={device}
codeScanner={codeScanner}
>
{props.showMask && (
<BarcodeMask
edgeColor={styles.mask.color}
width={styles.mask.width}
height={styles.mask.height}
backgroundColor={styles.mask.backgroundColor}
showAnimatedLine={props.showAnimatedLine}
/>
)}
</Camera>
)}
</View>
);
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
// __tests__/BarcodeScanner.spec.tsx
import { actionValue, EditableValueBuilder } from "@mendix/piw-utils-internal";
import { render } from "@testing-library/react-native";
import { createElement } from "react";
import { fireEvent, render, RenderAPI } from "@testing-library/react-native";
import { View } from "react-native";
import { BarcodeScanner, Props } from "../BarcodeScanner";

import { BarcodeScanner, Props, throttle } from "../BarcodeScanner";
import { RNCamera } from "./__mocks__/RNCamera";
let mockOnCodeScanned: ((codes: Array<{ value: string }>) => void) | undefined;

jest.mock("react-native-camera", () => jest.requireActual("./__mocks__/RNCamera"));
jest.mock("react-native-vision-camera", () => ({
Camera: ({ children, ...props }: any) => <View {...props}>{children}</View>,
useCameraDevice: () => "mock-device",
useCodeScanner: (options: any) => {
mockOnCodeScanned = options.onCodeScanned;
return "mockCodeScanner";
}
}));

jest.useFakeTimers();
jest.mock("react-native-barcode-mask", () => "BarcodeMask");

describe("BarcodeScanner", () => {
let defaultProps: Props;

beforeEach(() => {
jest.useFakeTimers();
defaultProps = {
showAnimatedLine: false,
showMask: false,
Expand All @@ -22,84 +32,41 @@ describe("BarcodeScanner", () => {
};
});

afterEach(() => {
jest.clearAllTimers();
});

it("renders", () => {
const component = render(<BarcodeScanner {...defaultProps} />);

expect(component.toJSON()).toMatchSnapshot();
});

it("renders with mask", () => {
const component = render(<BarcodeScanner {...defaultProps} showMask />);

expect(component.toJSON()).toMatchSnapshot();
});

it("renders with mask with animated line", () => {
const component = render(<BarcodeScanner {...defaultProps} showMask showAnimatedLine />);

expect(component.toJSON()).toMatchSnapshot();
});

it("sets a value and executes the on detect action when a new barcode is scanned", async () => {
it("sets a value and executes the onDetect action when a new barcode is scanned", () => {
const onDetectAction = actionValue();
const component = render(<BarcodeScanner {...defaultProps} onDetect={onDetectAction} />);
render(<BarcodeScanner {...defaultProps} onDetect={onDetectAction} />);

detectBarcode(component, "value");
// Simulate scanning
mockOnCodeScanned?.([{ value: "value" }]);
jest.advanceTimersByTime(2000);

expect(defaultProps.barcode.setValue).toHaveBeenCalledWith("value");
expect(onDetectAction.execute).toHaveBeenCalledTimes(1);

detectBarcode(component, "value1");
jest.advanceTimersByTime(100);
detectBarcode(component, "value2");
// Events are not fired immediately by testing-library, so firing with 1999 will be already too late for the previous action
jest.advanceTimersByTime(1800);
detectBarcode(component, "value3");

// Another scan
mockOnCodeScanned?.([{ value: "value1" }]);
jest.advanceTimersByTime(2000);

expect(defaultProps.barcode.setValue).toHaveBeenCalledWith("value1");
expect(onDetectAction.execute).toHaveBeenCalledTimes(2);

detectBarcode(component, "value2");
detectBarcode(component, "value3");
detectBarcode(component, "value4");

jest.advanceTimersByTime(2000);

expect(defaultProps.barcode.setValue).toHaveBeenCalledWith("value2");
expect(onDetectAction.execute).toHaveBeenCalledTimes(3);
});

describe("throttling", () => {
const func: (...args: any) => void = jest.fn();
const args = ["argument", { prop: "arguments" }];

it("should execute function in correct time intervals", () => {
const throttleFunc = throttle(func, 100);

throttleFunc(...args);
expect(func).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(100);
throttleFunc(...args);
jest.advanceTimersByTime(99);
throttleFunc(...args);
throttleFunc(...args);
expect(func).toHaveBeenCalledTimes(2);
jest.advanceTimersByTime(100);
expect(func).toHaveBeenCalledTimes(2);
});
});
});

function detectBarcode(component: RenderAPI, barcode: string): void {
fireEvent(component.UNSAFE_getByType(RNCamera), "barCodeRead", {
data: barcode,
type: "qr",
bounds: [
{ x: "", y: "" },
{ x: "", y: "" }
]
});
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@ exports[`BarcodeScanner renders 1`] = `
}
>
<View
audio={false}
codeScanner="mockCodeScanner"
device="mock-device"
isActive={true}
style={
{
"alignItems": "center",
"flex": 1,
"justifyContent": "center",
}
}
testID="barcode-scanner-test"
/>
</View>
`;
Expand All @@ -33,14 +38,25 @@ exports[`BarcodeScanner renders with mask 1`] = `
}
>
<View
audio={false}
codeScanner="mockCodeScanner"
device="mock-device"
isActive={true}
style={
{
"alignItems": "center",
"flex": 1,
"justifyContent": "center",
}
}
/>
testID="barcode-scanner-test"
>
<BarcodeMask
backgroundColor="rgba(0, 0, 0, 0.6)"
edgeColor="#62B1F6"
showAnimatedLine={false}
/>
</View>
</View>
`;

Expand All @@ -55,13 +71,24 @@ exports[`BarcodeScanner renders with mask with animated line 1`] = `
}
>
<View
audio={false}
codeScanner="mockCodeScanner"
device="mock-device"
isActive={true}
style={
{
"alignItems": "center",
"flex": 1,
"justifyContent": "center",
}
}
/>
testID="barcode-scanner-test"
>
<BarcodeMask
backgroundColor="rgba(0, 0, 0, 0.6)"
edgeColor="#62B1F6"
showAnimatedLine={true}
/>
</View>
</View>
`;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<package xmlns="http://www.mendix.com/package/1.0/">
<clientModule name="BarcodeScanner" version="4.1.0" xmlns="http://www.mendix.com/clientModule/1.0/">
<clientModule name="BarcodeScanner" version="4.2.0" xmlns="http://www.mendix.com/clientModule/1.0/">
<widgetFiles>
<widgetFile path="BarcodeScanner.xml" />
</widgetFiles>
Expand Down
Loading