diff --git a/packages/jsActions/nanoflow-actions-native/src/other/Base64DecodeToImage.ts b/packages/jsActions/nanoflow-actions-native/src/other/Base64DecodeToImage.ts index fbd420f40..3cafeb8b6 100644 --- a/packages/jsActions/nanoflow-actions-native/src/other/Base64DecodeToImage.ts +++ b/packages/jsActions/nanoflow-actions-native/src/other/Base64DecodeToImage.ts @@ -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 @@ -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) => { @@ -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); } ); diff --git a/packages/pluggableWidgets/signature-native/CHANGELOG.md b/packages/pluggableWidgets/signature-native/CHANGELOG.md index a1389185f..dc8d7042f 100644 --- a/packages/pluggableWidgets/signature-native/CHANGELOG.md +++ b/packages/pluggableWidgets/signature-native/CHANGELOG.md @@ -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 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 9a88f76d7..a4b06788f 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.tsx b/packages/pluggableWidgets/signature-native/src/Signature.tsx index 02b99ceb1..01e3c27c7 100644 --- a/packages/pluggableWidgets/signature-native/src/Signature.tsx +++ b/packages/pluggableWidgets/signature-native/src/Signature.tsx @@ -10,6 +10,12 @@ import { SignatureStyle, defaultSignatureStyle, webStyles } from "./ui/Styles"; export type Props = SignatureProps; +async function dataUriToFile(dataUri: string): Promise { + 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(null); const styles = mergeNativeStyles(defaultSignatureStyle, props.style); @@ -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 => { + try { + /* + if (props.imageSource.status !== "available" || props.imageSource.readOnly) { + return; + } This check needs to add once the EditableImageValue 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 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 ( @@ -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} /> 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. - iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAQKADAAQAAAABAAAAQAAAAABGUUKwAAAJmklEQVR4Ae1bfWwc1RHfdz77zh8QgyCEgPgITYnjtqiNTFUJaIKgiZ1go4K/Gqc5XAhqqVOKUkT/ASOVj/APoo5ANS25BtmJ4+LYF0gsCElRo4qAgIioGAREJkQ0LSFxwfZ92Levv1nubWbfns+XOMmthZ+0fjPz5u3OzJudN2/2bBgzbcYCMxb4JltATDfla2trr4bMD+K6DtcsXP8SQuz2+/0bOzs7DwI/qTatDADl66Hdc7iK0mgpYYi/+3y+h7Zs2fKPNONpSdPGACtXrrw0kUi8By3OSavJCSIZYsPWrVvXniBNDPkmHvLWyNjY2COQaDLlSWghpWypq6u7KxsNpoUB4PqzoFQtVwir/OtgMHgRaHfi+icfS8GPYs6kHu5PM9FzJCjbCGUKlWDAB7u6up5GL0H7C1319fUVpmm+ApgCowH+CxoaGhYAHCB8ojYtPADCN3MFoNzGlPI2GQZ5E4hpEwCArx+vwv3wINt4fJxgzxsAK/sdKFKhBIfipGRY4aqHkpcBLlU49Zh3Ga71mHMA4z/iYwr2vAGgwC+UsNQD39Xd3X2I01JwCH3adx5zroIRtq1atWq2Ps/TBsCqFUDgJk1oygMcDQpCP3EHJwIf4zh4LorH4w9zGsGeNgCUuAWCX8CEPjZnzpxehltgY2PjjeC7gtGj4+Pj8zG/jdHIexZznGBPGwACO9wfCnW0tbXFdSWSyWQzp4HvhZ6enk8wv5XTAV+u4d41QCqoLeUCQyGX+4dCoVIo/FPOh3SYtkYD/WLqWXufwRboZQ/YAAm5fPSiP45tra6lpSWgFIlGoz+DYYIKR39w8+bNrxGexoP2Mj4L5A/Qx3KGQ3CK5nTa441S3KW4uo4cOfIZ7PAUtshrkPw0O5iEsHIEJEFziV8bC3OcYE8aAMrdANnO04Vl+PlQbi2U3w/aIkWHh5j5+flhwjG2Gl0ewam2H8nS2wpRvScNAEUcqwphv1ICZ+phlJc7OjoOEw9gx7aIeOCKH8TnOQPg2HsuhL+dhGPtNhQ8rgH+FK4vGN0B5uXlbSNCyoPms8E4PKKD4TboOQPg2EtFD17w+KS8vPxVBLZ3kQHei7G5uGrhJbo7YzdMPp/STPegXsw9lhpzdJ4zAFZf3/vDra2tppIaiiRw/Q3454qW6sOgR5ubm6lm4PCgidyf5nnKANj7F0KmH5JgqSYhfFghqqfqEOCbFU49XpE/Uj8yMlIPIxYTnGqHysrKdilE7z1lgDTBbzfqe4O60HhNQlCSy/4WvSLEB/pijd/hQdqYdzxgzZo1+RB+lSagK3KDh/KBCSM8xnhsGEVg/LN2TwfqmYrQ8ePHV0AyflwdKi4u7nFICwTJz2J08xQdXhMrLCzsVDh62ikSoFMd4a/woE/ZmAv0jAEgWTOXDgp0hsPhGKelYAcflOwB35DiQyBMAqY0OqvG36OsJpwJpqampotx30p+byjmcn8ESSqO3sb54OIuPj4+GewJA6De/3MIytNW2vPf0oXHjtAAml3fg5cMIvjt1vlOBveEASCww62hmHWc1RXB6jtyBOCu4qg+ZzI85waAW18HRb7NBE2g3u9KW7MtjrL7ZAXm3ABYbX31+zZt2uTK92EkBx/wV/GaHMpKywxMOTUAVr8EstVx+aCYK6ilcoQmzpcpveV8k8E5NQCEuxYK87T1MA4+L+tCDw0NVYN2oaLDa47Pnj3bOvkp2qn2uTYA7dm8HeUHHzbQzGBKd9MWRzlPtnCuDXBAE7QMBnAkZ3TwgcKO0tZU937+zJwaAEHsGNz5MyZQYGBgwC5k0OkQOQKd5HiOsB/p7TtszpTAnBogJbnDC1C5+R7RUdWh4LgP19WEqwaDZTzcKL5se4e76ZOW90WXmNK4G/XoBcKQyLfFdiM/sGFnlXB9nNDnngROBrBdHAp+HytPwfG+NPfYW1pa2p6GfsqkCQ1QGYmtS0rjCZywobs06EM84B+LRHz1rXvkDb1LhH0AOeWnYyIUPgBl7VsAXgeEu7w1Br7nioqK7mlvb3d887MnniKQ1gDLIokK1JUft5TXbgxTfDfxZawNZP3srnFmh5IBNE5d+Th4WvCbn2c1vtOCpo8BMrkOiuqC2A+Uhqit3CHPtQlTAJD20i849O1Q3fEQEp7rz5Ty9BCXAUJ7ZFAYokZJQD1W4Ff4O6hocNOAkGPzFD6VPnXm/1C/B565C3W+RalffujDpw13GeDzkfEKUlA9QQjjaEl1oB3fqt5TNOoRrak8fVoalH2J3Yi+AT62cOHCpTjqHmX0MwK6YoA05RX8SXD3Pd1CJKv6osdYrIIFxPmcbyowDN4KpSnDuxIu/yesuisdnsr9M811GcCQplMxKa2yFGqRqNediNbCJ/+b6catqNrui8RCeHVoX/8I22cXtk+9lm/dAgnRMACK/me9uQyA4DfKpcDCmBYujCuZ/ghb4jDn0+F9kXg/PAa1+6+NJsbi6yv7Yu34GeN2YYoyUxgtGLpE+MQfdlYH1uvzzxZOn6EdrSoSr8H73auIMMBr2JZ/K2XybZtmiGTJrEBp9xIxXCtl3leRRDmN9dcErNr88hdjS5Pjsl/xZ+zxW78CI3BppMaREmeccjoHXR7glwWvJwxejBUV0kj+zvFQIfeS8iu2y0uGI7EXsJLW15zKvuibwp+3xhw3HQVOx1wNwU8dpT/o8C2N48yirl0AK/Ef7HsfqcciMBVBvEaFUy98PisdTZqx9XBz+1MW4Aoovxf8TgOgds/nKxiBbxQedl/PMvFvRTvbvesVIAGq+uK/N6X5aFphhPjgnOpAubHLKBkeiX+RKWGy5+cZN4mk7zxDmCtgpHlQ/CiC6huFJQXPbrtJuMpf9ryzALheAXpmcVHB08Oj8Qew+q5sDy7TQttiZSS6COFtwmyRyx70Gx/33hIYBI2+6nqquV4Bkq77ZvE/rCxPTiyhkSFGDZ+8vPYVOQsvwg+4Jhj7GNfXOwYbAG2gt7JwkJE8BaZ/BSLxRhNlJ2QmacctDeiX2mwc29laxI5PZVKGsfXBQDCRIRJIpBr6bw1ss+Z48I9Lwaq+2DLUACJQIj9beWnlfX5j/ksrggfJO0ZG4yG8HnOF37d5x/KC/dneJxd8LgMs64t9iJX9lhIGAYuKHxsR2VdjyQsVnffgeWZnTRAHpunXXDEAFrFptLJw4RCU+6U/L7DA5xNPws0/sNXE73SgfMuO6sA9Nm2aAS4PqNwe+wkORLTifmz4v0F2t0XXCbsD9Lb+W0MfmsFnLDBjgRkLTCsL/B8F5W5JaybTrgAAAABJRU5ErkJggg== + iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAQKADAAQAAAABAAAAQAAAAABGUUKwAAAJmklEQVR4Ae1bfWwc1RHfdz77zh8QgyCEgPgITYnjtqiNTFUJaIKgiZ1go4K/Gqc5XAhqqVOKUkT/ASOVj/APoo5ANS25BtmJ4+LYF0gsCElRo4qAgIioGAREJkQ0LSFxwfZ92Levv1nubWbfns+XOMmthZ+0fjPz5u3OzJudN2/2bBgzbcYCMxb4JltATDfla2trr4bMD+K6DtcsXP8SQuz2+/0bOzs7DwI/qTatDADl66Hdc7iK0mgpYYi/+3y+h7Zs2fKPNONpSdPGACtXrrw0kUi8By3OSavJCSIZYsPWrVvXniBNDPkmHvLWyNjY2COQaDLlSWghpWypq6u7KxsNpoUB4PqzoFQtVwir/OtgMHgRaHfi+icfS8GPYs6kHu5PM9FzJCjbCGUKlWDAB7u6up5GL0H7C1319fUVpmm+ApgCowH+CxoaGhYAHCB8ojYtPADCN3MFoNzGlPI2GQZ5E4hpEwCArx+vwv3wINt4fJxgzxsAK/sdKFKhBIfipGRY4aqHkpcBLlU49Zh3Ga71mHMA4z/iYwr2vAGgwC+UsNQD39Xd3X2I01JwCH3adx5zroIRtq1atWq2Ps/TBsCqFUDgJk1oygMcDQpCP3EHJwIf4zh4LorH4w9zGsGeNgCUuAWCX8CEPjZnzpxehltgY2PjjeC7gtGj4+Pj8zG/jdHIexZznGBPGwACO9wfCnW0tbXFdSWSyWQzp4HvhZ6enk8wv5XTAV+u4d41QCqoLeUCQyGX+4dCoVIo/FPOh3SYtkYD/WLqWXufwRboZQ/YAAm5fPSiP45tra6lpSWgFIlGoz+DYYIKR39w8+bNrxGexoP2Mj4L5A/Qx3KGQ3CK5nTa441S3KW4uo4cOfIZ7PAUtshrkPw0O5iEsHIEJEFziV8bC3OcYE8aAMrdANnO04Vl+PlQbi2U3w/aIkWHh5j5+flhwjG2Gl0ewam2H8nS2wpRvScNAEUcqwphv1ICZ+phlJc7OjoOEw9gx7aIeOCKH8TnOQPg2HsuhL+dhGPtNhQ8rgH+FK4vGN0B5uXlbSNCyoPms8E4PKKD4TboOQPg2EtFD17w+KS8vPxVBLZ3kQHei7G5uGrhJbo7YzdMPp/STPegXsw9lhpzdJ4zAFZf3/vDra2tppIaiiRw/Q3454qW6sOgR5ubm6lm4PCgidyf5nnKANj7F0KmH5JgqSYhfFghqqfqEOCbFU49XpE/Uj8yMlIPIxYTnGqHysrKdilE7z1lgDTBbzfqe4O60HhNQlCSy/4WvSLEB/pijd/hQdqYdzxgzZo1+RB+lSagK3KDh/KBCSM8xnhsGEVg/LN2TwfqmYrQ8ePHV0AyflwdKi4u7nFICwTJz2J08xQdXhMrLCzsVDh62ikSoFMd4a/woE/ZmAv0jAEgWTOXDgp0hsPhGKelYAcflOwB35DiQyBMAqY0OqvG36OsJpwJpqampotx30p+byjmcn8ESSqO3sb54OIuPj4+GewJA6De/3MIytNW2vPf0oXHjtAAml3fg5cMIvjt1vlOBveEASCww62hmHWc1RXB6jtyBOCu4qg+ZzI85waAW18HRb7NBE2g3u9KW7MtjrL7ZAXm3ABYbX31+zZt2uTK92EkBx/wV/GaHMpKywxMOTUAVr8EstVx+aCYK6ilcoQmzpcpveV8k8E5NQCEuxYK87T1MA4+L+tCDw0NVYN2oaLDa47Pnj3bOvkp2qn2uTYA7dm8HeUHHzbQzGBKd9MWRzlPtnCuDXBAE7QMBnAkZ3TwgcKO0tZU937+zJwaAEHsGNz5MyZQYGBgwC5k0OkQOQKd5HiOsB/p7TtszpTAnBogJbnDC1C5+R7RUdWh4LgP19WEqwaDZTzcKL5se4e76ZOW90WXmNK4G/XoBcKQyLfFdiM/sGFnlXB9nNDnngROBrBdHAp+HytPwfG+NPfYW1pa2p6GfsqkCQ1QGYmtS0rjCZywobs06EM84B+LRHz1rXvkDb1LhH0AOeWnYyIUPgBl7VsAXgeEu7w1Br7nioqK7mlvb3d887MnniKQ1gDLIokK1JUft5TXbgxTfDfxZawNZP3srnFmh5IBNE5d+Th4WvCbn2c1vtOCpo8BMrkOiuqC2A+Uhqit3CHPtQlTAJD20i849O1Q3fEQEp7rz5Ty9BCXAUJ7ZFAYokZJQD1W4Ff4O6hocNOAkGPzFD6VPnXm/1C/B565C3W+RalffujDpw13GeDzkfEKUlA9QQjjaEl1oB3fqt5TNOoRrak8fVoalH2J3Yi+AT62cOHCpTjqHmX0MwK6YoA05RX8SXD3Pd1CJKv6osdYrIIFxPmcbyowDN4KpSnDuxIu/yesuisdnsr9M811GcCQplMxKa2yFGqRqNediNbCJ/+b6catqNrui8RCeHVoX/8I22cXtk+9lm/dAgnRMACK/me9uQyA4DfKpcDCmBYujCuZ/ghb4jDn0+F9kXg/PAa1+6+NJsbi6yv7Yu34GeN2YYoyUxgtGLpE+MQfdlYH1uvzzxZOn6EdrSoSr8H73auIMMBr2JZ/K2XybZtmiGTJrEBp9xIxXCtl3leRRDmN9dcErNr88hdjS5Pjsl/xZ+zxW78CI3BppMaREmeccjoHXR7glwWvJwxejBUV0kj+zvFQIfeS8iu2y0uGI7EXsJLW15zKvuibwp+3xhw3HQVOx1wNwU8dpT/o8C2N48yirl0AK/Ef7HsfqcciMBVBvEaFUy98PisdTZqx9XBz+1MW4Aoovxf8TgOgds/nKxiBbxQedl/PMvFvRTvbvesVIAGq+uK/N6X5aFphhPjgnOpAubHLKBkeiX+RKWGy5+cZN4mk7zxDmCtgpHlQ/CiC6huFJQXPbrtJuMpf9ryzALheAXpmcVHB08Oj8Qew+q5sDy7TQttiZSS6COFtwmyRyx70Gx/33hIYBI2+6nqquV4Bkq77ZvE/rCxPTiyhkSFGDZ+8vPYVOQsvwg+4Jhj7GNfXOwYbAG2gt7JwkJE8BaZ/BSLxRhNlJ2QmacctDeiX2mwc29laxI5PZVKGsfXBQDCRIRJIpBr6bw1ss+Z48I9Lwaq+2DLUACJQIj9beWnlfX5j/ksrggfJO0ZG4yG8HnOF37d5x/KC/dneJxd8LgMs64t9iJX9lhIGAYuKHxsR2VdjyQsVnffgeWZnTRAHpunXXDEAFrFptLJw4RCU+6U/L7DA5xNPws0/sNXE73SgfMuO6sA9Nm2aAS4PqNwe+wkORLTifmz4v0F2t0XXCbsD9Lb+W0MfmsFnLDBjgRkLTCsL/B8F5W5JaybTrgAAAABJRU5ErkJggg== - - - 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..1e07f1c5b 100644 --- a/packages/pluggableWidgets/signature-native/src/__tests__/Signature.android.spec.tsx +++ b/packages/pluggableWidgets/signature-native/src/__tests__/Signature.android.spec.tsx @@ -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"); @@ -45,10 +52,14 @@ jest.mock("react-native/Libraries/Components/Touchable/TouchableNativeFeedback", }; }); +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 +104,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..541562b3e 100644 --- a/packages/pluggableWidgets/signature-native/src/__tests__/Signature.ios.spec.tsx +++ b/packages/pluggableWidgets/signature-native/src/__tests__/Signature.ios.spec.tsx @@ -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"); @@ -17,10 +25,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") }; @@ -75,13 +87,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 ebc9afc79..5eec25705 100644 --- a/packages/pluggableWidgets/signature-native/typings/SignatureProps.d.ts +++ b/packages/pluggableWidgets/signature-native/typings/SignatureProps.d.ts @@ -4,16 +4,17 @@ * @author Mendix Widgets Framework Team */ import { CSSProperties } from "react"; -import { ActionValue, DynamicValue, EditableValue } from "mendix"; +import { ActionValue, DynamicValue, EditableValue, NativeImage } from "mendix"; export interface SignatureProps