Skip to content

feat: add image crop picker module (OK-48227) - #117

Merged
huhuanming merged 6 commits into
mainfrom
feat/image-crop-picker
Sep 18, 2026
Merged

huhuanming merged 6 commits into
mainfrom
feat/image-crop-picker

Conversation

@huhuanming

@huhuanming huhuanming commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds @onekeyfe/react-native-image-crop-picker, a Nitro module that replaces react-native-image-crop-picker 0.51.1 in app-monorepo. It is installed under the react-native-image-crop-picker npm alias, so app imports stay the same.
  • iOS: picking uses PHPickerViewController, which needs no photo library permission. This is the fix for OK-48227: the old library asked for full library access first, and after one accidental "Don't Allow" iOS never asked again, so the OneKey ID avatar and hardware wallpaper entries did nothing on every later tap.
  • iOS: cropping uses a vendored TOCropViewController 3.2.0 (up from the 2.8.0 the app carried as a pod git override), with the OK-51551 crop-box shrink fix re-applied. app-monorepo can drop its Podfile override.
  • Android: the existing flow is ported to Kotlin — the system Photo Picker (no storage or media permission) and uCrop 2.2.11-native, the latest release.
  • Released as 3.0.147, bumping all 41 publishable packages.

Root cause (OK-48227)

react-native-image-crop-picker's iOS openPicker calls [PHPhotoLibrary requestAuthorization:] before showing its own QBImagePickerController, and rejects with E_NO_LIBRARY_PERMISSION when the status is not authorized. iOS shows that system prompt only once. After a user taps "Don't Allow", every later call rejects immediately, and the app has no way to re-request. HardwareHomeScreenModal awaits the promise without a catch, and ProfileEditPage maps unknown errors to a toast, so the button appeared dead — which is what the OK-48227 recording shows.

PHPickerViewController runs in a separate process and returns only the items the user picked, so it needs no authorization at all and the denied state cannot block it.

API

openPicker, openCropper, clean, cleanSingle, and the E_* codes OneKey relies on (E_PICKER_CANCELLED and friends) are preserved; rejections are ImageCropPickerError instances with a code. Multiple selection, video, the camera and includeExif are dropped — OneKey never used them. mediaType, forceJpg and sortOrder are accepted and ignored.

Behavior notes:

  • Results are always JPEG, written to <tmp>/react-native-image-crop-picker/ (iOS) or <cache>/react-native-image-crop-picker/ (Android).
  • width/height lock the crop aspect ratio, and the crop is scaled to exactly that size; free-style crops fit inside it.
  • Photos are decoded at most 4096 px on the long side, so a 48 MP photo no longer needs ~200 MB before cropping.
  • Only one picker or cropper at a time; a second call rejects with E_PICKER_IN_PROGRESS.
  • Android results come back through the activity's ActivityResultRegistry, so no ActivityEventListener is involved and the background React runtime does not matter.

Verification

app-monorepo x (1ba681f9f6) on an iPhone 17 Pro simulator (iOS 26.5), Developer → Gallery → ImageCrop, with Photos access denied for the app the whole time (kTCCServicePhotos|so.onekey.wallet|0).

Baseline, the unmodified branch with react-native-image-crop-picker 0.51.1:

  • First tap shows the system prompt; "Don't Allow" denies it.
  • Every later tap does nothing. LogBox shows Uncaught (in promise): Error: User did not grant library permission.

This package (local build of this branch, installed into app-monorepo):

  • The picker opens immediately with no prompt while Photos is still denied, showing the iOS "Private Access to Photos" picker.
  • Picking a photo opens the cropper with the locked 1:1 box, and Done returns {width: 500, height: 500, mime: "image/jpeg", size: 77531} with base64 data and the file under tmp/react-native-image-crop-picker/. sips confirms the saved JPEG is 500×500.
  • Five rotations in a row keep the crop box at its full size (the OK-51551 regression does not return on 3.2.0).
  • Cancelling the picker, and cancelling the cropper opened from openCropper, both reject with E_PICKER_CANCELLED: User cancelled image selection.
  • Cancelling the cropper that openPicker opened returns to the picker, and picking another photo opens the cropper again.
  • openCropper on the previous result's file:// path opens the cropper with a 3:2 box and returns a JPEG of that size.

Android, on an API 36 emulator with the same Gallery page:

  • The button opens com.google.android.photopicker with no permission dialog ("OneKey Dev will only have access to the photos you select").
  • Picking a photo opens com.yalantis.ucrop.UCropActivity with the locked 1:1 box, and confirming returns {width: 500, height: 500, mime: "image/jpeg", size: 29389} with base64 data, a cropRect, the media-store filename, and the file under the app's cache/react-native-image-crop-picker/.
  • openCropper on that result's file:// path opens uCrop again and returns a 3:2 JPEG.
  • Cancelling uCrop rejects with E_PICKER_CANCELLED: User cancelled image selection.

