Skip to content

Fix cold-start file sharing to open Submit expense - #101208

Open
nabi-ebrahimi wants to merge 18 commits into
Expensify:mainfrom
nabi-ebrahimi:fix/cold-start-file-share-submit-expense
Open

nabi-ebrahimi wants to merge 18 commits into
Expensify:mainfrom
nabi-ebrahimi:fix/cold-start-file-share-submit-expense

Conversation

@nabi-ebrahimi

@nabi-ebrahimi nabi-ebrahimi commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

Fixed Android cold-start file sharing by persisting the incoming ACTION_SEND attachment before React Native initializes. After persistence succeeds, the activity is initialized with new-expensify://share/root as its initial URL, rather than relying on a re-entrant deep link that can arrive before React Native is ready and be dropped.

The native intent handlers now report whether attachment persistence succeeded and open the Share flow only after a successful handoff. The native share module safely handles missing, empty, and incomplete MIME data.

ShareRootPage now safely handles native responses returned as an array, object, JSON string, or null; normalizes MIME types; and shows an attachment error for invalid or unavailable files instead of leaving the Share flow indefinitely on its loading skeleton.

Fixed Issues

$ #101194
PROPOSAL: N/A

Tests

  1. On Android Standalone/New Expensify, force quit the app.
  2. Share a receipt image from Android Gallery and select New Expensify.
  3. Verify the app opens the Share flow with the shared file processed and the Submit expense tab selected.
  4. Open New Expensify first, then share a receipt image.
  5. Verify the existing warm-start share flow still opens with the shared file handled and the Submit tab active.
  • Verify that no errors appear in the JS console

Offline tests

  1. Disable network connectivity and force quit New Expensify.
  2. Share a receipt image from Android Gallery and select New Expensify.
  3. Verify the Share flow launches with the shared file handled and does not remain stuck on the loading skeleton.
  4. Open New Expensify first with network connectivity still disabled, then share an image.
  5. Verify the existing warm-start share flow still opens with the shared file handled and does not remain stuck on the loading skeleton.

QA Steps

// TODO: These must be filled out, or the issue title must include "[No QA]."

  1. On staging, force quit New Expensify on Android Standalone/New Expensify.
  2. Share a supported receipt image from the Android Gallery and choose New Expensify.
  3. Verify the Share flow opens instead of the default screen.
  4. Verify the receipt is attached and the Submit expense tab is selected.
  5. Repeat the cold-start test several times to check for intermittent failures.
  6. Repeat steps 1–5 on Android HybridApp.
  7. Launch the app first, share the same receipt, and verify the warm-start flow remains unchanged.
  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above

  • I wrote clear testing steps that cover the changes made in this PR

    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms

  • I ran the tests on all platforms & verified they passed on:

    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)

  • I followed proper code patterns (see Reviewing the code)

    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers

  • I followed the guidelines as stated in the Review Guidelines

  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)

  • If a new CSS style is added I verified that:

    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:

    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.

  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)

  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.

  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.

  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:

    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
    • If the PR adds or modifies the UI:
  • I asked an AI agent to review the changes for accessibility issues and addressed its findings.

  • I tested with a screen reader (VoiceOver on macOS) and verified all new/changed elements are reachable with a logical focus order.

  • I verified all new/changed elements have meaningful accessible names and roles.

  • I verified state changes are announced (e.g. checked/unchecked, expanded/collapsed, selected).

  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.

  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Screen_Recording_20260916_120030_New.Expensify.Dev.mp4
Android: mWeb Chrome

N/A

iOS: Native
Screen.Recording.2026-09-16.at.12.12.22.PM.mp4
iOS: mWeb Safari

N/A

MacOS: Chrome / Safari

N/A

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ This PR is possibly changing native code and/or updating libraries, it may cause problems with HybridApp. Please check if any patch updates are required in the HybridApp repo and run an AdHoc build to verify that HybridApp will not break. Ask Contributor Plus for help if you are not sure how to handle this. ⚠️

@codecov

