Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
02e5d4f
fix: signature direct image implementation with allowUpload property
saurabhchavan1711 Apr 23, 2026
25b967c
fix: changelog and updated version
saurabhchavan1711 Apr 23, 2026
9a345ed
fix: removed temp file logic from signature
saurabhchavan1711 Apr 23, 2026
7c79e26
fix: removed temp file logic from base64decodetoimage action
saurabhchavan1711 Apr 23, 2026
f2f4d02
fix: removed rn blob util import from base64decodetoimage
saurabhchavan1711 Apr 24, 2026
695c6ab
fix: added new option hasSignatureAttribute to signature widget
saurabhchavan1711 Apr 24, 2026
0db0793
fix: added comments to be addressed once next widget tools release
saurabhchavan1711 Apr 24, 2026
a8d71a9
fix: renamed onsave action to on signend just to match with web
saurabhchavan1711 Apr 24, 2026
8623bb4
fix: removal of save mode and imageattribute to keep imageSource only…
saurabhchavan1711 Apr 24, 2026
2b5e4d4
fix: signature name unknown fix for savedocument method
saurabhchavan1711 Apr 27, 2026
bc32f86
fix: changelog updated
saurabhchavan1711 Apr 27, 2026
5b7749d
fix: updated unit testcases
saurabhchavan1711 Apr 27, 2026
f31364c
fix: add migration document and its related images
saurabhchavan1711 May 4, 2026
0dc8b62
Merge remote-tracking branch 'origin' into moo/MOO-2069-Add-Direct-Im…
saurabhchavan1711 May 4, 2026
f88ec13
fix: update version
saurabhchavan1711 May 4, 2026
13e19b0
fix: update migration doc with image type
saurabhchavan1711 May 5, 2026
a1b4354
Merge remote-tracking branch 'origin' into moo/MOO-2069-Add-Direct-Im…
saurabhchavan1711 May 14, 2026
1a3db22
fix: update signature e2e testcase
saurabhchavan1711 May 14, 2026
b57f073
fix: revert change in yaml file
saurabhchavan1711 May 14, 2026
df92f2a
Merge remote-tracking branch 'origin' into moo/MOO-2069-Add-Direct-Im…
saurabhchavan1711 Aug 21, 2026
aa3aca8
fix: added back the temp file removal logic in base64decodetoimage
saurabhchavan1711 Aug 21, 2026
f8bd79a
fix: implement signature widget code with blob functionality
saurabhchavan1711 Aug 24, 2026
0d35805
Merge remote-tracking branch 'origin' into moo/MOO-2069-Add-Direct-Im…
saurabhchavan1711 Aug 24, 2026
c05ea71
fix: fixed eslint error
saurabhchavan1711 Aug 24, 2026
ea03a98
fix: fixed unit tests
saurabhchavan1711 Aug 24, 2026
fff9acf
fix: added call nanoflow as default action type and update message
saurabhchavan1711 Aug 25, 2026
00c75a3
fix: removed hasSignatureAttribute property
saurabhchavan1711 Aug 25, 2026
07ec55f
Merge remote-tracking branch 'origin' into moo/MOO-2069-Add-Direct-Im…
saurabhchavan1711 Aug 25, 2026
261d1a1
fix: updated changelog
saurabhchavan1711 Aug 25, 2026
c39af01
fix: temp file cleanup logic added
saurabhchavan1711 Aug 26, 2026
2cfea5a
Merge remote-tracking branch 'origin' into moo/MOO-2069-Add-Direct-Im…
saurabhchavan1711 Aug 26, 2026
6ab4e0c
Revert "fix: temp file cleanup logic added"
saurabhchavan1711 Aug 26, 2026
d97b285
fix: update on signend action description
saurabhchavan1711 Aug 26, 2026
684f088
fix: temp file cleanup code
saurabhchavan1711 Aug 26, 2026
fca5c7b
fix: added signature widget config doc
saurabhchavan1711 Aug 26, 2026
f8ca253
fix: updated the config doc
saurabhchavan1711 Aug 26, 2026
8efe7f1
fix: update signature e2e testcase
saurabhchavan1711 Aug 26, 2026
7893fab
fix: testcases comment updated
saurabhchavan1711 Aug 26, 2026
1c46cc3
fix: updated git revision hash
saurabhchavan1711 Aug 26, 2026
c1cece7
fix: updated commit hash with main branch
saurabhchavan1711 Aug 27, 2026
b2ccdbc
fix: version major bump
saurabhchavan1711 Aug 27, 2026
7a464ee
fix: fixed pr comments
saurabhchavan1711 Sep 2, 2026
c571a76
fix: removed array from temp files and unwanted type from typing
saurabhchavan1711 Sep 3, 2026
e23b4a6
fix: removed consoles
saurabhchavan1711 Sep 3, 2026
381a000
fix: added configuration guide to Readme instead of separate file
saurabhchavan1711 Sep 3, 2026
eb9f043
Merge branch 'main' into moo/MOO-2069-Add-Direct-Image-option-to-sign…
saurabhchavan1711 Sep 4, 2026
790cf9d
fix: update the nmr commit sha
saurabhchavan1711 Sep 8, 2026
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 .github/workflows/NativePipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ env:
# Pin the test project (mendix/Native-Mobile-Resources) to a SHA for reproducibility instead
# of tracking a moving `main`. Single source of truth — bump deliberately. The repo publishes
# no tags, so a commit SHA is the only stable ref. Override per run via the dispatch input.
NATIVE_MOBILE_RESOURCES_REF: ${{ github.event.inputs.test_project_ref || '258f55e1d4b0a25529b1c08a5b17f76a43663e61' }}
NATIVE_MOBILE_RESOURCES_REF: ${{ github.event.inputs.test_project_ref || '82577e93a76d0cbfb3e87f37ead39f7dac94e49e' }}
permissions:
packages: write
jobs:
Expand Down
5 changes: 5 additions & 0 deletions packages/pluggableWidgets/signature-native/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ 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 type `image` with `allowUpload` set to true.
- Renamed `onSave` action to `onSignEnd` to match web signature widget naming convention

