diff --git a/.github/workflows/NativePipeline.yml b/.github/workflows/NativePipeline.yml index 2175109d3..bcb5518f1 100644 --- a/.github/workflows/NativePipeline.yml +++ b/.github/workflows/NativePipeline.yml @@ -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: diff --git a/packages/pluggableWidgets/signature-native/CHANGELOG.md b/packages/pluggableWidgets/signature-native/CHANGELOG.md index 2f1750692..06447409f 100644 --- a/packages/pluggableWidgets/signature-native/CHANGELOG.md +++ b/packages/pluggableWidgets/signature-native/CHANGELOG.md @@ -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 diff --git a/packages/pluggableWidgets/signature-native/SIGNATURE_WIDGET_CONFIGURATION.md b/packages/pluggableWidgets/signature-native/SIGNATURE_WIDGET_CONFIGURATION.md new file mode 100644 index 000000000..b18a903ed --- /dev/null +++ b/packages/pluggableWidgets/signature-native/SIGNATURE_WIDGET_CONFIGURATION.md @@ -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. 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..e8014fdac 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..1aba0b1cb Binary files /dev/null and b/packages/pluggableWidgets/signature-native/assets/migration/AssignObjectToImageProperty.png differ diff --git a/packages/pluggableWidgets/signature-native/assets/migration/NanoflowOnSignEnd.png b/packages/pluggableWidgets/signature-native/assets/migration/NanoflowOnSignEnd.png new file mode 100644 index 000000000..f4ba8117c Binary files /dev/null and b/packages/pluggableWidgets/signature-native/assets/migration/NanoflowOnSignEnd.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/e2e/specs/maestro/Signature.yaml b/packages/pluggableWidgets/signature-native/e2e/specs/maestro/Signature.yaml index e60d305ae..bb42978b2 100644 --- a/packages/pluggableWidgets/signature-native/e2e/specs/maestro/Signature.yaml +++ b/packages/pluggableWidgets/signature-native/e2e/specs/maestro/Signature.yaml @@ -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" diff --git a/packages/pluggableWidgets/signature-native/package.json b/packages/pluggableWidgets/signature-native/package.json index 99c1e3778..8dadea87a 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": "3.0.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..7cc2b47d3 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 must be a dynamic image type. 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..431b03cc8 100644 --- a/packages/pluggableWidgets/signature-native/src/Signature.tsx +++ b/packages/pluggableWidgets/signature-native/src/Signature.tsx @@ -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; +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(null); + const pendingTempPathsRef = useRef([]); + const wasExecutingRef = useRef(false); const styles = mergeNativeStyles(defaultSignatureStyle, props.style); const [signatureProps, containerStyles] = extractStyles(styles.container, ["penColor", "backgroundColor"]); const [buttonClearContainerProps, buttonClearContainerStyles] = extractStyles(styles.buttonClearContainer, [ @@ -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 => { + 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 ( diff --git a/packages/pluggableWidgets/signature-native/src/Signature.xml b/packages/pluggableWidgets/signature-native/src/Signature.xml index aef8609f4..8721f3668 100644 --- a/packages/pluggableWidgets/signature-native/src/Signature.xml +++ b/packages/pluggableWidgets/signature-native/src/Signature.xml @@ -1,22 +1,16 @@ - - + + 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 generalization object the signature will be saved into. Select the image type Dynamic @@ -35,9 +29,9 @@ On clear Fired when the clear button is clicked. - - On save - Fired when the save button is clicked. + + On sign end + Fired when the save button is clicked which saves the signature to the image object. Must use a nanoflow, microflow or save changes action to save the signature. On end @@ -49,7 +43,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..563eb9757 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..76bb978ad 100644 --- a/packages/pluggableWidgets/signature-native/typings/SignatureProps.d.ts +++ b/packages/pluggableWidgets/signature-native/typings/SignatureProps.d.ts @@ -3,17 +3,17 @@ * 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