Not covered: real devices, iPad, iOS versions below 26, Android versions without the system Photo Picker, and the OneKey ID avatar / hardware wallpaper screens themselves (both call the same ImageCrop.openPicker).

Release

Commit Change
4222e4cc7 feat: add image crop picker module
b21fdcd7a chore: bump packages to 3.0.147

Rebased onto main after #116, so 3.0.147 carries that Android font fix. Published from this branch with the latest dist-tag in run 35259897741; the run's registry check passed.

The published @onekeyfe/react-native-image-crop-picker@3.0.147 tarball's ios/, android/src/ and src/index.tsx are byte-identical to this branch. app-monorepo consumes it in app-monorepo#13553, where both platforms were re-verified against the published package.

3.0.148: one cropper screen on both platforms

The 3.0.147 cropper was TOCropViewController's own screen on iOS (a dark toolbar with plain-text Cancel and Done that ignored the app theme) and uCrop's screen on Android (a toolbar check mark plus aspect, rotate and scale tabs). Neither looked like the app, and they looked nothing like each other.

Both platforms now show the same page, drawn by this package:

  • A header with the title centered and OneKey's RotateCounterclockwise icon on the right, the crop area with the crop box 20 pt from every edge, and a footer with capsule Cancel and Confirm buttons. The buttons match the app's large Button: 50 pt tall, 10 pt apart, $bgStrong/$bgPrimary with their pressed colors, and a spinner on Confirm while the image is saved.
  • iOS hosts TOCropView in the new ImageCropperViewController; Android hosts uCrop's UCropView in the new ImageCropperActivity, which replaces UCropActivity.
  • New cropperAppearance option: colorScheme, background, title, icon and button colors, title and button fonts (iOS UIFont(name:), Android ReactFontManager), and a size scale. Colors are CSS hex strings; anything left out falls back to OneKey's light or dark palette.
  • The area outside the crop box shows the page background at 70% instead of a dark blur or black dimming. The border uses the title color, corner handles only show when the box can be resized, and the grid only shows while the image is moved, on both platforms.
  • Android draws edge to edge with bar icons that follow the scheme, keeps the caller's requested orientation (portrait in OneKey), animates the rotate button and drops two-finger rotation, which iOS never had.
  • Removed the options that only styled the old screens: cropperChooseColor, cropperCancelColor, cropperActiveWidgetColor, cropperToolbarColor, cropperToolbarWidgetColor, cropperStatusBarLight, cropperNavigationBarLight, showCropFrame, enableRotationGesture, hideBottomControls, disableCropperColorSetters.

Also fixed on iOS: after cancelling the cropper that openPicker opened, tapping the same photo again did nothing, because PHPickerViewController keeps the photo selected and the first tap only deselected it. On iOS 17 and later the selection is now cleared when returning to the picker. The configuration passes the shared photo library to get asset identifiers, which asks for no permission; the picker still opened without a prompt.

Verification (3.0.148)

app-monorepo #13553 with this branch's package synced into node_modules, Developer → Gallery → ImageCrop, native shells built locally (--shell local):

  • iOS (iPhone 17 Pro simulator, iOS 26.5), light and dark: the header, crop box, dimming and buttons render as above with Roobert. openPicker 500×500 returns a 500×500 JPEG, openCropper 300×200 opens a 3:2 box and returns 300×200, Cancel from openCropper rejects with E_PICKER_CANCELLED, and Cancel from openPicker returns to the picker, where one tap on the same photo now reopens the cropper.
  • Android (API 36 emulator), light and dark: the same layout edge to edge, with dark or light bar icons. 500×500 and 300×200 results have exact sizes, the grid shows while dragging and fades after release, and the back gesture rejects with E_PICKER_CANCELLED.
  • Rotation: one tap turns the image 90° counterclockwise on both platforms. The iOS log showed angle 0 → -90 per tap with the crop still centered, and both outputs matched a counterclockwise rotation of the source photo (mean pixel difference 2.7 on iOS and 3.5 on Android, against 73 or more for clockwise).

Release (3.0.148)

Commit Change
4fd15800e fix: keep iOS log files after the first roll
f2aaa9e74 fix: stop iOS Lite card callbacks from firing twice on NFC cancel
aea437922 feat: unify the image cropper screen with the app on iOS and Android
a2a43c278 chore: bump packages to 3.0.148

The CHANGELOG 3.0.148 section covers all four. Not published yet.

🤖 Generated with Claude Code

huhuanming and others added 2 commits September 18, 2026 02:31
Add @onekeyfe/react-native-image-crop-picker, a Nitro replacement for
react-native-image-crop-picker 0.51.1. app-monorepo installs it under the
react-native-image-crop-picker npm alias, so imports stay the same. It keeps
openPicker, openCropper, clean, cleanSingle and the E_* rejection codes that
OneKey uses, and drops multiple selection, video and the camera.