## [2.4.0] - 2026-5-15

### Changed
Expand Down
139 changes: 138 additions & 1 deletion packages/pluggableWidgets/signature-native/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,141 @@
# Signature widget

Creates a canvas on which a user can draw.
The drawing will be saved as a base64 string.

## Configuration Guide

### Overview

This guide covers the required configuration and best practices for implementing the Signature widget in your Mendix Native Mobile application. It includes instructions for new implementations as well as migration steps for existing projects using the Signature widget.

---

### Advantages of the New Signature Widget

The updated Signature widget simplifies configuration and reduces manual setup:

- No need to manually assign a base64 string to an entity attribute.
- The base64DecodeToImage action and commit object activity are no longer required in your nanoflow.
- For entities with synchronization mode set to **All Objects**, you only need to add a synchronize action if required by your implementation. For entities with synchronization mode set to **Online**, synchronization is not required.

---

### Required Configuration for New Implementations

#### 1. Image Property (Required)

The `Image` property must be configured with an entity that **generalizes System.Image**.

**Configuration Steps:**

1. **Create an entity that generalizes System.Image:**

```
Example:
Entity: CustomerSignature
Generalization: System.Image
```

2. **Configure the widget:**
- In the **Data source** section, for `Image` property select Image type as **Dynamic**.
- Set the `System.Image` entity object to it.

**Studio Pro Validation:**

- Selecting Static images will display the error: _"Image must be a dynamic image type. Static images cannot be uploaded to."_

---

#### 2. On Sign End Action (Required)

The `On sign end` action is **required** and triggers when the user clicks the Save button to save the signature to the image object.

**Supported Action Types:**

- **Call a nanoflow**
- **Call a microflow**
- **Save changes**

**Important:**

- If the `On sign end` action is not configured with one of the supported action types, the signature will not be saved. The operation will fail silently without displaying a Studio Pro error. You can set any required actions (for ex.- show pages, close pages) inside that nanoflow/microflow.

---

### Migration Steps for Existing Projects

#### 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 the string into an image, and finally commit the object before continuing with your flow. With this update, the widget now handles the base64-to-image conversion internally, eliminating this overhead.

**Key Changes:**

1. The widget now saves the signature directly to an entity that generalizes `System.Image`, instead of storing a base64-encoded string in a String attribute and then converting it to Image.
2. The **On save** event has been renamed to **On sign end** under the **Events** tab.

#### Example: Migrating from Studio Pro 10.24 or Earlier 11.x.x

The following is a step-by-step walkthrough of migrating a Mendix app from Studio Pro 10.24 or an earlier 11.x.x version to a version that includes the updated Signature widget.

1. **Resolve migration errors** — After converting the app to the new Studio Pro version, you may see errors about outdated modules: **NanoflowCommons** and **Native Mobile Resources**. These errors are unrelated to the Signature widget. Update both modules from the Marketplace to resolve these errors.

![Update NanoflowCommons and Native Mobile Resources](assets/migration/Update-NC-NMR.png)

2. **Update the widget** — If an **Update widget** error appears, click **Update** (or **Update all widgets**) to apply the new version.

![Update Widget](assets/migration/Update-Widget.png)

3. **Configure the Image property** — After updating, a new validation error will appear: _Property Image is required_.

![Property Image Required Error](assets/migration/PropertyImageRequiredError.png)

To resolve this, open the Signature widget configuration and select the appropriate `System.Image` object reference in the **Image** property with image type set to **Dynamic**:

![Assign Object to Image Property](assets/migration/AssignObjectToImageProperty.png)

4. **Update the On sign end action** — In the **Events** tab, assign the nanoflow or other supported action that was previously used for **On save** to the **On sign end** action. You must assign either a nanoflow, microflow, or save changes action.

![Assign Nanoflow to On Sign End](assets/migration/AssignNanoflowToOnSignEnd.png)

