You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Due for payment 2026-09-17] [$250] iOS - Scan - App crashes when creating an expense with 30 HEIC images #96404
Version Number: 9.4.36-0 Reproducible in staging?: Yes Reproducible in production?: Yes If this was caught during regression testing, add the test name, ID and link from BrowserStack:#95813 Email or phone of affected tester (no customers):applausetester+js2026yyyy@applause.expensifail.com Issue reported by: Applause Internal Team Bug source: Pull Request QA execution Device used: iPhone 13/18.6.2 App Component: Other
Action Performed:
Precondition:
Download the HEIC image, create 30 copies of it, and save them on your device (File).
Steps :
Launch the Expensify app.
Log in with a valid high-traffic account.
Tap FAB > Create Expense > Scan.
Tap "Choose file".
Select the 30 HEIC image copies you created in the precondition.
Complete the expense creation flow.
Expected Result:
The expense should be created successfully, and the app should remain stable without crashing.
Actual Result:
The app crashes while creating the expense with the 30 imported HEIC images.
When 30 HEIC images are selected in the Scan flow, the app decodes all 30 full-resolution images into native memory simultaneously, causing an iOS out-of-memory (jetsam / std::bad_alloc) crash.
The multi-file scan hands the selection to validateFiles in useFilesValidation.tsx. Validation itself is deliberately sequential to keep only one decoded image in memory at a time (see the explicit comment at useFilesValidation.tsx:181-184) — but the HEIC→JPEG conversion step immediately undoes that protection by fanning out every file at once:
Evidence (file:line)
Unbounded parallel conversion — all HEIC files are converted at once with Promise.all, no concurrency cap: useFilesValidation.tsx:221-253
Peak memory scales linearly with the number of selected HEIC images (≈ count × width × height × 4 bytes for the decoded RGBA bitmaps), so 30 × ~12MP images blows past the iOS per-app memory budget.
What changes do you think we should make in order to solve the problem?
Cap the concurrency of HEIC conversion instead of converting the whole batch in parallel. Replace the unbounded Promise.all at useFilesValidation.tsx:221 with either strictly sequential conversion (matching the sequential-validation rationale already documented above it) or a small concurrency pool (e.g. 2–3 at a time) so only a bounded number of full-resolution bitmaps are held in memory at once. Apply the same bound to the parallel loop in AttachmentPicker/index.native.tsx:233-285, since the gallery picker is an independent OOM vector for the same scenario.
What alternative solutions did you explore? (Optional)
Downscale/resize during conversion (pass a resize op to ImageManipulator.manipulate) so each decoded bitmap is smaller — reduces per-image memory but doesn't bound total concurrency.
Lower MAX_FILE_LIMIT for HEIC batches — a blunt mitigation that reduces the user-facing limit rather than fixing the memory behavior.
Next Steps for Contributor+ team: To accept:@MelvinBot implement [this](https://github.com/Expensify/App/issues/96404) to create a draft PR. To refine:@MelvinBot <your feedback> To reject: Explain why you are rejecting Melvin's proposal.
Please re-state the problem that we are trying to solve in this issue.
On iOS, selecting 30 HEIC copies via "Choose file" when creating a Scan expense crashes the app during the expense creation flow, instead of successfully creating the expense.
What is the root cause of that problem?
useFilesValidation's validateAndResizeFiles is deliberately designed to avoid decoding multiple full‑resolution images at the same time — the initial per‑file validation loop explicitly runs sequentially (for...of with await inside the loop) specifically to keep peak memory bounded, because decoding a whole batch concurrently multiplies memory usage by the number of selected files and can kill the app/tab:
However, the HEIC-to-JPEG conversion step that runs right after does the opposite: it fires off a Promise.all over every file needing conversion, so all HEIC files are converted concurrently:
Log.warn('HEIC conversion failed, falling back to original file',{fileName: file.name});
convertedFiles.push(file);
resolve();
},
});
}),
),
);
Each conversion (native path) calls ImageManipulator.manipulate(...).renderAsync() then .saveAsync(), which decodes the full-resolution HEIC image into memory, renders it, and re-encodes it as JPEG:
Log.warn('Error converting HEIC/HEIF to JPEG',{error: errinstanceofError ? err.message : String(err)});
onError(err,file);
})
.finally(()=>{
onFinish();
});
};
When 30 HEIC images are selected at once, Promise.all kicks off all 30 of these decode/render/encode operations in parallel instead of one at a time, spiking native memory far above what the sequential validation loop above it was designed to guarantee, and the iOS app is killed by the OS for exceeding its memory budget (the same class of issue the sequential-validation comment already warns about, just reintroduced in the conversion step).
What changes do you think we should make in order to solve the problem?
Convert the HEIC files sequentially (one at a time), matching the pattern already used for the initial validation loop, instead of kicking off all conversions in parallel with Promise.all.
Log.warn('HEIC conversion failed, falling back to original file',{fileName: file.name});
convertedFiles.push(file);
resolve();
},
});
}),
),
);
if(filesToConvert.length>0){showLoader();constconvertedFilesToResize: FileObject[]=[];constconvertedFiles: FileObject[]=[];// Convert sequentially so only one HEIC image is decoded/rendered in memory at a time,// for the same reason the validation loop above avoids Promise.all.for(constfileoffilesToConvert){// eslint-disable-next-line no-await-in-loopawaitnewPromise<void>((resolve)=>{convertHeicImage(file,{onSuccess: (convertedFile)=>{if(validationState.isValidatingReceipts&&convertedFile.size&&convertedFile.size>CONST.API_ATTACHMENT_VALIDATIONS.RECEIPT_MAX_SIZE){convertedFilesToResize.push(convertedFile);resolve();return;}if(!validationState.isValidatingReceipts&&convertedFile.size&&convertedFile.size>CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE){collectedErrors.current.push({error: CONST.FILE_VALIDATION_ERRORS.FILE_TOO_LARGE,isValidatingMultipleFiles: validationState.isValidatingMultipleFiles,});resolve();return;}convertedFiles.push(convertedFile);resolve();},onError: ()=>{Log.warn('HEIC conversion failed, falling back to original file',{fileName: file.name});convertedFiles.push(file);resolve();},});});}
This bounds peak memory during HEIC conversion to a single image at a time, regardless of how many HEIC files are selected, eliminating the OOM crash for large batches (e.g. 30 files) while keeping the rest of the conversion/resize logic unchanged.
Please re-state the problem that we are trying to solve in this issue.
On the iOS app, picking 30 HEIC images via FAB > Create Expense > Scan > "Choose file" crashes the app. The crash happens while the selected files are being processed for the scan flow, before the expenses are created.
What is the root cause of that problem?
Every selected .heic file fails validation with the dedicated HEIC_OR_HEIF_IMAGE error in validateAttachmentFile (src/libs/validateAttachmentFile.ts L29-L31), so in validateAndResizeFiles in src/hooks/useFilesValidation.tsx all 30 files are routed into filesToConvert. That array is then converted with an unbounded Promise.all fan-out, which starts all 30 HEIC-to-JPEG conversions concurrently:
On native, each conversion goes through convertImageWithManipulator in src/libs/fileDownload/heicConverter/index.native.ts, which drives expo-image-manipulator:
On the iOS side, ImageManipulator.manipulate(uri) loads the full resolution UIImage and immediately queues an ImageFixOrientationTransformer that redraws the whole bitmap (ImageManipulatorModule.swift in expo-image-manipulator@56.0.16), and renderAsync() returns an ImageRef, a SharedRef<UIImage> whose native footprint is cgImage.bytesPerRow * cgImage.height (ImageRef.swift). A standard 12 MP iPhone HEIC (4032x3024) decodes to roughly 46 MB of RGBA, so 30 concurrent conversions hold about 1.4 GB of decoded bitmaps at once, plus transient orientation-fix copies and JPEG encode buffers. Because Promise.all keeps every context and rendered ImageRef live simultaneously, Hermes GC cannot reclaim any of them mid-flight, and convertImageWithManipulator never calls release() on either shared object, so even completed conversions keep their UIImage resident until a later GC pass. The process blows past the jetsam memory limit on an iPhone 13 and iOS kills the app (native OOM, no JS error). Note this is not a regression from PR #95813 (that PR only removed retryParams from Onyx error payloads); git history shows the Promise.all conversion block ships since ec9439950e5 ("feat: convert and resize files when validating"), which matches the issue being reproducible in production. The codebase already recognizes this exact hazard: the validation loop directly above was deliberately made sequential, and PDF thumbnails are validated one at a time, but the HEIC conversion and the Promise.allSettled(filesToResize.map((file) => resizeImageIfNeeded(file))) resize step at L266 were left as unbounded fan-outs.
What changes do you think we should make in order to solve the problem?
We should bound peak memory at the layer that fans out the work, following the precedent already documented in the same function:
// Validate sequentially so only one image is decoded and held in memory at a time. Validating the
// whole batch with Promise.all multiplies peak memory by the number of selected files (each pass
// decodes the full-resolution image), which can kill the WebContent process on memory-constrained
// mobile Safari when 3+ photos are selected — the tab reloads and the attachments are lost.
for(const[index,file]offiles.entries()){
In validateAndResizeFiles, replace the Promise.all over filesToConvert with a sequential loop so only one image is decoded and held at a time, keeping the existing onSuccess/onError bodies unchanged:
The same treatment applies to the Promise.allSettled resize fan-out at L266, since converted JPEGs over RECEIPT_MAX_SIZE trigger a second concurrent full-resolution decode pass with the identical hazard. Additionally, in convertImageWithManipulator in heicConverter/index.native.ts we should release the native bitmaps deterministically instead of waiting for GC, since both the context and the rendered image are Expo SharedObjects that expose release() (expo-modules-coreSharedObject.d.ts):
With sequential conversion plus explicit release, peak native memory is one decoded image regardless of batch size, so 30 files convert in roughly 10 to 15 seconds behind the existing full-screen loader (showLoader() already covers this path) instead of crashing. The change is safe for every caller of useFilesValidation (attachments, share, odometer, scan) because it only changes scheduling, not results: convertedFiles/convertedFilesToResize are populated identically and sortFilesByOriginalOrder already restores original ordering, and single-file callers are unaffected since a one-element loop is equivalent to today. It also hardens mWeb Safari, which shares this hook and has the same fan-out through the web converter.
What alternative solutions did you explore? (Optional)
A small concurrency pool (2 or 3 conversions in flight) would be slightly faster on multi-core devices, but it adds scheduling code for marginal gain and diverges from the sequential pattern already established for validation and PDF parsing in this hook, so a follow-up could tune it if the loader duration ever matters. We also considered downscaling inside the manipulator chain before rendering, but that would change output resolution for files that are within size limits and does not fix the real problem, which is concurrency times full-resolution decode.
🚨 Edited by proposal-police: This proposal was edited at 2026-07-30 11:57:00 UTC.
Proposal
What is the root cause of that problem?
Multi-scan intentionally allows selecting up to 30 files at once — ScannerControlsBar passes CONST.API_ATTACHMENT_VALIDATIONS.MAX_FILE_LIMIT (30) as the picker's fileLimit:
validateAndResizeFiles() in useFilesValidation validates that whole selection sequentially — an explicit, documented fix for exactly this class of native-memory issue. But HEIC post-processing doesn't follow that safeguard: it converts every HEIC file concurrently with Promise.all(), then resizes oversized results concurrently with Promise.allSettled():
Starting 30 of these at once spikes native image memory. I confirmed this precisely with a Jest test that mocks the native converter to hold every call open and records peak concurrency: against the current code, selecting 30 HEIC files drives concurrency to exactly 30 — every file starts converting before any of them finish.
This concurrency was introduced by 73b5ca33fda3506f138a20787c0d69dbb3b28aef. Two later commits by the same author sequentialized other parts of this exact hook for the identical reason, but never touched HEIC conversion or resize:
Live verification, and why the reported symptom differs by platform. I reproduced this hands-on in iOS simulators (iPhone 17 Pro and iPhone 15) with 30 real HEIC receipt photos at genuine 12MP resolution, through the actual "Choose file" flow. On a simulator this doesn't reproduce as a hard crash — it reproduces as an indefinitely stuck loading spinner followed by a false "You appear to be offline" banner, which traces to the same root cause under a different resource ceiling:
The spinner is useFilesValidation's own showLoader() (
), rendered via FullScreenLoadingIndicator with no timeout or fallback UI — if Promise.all() takes a very long time to settle under contention, it just spins with no recovery path.
While the JS thread/RN bridge is saturated marshaling 30 concurrent native callbacks, the app's api/Ping reachability poll (10s timeout, src/libs/NetworkState.ts:339-356) and other in-flight requests can't be serviced in time. A failed Ping flips internetUnreachable immediately (src/libs/NetworkState.ts:371-373), and/or FailureTracker trips its sustained-failure hard stop after 3 failures in a 10s window (src/libs/FailureTracker.ts:54-76) — either way NetworkState.getIsOffline() returns true and OfflineIndicator shows the banner, even though the device's actual connection never dropped.
A real iPhone 13 has a hard, OS-enforced memory ceiling (jetsam) that a 30x concurrent decode burst blows through almost immediately, so the OS kills the app outright — that's the reported "crash." A Mac simulator process shares the host Mac's much larger RAM pool and isn't subject to that same jetsam ceiling, so instead of being killed it just gets CPU/bridge-starved: same unbounded-concurrency defect, two different failure ceilings. This is also consistent with the issue being reported only on iOS: App, never web/desktop.
I applied the fix below and verified live, before/after, in the running app: with the fix, the same 30-HEIC-file flow completes normally (files convert one at a time, loader resolves, no offline banner); without it, the stuck-spinner-then-offline pattern reproduces reliably.
What changes do you think we should make in order to solve the problem?
Make filesToConvert and filesToResize processing sequential in useFilesValidation, mirroring the pattern already established for initial validation and PDF thumbnails in this same file:
All existing success/error handling, loader behavior, updateFileOrderMapping() calls, and the final result shape stay the same — only the scheduling changes from concurrent to sequential, bounding peak decoding memory (and JS-thread/bridge contention) to one image at a time regardless of how many of the 30 selected files are HEIC.
I verified this with a Jest test (tests/unit/hooks/useFilesValidation.test.tsx) asserting peak concurrent conversions never exceeds 1 across 30 mocked HEIC files, and that all 30 come back in original order. It fails (max: 30) against current code and passes after this change. I also verified live in an iOS simulator, before and after, with 30 real HEIC receipt images through the actual picker flow: the stuck-loader-then-offline-banner pattern reproduces without the fix and is gone with it. QA should repeat the reported flow with 30 HEIC files on a real device, plus mixed HEIC/JPEG batches, conversion failures, and oversized receipts.
What alternative solutions did you explore? (Optional)
I considered lowering the 30-file limit specifically for HEIC images. That would reduce the peak memory spike but would contradict the advertised receipt limit and only mask the unbounded concurrency rather than fix it. I also considered adding shouldUseGoBackButton to the loader as a defensive fallback so users aren't stuck on a dead spinner even if some other slow path occurs — but that only treats a symptom (a stuck spinner) and doesn't address why the loader stalls, so I kept the fix scoped to the actual concurrency defect. Sequential processing fixes the root cause without rejecting valid receipts, and matches the fix pattern this codebase already uses twice in the same file for the same class of bug.
🚨 Edited by proposal-police: This proposal was edited at 2026-07-17 14:54:27 UTC.
Proposal
Please re-state the problem that we are trying to solve in this issue.
On iOS, selecting 30 HEIC images via FAB > Create expense > Scan > "Choose file" crashes the app during the expense-creation flow. The crash is an out-of-memory kill (jetsam), not a JS error.
What is the root cause of that problem?
Structural cause: useFilesValidation enforces a deliberate one-image-at-a-time memory discipline for validation and PDF parsing (added in PR #93234 to stop exactly this OOM class), but that discipline was never applied to its two remaining batch stages — HEIC conversion and image resizing — which decode all selected files concurrently. Chain:
The Scan picker feeds picked files into useFilesValidation's validateFiles:
Log.warn('HEIC conversion failed, falling back to original file',{fileName: file.name});
convertedFiles.push(file);
resolve();
},
});
}),
),
);
On iOS each conversion is ImageManipulator.manipulate(uri).renderAsync() → saveAsync(JPEG); renderAsync decodes the full-resolution image into a native bitmap (~48 MB for a 12 MP photo) and holds it until saveAsync finishes:
Log.warn('Error converting HEIC/HEIF to JPEG',{error: errinstanceofError ? err.message : String(err)});
onError(err,file);
})
.finally(()=>{
onFinish();
});
};
30 concurrent full-res decodes ≈ 1.4 GB of transient native memory — far past the foreground jetsam limit on an iPhone 13 → iOS kills the process. That is the "crash".
Second affected stage in the SAME repro: converted JPEGs over RECEIPT_MAX_SIZE (10 MB) queue into convertedFilesToResize (
The same file already serializes its other two decode-heavy stages for precisely this reason: sequential validation "so only one image is decoded and held in memory at a time" (
). The conversion/resize batches are the two spots the discipline missed.
This explains every symptom: iOS-only (HEIC is the iPhone capture format; the cost is native decode memory), exactly 30 images (peak memory scales linearly and 30 is the max the gate admits), a hard crash rather than an error dialog (jetsam), and why QA of PR #95813 still crashed — that PR fixed a different OOM (retryParams duplication in Onyx at submit time); this one happens earlier, at conversion. I verified these two lines are the only batch callers of convertHeicImage/resizeImageIfNeeded in the codebase, so the hook is the single convergence point: every consumer (Scan global-create/from-report/edit-receipt, confirmation step, odometer, report attachments, Share flow, receipt drag-and-drop) inherits the fix, and web does too (the web converter decodes via wasm/canvas under the same unbounded Promise.all — the mWeb Safari tab-reload class of #93234).
What changes do you think we should make in order to solve the problem?
In useFilesValidation.tsx, serialize the two remaining batch stages, matching the pattern the file already uses twice:
Replace Promise.all(filesToConvert.map(...)) at L221 with a sequential loop (branching inside onSuccess/onError stays identical):
Replace Promise.allSettled(filesToResize.map(...)) at L266 with the same for…of + await resizeImageIfNeeded(file) in try/catch, keeping the existing error mapping.
Peak memory drops from N decoded bitmaps to 1 regardless of batch size; wall time is nearly unchanged since the 30 decodes already contended for the same CPU/memory. Regression safety in one sentence: no signature, ordering, or state-shape changes — result order is already restored by originalFileOrder + sortFilesByOriginalOrder, so all consumer screens and both platforms are untouched.
Why the obvious spot-fixes fail: lowering the file cap breaks an intentional product limit (MAX_FILE_LIMIT: 30) this QA test explicitly exercises; lowering JPEG save quality changes nothing because the OOM happens at decode time — renderAsync materializes the full-resolution bitmap before any save option applies; and a concurrency pool of N still spikes to N full bitmaps while diverging from the one-at-a-time pattern this file already established for this exact bug class.
What alternative solutions did you explore? (Optional)
Bounded concurrency (N = 2–3) — works, but still multiplies peak memory by N; PR #93234 chose strict serialization for the sibling stages, and consistency keeps the memory bound self-evident.
Downsample during conversion (pass target dimensions to manipulate) so converted receipts land under 10 MB — trims the resize stage but leaves concurrent full-res decodes unbounded, so the crash remains.
Secondary (optional, not the crash): the native converter stamps the converted JPEG with the ORIGINAL HEIC byte size (size: file.size,
Please re-state the problem that we are trying to solve in this issue.
On iOS, when a user selects 30 HEIC images in the Scan flow (FAB → Create Expense → Scan → Choose file), the app crashes while creating the expense. Expected: the expense creates successfully and the app stays stable regardless of how many HEIC images are selected (up to the configured file-limit cap).
What is the root cause of that problem?
In src/hooks/useFilesValidation.tsx the sequential for…of loop at line 185 was intentionally written to avoid multiplying peak memory by the batch size (see the comment on lines 181–184), but the HEIC conversion step immediately below it does the exact opposite: await Promise.all(filesToConvert.map(file => new Promise(resolve => convertHeicImage(file, …)))) at lines 221–253 fires all conversions concurrently. Each convertHeicImage in src/libs/fileDownload/heicConverter/index.native.ts (lines 32–35) calls ImageManipulator.manipulate(sourceUri).renderAsync().then(m => m.saveAsync({format: JPEG})), which fully decodes the source HEIC into an in-memory bitmap before re-encoding to JPEG. With 30 large HEIC images decoded in parallel, iOS jetsams the app before the batch finishes — reproducing the reported crash.
What changes do you think we should make in order to solve the problem?
src/hooks/useFilesValidation.tsx lines 221–253: replace the Promise.all(filesToConvert.map(...)) with a sequential for (const file of filesToConvert) loop that awaits each convertHeicImage (wrapped in a small promise adapter), mirroring the sequential validation loop above and its rationale in the lines 181–184 comment. This keeps peak memory to one decoded image at a time.
Alternatively (or in addition), process the conversions in small concurrency-limited chunks (e.g. 2–3 at a time) if pure-sequential wall time is too slow — but sequential is safest given the same file already documents the batch-decode failure mode.
Apply the same sequential/limited-concurrency treatment to the Promise.allSettled(filesToResize.map(resizeImageIfNeeded)) at line 266: resizeImageIfNeeded also decodes the full image, so a 30-file batch of images that pass the HEIC step but need resizing will hit the same crash on iOS.
Verify the MAX_FILE_LIMIT guard at line 354 permits ≥30 files (the repro used 30 and passed the guard, so no change needed unless product wants a lower cap for HEIC specifically).
Add a short comment above the new sequential loop pointing at the existing rationale block so future refactors don't regress it back to Promise.all.
What alternative solutions did you explore? (Optional)
Selecting 30 HEIC receipts via Scan -> Choose file crashes the iOS app during local preprocessing, before expenses are created. The kill is a native jetsam OOM, not a JS exception.
Choose file goes through the document picker into useFilesValidation on the Scan create path:
// Validate sequentially so only one image is decoded and held in memory at a time. Validating the
// whole batch with Promise.all multiplies peak memory by the number of selected files (each pass
// decodes the full-resolution image), which can kill the WebContent process on memory-constrained
// mobile Safari when 3+ photos are selected — the tab reloads and the attachments are lost.
That protection is undone immediately afterward. Every HEIC lands in filesToConvert and is converted with unbounded Promise.all, so all 30 ImageManipulator.renderAsync() calls decode full-resolution bitmaps concurrently (~40 to 50 MB RGBA each for a 12MP photo):
Two compounding defects make serialization alone incomplete:
The converter never calls release() on the Expo SharedObject context or the rendered ImageRef. Those hold the native UIImage until Hermes GC, so even a sequential loop can retain decoded bitmaps across the batch.
The converted file is stamped with size: file.size (the HEIC byte size). ImageResult from saveAsync has no size field, so the real JPEG size is never read. HEIC is more compact than JPEG, so the later RECEIPT_MAX_SIZE check under-reports and can skip the resize safety-net:
The same unbounded decode fan-out also exists for resize (Promise.allSettled) and for the gallery picker path in AttachmentPicker (not the Choose file path, but the same crash class):
// react-native-image-picker incorrectly changes file extension without transcoding the HEIC file, so we are doing it manually if we detect HEIC signature
Replace HEIC Promise.all and resize Promise.allSettled with sequential loops. Map order inside each iteration (the post-Promise.all index mapping is wrong when some files resize and some do not):
// react-native-image-picker incorrectly changes file extension without transcoding the HEIC file, so we are doing it manually if we detect HEIC signature
for(constassetofassets){if(!asset.uri){continue;}if(!asset.type?.startsWith('image')){processedAssets.push(processAssetWithFallbacks(asset));continue;}constisHEIC=awaitverifyFileFormat({fileUri: asset.uri,formatSignatures: CONST.HEIC_SIGNATURES});if(!isHEIC||!asset.uri){processedAssets.push(processAssetWithFallbacks(asset));continue;}constimageManipulatorContext=ImageManipulator.manipulate(asset.uri);try{constmanipulatedImage=awaitimageManipulatorContext.renderAsync();try{constmanipulationResult=awaitmanipulatedImage.saveAsync({format: SaveFormat.JPEG});processedAssets.push({uri: manipulationResult.uri,fileName: manipulationResult.uri.substring(manipulationResult.uri.lastIndexOf('/')+1).split('?').at(0),type: 'image/jpeg',width: manipulationResult.width,height: manipulationResult.height,});}finally{manipulatedImage.release();}}catch(error){Log.warn('Failed to convert HEIC image, falling back to original',{error: errorinstanceofError ? error.message : String(error),});processedAssets.push(processAssetWithFallbacks(asset));}finally{imageManipulatorContext.release();}}
What alternative solutions did you explore? (Optional)
Reminder: Please use plain English, be brief and avoid jargon. Feel free to use images, charts or pseudo-code if necessary. Do not post large multi-line diffs or write walls of text. Do not create PRs unless you have been hired for this job.
Activity
applause-bot commentedon Jul 17, 2026
While Applause doesn't embed attachments for this bug, here are the links for quick view:
https://platform.applause.com/services/links/v1/external/99091c60ec5e0e05fea302764e3fe9b84dc277cd8b0c494e35b6b9f5ed105d3a
https://platform.applause.com/services/links/v1/external/9bacb23a05be6fe5811915af0555ca4b015a580aef9c659413e794a6544e861b
https://platform.applause.com/services/links/v1/external/4d74061cec1a0b5e8510a57721013ceea1f1fd415f445f30460e875d6c0397c8
MelvinBot commentedon Jul 17, 2026
Proposal
What is the root cause of that problem?
When 30 HEIC images are selected in the Scan flow, the app decodes all 30 full-resolution images into native memory simultaneously, causing an iOS out-of-memory (jetsam /
std::bad_alloc) crash.The multi-file scan hands the selection to
validateFilesinuseFilesValidation.tsx. Validation itself is deliberately sequential to keep only one decoded image in memory at a time (see the explicit comment atuseFilesValidation.tsx:181-184) — but the HEIC→JPEG conversion step immediately undoes that protection by fanning out every file at once:Evidence (file:line)
Promise.all, no concurrency cap:useFilesValidation.tsx:221-253heicConverter/index.native.ts:32-35. Resize only happens after conversion (useFilesValidation.tsx:263-266), so it can't cap the peak spike.MAX_FILE_LIMITcheck and all flow into the parallel converter:CONST/index.ts:360launchImageLibrarypath) — also converts HEIC per-asset without awaiting:AttachmentPicker/index.native.tsx:233-285Peak memory scales linearly with the number of selected HEIC images (≈ count × width × height × 4 bytes for the decoded RGBA bitmaps), so 30 × ~12MP images blows past the iOS per-app memory budget.
What changes do you think we should make in order to solve the problem?
Cap the concurrency of HEIC conversion instead of converting the whole batch in parallel. Replace the unbounded
Promise.allatuseFilesValidation.tsx:221with either strictly sequential conversion (matching the sequential-validation rationale already documented above it) or a small concurrency pool (e.g. 2–3 at a time) so only a bounded number of full-resolution bitmaps are held in memory at once. Apply the same bound to the parallel loop inAttachmentPicker/index.native.tsx:233-285, since the gallery picker is an independent OOM vector for the same scenario.What alternative solutions did you explore? (Optional)
ImageManipulator.manipulate) so each decoded bitmap is smaller — reduces per-image memory but doesn't bound total concurrency.MAX_FILE_LIMITfor HEIC batches — a blunt mitigation that reduces the user-facing limit rather than fixing the memory behavior.Related issues
bad_allocOOM crash (Sentry APP-63S); same crash signature this scenario would produce.Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/96404)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
abbasifaizan70 commentedon Jul 17, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
On iOS, selecting 30 HEIC copies via "Choose file" when creating a Scan expense crashes the app during the expense creation flow, instead of successfully creating the expense.
What is the root cause of that problem?
useFilesValidation'svalidateAndResizeFilesis deliberately designed to avoid decoding multiple full‑resolution images at the same time — the initial per‑file validation loop explicitly runs sequentially (for...ofwithawaitinside the loop) specifically to keep peak memory bounded, because decoding a whole batch concurrently multiplies memory usage by the number of selected files and can kill the app/tab:App/src/hooks/useFilesValidation.tsx
Lines 181 to 214 in 58ec58c
However, the HEIC-to-JPEG conversion step that runs right after does the opposite: it fires off a
Promise.allover every file needing conversion, so all HEIC files are converted concurrently:App/src/hooks/useFilesValidation.tsx
Lines 216 to 253 in 58ec58c
Each conversion (native path) calls
ImageManipulator.manipulate(...).renderAsync()then.saveAsync(), which decodes the full-resolution HEIC image into memory, renders it, and re-encodes it as JPEG:App/src/libs/fileDownload/heicConverter/index.native.ts
Lines 32 to 54 in 58ec58c
When 30 HEIC images are selected at once,
Promise.allkicks off all 30 of these decode/render/encode operations in parallel instead of one at a time, spiking native memory far above what the sequential validation loop above it was designed to guarantee, and the iOS app is killed by the OS for exceeding its memory budget (the same class of issue the sequential-validation comment already warns about, just reintroduced in the conversion step).What changes do you think we should make in order to solve the problem?
Convert the HEIC files sequentially (one at a time), matching the pattern already used for the initial validation loop, instead of kicking off all conversions in parallel with
Promise.all.App/src/hooks/useFilesValidation.tsx
Lines 216 to 253 in 58ec58c
This bounds peak memory during HEIC conversion to a single image at a time, regardless of how many HEIC files are selected, eliminating the OOM crash for large batches (e.g. 30 files) while keeping the rest of the conversion/resize logic unchanged.
Test Branch
Link
Demo
Screen.Recording.2026-07-17.at.8.34.59.PM.mp4
MobileMage commentedon Jul 17, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
On the iOS app, picking 30 HEIC images via FAB > Create Expense > Scan > "Choose file" crashes the app. The crash happens while the selected files are being processed for the scan flow, before the expenses are created.
What is the root cause of that problem?
Every selected
.heicfile fails validation with the dedicatedHEIC_OR_HEIF_IMAGEerror invalidateAttachmentFile(src/libs/validateAttachmentFile.tsL29-L31), so invalidateAndResizeFilesinsrc/hooks/useFilesValidation.tsxall 30 files are routed intofilesToConvert. That array is then converted with an unboundedPromise.allfan-out, which starts all 30 HEIC-to-JPEG conversions concurrently:App/src/hooks/useFilesValidation.tsx
Lines 221 to 225 in 90ec1a9
On native, each conversion goes through
convertImageWithManipulatorinsrc/libs/fileDownload/heicConverter/index.native.ts, which drivesexpo-image-manipulator:App/src/libs/fileDownload/heicConverter/index.native.ts
Lines 32 to 35 in 90ec1a9
On the iOS side,
ImageManipulator.manipulate(uri)loads the full resolutionUIImageand immediately queues anImageFixOrientationTransformerthat redraws the whole bitmap (ImageManipulatorModule.swiftinexpo-image-manipulator@56.0.16), andrenderAsync()returns anImageRef, aSharedRef<UIImage>whose native footprint iscgImage.bytesPerRow * cgImage.height(ImageRef.swift). A standard 12 MP iPhone HEIC (4032x3024) decodes to roughly 46 MB of RGBA, so 30 concurrent conversions hold about 1.4 GB of decoded bitmaps at once, plus transient orientation-fix copies and JPEG encode buffers. BecausePromise.allkeeps every context and renderedImageReflive simultaneously, Hermes GC cannot reclaim any of them mid-flight, andconvertImageWithManipulatornever callsrelease()on either shared object, so even completed conversions keep theirUIImageresident until a later GC pass. The process blows past the jetsam memory limit on an iPhone 13 and iOS kills the app (native OOM, no JS error). Note this is not a regression from PR #95813 (that PR only removedretryParamsfrom Onyx error payloads); git history shows thePromise.allconversion block ships sinceec9439950e5("feat: convert and resize files when validating"), which matches the issue being reproducible in production. The codebase already recognizes this exact hazard: the validation loop directly above was deliberately made sequential, and PDF thumbnails are validated one at a time, but the HEIC conversion and thePromise.allSettled(filesToResize.map((file) => resizeImageIfNeeded(file)))resize step at L266 were left as unbounded fan-outs.What changes do you think we should make in order to solve the problem?
We should bound peak memory at the layer that fans out the work, following the precedent already documented in the same function:
App/src/hooks/useFilesValidation.tsx
Lines 181 to 185 in 90ec1a9
In
validateAndResizeFiles, replace thePromise.alloverfilesToConvertwith a sequential loop so only one image is decoded and held at a time, keeping the existingonSuccess/onErrorbodies unchanged:The same treatment applies to the
Promise.allSettledresize fan-out at L266, since converted JPEGs overRECEIPT_MAX_SIZEtrigger a second concurrent full-resolution decode pass with the identical hazard. Additionally, inconvertImageWithManipulatorinheicConverter/index.native.tswe should release the native bitmaps deterministically instead of waiting for GC, since both the context and the rendered image are ExpoSharedObjects that exposerelease()(expo-modules-coreSharedObject.d.ts):With sequential conversion plus explicit release, peak native memory is one decoded image regardless of batch size, so 30 files convert in roughly 10 to 15 seconds behind the existing full-screen loader (
showLoader()already covers this path) instead of crashing. The change is safe for every caller ofuseFilesValidation(attachments, share, odometer, scan) because it only changes scheduling, not results:convertedFiles/convertedFilesToResizeare populated identically andsortFilesByOriginalOrderalready restores original ordering, and single-file callers are unaffected since a one-element loop is equivalent to today. It also hardens mWeb Safari, which shares this hook and has the same fan-out through the web converter.What alternative solutions did you explore? (Optional)
A small concurrency pool (2 or 3 conversions in flight) would be slightly faster on multi-core devices, but it adds scheduling code for marginal gain and diverges from the sequential pattern already established for validation and PDF parsing in this hook, so a follow-up could tune it if the loader duration ever matters. We also considered downscaling inside the manipulator chain before rendering, but that would change output resolution for files that are within size limits and does not fix the real problem, which is concurrency times full-resolution decode.
mukhrr commentedon Jul 17, 2026
🚨 Edited by proposal-police: This proposal was edited at 2026-07-30 11:57:00 UTC.
Proposal
What is the root cause of that problem?
Multi-scan intentionally allows selecting up to 30 files at once —
ScannerControlsBarpassesCONST.API_ATTACHMENT_VALIDATIONS.MAX_FILE_LIMIT(30) as the picker'sfileLimit:App/src/pages/iou/request/step/IOURequestStepScan/components/ScannerControlsBar.tsx
Line 89 in 3a15c17
validateAndResizeFiles()inuseFilesValidationvalidates that whole selection sequentially — an explicit, documented fix for exactly this class of native-memory issue. But HEIC post-processing doesn't follow that safeguard: it converts every HEIC file concurrently withPromise.all(), then resizes oversized results concurrently withPromise.allSettled():App/src/hooks/useFilesValidation.tsx
Lines 216 to 282 in 3a15c17
On iOS, every conversion creates its own
ImageManipulatorcontext, decodes the full-resolution image, and re-encodes it as JPEG:App/src/libs/fileDownload/heicConverter/index.native.ts
Lines 31 to 45 in 3a15c17
Starting 30 of these at once spikes native image memory. I confirmed this precisely with a Jest test that mocks the native converter to hold every call open and records peak concurrency: against the current code, selecting 30 HEIC files drives concurrency to exactly 30 — every file starts converting before any of them finish.
This concurrency was introduced by
73b5ca33fda3506f138a20787c0d69dbb3b28aef. Two later commits by the same author sequentialized other parts of this exact hook for the identical reason, but never touched HEIC conversion or resize:bfe2b34
bf7ea9f
Live verification, and why the reported symptom differs by platform. I reproduced this hands-on in iOS simulators (iPhone 17 Pro and iPhone 15) with 30 real HEIC receipt photos at genuine 12MP resolution, through the actual "Choose file" flow. On a simulator this doesn't reproduce as a hard crash — it reproduces as an indefinitely stuck loading spinner followed by a false "You appear to be offline" banner, which traces to the same root cause under a different resource ceiling:
useFilesValidation's ownshowLoader()(App/src/hooks/useFilesValidation.tsx
Lines 216 to 221 in 3a15c17
FullScreenLoadingIndicatorwith no timeout or fallback UI — ifPromise.all()takes a very long time to settle under contention, it just spins with no recovery path.api/Pingreachability poll (10s timeout,src/libs/NetworkState.ts:339-356) and other in-flight requests can't be serviced in time. A failed Ping flipsinternetUnreachableimmediately (src/libs/NetworkState.ts:371-373), and/orFailureTrackertrips its sustained-failure hard stop after 3 failures in a 10s window (src/libs/FailureTracker.ts:54-76) — either wayNetworkState.getIsOffline()returnstrueandOfflineIndicatorshows the banner, even though the device's actual connection never dropped.A real iPhone 13 has a hard, OS-enforced memory ceiling (jetsam) that a 30x concurrent decode burst blows through almost immediately, so the OS kills the app outright — that's the reported "crash." A Mac simulator process shares the host Mac's much larger RAM pool and isn't subject to that same jetsam ceiling, so instead of being killed it just gets CPU/bridge-starved: same unbounded-concurrency defect, two different failure ceilings. This is also consistent with the issue being reported only on iOS: App, never web/desktop.
I applied the fix below and verified live, before/after, in the running app: with the fix, the same 30-HEIC-file flow completes normally (files convert one at a time, loader resolves, no offline banner); without it, the stuck-spinner-then-offline pattern reproduces reliably.
What changes do you think we should make in order to solve the problem?
Make
filesToConvertandfilesToResizeprocessing sequential inuseFilesValidation, mirroring the pattern already established for initial validation and PDF thumbnails in this same file:All existing success/error handling, loader behavior,
updateFileOrderMapping()calls, and the final result shape stay the same — only the scheduling changes from concurrent to sequential, bounding peak decoding memory (and JS-thread/bridge contention) to one image at a time regardless of how many of the 30 selected files are HEIC.I verified this with a Jest test (
tests/unit/hooks/useFilesValidation.test.tsx) asserting peak concurrent conversions never exceeds 1 across 30 mocked HEIC files, and that all 30 come back in original order. It fails (max: 30) against current code and passes after this change. I also verified live in an iOS simulator, before and after, with 30 real HEIC receipt images through the actual picker flow: the stuck-loader-then-offline-banner pattern reproduces without the fix and is gone with it. QA should repeat the reported flow with 30 HEIC files on a real device, plus mixed HEIC/JPEG batches, conversion failures, and oversized receipts.What alternative solutions did you explore? (Optional)
I considered lowering the 30-file limit specifically for HEIC images. That would reduce the peak memory spike but would contradict the advertised receipt limit and only mask the unbounded concurrency rather than fix it. I also considered adding
shouldUseGoBackButtonto the loader as a defensive fallback so users aren't stuck on a dead spinner even if some other slow path occurs — but that only treats a symptom (a stuck spinner) and doesn't address why the loader stalls, so I kept the fix scoped to the actual concurrency defect. Sequential processing fixes the root cause without rejecting valid receipts, and matches the fix pattern this codebase already uses twice in the same file for the same class of bug.Before:
before.mp4
After:
after.mp4
ahmdshrif commentedon Jul 17, 2026
🚨 Edited by proposal-police: This proposal was edited at 2026-07-17 14:54:27 UTC.
Proposal
Please re-state the problem that we are trying to solve in this issue.
On iOS, selecting 30 HEIC images via FAB > Create expense > Scan > "Choose file" crashes the app during the expense-creation flow. The crash is an out-of-memory kill (jetsam), not a JS error.
What is the root cause of that problem?
Structural cause:
useFilesValidationenforces a deliberate one-image-at-a-time memory discipline for validation and PDF parsing (added in PR#93234to stop exactly this OOM class), but that discipline was never applied to its two remaining batch stages — HEIC conversion and image resizing — which decode all selected files concurrently. Chain:useFilesValidation'svalidateFiles:App/src/pages/iou/request/step/IOURequestStepScan/components/ScanGlobalCreate.tsx
Lines 110 to 127 in 90ec1a9
MAX_FILE_LIMIT, so the batch passes the count gate untrimmed:App/src/CONST/index.ts
Lines 358 to 360 in 90ec1a9
heicis an allowed receipt extension, so each file instead hits the HEIC gate invalidateAttachmentFileand returnsHEIC_OR_HEIF_IMAGE:App/src/libs/validateAttachmentFile.ts
Lines 29 to 31 in 90ec1a9
filesToConvert:App/src/hooks/useFilesValidation.tsx
Lines 203 to 206 in 90ec1a9
Promise.all— 30 conversions in flight at once:App/src/hooks/useFilesValidation.tsx
Lines 221 to 253 in 90ec1a9
ImageManipulator.manipulate(uri).renderAsync()→saveAsync(JPEG);renderAsyncdecodes the full-resolution image into a native bitmap (~48 MB for a 12 MP photo) and holds it untilsaveAsyncfinishes:App/src/libs/fileDownload/heicConverter/index.native.ts
Lines 32 to 54 in 90ec1a9
RECEIPT_MAX_SIZE(10 MB) queue intoconvertedFilesToResize(App/src/hooks/useFilesValidation.tsx
Lines 227 to 231 in 90ec1a9
Promise.allSettled— again the whole batch at once, each doing another full native decode:App/src/hooks/useFilesValidation.tsx
Line 266 in 90ec1a9
App/src/hooks/useFilesValidation.tsx
Lines 181 to 187 in 90ec1a9
App/src/hooks/useFilesValidation.tsx
Lines 409 to 414 in 90ec1a9
This explains every symptom: iOS-only (HEIC is the iPhone capture format; the cost is native decode memory), exactly 30 images (peak memory scales linearly and 30 is the max the gate admits), a hard crash rather than an error dialog (jetsam), and why QA of PR
#95813still crashed — that PR fixed a different OOM (retryParamsduplication in Onyx at submit time); this one happens earlier, at conversion. I verified these two lines are the only batch callers ofconvertHeicImage/resizeImageIfNeededin the codebase, so the hook is the single convergence point: every consumer (Scan global-create/from-report/edit-receipt, confirmation step, odometer, report attachments, Share flow, receipt drag-and-drop) inherits the fix, and web does too (the web converter decodes via wasm/canvas under the same unboundedPromise.all— the mWeb Safari tab-reload class of#93234).What changes do you think we should make in order to solve the problem?
In
useFilesValidation.tsx, serialize the two remaining batch stages, matching the pattern the file already uses twice:Promise.all(filesToConvert.map(...))at L221 with a sequential loop (branching insideonSuccess/onErrorstays identical):Promise.allSettled(filesToResize.map(...))at L266 with the samefor…of+await resizeImageIfNeeded(file)in try/catch, keeping the existing error mapping.Peak memory drops from N decoded bitmaps to 1 regardless of batch size; wall time is nearly unchanged since the 30 decodes already contended for the same CPU/memory. Regression safety in one sentence: no signature, ordering, or state-shape changes — result order is already restored by
originalFileOrder+sortFilesByOriginalOrder, so all consumer screens and both platforms are untouched.Why the obvious spot-fixes fail: lowering the file cap breaks an intentional product limit (
MAX_FILE_LIMIT: 30) this QA test explicitly exercises; lowering JPEG save quality changes nothing because the OOM happens at decode time —renderAsyncmaterializes the full-resolution bitmap before any save option applies; and a concurrency pool of N still spikes to N full bitmaps while diverging from the one-at-a-time pattern this file already established for this exact bug class.What alternative solutions did you explore? (Optional)
#93234chose strict serialization for the sibling stages, and consistency keeps the memory bound self-evident.manipulate) so converted receipts land under 10 MB — trims the resize stage but leaves concurrent full-res decodes unbounded, so the crash remains.size: file.size,App/src/libs/fileDownload/heicConverter/index.native.ts
Lines 36 to 44 in 90ec1a9
> RECEIPT_MAX_SIZEcheck at L227 evaluates the wrong number; worth correcting while in the area.aswin-s commentedon Jul 17, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
On iOS, when a user selects 30 HEIC images in the Scan flow (FAB → Create Expense → Scan → Choose file), the app crashes while creating the expense. Expected: the expense creates successfully and the app stays stable regardless of how many HEIC images are selected (up to the configured file-limit cap).
What is the root cause of that problem?
In
src/hooks/useFilesValidation.tsxthe sequentialfor…ofloop at line 185 was intentionally written to avoid multiplying peak memory by the batch size (see the comment on lines 181–184), but the HEIC conversion step immediately below it does the exact opposite:await Promise.all(filesToConvert.map(file => new Promise(resolve => convertHeicImage(file, …))))at lines 221–253 fires all conversions concurrently. EachconvertHeicImageinsrc/libs/fileDownload/heicConverter/index.native.ts(lines 32–35) callsImageManipulator.manipulate(sourceUri).renderAsync().then(m => m.saveAsync({format: JPEG})), which fully decodes the source HEIC into an in-memory bitmap before re-encoding to JPEG. With 30 large HEIC images decoded in parallel, iOS jetsams the app before the batch finishes — reproducing the reported crash.What changes do you think we should make in order to solve the problem?
src/hooks/useFilesValidation.tsxlines 221–253: replace thePromise.all(filesToConvert.map(...))with a sequentialfor (const file of filesToConvert)loop thatawaits eachconvertHeicImage(wrapped in a small promise adapter), mirroring the sequential validation loop above and its rationale in the lines 181–184 comment. This keeps peak memory to one decoded image at a time.Promise.allSettled(filesToResize.map(resizeImageIfNeeded))at line 266:resizeImageIfNeededalso decodes the full image, so a 30-file batch of images that pass the HEIC step but need resizing will hit the same crash on iOS.MAX_FILE_LIMITguard at line 354 permits ≥30 files (the repro used 30 and passed the guard, so no change needed unless product wants a lower cap for HEIC specifically).Promise.all.What alternative solutions did you explore? (Optional)
samranahm commentedon Jul 17, 2026
Proposal
What is the root cause of that problem?
Selecting 30 HEIC receipts via Scan -> Choose file crashes the iOS app during local preprocessing, before expenses are created. The kill is a native jetsam OOM, not a JS exception.
Choose file goes through the document picker into
useFilesValidationon the Scan create path:App/src/pages/iou/request/step/IOURequestStepScan/components/ScanGlobalCreate.tsx
Lines 110 to 127 in e68dd63
30 is exactly
MAX_FILE_LIMIT, so the batch is not trimmed:App/src/CONST/index.ts
Line 358 in e68dd63
Per-file validation is already sequential on purpose, to keep only one decoded image in memory at a time:
App/src/hooks/useFilesValidation.tsx
Lines 181 to 184 in e68dd63
That protection is undone immediately afterward. Every HEIC lands in
filesToConvertand is converted with unboundedPromise.all, so all 30ImageManipulator.renderAsync()calls decode full-resolution bitmaps concurrently (~40 to 50 MB RGBA each for a 12MP photo):App/src/hooks/useFilesValidation.tsx
Lines 216 to 253 in e68dd63
App/src/libs/fileDownload/heicConverter/index.native.ts
Lines 32 to 45 in e68dd63
Two compounding defects make serialization alone incomplete:
The converter never calls
release()on the ExpoSharedObjectcontext or the renderedImageRef. Those hold the nativeUIImageuntil Hermes GC, so even a sequential loop can retain decoded bitmaps across the batch.The converted file is stamped with
size: file.size(the HEIC byte size).ImageResultfromsaveAsynchas no size field, so the real JPEG size is never read. HEIC is more compact than JPEG, so the laterRECEIPT_MAX_SIZEcheck under-reports and can skip the resize safety-net:App/src/libs/fileDownload/heicConverter/index.native.ts
Lines 36 to 44 in e68dd63
App/src/hooks/useFilesValidation.tsx
Lines 227 to 228 in e68dd63
The same unbounded decode fan-out also exists for resize (
Promise.allSettled) and for the gallery picker path inAttachmentPicker(not the Choose file path, but the same crash class):App/src/hooks/useFilesValidation.tsx
Lines 263 to 266 in e68dd63
App/src/components/AttachmentPicker/index.native.tsx
Lines 234 to 285 in e68dd63
What changes do you think we should make in order to solve the problem?
1.
heicConverter/index.native.ts-convertImageWithManipulatorRelease native bitmaps and stamp the real JPEG size (same
fs.statpattern ascropOrRotateImage):App/src/libs/fileDownload/heicConverter/index.native.ts
Lines 18 to 54 in e68dd63
App/src/libs/cropOrRotateImage/index.native.ts
Lines 34 to 45 in e68dd63
2.
useFilesValidation.tsx-validateAndResizeFilesReplace HEIC
Promise.alland resizePromise.allSettledwith sequential loops. Map order inside each iteration (the post-Promise.allindex mapping is wrong when some files resize and some do not):App/src/hooks/useFilesValidation.tsx
Lines 216 to 282 in e68dd63
3.
AttachmentPicker/index.native.tsx-showImagePickerProcess assets sequentially and release manipulator objects after each HEIC conversion:
App/src/components/AttachmentPicker/index.native.tsx
Lines 234 to 285 in e68dd63
What alternative solutions did you explore? (Optional)
Reminder: Please use plain English, be brief and avoid jargon. Feel free to use images, charts or pseudo-code if necessary. Do not post large multi-line diffs or write walls of text. Do not create PRs unless you have been hired for this job.
TEST BRANCH
IOS.MP4
IOS.Simulator.mp4
IOS.solution.mp4
melvin-bot commentedon Jul 17, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @ahmedGaber93 (
External)99 remaining items
melvin-bot commentedon Sep 16, 2026
Triggered auto assignment to @mallenexpensify (
Awaiting Payment)melvin-bot commentedon Sep 16, 2026
Payment Summary
Resolving PRs:
Upwork Job
BugZero Checklist (@mallenexpensify)
ahmedGaber93 commentedon Sep 16, 2026
No payment for me, @marufsharifi has taken over this.
melvin-bot commentedon Sep 21, 2026
@mallenexpensify Huh... This is 4 days overdue. Who can take care of this?
mallenexpensify commentedon Sep 22, 2026
Payment Summary
Contributor: @nabi-ebrahimi paid $250 via Upwork
Contributor+: @marufsharifi paid $250 via Upwork
Both, can you please accept the job below? Please reply here and tag me once you have.
https://www.upwork.com/jobs/~022102199071142146770
marufsharifi commentedon Sep 22, 2026
@mallenexpensify, I've accepted the offer. thanks.
nabi-ebrahimi commentedon Sep 22, 2026
@mallenexpensify, Accepted the offer. thx.
mallenexpensify commentedon Sep 22, 2026
@abbasifaizan70 @marufsharifi paid, summary updated above. Thx.