Skip to content

Migrate Image Cropping to a KMP-Supported Library - #2627

Merged
therajanmaurya merged 3 commits into
openMF:developmentfrom
Arinyadav1:Migrate-Image-Crop-library
Mar 1, 2026
Merged

Migrate Image Cropping to a KMP-Supported Library#2627
therajanmaurya merged 3 commits into
openMF:developmentfrom
Arinyadav1:Migrate-Image-Crop-library

Conversation

@Arinyadav1

@Arinyadav1 Arinyadav1 commented Feb 28, 2026

Copy link
Copy Markdown
Member

Fixes - Jira-#677

Removed feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/PlatformFile.kt file and updated the image upload flow to improve KMP compatibility.

Reason:
outFile.write() and outFile.write(this) are not supported on JS and wasmJs targets. Because of this limitation, creating or writing files dynamically using FileKit was not working across all platforms, which prevented sending images to the server reliably.

Changes made:

  • Removed PlatformFile creation via manual file writing.
  • Instead of converting ImageBitmap/ByteArray into a PlatformFile using file system access, the new approach is:
    1. Select the file directly as a PlatformFile using FileKit.
    2. Convert the selected PlatformFile into ByteArray.
    3. Pass the ByteArray into multipart form data for upload.

Result:
This approach works consistently across all KMP targets (Android, iOS, Desktop, JS, wasmJs) and avoids unsupported file system operations on web platforms.

AndroidClient.ClientProfileEditScreen.kt.AndroidClient.feature.client.commonMain.2026-02-28.17-40-10.mp4

Summary by CodeRabbit

  • New Features

    • Added a Camera icon to the design system
    • Added a dedicated image selection UI with camera, gallery, and cropping flows
  • Improvements

    • Configurable cropping aspect ratio and refined crop dialogs
    • Image uploads now send explicit file bytes and metadata for more reliable uploads
    • Mobile camera launcher fully implemented
    • Document name inputs now strip file extensions
  • Removals

    • Removed legacy PlatformFile-to-file conversion API

@coderabbitai

coderabbitai Bot commented Feb 28, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7c8a836 and a866aa1.

📒 Files selected for processing (1)
  • feature/client/src/commonMain/composeResources/values/strings.xml

📝 Walkthrough

Walkthrough

Refactors file upload APIs from PlatformFile to ByteArray+metadata, adds camera capture and image-cropping workflows, makes the cropper aspect ratio configurable, updates multipart request construction across features, and adds a Camera icon reference.

Changes

Cohort / File(s) Summary
Design System
core/designsystem/src/.../MifosIcons.kt
Added val Camera: ImageVector = Icons.Outlined.Camera.
Image Cropper Component
core/ui/src/.../MifosImageCropperDialog.kt
Added aspectRatio: AspectRatio parameter; replaced hardcoded AspectRatio(2,1) with the new argument.
Multipart Request API & Mime Resolution
core/ui/src/.../MultipartRequestBody.kt, feature/client/src/androidMain/.../OpenFileInDefaultExternalApp.android.kt
Replaced suspend multipartRequestBody(PlatformFile) with multipartRequestBody(file: ByteArray, name: String, extension: String, description: String? = null); mime resolver now takes file extension string. Updated call sites.
Client Profile Edit UI & Flow
feature/client/src/commonMain/.../ClientProfileEditScreen.kt, .../ClientProfileEditViewModel.kt
Added bottom-sheet upload options (camera/gallery/more), integrated imageCropper state and ShowImageCrop dialog, introduced OpenCamera/OpenImagePicker/OnDismissDialog actions and crop→upload pipeline.
Client Identifiers & Document Uploads
feature/client/src/commonMain/.../ClientIdentifiersAddUpdateViewModel.kt, .../ClientIdentitiesAddUpdateScreen.kt
Switched internal create/update document helpers to accept ByteArray; UI now strips file extensions from displayed names (substringBeforeLast(".")).
Client Signature
feature/client/src/commonMain/.../ClientSignatureScreen.kt, .../ClientSignatureViewModel.kt
Pass AspectRatio(2,1) to cropper in UI; encode signature to PNG ByteArray and use new multipart API with explicit name and extension.
ViewModel Upload Integrations
feature/client/src/commonMain/.../ClientDetailsViewModel.kt, .../CreateNewClientViewModel.kt, feature/document/src/.../DocumentDialogViewModel.kt
Updated upload/createDocument calls to use file.readBytes(), explicit name and extension; fixed desciptiondescription.
Strings / Resources
feature/client/src/commonMain/composeResources/values/strings.xml
Renamed upload option resources: removed from_camera/from_gallery, added camera and gallery.
Platform File API Removed
feature/client/src/commonMain/.../PlatformFile.kt
Removed ImageBitmap.toPlatformFile() and ByteArray.toPlatformFile() extensions (platform file conversion API removed).
Platform Camera Launcher Implementation
feature/client/src/nativeMain/.../PlatformCameraLauncher.native.kt
Implemented launcher-backed PlatformCameraLauncher (internal constructor with PhotoResultLauncher); launch() delegates to launcher; rememberPlatformCameraLauncher returns configured launcher.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Screen as ClientProfileEditScreen
    participant VM as ClientProfileEditViewModel
    participant Camera as PlatformCameraLauncher
    participant Cropper as ImageCropper
    participant API as UploadAPI

    User->>Screen: Tap upload image
    Screen->>VM: OpenCamera / OpenImagePicker
    alt Camera chosen
        VM->>Camera: launch()
        Camera-->>VM: Photo result (bytes)
    else Gallery chosen
        VM-->>VM: open file picker (returns bytes)
    end
    VM->>Cropper: start crop (image bytes + aspectRatio)
    Cropper->>Screen: ShowImageCrop dialog
    User->>Screen: Confirm crop
    Screen->>VM: CropResult.Success (cropped bytes)
    VM->>API: multipartRequestBody(croppedBytes, name, extension)
    API-->>VM: Upload response
    VM->>Screen: Update UI state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • kartikey004
  • biplab1

