diff --git a/packages/pluggableWidgets/signature-native/CHANGELOG.md b/packages/pluggableWidgets/signature-native/CHANGELOG.md index 2f1750692..95d116a1a 100644 --- a/packages/pluggableWidgets/signature-native/CHANGELOG.md +++ b/packages/pluggableWidgets/signature-native/CHANGELOG.md @@ -6,10 +6,17 @@ 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 + ## [2.4.0] - 2026-5-15 ### Changed +- Renamed `onSave` action to `onSignEnd` to match web signature widget naming convention +- Removed `imageAttribute` property in favor of unified `imageSource` property - Updated react-native-webview from version v13.13.2 to v13.16.1. ## [2.3.0] - 2025-7-7 diff --git a/packages/pluggableWidgets/signature-native/MIGRATION.md b/packages/pluggableWidgets/signature-native/MIGRATION.md new file mode 100644 index 000000000..213241823 --- /dev/null +++ b/packages/pluggableWidgets/signature-native/MIGRATION.md @@ -0,0 +1,101 @@ +# Signature (Native) — Migration Guide + +This document covers breaking changes and migration steps when upgrading the Signature native widget. + +--- + +## Migrating to v2.4.0 — Direct Image Save Mode + +### What Changed + +Previously, you needed to store the base64-encoded signature string in a String attribute, then call a nanoflow that used the **base64DecodeToImage** action to convert that string into an image, and then commit the object before continuing with the rest of your flow. With this update, that overhead is removed — the widget now handles the base64-to-image conversion internally. + +1. The widget now saves the signature directly into an entity object of generalization `System.Image`, instead of storing a base64-encoded string in a String attribute. +2. The **On save** event has been renamed to **On sign end** under the **Events** tab. +3. A new optional property, **Has signature**, has been added. + +--- + +### New Required Property: Image + +You must assign an object of an entity with generalization `System.Image` to the **Image** property. The widget populates this object after the user taps **Save**. You are responsible for committing it inside the **On sign end** action. + +**Before (pre-v2.4.0):** A String attribute holding a base64-encoded PNG was assigned to the widget. + +**After (v2.4.0):** A `System.Image` object reference is assigned to the **Image** property. + +**Steps:** + +1. In your domain model, ensure you have an entity with generalization `System.Image`. +2. In your page, create or retrieve an instance of that entity and pass it as the data source of the widget. +3. In the widget's **Image** property, select the image type as Dynamic and assign the object to the entity property. + +--- + +### New Optional Property: Has Signature + +A Boolean attribute can be linked via the **Has signature** property. The widget automatically sets this attribute to `true` when a signature is captured and to `false` when the canvas is cleared. You can use it for: + +- Conditional visibility (for example, showing a "Signed" indicator) +- Validation in nanoflows before form submission + +This property is optional — existing configurations work without it. + +--- + +### Event Handler Changes: on sign end + +The **On save** event handler has been renamed to **On sign end**. You can assign the nanoflow that was previously used for **On save** to this action. Make sure to: + +1. Remove the **base64DecodeToImage** action from the nanoflow, as it is no longer needed and not removing it may produce some errors. +2. Keep the **Commit object** activity in the nanoflow. +3. Keep any subsequent actions in the nanoflow, such as **Synchronize** or **Close page**. + +> **Important:** If you do not commit the object in **On sign end**, the signature will not be persisted. The widget populates the in-memory object, but you are responsible for committing it. + +--- + +## Quick Checklist + +- [ ] An entity with generalization `System.Image` exists in the domain model +- [ ] The widget's **Image** property is set to an instance of that entity +- [ ] The **On sign end** action commits the image object; assign the nanoflow that was previously used for **On save** +- [ ] The **base64DecodeToImage** action has been removed from the nanoflow +- [ ] Optional: A Boolean attribute is wired to **Has signature** for validation + +--- + +## Example: Migrating from Studio Pro 10.24 + +The following is a real-world walkthrough of migrating a Mendix app from Studio Pro 10.24 to the version that includes this signature widget update. + +1. **Resolve migration errors** — Opening the app in the new Studio Pro version showed two errors about outdated modules: **NanoflowCommons** and **Native Mobile Resources**. These are unrelated to the signature widget. Updating both modules resolved the errors. + + ![Update NanoflowCommons and Native Mobile Resources](assets/migration/Update-NC-NMR.png) + +2. **Update the widget** — An **Update widget** error appeared. Click **Update** (or **Update all widgets**) to apply the new version. + + ![Update Widget](assets/migration/Update-Widget.png) + +3. **Fix the required Image property** — After updating, a new validation error appeared: _Property Image is required_. + + ![Property Image Required Error](assets/migration/PropertyImageRequiredError.png) + + To fix this, open the signature widget configuration and select the appropriate `System.Image` object reference in the **Image** property: + + ![Assign Object to Image Property](assets/migration/AssignObjectToImageProperty.png) + +4. **Update the On sign end action** — In the **Events** tab, assign the nanoflow that was previously used for **On save** to the **On sign end** action. + + ![Assign Nanoflow to On Sign End](assets/migration/AssignNanoflowToOnSignEnd.png) + + Next, open that nanoflow and remove the **base64DecodeToImage** action. Ensure the nanoflow still has a **Commit object** activity, followed by any other required actions such as **Synchronize** or **Close page** etc. + + ![Nanoflow with Removed Base64DecodeToImage](assets/migration/NanoflowWithRemovedBase64DecodeToImageAndCommitObject.png) + +--- + +## Known Limitations + +- The signature widget currently does not work on Android due to a known issue in React Native. +- With this update, the previous String attribute property has been removed. The widget no longer stores a base64-encoded string. If you were using that base64 value as input to any API, integration, or export, those values will be empty going forward, as nothing is written to that attribute anymore. diff --git a/packages/pluggableWidgets/signature-native/assets/migration/AssignNanoflowToOnSignEnd.png b/packages/pluggableWidgets/signature-native/assets/migration/AssignNanoflowToOnSignEnd.png new file mode 100644 index 000000000..eb180c4f2 Binary files /dev/null and b/packages/pluggableWidgets/signature-native/assets/migration/AssignNanoflowToOnSignEnd.png differ diff --git a/packages/pluggableWidgets/signature-native/assets/migration/AssignObjectToImageProperty.png b/packages/pluggableWidgets/signature-native/assets/migration/AssignObjectToImageProperty.png new file mode 100644 index 000000000..9450f4fca Binary files /dev/null and b/packages/pluggableWidgets/signature-native/assets/migration/AssignObjectToImageProperty.png differ diff --git a/packages/pluggableWidgets/signature-native/assets/migration/NanoflowWithRemovedBase64DecodeToImageAndCommitObject.png b/packages/pluggableWidgets/signature-native/assets/migration/NanoflowWithRemovedBase64DecodeToImageAndCommitObject.png new file mode 100644 index 000000000..42229ec14 Binary files /dev/null and b/packages/pluggableWidgets/signature-native/assets/migration/NanoflowWithRemovedBase64DecodeToImageAndCommitObject.png differ diff --git a/packages/pluggableWidgets/signature-native/assets/migration/PropertyImageRequiredError.png b/packages/pluggableWidgets/signature-native/assets/migration/PropertyImageRequiredError.png new file mode 100644 index 000000000..946532781 Binary files /dev/null and b/packages/pluggableWidgets/signature-native/assets/migration/PropertyImageRequiredError.png differ diff --git a/packages/pluggableWidgets/signature-native/assets/migration/Update-NC-NMR.png b/packages/pluggableWidgets/signature-native/assets/migration/Update-NC-NMR.png new file mode 100644 index 000000000..bd2369a6e Binary files /dev/null and b/packages/pluggableWidgets/signature-native/assets/migration/Update-NC-NMR.png differ diff --git a/packages/pluggableWidgets/signature-native/assets/migration/Update-Widget.png b/packages/pluggableWidgets/signature-native/assets/migration/Update-Widget.png new file mode 100644 index 000000000..043fa67e4 Binary files /dev/null and b/packages/pluggableWidgets/signature-native/assets/migration/Update-Widget.png differ diff --git a/packages/pluggableWidgets/signature-native/package.json b/packages/pluggableWidgets/signature-native/package.json index 99c1e3778..f352206b7 100644 --- a/packages/pluggableWidgets/signature-native/package.json +++ b/packages/pluggableWidgets/signature-native/package.json @@ -1,7 +1,7 @@ { "name": "signature-native", "widgetName": "Signature", - "version": "2.4.0", + "version": "2.5.0", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/pluggableWidgets/signature-native/src/Signature.editorConfig.ts b/packages/pluggableWidgets/signature-native/src/Signature.editorConfig.ts index aa4f68ac0..c51334f9f 100644 --- a/packages/pluggableWidgets/signature-native/src/Signature.editorConfig.ts +++ b/packages/pluggableWidgets/signature-native/src/Signature.editorConfig.ts @@ -1,6 +1,21 @@ import { StructurePreviewProps, topBar } from "@mendix/piw-utils-internal"; +import { Problem } from "@mendix/pluggable-widgets-tools"; import { SignaturePreviewProps } from "../typings/SignatureProps"; export const getPreview = (_: SignaturePreviewProps, isDarkMode: boolean): StructurePreviewProps => topBar("Signature", [], isDarkMode); + +export function check(values: SignaturePreviewProps): Problem[] { + const errors: Problem[] = []; + + if (values.imageSource !== null && values.imageSource.type === "static") { + errors.push({ + property: "imageSource", + severity: "error", + message: "Image Source must be a dynamic (entity) image. Static images cannot be uploaded to." + }); + } + + return errors; +} diff --git a/packages/pluggableWidgets/signature-native/src/Signature.tsx b/packages/pluggableWidgets/signature-native/src/Signature.tsx index 02b99ceb1..fa77a962b 100644 --- a/packages/pluggableWidgets/signature-native/src/Signature.tsx +++ b/packages/pluggableWidgets/signature-native/src/Signature.tsx @@ -1,15 +1,63 @@ import { mergeNativeStyles, extractStyles } from "@mendix/pluggable-widgets-tools"; import { executeAction } from "@mendix/piw-utils-internal"; import { ReactElement, useCallback, useRef } from "react"; -import { View, Text } from "react-native"; +import { View, Text, NativeModules } from "react-native"; import SignatureScreen, { SignatureViewRef } from "react-native-signature-canvas"; import { Touchable } from "./components/Touchable"; +import RNBlobUtil from "react-native-blob-util"; import { SignatureProps } from "../typings/SignatureProps"; import { SignatureStyle, defaultSignatureStyle, webStyles } from "./ui/Styles"; export type Props = SignatureProps; +async function dataUriToBlob(base64: string): Promise { + // 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 fileName = `image_${Date.now()}.png`; + const tempPath = `${RNBlobUtil.fs.dirs.CacheDir}/${fileName}`; + + // Write Base64 data to a temporary file + await RNBlobUtil.fs.writeFile(tempPath, cleanBase64, "base64"); + + // Read the file into the native blob store so offline mode works: + // NativeFileBackend.storeFile calls NativeFileSystem.save(blob.data, path) + // and blob.close() — a plain object has no .data getter or .close(), which + // crashes iOS via [NSInvocation invokeWithTarget:]. + const nativeBlob = await NativeModules.MxFileSystem.read(tempPath.replace("file://", "")); + // Normalize: MxFileSystem.read may return 'length' instead of 'size'. + const blobData = { ...(nativeBlob as any) }; + if (blobData.size === undefined && blobData.length !== undefined) { + blobData.size = blobData.length; + } + const blob = new Blob(); + Object.assign(blob, { + data: blobData, + name: fileName, + lastModified: Date.now() + }); + // Set nativePayload so the patched FormData.prototype.append in NativeFileBackend + // replaces the blob value with { uri, name, type } for online uploads. The patch + // reads the third append() argument (fileName) and writes it onto nativePayload.name, + // which FormData.getParts() uses as the Content-Disposition filename. + (blob as any).nativePayload = { uri: `file://${tempPath}`, name: fileName, type: "image/png" }; + const fileBlob = blob as unknown as File; + return fileBlob; +} + export function Signature(props: Props): ReactElement { const ref = useRef(null); const styles = mergeNativeStyles(defaultSignatureStyle, props.style); @@ -28,11 +76,20 @@ 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 => { + try { + if (props.imageSource.readOnly) { + return; + } + const blob = await dataUriToBlob(dataUri); + props.imageSource.setValue(blob); + 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 ( @@ -43,7 +100,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} /> diff --git a/packages/pluggableWidgets/signature-native/src/Signature.xml b/packages/pluggableWidgets/signature-native/src/Signature.xml index aef8609f4..d66a3f783 100644 --- a/packages/pluggableWidgets/signature-native/src/Signature.xml +++ b/packages/pluggableWidgets/signature-native/src/Signature.xml @@ -1,21 +1,22 @@ - - + + Signature Display signature. - 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 - - - Attribute - + + + Image + The System.Image (or specialization) object the signature will be saved into. Commit the object in your On sign end handler to persist it. + + + Has signature + Optional Boolean attribute that is set to true when a signature has been captured and false when cleared. Useful for validation and conditional visibility. - + @@ -35,9 +36,9 @@ On clear Fired when the clear button is clicked. - - On save - Fired when the save button is clicked. + + On sign end + Fired after the signature image has been saved to the image object. Commit the object here to persist it. On end @@ -49,7 +50,7 @@ - + diff --git a/packages/pluggableWidgets/signature-native/src/__tests__/Signature.android.spec.tsx b/packages/pluggableWidgets/signature-native/src/__tests__/Signature.android.spec.tsx index 14440449c..54e101382 100644 --- a/packages/pluggableWidgets/signature-native/src/__tests__/Signature.android.spec.tsx +++ b/packages/pluggableWidgets/signature-native/src/__tests__/Signature.android.spec.tsx @@ -4,11 +4,34 @@ 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"); RN.NativeModules.RNCWebView = { isFileUploadSupported: jest.fn(() => true) }; + RN.NativeModules.MxFileSystem = { + read: jest.fn(() => + Promise.resolve({ + data: "mock-blob-data", + size: 1024, + type: "image/png", + name: "signature.png", + nativePayload: { + uri: "file:///mock/path", + name: "signature.png", + type: "image/png" + }, + close: jest.fn() + }) + ) + }; return RN; }); @@ -45,10 +68,27 @@ jest.mock("react-native/Libraries/Components/Touchable/TouchableNativeFeedback", }; }); +jest.mock("react-native-blob-util", () => ({ + __esModule: true, + default: { + fs: { + dirs: { + CacheDir: "/mock/cache" + }, + writeFile: jest.fn(() => Promise.resolve()), + unlink: jest.fn(() => Promise.resolve()) + } + } +})); + +const mockImageSource: any = { + setValue: jest.fn() +}; + const defaultProps: Props = { name: "signature-test", style: [], - imageAttribute: new EditableValueBuilder().withValue("").build(), + imageSource: mockImageSource, buttonCaptionClear: dynamicValue("Clear"), buttonCaptionSave: dynamicValue("Save") }; @@ -93,13 +133,17 @@ describe("Signature Android", () => { fireEvent(canvas, "onClear"); expect(onClearAction.execute).toHaveBeenCalledTimes(1); }); - it("on save", () => { - const onSaveAction = actionValue(); - const component = render(); + it("on sign end", async () => { + const onSignEndAction = actionValue(); + const component = render(); 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(); diff --git a/packages/pluggableWidgets/signature-native/src/__tests__/Signature.ios.spec.tsx b/packages/pluggableWidgets/signature-native/src/__tests__/Signature.ios.spec.tsx index 1aefbb553..4ca66aa2d 100644 --- a/packages/pluggableWidgets/signature-native/src/__tests__/Signature.ios.spec.tsx +++ b/packages/pluggableWidgets/signature-native/src/__tests__/Signature.ios.spec.tsx @@ -2,11 +2,35 @@ 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"); RN.NativeModules.RNCWebView = { isFileUploadSupported: jest.fn(() => true) }; + RN.NativeModules.MxFileSystem = { + read: jest.fn(() => + Promise.resolve({ + data: "mock-blob-data", + size: 1024, + type: "image/png", + name: "signature.png", + nativePayload: { + uri: "file:///mock/path", + name: "signature.png", + type: "image/png" + }, + close: jest.fn() + }) + ) + }; return RN; }); @@ -17,10 +41,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().withValue("").build(), + imageSource: mockImageSource, buttonCaptionClear: dynamicValue("Clear"), buttonCaptionSave: dynamicValue("Save") }; @@ -35,6 +63,19 @@ jest.mock("react-native-webview", () => { return { WebView }; }); +jest.mock("react-native-blob-util", () => ({ + __esModule: true, + default: { + fs: { + dirs: { + CacheDir: "/mock/cache" + }, + writeFile: jest.fn(() => Promise.resolve()), + unlink: jest.fn(() => Promise.resolve()) + } + } +})); + describe("Signature iOS", () => { it("renders with default styles", () => { const component = render(); @@ -75,13 +116,17 @@ describe("Signature iOS", () => { fireEvent(canvas, "onClear"); expect(onClearAction.execute).toHaveBeenCalledTimes(1); }); - it("on save", () => { - const onSaveAction = actionValue(); - const component = render(); + it("on sign end", async () => { + const onSignEndAction = actionValue(); + const component = render(); 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(); diff --git a/packages/pluggableWidgets/signature-native/src/package.xml b/packages/pluggableWidgets/signature-native/src/package.xml index 457e35131..5254fd20c 100644 --- a/packages/pluggableWidgets/signature-native/src/package.xml +++ b/packages/pluggableWidgets/signature-native/src/package.xml @@ -1,6 +1,6 @@ - + diff --git a/packages/pluggableWidgets/signature-native/typings/SignatureProps.d.ts b/packages/pluggableWidgets/signature-native/typings/SignatureProps.d.ts index 1742ee217..6ce80ce41 100644 --- a/packages/pluggableWidgets/signature-native/typings/SignatureProps.d.ts +++ b/packages/pluggableWidgets/signature-native/typings/SignatureProps.d.ts @@ -3,17 +3,18 @@ * WARNING: All changes made to this file will be overwritten * @author Mendix Widgets Framework Team */ -import { ActionValue, DynamicValue, EditableValue } from "mendix"; +import { ActionValue, DynamicValue, EditableImageValue, EditableValue, NativeImage } from "mendix"; import { CSSProperties } from "react"; export interface SignatureProps