Next, open the nanoflow and remove the **base64DecodeToImage** action and **Commit object** activity, as they are no longer required. Keeping the **base64DecodeToImage** action will fail the signature from being stored. Keep your existing actions for ex.- **Synchronize** or **Close page**.

![Nanoflow with Removed Base64DecodeToImage](assets/migration/NanoflowOnSignEnd.png)

---

### Troubleshooting

#### Error: "Property Image is required"

**Cause:** The Image property is not configured.

**Solution:**

1. Select the Signature widget.
2. In the **Data source** section, configure the Image property.
3. Select a System.Image entity object.

#### Error: "Image must be a dynamic image type. Static images cannot be upload to"

**Cause:** You selected a static image type.

**Solution:**

1. Create an entity that generalizes System.Image.
2. In the Signature widget, set the Image property to use a dynamic image type.
3. Select the System.Image entity object.

#### Signature is not being stored in the object

**Cause:** The `On sign end` action is not configured with a supported action type.

**Solution:**

1. Configure the `On sign end` action with a supported action type: call a nanoflow, call a microflow, or save changes.
2. Implement your project-specific logic inside the nanoflow or microflow. You can use other actions (for ex.- show page or close page) within the nanoflow or microflow as needed.

---

### Breaking Changes

- With this update, the previous **Attribute** property has been removed. The widget no longer stores a base64-encoded string into it. 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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ appId: "${APP_ID}"
end: 10%, 75%
- tapOn:
text: "Save"
- extendedWaitUntil:
visible:
text: "Base64"
timeout: 30000
optional: true
# Widgets menu will be visible only after successfully saving the signature, as the nanoflow has a Close page action to navigate back to the widgets menu.
- assertVisible:
text: "Widgets menu"
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.4.0",
"version": "3.0.0",
"license": "Apache-2.0",
"repository": {
"type": "git",
Expand Down
Original file line number Diff line number Diff line change
@@ -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 must be a dynamic image type. Static images cannot be uploaded to."
});
}

return errors;
}
94 changes: 87 additions & 7 deletions packages/pluggableWidgets/signature-native/src/Signature.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,67 @@
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 { ReactElement, useCallback, useEffect, useRef } from "react";
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<SignatureStyle>;

async function dataUriToBlob(base64: string): Promise<{ blob: File; tempPath: string }> {
// 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 { blob: fileBlob, tempPath };
}

export function Signature(props: Props): ReactElement {
const ref = useRef<SignatureViewRef>(null);
const pendingTempPathRef = useRef<string | null>(null);
const wasExecutingRef = useRef<boolean>(false);
const styles = mergeNativeStyles(defaultSignatureStyle, props.style);
const [signatureProps, containerStyles] = extractStyles(styles.container, ["penColor", "backgroundColor"]);
const [buttonClearContainerProps, buttonClearContainerStyles] = extractStyles(styles.buttonClearContainer, [
Expand All @@ -27,12 +77,40 @@ export function Signature(props: Props): ReactElement {
const buttonCaptionClear = props.buttonCaptionClear?.value ?? "Clear";
const buttonCaptionSave = props.buttonCaptionSave?.value ?? "Save";

// Clean up temp file after onSignEndAction completes. We cannot await action.execute() (it returns void),
// so we observe the isExecuting transition (true → false) as the signal that the action — including the
// file upload triggered by publish("submit") inside callNanoflow/callMicroflow/saveChanges — has finished.
useEffect(() => {
const isExecuting = props.onSignEndAction?.isExecuting ?? false;
if (wasExecutingRef.current && !isExecuting && pendingTempPathRef.current) {
const tempPath = pendingTempPathRef.current;
pendingTempPathRef.current = null;
RNBlobUtil.fs.unlink(tempPath).catch(e => console.info("Temp file cleanup failed:", e));
}
wasExecutingRef.current = isExecuting;
}, [props.onSignEndAction?.isExecuting]);

const handleSignature = useCallback(
(base64signature: string): void => {
props.imageAttribute.setValue(base64signature);
executeAction(props.onSave);
async (dataUri: string): Promise<void> => {
let tempPath: string | undefined;
try {
if (props.imageSource.readOnly) {
return;
}
const result = await dataUriToBlob(dataUri);
tempPath = result.tempPath;
pendingTempPathRef.current = tempPath;
props.imageSource.setValue(result.blob);
executeAction(props.onSignEndAction);
} catch (error) {
if (tempPath) {
pendingTempPathRef.current = null;
RNBlobUtil.fs.unlink(tempPath).catch(e => console.info("Temp file cleanup failed:", e));
}
console.error("Signature: failed to save image", error);
}
},
[props.imageAttribute, props.onSave]
[props.imageSource, props.onSignEndAction]
);

return (
Expand All @@ -43,7 +121,9 @@ export function Signature(props: Props): ReactElement {
onEmpty={() => executeAction(props.onEmpty)}
onEnd={() => executeAction(props.onEnd)}
onOK={handleSignature}
onClear={() => executeAction(props.onClear)}
onClear={() => {
executeAction(props.onClear);
}}
webStyle={webStyles}
{...signatureProps}
/>
Expand Down
Loading
Loading