codecov Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
src/libs/ShareActionHandlerModule/index.ts 0.00% <ø> (ø)
src/libs/ShareActionHandlerModule/utils.ts 100.00% <100.00%> (ø)
src/pages/Share/ShareRootPage.tsx 0.00% <0.00%> (ø)
... and 238 files with indirect coverage changes

@nabi-ebrahimi
nabi-ebrahimi marked this pull request as ready for review September 16, 2026 12:12
@nabi-ebrahimi
nabi-ebrahimi requested review from a team as code owners September 16, 2026 12:12
@melvin-bot
melvin-bot Bot requested review from dmkt9 and flaviadefaria and removed request for a team September 16, 2026 12:13
@melvin-bot

melvin-bot Bot commented Sep 16, 2026

Copy link
Copy Markdown

@dmkt9 Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot
melvin-bot Bot removed the request for review from a team September 16, 2026 12:13
Comment thread src/pages/Share/ShareRootPage.tsx Outdated
ShareActionHandler.processFiles((processedFiles) => {
const tempFile = Array.isArray(processedFiles) ? processedFiles.at(0) : (JSON.parse(processedFiles) as ShareTempFile);
ShareActionHandler.processFiles((processedFiles: unknown) => {
let tempFile: ShareTempFile | undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ PERF-2 (docs)

The callback runs the full parsing pipeline (array handling, isShareTempFile type guards, and a JSON.parse of the string branch) before checking if (errorTitle) { return; }. When errorTitle is already set, the result is discarded anyway, so all of that work is wasted. errorTitle is available up front, so this cheap guard should short-circuit before the expensive parsing.

Move the guard to the top of the callback:

ShareActionHandler.processFiles((processedFiles: unknown) => {
    if (errorTitle) {
        return;
    }

    let tempFile: ShareTempFile | undefined;
    if (Array.isArray(processedFiles)) {
        // ...parse...
    }
    // ...
});

Reviewed at: 1ba8767 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

@nabi-ebrahimi

Copy link
Copy Markdown
Contributor Author

@dmkt9, Gentle bump. thanks.

@nabi-ebrahimi

Copy link
Copy Markdown
Contributor Author

@dmkt9, Friendly bump. thanks.

object FileUtils {
private const val tag = "FileUtils"
private const val directoryName = "Expensify"
private const val shareDirectoryName = "sharedFiles"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do we need this changes?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This directory is specifically used for temporary files received through the Android share intent, so renaming it to sharedFiles isolates these attachments from the app's general Expensify internal storage directory.

During testing, I verified that reusing the existing Expensify folder causes the shared attachment to fail validation (triggering the corrupted-attachment error) because temporary and general storage contents conflict when cleared. Using a dedicated sharedFiles directory ensures the attachment is cleanly copied, validated, and processed. Additionally, this aligns directly with the iOS share extension implementation, which already utilizes sharedFiles.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think this explanation matches the current implementation/history.

The existing Expensify directory appears to have been introduced specifically for Android share-intent temporary files, and I couldn't find any other Android code using that subdirectory as general app storage. clearTemporaryFiles() already clears that directory before copying the newly shared attachment into it, so it's unclear what "temporary and general storage contents" would conflict.

Also, the new implementation still clears the legacy Expensify directory in clearInternalStorageDirectory(), so if that directory actually contained general app data, those contents would still be deleted.

Using sharedFiles is reasonable for clearer semantics and consistency with iOS, but could you clarify the concrete cause of the validation failure you observed when using Expensify? For example, which other files were present there and how did they cause the newly copied attachment to fail validation?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Comment on lines +10 to +12
when(intent.action) {
Intent.ACTION_SEND -> {
handleSingleFileIntent(intent, context)
onCompleted()
super.clearTemporaryFiles(context)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If the action isn't Intent.ACTION_SEND, the temporary files aren't cleared, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. I moved clearTemporaryFiles() prior to the intent.action evaluation to preserve the previous cleanup behavior across all intents handled by FileIntentHandler, including non-ACTION_SEND intents. This guarantees that stale temporary files are consistently cleared regardless of whether the incoming intent is ACTION_SEND, ACTION_SEND_MULTIPLE, or another unhandled action.

@dmkt9

dmkt9 commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

@MelvinBot review

@MelvinBot

Copy link
Copy Markdown
Contributor

Code review

The approach is sound. Saving the shared file before React Native starts, then setting new-expensify://share/root as the initial URL, avoids dropping the deep link on cold start. I found no blocking bugs. Please address these before approval:

Should fix

  1. Duplicate cleanup call. TextIntentHandler.kt:11-14 calls clearTemporaryFiles twice. The old call on line 11 was left in, so it still runs for non-ACTION_SEND intents. Remove line 11, as you did in FileIntentHandler.
  2. Old share files are never deleted. FileUtils.kt:16 renames the storage folder from Expensify to sharedFiles. After upgrading, files left in filesDir/Expensify stay on the device forever. Keep the old name, or explain the rename and add a one-time cleanup.
  3. Wildcard MIME check is too loose. ShareRootPage.tsx:136-138 compares startsWith('image') instead of startsWith('image/'). Because image/* is in SHARE_FILE_MIMETYPE, any image/... type now passes (for example, image/svg+xml). Before, only exact matches passed. Please confirm this is intended.
  4. No unit tests. The checklist says tests were added, but the diff has none. Move the response parsing and MIME normalization into a pure helper and test the array, object, JSON string, null, and type; charset cases.
  5. HybridApp not verified. This PR changes the IntentHandler.handle signature (IntentHandler.kt:10) and MainActivity. The HybridApp build checks were skipped. QA step 6 expects the fix on HybridApp. Please run an AdHoc build to confirm it compiles and that the cold-start fix actually applies there.

Nits

  • ShareActionHandlerModule.kt:46-58: a zero-byte text/* file goes down the text path, which returns the internal file path as the shared text. Use the text fallback only when !file.exists(), and return null for empty files.
  • ShareRootPage.tsx:129-144: the wrong-type path calls setIsFileReady(true), but the missing-file path does not. Both navigate away right away, so drop the call or add it to both.
  • The InboxTabSelector.tsx change is unrelated, and main already fixes it. Merge main to remove it from the diff.
  • The QA Steps section still contains the // TODO template line.

I did not run a web test because this fix covers the Android share intent, which web can't exercise.


view run

@nabi-ebrahimi

Copy link
Copy Markdown
Contributor Author

Code review

The approach is sound. Saving the shared file before React Native starts, then setting new-expensify://share/root as the initial URL, avoids dropping the deep link on cold start. I found no blocking bugs. Please address these before approval:

Should fix

  1. Duplicate cleanup call. TextIntentHandler.kt:11-14 calls clearTemporaryFiles twice. The old call on line 11 was left in, so it still runs for non-ACTION_SEND intents. Remove line 11, as you did in FileIntentHandler.
  2. Old share files are never deleted. FileUtils.kt:16 renames the storage folder from Expensify to sharedFiles. After upgrading, files left in filesDir/Expensify stay on the device forever. Keep the old name, or explain the rename and add a one-time cleanup.
  3. Wildcard MIME check is too loose. ShareRootPage.tsx:136-138 compares startsWith('image') instead of startsWith('image/'). Because image/* is in SHARE_FILE_MIMETYPE, any image/... type now passes (for example, image/svg+xml). Before, only exact matches passed. Please confirm this is intended.
  4. No unit tests. The checklist says tests were added, but the diff has none. Move the response parsing and MIME normalization into a pure helper and test the array, object, JSON string, null, and type; charset cases.
  5. HybridApp not verified. This PR changes the IntentHandler.handle signature (IntentHandler.kt:10) and MainActivity. The HybridApp build checks were skipped. QA step 6 expects the fix on HybridApp. Please run an AdHoc build to confirm it compiles and that the cold-start fix actually applies there.

Nits

  • ShareActionHandlerModule.kt:46-58: a zero-byte text/* file goes down the text path, which returns the internal file path as the shared text. Use the text fallback only when !file.exists(), and return null for empty files.
  • ShareRootPage.tsx:129-144: the wrong-type path calls setIsFileReady(true), but the missing-file path does not. Both navigate away right away, so drop the call or add it to both.
  • The InboxTabSelector.tsx change is unrelated, and main already fixes it. Merge main to remove it from the diff.
  • The QA Steps section still contains the // TODO template line.

I did not run a web test because this fix covers the Android share intent, which web can't exercise.

view run

Addressed the applicable feedback:

  • Removed the duplicate cleanup call in TextIntentHandler while retaining the initial cleanup so non-ACTION_SEND intents still clear stale share data.
  • Kept sharedFiles for new shared attachments and added cleanup for stale files left in the previous Expensify directory.
  • Empty shared text files now return null instead of being treated as text content.
  • Tightened wildcard MIME matching to require the image/ prefix and removed an unnecessary loading-state update from the immediate error path.
    I did not restrict valid image/* MIME types further, since Android can provide generic image MIME types and the wildcard is intentional. HybridApp verification and test coverage still need to be completed separately; the unrelated InboxTabSelector diff will disappear after merging/rebasing main.

@nabi-ebrahimi

Copy link
Copy Markdown
Contributor Author

@dmkt9, thanks for your review! I’ve addressed all your feedback. Please take another look when you have a chance.

Comment on lines +114 to +117
if (!destinationFile.exists() || destinationFile.length() == 0L) {
destinationFile.delete()
return null
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Empty/corrupted files are silently dropped instead of displaying an attachment error

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you for pointing this out. When saving an empty or corrupted shared attachment failed, the native handler returned false, which prevented the Share route from opening and caused the attachment to be silently discarded.

I have updated both the file and text share handlers to proceed to the Share route upon receiving an ACTION_SEND intent, even if attachment persistence fails. Because temporary share data is cleared beforehand, processFiles() resolves to null, allowing ShareRootPage to display the existing corrupted-attachment error UI. Valid attachments continue to follow the standard save-and-open flow.

Comment on lines +28 to +32
val mimeType = try {
context.contentResolver.getType(fileUri)
} catch (exception: Exception) {
null
} ?: intent.type

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do we need to prioritize contentResolver.getType over intent.type? If contentResolver.getType resolves to the wrong type while intent.type is correct, we could incorrectly report a wrong file type error. Also, I haven't seen any bug reports related to intent.type.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you for pointing this out. The MIME-type precedence change is indeed not required for the cold-start fix.

I have updated both the file and text share handlers to prioritize intent.type whenever available, preserving the established behavior. contentResolver.getType() is now utilized strictly as a fallback for cases where the share intent does not provide a MIME type.

val editor = sharedPreferences.edit()
editor.clear()
editor.apply()
editor.commit()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I was also quite concerned about this earlier. If the file is large enough, it could delay startup until we reach BootSplash

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you for pointing this out. I have restored apply() for clearing the previous share data.

Since apply() updates the value in memory immediately for the current process, the cold-start share handoff remains intact while avoiding unnecessary synchronous disk I/O on the main thread prior to BootSplash initialization.

@dmkt9

dmkt9 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

@nabi-ebrahimi Can you check whether this is caused by our code? I see that it always displays a loading state and an empty list at the beginning:

https://github.com/user-attachments/assets/c5d6dab2-8e33-4102-b606-a27fe89049c4

I see it on the latest main as well.

@dmkt9

dmkt9 commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

@MelvinBot review

@MelvinBot

Copy link
Copy Markdown
Contributor

Code review (re-review of 1b7804a)

Not ready to approve yet. Most of the earlier feedback is fixed. A failed share is still silently dropped, there are still no unit tests, and HybridApp is still unverified.

Earlier feedback

  • ✅ Duplicate clearTemporaryFiles call is removed.
  • ✅ The MIME wildcard check now matches the image/ prefix.
  • ✅ Zero-byte files now return null, and the wrong-type path no longer calls setIsFileReady.
  • ❌ No unit tests. The checklist says tests were added, but the diff has none. Move the response parsing and MIME normalization in ShareRootPage into a pure helper and test it.
  • ❌ HybridApp not verified. The HybridApp build checks are still skipped. Please run an AdHoc build, since QA step 6 expects the fix on HybridApp.
  • ⚠️ InboxTabSelector.tsx is still in the diff. Merge main to drop it.

On the open review threads

  1. Failed shares are silently dropped (FileIntentHandler.kt:13-14, FileUtils.kt:114). I agree with the reviewer. When the copy fails or the file is empty, handle returns false. On a cold start, the app opens to the inbox. On a warm start, nothing happens. Before this PR, the warm path still opened the Share flow. ShareRootPage now shows the corrupted-attachment error when the native response is null. So still open share/root when the copy fails, and let that error appear.
  2. contentResolver.getType before intent.type (FileIntentHandler.kt:29, TextIntentHandler.kt:66). I agree with the reviewer. The cold-start bug doesn't need this change. ShareActionHandlerModule already resolves empty or */*-style types from the file extension. The swap also lets the saved type differ from the intent.type that IntentHandlerFactory used to pick the handler. Use intent.type first, and call getType only when intent.type is null or a wildcard.
  3. Folder rename (FileUtils.kt:16-17). I agree with the reviewer. No other Android code in App uses filesDir/Expensify, and clearTemporaryFiles empties it before each copy. Keep the old name and drop legacyShareDirectoryName, unless you can share steps that reproduce the corrupted-attachment error with the old folder.
  4. Copy on the main thread before startup (MainActivity.kt:44-50). The cost isn't new. Before this PR, a cold start already copied the file on the main thread, right after super.onCreate. The copy now runs before BootSplash.init, so the system starting window stays up a bit longer. I wouldn't block on this, but please check the startup time with a file close to the size limit.

view run

@melvin-bot

melvin-bot Bot commented Oct 14, 2026

Copy link
Copy Markdown

Hey, I noticed you changed src/languages/en.ts in a PR from a fork. For security reasons, translations are not generated automatically for PRs from forks.

If you want to automatically generate translations for other locales, an Expensify employee will have to:

  1. Look at the code and make sure there are no malicious changes.
  2. Run the Generate static translations GitHub workflow. If you have write access and the K2 extension, you can simply click: [this button]

Alternatively, if you are an external contributor, you can run the translation script locally with your own OpenAI API key. To learn more, try running:

npx bun ./scripts/generateTranslations.ts --help

Typically, you'd want to translate only what you changed by running npx bun ./scripts/generateTranslations.ts --compare-ref main

@github-actions

Copy link
Copy Markdown
Contributor

This PR adds a new Onyx.connectWithoutView call, so I've requested a review from the Onyx performance reviewers (@tgolen, @mountiny, @luacmartins, @chuckdries) — a review from any one of them is enough. Please add a link in your PR description to the Slack discussion where the @frontend-performance team approved using connectWithoutView here.

@nabi-ebrahimi
nabi-ebrahimi force-pushed the fix/cold-start-file-share-submit-expense branch from 2dcd06b to 1b8bc19 Compare October 14, 2026 13:01

@justinpersaud justinpersaud left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bad merge? Why are there so many changes?

@AndrewGable
AndrewGable removed their request for review October 14, 2026 15:29
@luacmartins
luacmartins removed their request for review October 14, 2026 16:50
@chuckdries
chuckdries removed their request for review October 14, 2026 19:21
@stephanieelliott
stephanieelliott removed their request for review October 14, 2026 20:51
@nabi-ebrahimi

Copy link
Copy Markdown
Contributor Author

@dmkt9, Gentle bump. thanks.

@dmkt9

dmkt9 commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

@nabi-ebrahimi Can you take a look at this comment? #101208 (comment)

@tgolen
tgolen removed their request for review October 16, 2026 15:52
@nabi-ebrahimi

Copy link
Copy Markdown
Contributor Author

I don't think this explanation matches the current implementation/history.

The existing Expensify directory appears to have been introduced specifically for Android share-intent temporary files, and I couldn't find any other Android code using that subdirectory as general app storage. clearTemporaryFiles() already clears that directory before copying the newly shared attachment into it, so it's unclear what "temporary and general storage contents" would conflict.

Also, the new implementation still clears the legacy Expensify directory in clearInternalStorageDirectory(), so if that directory actually contained general app data, those contents would still be deleted.

Using sharedFiles is reasonable for clearer semantics and consistency with iOS, but could you clarify the concrete cause of the validation failure you observed when using Expensify? For example, which other files were present there and how did they cause the newly copied attachment to fail validation?

@dmkt9, You’re right—the Expensify directory was originally introduced for Android share-intent temporary files, not general app storage. I re-tested the flow with the existing directory and confirmed that shared attachments are copied and processed correctly.
I reverted the sharedFiles rename and removed the legacy-directory cleanup, keeping the cold-start fix focused on attachment persistence and Share-route initialization.

please take another look when you get a chance thanks.

Intent.ACTION_SEND -> {
handleSingleFileIntent(intent, context)
onCompleted()
handleSingleFileIntent(intent, context, shouldLaunchActivity)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NAB since it's existing code: functions should be named after what they do, not what they handle.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks! I renamed the private helper methods to reflect what they do: saving the shared file or text data. No behavior was changed.

handleSingleFileIntent(intent, context)
onCompleted()
handleSingleFileIntent(intent, context, shouldLaunchActivity)
// Open the Share screen even if persisting the attachment fails so it can display an error instead of silently discarding the share.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This comment makes me wonder if the handleSingleFileIntent call should be in a try block and onCompleted should be in a finally block. But then I look and see that that's not the behavior we have here - an exception thrown in handleSingleFileIntent would result in onCompleted not being called.

So, I suppose this comment left me with more questions than answers:

  • How do we know if persisting the attachment failed?
  • When is shouldLaunchActivity false?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good questions!

  1. How do we know if persisting the attachment failed?
    Expected persistence failures in saveSharedFile() / saveSharedText() return normally rather than throwing. For a warm start, execution then continues to onCompleted(), which opens the Share screen. Since no valid shared file was saved, the Share screen displays the attachment error instead of silently dropping the share.
  2. When is shouldLaunchActivity false?
    shouldLaunchActivity is false during a cold start. In that case, MainActivity processes the share intent before React Native initializes and sets new-expensify://share/root as the activity’s initial URL, rather than calling onCompleted().
    I updated the inline comments in FileIntentHandler.kt and TextIntentHandler.kt to clarify this flow.

} catch (e: Exception) {
null
}
val extension = if (mimeType != null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Here's a nice bit of Kotlin syntactic sugar you can use here:

val extension = mimeType?.let(mimeTypeMap::getExtensionFromMimeType) ?: "bin"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Nice suggestion, updated the MIME type extension fallback to use Kotlin’s null-safe let syntax. Behavior is unchanged.

override fun onCreate(savedInstanceState: Bundle?) {
val initialIntent = intent
val isColdShareIntent = initialIntent?.action == Intent.ACTION_SEND
if (isColdShareIntent && initialIntent != null && handleIntent(initialIntent, shouldLaunchActivity = false)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this conditional can be simplified?

Suggested change
if (isColdShareIntent && initialIntent != null && handleIntent(initialIntent, shouldLaunchActivity = false)) {
if (isColdShareIntent && handleIntent(initialIntent, shouldLaunchActivity = false)) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good suggestion, removed the redundant null check. isColdShareIntent can only be true when initialIntent is an ACTION_SEND intent, so the behavior is unchanged.

@nabi-ebrahimi

Copy link
Copy Markdown
Contributor Author

@roryabraham, thanks for your review! I’ve addressed all your feedback.

@dmkt9, could you please take a look at the latest changes when you have a chance? Thanks!

This branch has not been deployed

No deployments
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.

5 participants