Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
7 changes: 7 additions & 0 deletions packages/pluggableWidgets/signature-native/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@ 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
- Updated react-native-webview from version v13.13.2 to v13.16.1.

## [2.3.0] - 2025-7-7
Expand Down
101 changes: 101 additions & 0 deletions packages/pluggableWidgets/signature-native/MIGRATION.md
Original file line number Diff line number Diff line change
@@ -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.
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.
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": "2.5.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
Loading
Loading