diff --git a/packages/jsActions/mobile-resources-native/CHANGELOG.md b/packages/jsActions/mobile-resources-native/CHANGELOG.md index 8fb6a1328..c80d2393c 100644 --- a/packages/jsActions/mobile-resources-native/CHANGELOG.md +++ b/packages/jsActions/mobile-resources-native/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - We switched to a new sound library for the Play sound action to support react-native 0.84+. - The Play sound action now plays audio files from online (network) documents on Android by downloading them to a version-based cache before playback. - We have fixed the biometric authentication issue where it was not working on Android and crashing on iOS. +- Fixed an issue where the `TakePicture` and `TakePictureAdvanced` actions failed to capture photos on Android. ## [12.1.0] Native Mobile Resources - 2026-6-10 diff --git a/packages/jsActions/mobile-resources-native/src/camera/TakePicture.ts b/packages/jsActions/mobile-resources-native/src/camera/TakePicture.ts index 324d6c15d..69790ba27 100644 --- a/packages/jsActions/mobile-resources-native/src/camera/TakePicture.ts +++ b/packages/jsActions/mobile-resources-native/src/camera/TakePicture.ts @@ -118,13 +118,20 @@ export async function TakePicture( function storeFile(imageObject: mendix.lib.MxObject, uri: string): Promise { return new Promise((resolve, reject) => { - fetch(uri) - .then(response => response.blob()) - .then(blob => { + NativeModules.MxFileSystem.read(uri.replace("file://", "")) + .then((nativeBlob: unknown) => { + const blob = new Blob(); + Object.assign(blob, { data: nativeBlob }); // eslint-disable-next-line no-useless-escape const filename = /[^\/]*$/.exec(uri)![0]; const filePathWithoutFileScheme = uri.replace("file://", ""); + // 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://${uri}`, name: filename, type: "*/*" }; + mx.data.saveDocument( imageObject.getGuid(), filename, diff --git a/packages/jsActions/mobile-resources-native/src/camera/TakePictureAdvanced.ts b/packages/jsActions/mobile-resources-native/src/camera/TakePictureAdvanced.ts index c141b842e..562502f99 100644 --- a/packages/jsActions/mobile-resources-native/src/camera/TakePictureAdvanced.ts +++ b/packages/jsActions/mobile-resources-native/src/camera/TakePictureAdvanced.ts @@ -171,13 +171,20 @@ export async function TakePictureAdvanced( function storeFile(imageObject: mendix.lib.MxObject, uri: string): Promise { return new Promise((resolve, reject) => { - fetch(uri) - .then(response => response.blob()) - .then(blob => { + NativeModules.MxFileSystem.read(uri.replace("file://", "")) + .then((nativeBlob: unknown) => { + const blob = new Blob(); + Object.assign(blob, { data: nativeBlob }); // eslint-disable-next-line no-useless-escape const filename = /[^\/]*$/.exec(uri)![0]; const filePathWithoutFileScheme = uri.replace("file://", ""); + // 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://${uri}`, name: filename, type: "*/*" }; + mx.data.saveDocument( imageObject.getGuid(), filename, diff --git a/packages/jsActions/nanoflow-actions-native/CHANGELOG.md b/packages/jsActions/nanoflow-actions-native/CHANGELOG.md index 349bc64d3..1d085bb9d 100644 --- a/packages/jsActions/nanoflow-actions-native/CHANGELOG.md +++ b/packages/jsActions/nanoflow-actions-native/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +- Fixed an issue where base64-to-image decoding was failing. + +## [7.2.0] Nanoflow Commons - 2026-7-3 + - Close offline database connection before navigating between pages with OpenURL nanoflow action. ## [7.1.0] Nanoflow Commons - 2026-6-5 diff --git a/packages/jsActions/nanoflow-actions-native/src/other/Base64DecodeToImage.ts b/packages/jsActions/nanoflow-actions-native/src/other/Base64DecodeToImage.ts index fbd420f40..8a8f5c4cf 100644 --- a/packages/jsActions/nanoflow-actions-native/src/other/Base64DecodeToImage.ts +++ b/packages/jsActions/nanoflow-actions-native/src/other/Base64DecodeToImage.ts @@ -7,6 +7,7 @@ // 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"; +import { NativeModules } from "react-native"; // BEGIN EXTRA CODE // END EXTRA CODE @@ -45,21 +46,38 @@ export async function Base64DecodeToImage(base64: string, image: mendix.lib.MxOb } // Create a temporary file path - const tempPath = `${RNBlobUtil.fs.dirs.CacheDir}/temp_image_${Date.now()}.png`; + 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"); - // Fetch the file as a blob - const res = await fetch(`file://${tempPath}`); - const blob = await res.blob(); + // 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 }); + + // 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 Blob; return new Promise((resolve, reject) => { mx.data.saveDocument( image.getGuid(), - "camera image", + fileName, {}, - blob, + fileBlob, () => { RNBlobUtil.fs.unlink(tempPath).catch(e => console.info("Temp file cleanup failed:", e)); resolve(true); diff --git a/packages/pluggableWidgets/image-native/CHANGELOG.md b/packages/pluggableWidgets/image-native/CHANGELOG.md index fc991c97e..c58e080fc 100644 --- a/packages/pluggableWidgets/image-native/CHANGELOG.md +++ b/packages/pluggableWidgets/image-native/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Fixed + +- We fixed an issue that caused images from entities to not render on Android in Online Synchronization mode + ## [3.1.1] - 2026-6-10 ### Changed diff --git a/packages/pluggableWidgets/image-native/package.json b/packages/pluggableWidgets/image-native/package.json index 1a6d82e76..5a197a328 100644 --- a/packages/pluggableWidgets/image-native/package.json +++ b/packages/pluggableWidgets/image-native/package.json @@ -1,7 +1,7 @@ { "name": "image-native", "widgetName": "Image", - "version": "3.1.1", + "version": "3.1.2", "description": "Display an image and enlarge it on click", "copyright": "© Mendix Technology BV 2022. All rights reserved.", "license": "Apache-2.0", diff --git a/packages/pluggableWidgets/image-native/src/components/ImageIconSVG.tsx b/packages/pluggableWidgets/image-native/src/components/ImageIconSVG.tsx index d4bf52b49..3deb5eb09 100644 --- a/packages/pluggableWidgets/image-native/src/components/ImageIconSVG.tsx +++ b/packages/pluggableWidgets/image-native/src/components/ImageIconSVG.tsx @@ -1,5 +1,5 @@ import { FunctionComponent, Fragment, useCallback } from "react"; -import { View } from "react-native"; +import { ImageURISource, Platform, View } from "react-native"; import { SvgUri, SvgXml } from "react-native-svg"; import FastImageComponent, { Source } from "@d11/react-native-fast-image"; import { extractStyles } from "@mendix/pluggable-widgets-tools"; @@ -64,11 +64,19 @@ export const ImageIconSVG: FunctionComponent = props => { ); if (image && (type === "staticImage" || type === "dynamicImage")) { + // FastImage's Glide/OkHttp client on Android can be initialized before the app wires up + // its cookie-decrypting network interceptor, causing remote (online document) images to + // fail to load with 401s. RN's own Image component always picks up the interceptor, so + // fall back to it for remote urls on Android. + const uri = typeof image === "object" ? (image as ImageURISource)?.uri : undefined; + const useFallback = Platform.OS === "android" && typeof uri === "string" && /^https?:\/\//i.test(uri); + return ( - + diff --git a/packages/pluggableWidgets/image-native/src/utils/imageUtils.ts b/packages/pluggableWidgets/image-native/src/utils/imageUtils.ts index 309077c8d..5c14f1243 100644 --- a/packages/pluggableWidgets/image-native/src/utils/imageUtils.ts +++ b/packages/pluggableWidgets/image-native/src/utils/imageUtils.ts @@ -31,6 +31,12 @@ function getBundledAssetSource(value: NativeImage | Readonly | undefined, @@ -74,14 +80,14 @@ export async function convertImageProps( } else if (typeof imageValue === "object" && imageValue?.uri && imageValue?.name?.endsWith(".svg")) { return { type: "dynamicSVG", // Dynamic image SVG - image: (Platform.OS === "android" ? "file:///" : "") + imageValue.uri + image: toAndroidUri(imageValue.uri as string) }; } else if (typeof imageValue === "object" && imageValue?.uri) { return { type: "dynamicImage", // Dynamic image image: { ...imageValue, - uri: (Platform.OS === "android" ? "file:///" : "") + imageValue.uri + uri: toAndroidUri(imageValue.uri as string) } }; }