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
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 || 'ab858a34ce76d5395b8f1861a0dc2323147dbefd' }}
NATIVE_MOBILE_RESOURCES_REF: ${{ github.event.inputs.test_project_ref || '4fe9c777fc4f0e346e72025974e8fb4a4cb9e32c' }}
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Signature Widget - 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: 88 additions & 6 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 pendingTempPathsRef = useRef<string[]>([]);
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,44 @@ export function Signature(props: Props): ReactElement {
const buttonCaptionClear = props.buttonCaptionClear?.value ?? "Clear";
const buttonCaptionSave = props.buttonCaptionSave?.value ?? "Save";

// Clean up temp files 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 && pendingTempPathsRef.current.length > 0) {
const paths = pendingTempPathsRef.current.splice(0);
paths.forEach(tempPath =>
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;
pendingTempPathsRef.current.push(tempPath);
props.imageSource.setValue(result.blob);
executeAction(props.onSignEndAction);
} catch (error) {
if (tempPath) {
const idx = pendingTempPathsRef.current.indexOf(tempPath);
if (idx !== -1) {
pendingTempPathsRef.current.splice(idx, 1);
}
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 Down
Loading
Loading