iOS picks with PHPickerViewController, which runs out of process and needs no
photo library permission (OK-48227). The old library asked for full library
access first, so after one "Don't Allow" iOS never asked again and every later
openPicker call rejected with E_NO_LIBRARY_PERMISSION, which the OneKey ID
avatar and hardware wallpaper entries swallowed.

Cropping uses a vendored TOCropViewController 3.2.0, up from the 2.8.0 the app
carried as a pod override, with the OK-51551 crop box shrink fix re-applied;
upstream still has that bug.

Android ports the existing flow to Kotlin: the system Photo Picker, which needs
no storage or media permission, and uCrop 2.2.11-native. Activity results go
through the activity's ActivityResultRegistry instead of an
ActivityEventListener.

- decode photos at most 4096 px on the long side
- scale aspect-locked crops to exactly the requested size
- add an example page and the 3.0.146 changelog entry

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bump all 41 publishable packages, including the new image crop picker, to
3.0.147.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
huhuanming and others added 4 commits September 18, 2026 14:10
DDLogFileManagerDefault defaults logFilesDiskQuota to 20 MB, the same size
as the configured maximumFileSize, so a single rolled file already filled
the quota. Cleanup then deleted every log file — its "don't delete the
active file" guard only covers unarchived files — and the logger wrote
nothing for the rest of the session, so exported log bundles came back with
no .log files at all.

Size the quota after the retention above (7 files x 20 MB), matching
Android's TOTAL_SIZE_CAP. Reproduced on a simulator with a function-trace
build, which fills 20 MB in about 15 seconds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tapping Cancel on the iOS NFC sheet right after a Lite card connected
crashed the app with SIGABRT in RCTTurboModule.mm ("Callback arg cannot be
called more than once"), seen in the backup and restore flow on 6.6.0.

The user's cancel invalidates the session, and didInvalidateWithError:
reported it as a cancel because sessionType is only reset after the whole
card operation returns. The card operation was still running on another
thread of the concurrent delegate queue. Its APDUs then failed and it
reported a connection failure through the same React Native callback, which
aborts on the second call.

- Take each completion out of completionBlocks under a lock when it is
  delivered, so the card operation and the invalidation can't both
  deliver it.
- Don't treat the app's own invalidateSession as a user cancel. CoreNFC
  reports it with the same code 200, which could race a finished read and
  report it as cancelled.
- Report a connection failure when connectToTag fails or the card is not
  a Lite V1/V2. These paths used to end the session and rely on the racy
  cancel report, or leave the JS promise pending.
- Wrap the React Native callbacks so any repeated result is logged and
  dropped instead of aborting the app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The iOS cropper was TOCropViewController's own screen: a dark toolbar with
plain-text Cancel and Done that ignored the app theme. Android showed
uCrop's screen, with a toolbar check mark and aspect, rotate and scale
tabs, so the two platforms looked unrelated to each other and to the app.

Both platforms now show the same page, drawn by this package:

- iOS hosts TOCropView in ImageCropperViewController, Android hosts
  uCrop's UCropView in ImageCropperActivity. Both have a header with the
  title and OneKey's rotate icon, the crop area with 20pt margins, and a
  footer with capsule Cancel and Confirm buttons that match the app's
  large Button, including pressed colors and a loading spinner.
- cropperAppearance sets the color scheme, colors, fonts and a size
  scale. Colors are CSS hex strings; anything left out falls back to
  OneKey's light or dark palette.
- The area outside the crop box shows the page background at 70% instead
  of a dark blur or black dimming. Corner handles only show when the box
  can be resized, and the grid only shows while the image is moved.
- Android draws edge to edge with bar icons that follow the scheme, keeps
  the caller's requested orientation, animates the rotate button and
  drops two-finger rotation, which iOS never had.
- TOCropOverlayView gains frameColor, gridColor and cornerHandlesHidden.
- Remove the options that styled the old screens.

Also reopen the cropper on iOS when the same photo is tapped again after
cancelling it. PHPicker keeps the photo selected when the cropper is
dismissed back to it, so the first tap only deselected it. On iOS 17 and
later the selection is now cleared; asking for asset identifiers needs
no photo library permission.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sidmorizon

Copy link
Copy Markdown
Contributor

@codex review

@sidmorizon

Copy link
Copy Markdown
Contributor

@codex security review

@sidmorizon

Copy link
Copy Markdown
Contributor

Cursor review failed.

Remote error: cursor_request_failed: HTTP 400 Bad Request: Usage-based pricing required. Background Agent requires at least $2 remaining until your hard limit. Enable usage-based pricing and set a Spend Limit at https://www.cursor.com/dashboard?tab=settings.

@cursor

cursor Bot commented Sep 18, 2026

Copy link
Copy Markdown

Skipping Bugbot: Bugbot is disabled for this repository. Visit the Bugbot dashboard to update your settings.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 18, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-18T09:06:14.105207Z a2a43c2 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a2a43c278c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@huhuanming
huhuanming merged commit dfa0672 into main Sep 18, 2026
4 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.

3 participants