Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
// - the code between BEGIN EXTRA CODE and END EXTRA CODE
// Other code you write will be lost the next time you deploy the project.
import { Base64 } from "js-base64";
import RNBlobUtil from "react-native-blob-util";

// BEGIN EXTRA CODE
// END EXTRA CODE
Expand All @@ -30,28 +29,7 @@ export async function Base64DecodeToImage(base64: string, image: mendix.lib.MxOb
// Native platform
if (navigator && navigator.product === "ReactNative") {
try {
// Remove data URI prefix if present (e.g., "data:image/png;base64,")
let cleanBase64 = base64;
if (base64.includes(",")) {
cleanBase64 = base64.split(",")[1];
}

// Remove any whitespace/newlines
cleanBase64 = cleanBase64.replace(/\s/g, "");

// Validate base64 format
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(cleanBase64)) {
throw new Error("Invalid base64 format");
}

// Create a temporary file path
const tempPath = `${RNBlobUtil.fs.dirs.CacheDir}/temp_image_${Date.now()}.png`;

// Write Base64 data to a temporary file
await RNBlobUtil.fs.writeFile(tempPath, cleanBase64, "base64");

// Fetch the file as a blob
const res = await fetch(`file://${tempPath}`);
const res = await fetch(base64);
const blob = await res.blob();

return new Promise((resolve, reject) => {
Expand All @@ -61,11 +39,9 @@ export async function Base64DecodeToImage(base64: string, image: mendix.lib.MxOb
{},
blob,
() => {
RNBlobUtil.fs.unlink(tempPath).catch(e => console.info("Temp file cleanup failed:", e));
resolve(true);
},
error => {
RNBlobUtil.fs.unlink(tempPath).catch(e => console.info("Temp file cleanup failed:", e));
reject(error);
}
);
Expand Down
10 changes: 10 additions & 0 deletions packages/pluggableWidgets/signature-native/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Added

- Added direct image upload to System.Image object using the `imageSource` property with `allowUpload` enabled
- Added optional `hasSignatureAttribute` Boolean attribute to track whether a signature has been captured or cleared

### Changed

- Renamed `onSave` action to `onSignEnd` to match web signature widget naming convention
- Removed `imageAttribute` property in favor of unified `imageSource` property

## [2.3.0] - 2025-7-7

- Updated react-native-webview from version v13.12.5 to latest
Expand Down
2 changes: 1 addition & 1 deletion packages/pluggableWidgets/signature-native/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "signature-native",
"widgetName": "Signature",
"version": "2.3.0",
"version": "2.4.0",
"license": "Apache-2.0",
"repository": {
"type": "git",
Expand Down
30 changes: 25 additions & 5 deletions packages/pluggableWidgets/signature-native/src/Signature.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import { SignatureStyle, defaultSignatureStyle, webStyles } from "./ui/Styles";

export type Props = SignatureProps<SignatureStyle>;

async function dataUriToFile(dataUri: string): Promise<File> {
const response = await fetch(dataUri);
const blob = await response.blob();
return new File([blob], `signature_${Date.now()}.png`, { type: "image/png", lastModified: Date.now() });
}

export function Signature(props: Props): ReactElement {
const ref = useRef<SignatureViewRef>(null);
const styles = mergeNativeStyles(defaultSignatureStyle, props.style);
Expand All @@ -28,11 +34,22 @@ export function Signature(props: Props): ReactElement {
const buttonCaptionSave = props.buttonCaptionSave?.value ?? "Save";

const handleSignature = useCallback(
(base64signature: string): void => {
props.imageAttribute.setValue(base64signature);
executeAction(props.onSave);
async (dataUri: string): Promise<void> => {
try {
/*
if (props.imageSource.status !== "available" || props.imageSource.readOnly) {
return;
} This check needs to add once the EditableImageValue<NativeImage> is released from widget tools
*/
const blob = await dataUriToFile(dataUri);
(props.imageSource as any)?.setValue(blob); // as any hack needs to remove once the EditableImageValue<NativeImage> is released from widget tools
props.hasSignatureAttribute?.setValue(true);
executeAction(props.onSignEndAction);
} catch (error) {
console.error("Signature: failed to save image", error);
}
},
[props.imageAttribute, props.onSave]
[props.imageSource, props.hasSignatureAttribute, props.onSignEndAction]
);

return (
Expand All @@ -43,7 +60,10 @@ export function Signature(props: Props): ReactElement {
onEmpty={() => executeAction(props.onEmpty)}
onEnd={() => executeAction(props.onEnd)}
onOK={handleSignature}
onClear={() => executeAction(props.onClear)}
onClear={() => {
props.hasSignatureAttribute?.setValue(false);
executeAction(props.onClear);
}}
webStyle={webStyles}
{...signatureProps}
/>
Expand Down
31 changes: 16 additions & 15 deletions packages/pluggableWidgets/signature-native/src/Signature.xml
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<widget id="com.mendix.widget.native.signature.Signature" supportedPlatform="Native" needsEntityContext="true"
offlineCapable="true" pluginWidget="true" xmlns="http://www.mendix.com/widget/1.0/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.mendix.com/widget/1.0/ ../../../../node_modules/mendix/custom_widget.xsd">
<?xml version="1.0" encoding="utf-8" ?>
<widget id="com.mendix.widget.native.signature.Signature" supportedPlatform="Native" needsEntityContext="true" offlineCapable="true" pluginWidget="true" xmlns="http://www.mendix.com/widget/1.0/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mendix.com/widget/1.0/ ../../../../node_modules/mendix/custom_widget.xsd">
<name>Signature</name>
<description>Display signature.</description>
<icon>
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
</icon>
<properties>
<propertyGroup caption="General">
<propertyGroup caption="General">
<property key="imageAttribute" type="attribute">
<caption>Attribute</caption>
<description/>
<propertyGroup caption="Data source">
<property key="imageSource" type="image" required="true" allowUpload="true">
<caption>Image</caption>
<description>The System.Image (or specialization) object the signature will be saved into. Commit the object in your On sign end handler to persist it.</description>
</property>
<property key="hasSignatureAttribute" type="attribute" required="false">
<caption>Has signature</caption>
<description>Optional Boolean attribute that is set to true when a signature has been captured and false when cleared. Useful for validation and conditional visibility.</description>
<attributeTypes>
<attributeType name="String"/>
<attributeType name="Boolean" />
</attributeTypes>
</property>
</propertyGroup>
Expand All @@ -35,9 +36,9 @@
<caption>On clear</caption>
<description>Fired when the clear button is clicked.</description>
</property>
<property key="onSave" type="action" required="false">
<caption>On save</caption>
<description>Fired when the save button is clicked.</description>
<property key="onSignEndAction" type="action" required="false">
<caption>On sign end</caption>
<description>Fired after the signature image has been saved to the image object. Commit the object here to persist it.</description>
</property>
<property key="onEnd" type="action" required="false">
<caption>On end</caption>
Expand All @@ -49,7 +50,7 @@
</property>
</propertyGroup>
<propertyGroup caption="Common">
<systemProperty key="Name"/>
<systemProperty key="Name" />
</propertyGroup>
</properties>
</widget>
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import SignatureScreen from "react-native-signature-canvas";
import { fireEvent, render } from "@testing-library/react-native";

import { Signature, Props } from "../Signature";
import { actionValue, dynamicValue, EditableValueBuilder } from "@mendix/piw-utils-internal";
import { actionValue, dynamicValue } from "@mendix/piw-utils-internal";

// Mock fetch for dataUriToFile
global.fetch = jest.fn(() =>
Promise.resolve({
blob: () => Promise.resolve(new Blob())
})
) as jest.Mock;

jest.mock("react-native", () => {
const RN = jest.requireActual("react-native");
Expand Down Expand Up @@ -40,10 +47,14 @@ jest.mock("react-native/Libraries/Components/Touchable/TouchableNativeFeedback",
return TouchableNativeFeedback;
});

const mockImageSource: any = {
setValue: jest.fn()
};

const defaultProps: Props = {
name: "signature-test",
style: [],
imageAttribute: new EditableValueBuilder<string>().withValue("").build(),
imageSource: mockImageSource,
buttonCaptionClear: dynamicValue<string>("Clear"),
buttonCaptionSave: dynamicValue<string>("Save")
};
Expand Down Expand Up @@ -88,13 +99,17 @@ describe("Signature Android", () => {
fireEvent(canvas, "onClear");
expect(onClearAction.execute).toHaveBeenCalledTimes(1);
});
it("on save", () => {
const onSaveAction = actionValue();
const component = render(<Signature {...defaultProps} onSave={onSaveAction} />);
it("on sign end", async () => {
const onSignEndAction = actionValue();
const component = render(<Signature {...defaultProps} onSignEndAction={onSignEndAction} />);
const canvas = component.UNSAFE_getByType(SignatureScreen);

fireEvent(canvas, "onOK");
expect(onSaveAction.execute).toHaveBeenCalledTimes(1);
fireEvent(canvas, "onOK", "data:image/png;base64,test");

// Wait for async operations
await new Promise(resolve => setTimeout(resolve, 0));

expect(onSignEndAction.execute).toHaveBeenCalledTimes(1);
});
it("on empty", () => {
const onEmptyAction = actionValue();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ import SignatureScreen from "react-native-signature-canvas";
import { fireEvent, render } from "@testing-library/react-native";

import { Signature, Props } from "../Signature";
import { actionValue, dynamicValue, EditableValueBuilder } from "@mendix/piw-utils-internal";

import { actionValue, dynamicValue } from "@mendix/piw-utils-internal";

// Mock fetch for dataUriToFile
global.fetch = jest.fn(() =>
Promise.resolve({
blob: () => Promise.resolve(new Blob())
})
) as jest.Mock;

jest.mock("react-native", () => {
const RN = jest.requireActual("react-native");
Expand All @@ -16,10 +24,14 @@ jest.mock("react-native/Libraries/Utilities/Platform", () => {
return Platform;
});

const mockImageSource: any = {
setValue: jest.fn()
};

const defaultProps: Props = {
name: "signature-test",
style: [],
imageAttribute: new EditableValueBuilder<string>().withValue("").build(),
imageSource: mockImageSource,
buttonCaptionClear: dynamicValue<string>("Clear"),
buttonCaptionSave: dynamicValue<string>("Save")
};
Expand Down Expand Up @@ -74,13 +86,17 @@ describe("Signature iOS", () => {
fireEvent(canvas, "onClear");
expect(onClearAction.execute).toHaveBeenCalledTimes(1);
});
it("on save", () => {
const onSaveAction = actionValue();
const component = render(<Signature {...defaultProps} onSave={onSaveAction} />);
it("on sign end", async () => {
const onSignEndAction = actionValue();
const component = render(<Signature {...defaultProps} onSignEndAction={onSignEndAction} />);
const canvas = component.UNSAFE_getByType(SignatureScreen);

fireEvent(canvas, "onOK");
expect(onSaveAction.execute).toHaveBeenCalledTimes(1);
await fireEvent(canvas, "onOK", "data:image/png;base64,test");

// Wait for async operations
await new Promise(resolve => setTimeout(resolve, 0));

expect(onSignEndAction.execute).toHaveBeenCalledTimes(1);
});
it("on empty", () => {
const onEmptyAction = actionValue();
Expand Down
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="Signature" version="2.3.0" xmlns="http://www.mendix.com/clientModule/1.0/">
<clientModule name="Signature" version="2.4.0" xmlns="http://www.mendix.com/clientModule/1.0/">
<widgetFiles>
<widgetFile path="Signature.xml" />
</widgetFiles>
Expand Down
Loading
Loading