Poem

🐰 A Rabbit’s Crop & Bytes
I nibble bytes instead of files,
Hop through cameras, gallery aisles,
I crop with care, an aspect true,
Then multipart-hop straight to you,
A tiny hop—upload delight! 📸✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Migrate Image Cropping to a KMP-Supported Library' accurately describes the main objective of the pull request, which is to migrate image cropping functionality to improve Kotlin Multiplatform (KMP) compatibility and remove unsupported filesystem operations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/MultipartRequestBody.kt`:
- Around line 28-29: Normalize and validate the extension before using it to
build fileName and to call getMimeTypeFromPlatformFile: trim whitespace, strip
any leading '.' from the extension, and if the resulting extension is blank use
the original name without adding a trailing '.' (i.e. keep fileName = name) and
pass a safe empty/fallback value into getMimeTypeFromPlatformFile so MIME lookup
won't misbehave; apply the same normalization logic to both places where
fileName and mimeType are constructed (the occurrences around fileName and
mimeType and the later similar block at lines 46-47) and update references to
the normalized extension variable rather than the raw extension.

In `@feature/client/src/commonMain/composeResources/values/strings.xml`:
- Line 385: The string resource "camera" has a leading space in its value ("
Camera") causing uneven rendering; update the string with the exact resource
name string name="camera" to remove the leading whitespace so the value becomes
"Camera" (no leading space), and save the change so UI option lists render
correctly.

In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsViewModel.kt`:
- Around line 71-78: The call to imageFile.readBytes() happens outside the
DataState/error-handling boundary and can throw before saveClientImage's
try/catch runs; move or wrap the file-reading so any IO errors are thrown inside
the coroutine that collects uploadClientImageUseCase. Specifically, in
saveClientImage (the coroutine that calls uploadClientImageUseCase) ensure you
read the file bytes and build the multipartRequestBody (or use runCatching/try
around imageFile.readBytes()) inside that coroutine/try block so exceptions from
imageFile.readBytes() are caught and converted to a DataState error before
calling uploadClientImageUseCase.

In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientEditProfile/ClientProfileEditScreen.kt`:
- Around line 329-335: The "More" bottom-sheet option
(MifosBottomSheetOptionItem with label
stringResource(Res.string.client_signature_more) and icon MifosIcons.MoreHoriz)
is currently tappable but has an empty onClick; hide or disable it until real
behavior exists by either removing it from the options list or wrapping it in a
boolean guard/feature flag (e.g., showClientSignatureMore) and only add the
MifosBottomSheetOptionItem when the flag is true, or set a disabled/visible
property if the composable supports it; also add a brief TODO referencing
implementation work for the onClick handler so future maintainers know why it
was hidden.

In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientEditProfile/ClientProfileEditViewModel.kt`:
- Around line 196-205: Inside the MutableStateFlow.update block, avoid using the
captured outer `state` variable which can cause stale-state overwrites; instead
use the lambda parameter `it` to perform an atomic transformation: compute
newDialogState based on `it.dialogState` (checking for
ClientProfileEditState.DialogState.ShowStatusDialog), set `profileImage` to
imageToByteArray(result.data), and return `it.copy(...)` with the updated
profileImage and dialogState so updates are applied atomically.

In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersAddUpdate/ClientIdentifiersAddUpdateViewModel.kt`:
- Around line 542-549: ClientIdentifiersAddUpdateViewModel currently silently
no-ops when state.documentImageFile is null because create/update is guarded
with state.documentImageFile?.let { ... } and has no else branch; update the
create and update paths (where createDocument(...) and the corresponding
updateDocument(...) call are guarded) to handle the null case by emitting a UI
error/validation event (e.g., call the ViewModel's existing
showValidationError/emitUiEvent or create a submitDocumentError event) so the
user receives feedback like "Please attach a document image" instead of nothing;
locate the guards around state.documentImageFile, createDocument,
route.uniqueKeyForHandleDocument and the matching update call and add an else
branch that sends the error event.
- Around line 281-283: The code in ClientIdentifiersAddUpdateViewModel that
builds multipart metadata currently substitutes empty strings for name and
extension (the assignments where name = name ?: "" and extension =
state.fileExtension ?: "" and the similar block around lines 330-332); instead
validate that name and state.fileExtension are present and non-empty before
creating the metadata—if either is blank, surface a validation error
(return/fail/emit UI state) or skip the upload rather than sending empty
fallbacks, and update any callers to handle that validation result; locate the
metadata construction logic in the ViewModel (the method that sets
description/extension/name for the upload) and replace the empty-string defaults
with explicit checks and early error handling.

In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientSignature/ClientSignatureScreen.kt`:
- Around line 157-160: The call to MifosImageCropperDialog uses a named
parameter (state = it) followed by a positional parameter (AspectRatio(2, 1)),
which is invalid in Kotlin; update the call sites (ClientSignatureScreen's
MifosImageCropperDialog and the identical usage in ClientProfileEditScreen) to
pass the aspect ratio as a named argument (e.g., aspectRatio = AspectRatio(2,
1)) so that all arguments after the named parameter are also named.

In
`@feature/document/src/commonMain/kotlin/com/mifos/feature/document/documentDialog/DocumentDialogViewModel.kt`:
- Around line 78-83: DocumentDialogViewModel calls file.readBytes() directly
inside multipartRequestBody creation (in both createDocument and updateDocument
paths), which can throw and bypass the DataState.Error UI update; wrap the file
read in a try/catch and handle IO failures before constructing the multipart
body: call file.readBytes() inside a guarded block (catching
IOException/Throwable), set the view state to DataState.Error (or appropriate
error type) and return/early-exit so multipartRequestBody (and subsequent calls)
are not executed; apply the same pattern to the updateDocument flow to ensure UI
state is updated on file-read failures.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57bd303 and 2f876c6.

📒 Files selected for processing (16)
  • core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/icon/MifosIcons.kt
  • core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosImageCropperDialog.kt
  • core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/MultipartRequestBody.kt
  • feature/client/src/androidMain/kotlin/com/mifos/feature/client/utils/OpenFileInDefaultExternalApp.android.kt
  • feature/client/src/commonMain/composeResources/values/strings.xml
  • feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsViewModel.kt
  • feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientEditProfile/ClientProfileEditScreen.kt
  • feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientEditProfile/ClientProfileEditViewModel.kt
  • feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersAddUpdate/ClientIdentifiersAddUpdateViewModel.kt
  • feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersAddUpdate/ClientIdentitiesAddUpdateScreen.kt
  • feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientSignature/ClientSignatureScreen.kt
  • feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientSignature/ClientSignatureViewModel.kt
  • feature/client/src/commonMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientViewModel.kt
  • feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/PlatformFile.kt
  • feature/client/src/nativeMain/kotlin/com/mifos/feature/client/utils/PlatformCameraLauncher.native.kt
  • feature/document/src/commonMain/kotlin/com/mifos/feature/document/documentDialog/DocumentDialogViewModel.kt
💤 Files with no reviewable changes (1)
  • feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/PlatformFile.kt

Comment thread feature/client/src/commonMain/composeResources/values/strings.xml Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientEditProfile/ClientProfileEditScreen.kt (1)

327-333: ⚠️ Potential issue | 🟡 Minor

Disable or hide the “More” option until behavior exists.

This option is clickable but has no action, creating a dead-end interaction.

Suggested patch
-            MifosBottomSheetOptionItem(
-                label = stringResource(Res.string.client_signature_more),
-                icon = MifosIcons.MoreHoriz,
-                onClick = {
-                    // it implement further
-                },
-            )
+            // TODO: Re-enable "More" option once its action is implemented.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientEditProfile/ClientProfileEditScreen.kt`
around lines 327 - 333, The "More" option renders a clickable
MifosBottomSheetOptionItem with label
stringResource(Res.string.client_signature_more) but has an empty onClick
handler; hide or disable it until behavior exists by either: a) wrap the
MifosBottomSheetOptionItem in a conditional so it is not composed (e.g., if
(showSignatureMore) { MifosBottomSheetOptionItem(...) }), or b) set a disabled
state on the item (if MifosBottomSheetOptionItem supports an enabled/disabled
parameter) and remove the onClick body; update the location where
MifosBottomSheetOptionItem is declared in ClientProfileEditScreen so the "More"
entry is not clickable until you implement its behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientEditProfile/ClientProfileEditScreen.kt`:
- Around line 281-284: The call to MifosImageCropperDialog mixes a named
argument (state = it) with a positional one (AspectRatio(1, 1)) which won't
compile; update the invocation to pass the AspectRatio as a named parameter
(e.g., aspectRatio = AspectRatio(1, 1)) so both arguments are named and the call
in ClientProfileEditScreen using MifosImageCropperDialog compiles.

---

Duplicate comments:
In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientEditProfile/ClientProfileEditScreen.kt`:
- Around line 327-333: The "More" option renders a clickable
MifosBottomSheetOptionItem with label
stringResource(Res.string.client_signature_more) but has an empty onClick
handler; hide or disable it until behavior exists by either: a) wrap the
MifosBottomSheetOptionItem in a conditional so it is not composed (e.g., if
(showSignatureMore) { MifosBottomSheetOptionItem(...) }), or b) set a disabled
state on the item (if MifosBottomSheetOptionItem supports an enabled/disabled
parameter) and remove the onClick body; update the location where
MifosBottomSheetOptionItem is declared in ClientProfileEditScreen so the "More"
entry is not clickable until you implement its behavior.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f876c6 and 7c8a836.

📒 Files selected for processing (1)
  • feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientEditProfile/ClientProfileEditScreen.kt

@sonarqubecloud

Copy link
Copy Markdown

@therajanmaurya therajanmaurya left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: Migrate Image Cropping to a KMP-Supported Library

Summary

This PR migrates from platform-specific image handling to a KMP-compatible approach using crop-krop-ui library. The core change is shifting from PlatformFile to ByteArray + metadata pattern, which enables proper cross-platform compatibility.


✅ What's Working Well

  1. Clean API Migration: The multipartRequestBody function now accepts raw bytes with explicit metadata - this is the right pattern for KMP, avoiding platform-specific file APIs.

  2. Proper ImageCropper Integration: The imageCropper() state is correctly held in ViewModel and passed to composables. The crop dialog flow is well-structured.

  3. Consistent Pattern Across ViewModels: All ViewModels (ClientDetailsViewModel, ClientSignatureViewModel, CreateNewClientViewModel, DocumentDialogViewModel) now use the same pattern with file.readBytes(), file.name, and file.extension.

  4. Platform Camera Launcher: The native implementation in PlatformCameraLauncher.native.kt properly wraps rememberCameraPickerLauncher.

  5. Removed Dead Code: PlatformFile.kt with its TODO comments is appropriately deleted.


⚠️ Suggestions for Improvement

1. Error Message Hardcoded

ClientProfileEditViewModel.kt:149 - The "Unexpected error" string should be extracted to a string resource for i18n consistency.

2. "More" Option Without Implementation

ClientProfileEditScreen.kt:309-312 - The "More" bottom sheet option has an empty onClick handler with comment "it implement further". Consider either removing this option until implemented or adding a proper TODO comment explaining the plan.

3. CropState Held in Data Class

ClientProfileEditViewModel.kt:305 - Holding stateful objects like ImageCropper in a data class may cause issues with equals/hashCode and copy semantics. Consider moving cropState outside the data class or making it a separate property.


✅ Architecture Compliance

  • Follows existing MVI pattern ✅
  • Uses Koin DI correctly ✅
  • Proper separation of UI and business logic ✅
  • KMP-compatible (no platform-specific code in common) ✅

📊 Verdict

LGTM - The migration is well-executed with consistent patterns across all affected files. The suggestions above are minor improvements and don't block merging.

@therajanmaurya
therajanmaurya merged commit 2986eba into openMF:development Mar 1, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants