Skip to content

Wildcard deeplinks - #99347

Open
war-in wants to merge 9 commits into
Expensify:mainfrom
software-mansion-labs:war-in/wildcard-deeplinks
Open

war-in wants to merge 9 commits into
Expensify:mainfrom
software-mansion-labs:war-in/wildcard-deeplinks

Conversation

@war-in

@war-in war-in commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

Replaces the hand-maintained NewDot deeplink path allowlists with a wildcard, so every new.expensify.com / staging.new.expensify.com path opens the app and new routes need no native change. iOS: AASA components collapsed to {"/": "*"}. Android: pathPrefix entries replaced by host-only <data>, mirrored in Mobile-Expensify. Expensify Classic links unchanged.

Fixed Issues

$ #99412
PROPOSAL:

MOBILE-EXPENSIFY: https://github.com/Expensify/Mobile-Expensify/pull/14073 needs to be merged simultaneously

Tests

Note on iOS: the AASA is fetched from the server, so the wildcard only takes effect once this branch's .well-known/apple-app-site-association is deployed to new.expensify.com / staging.new.expensify.com. To test before deploy, append ?mode=developer to the applinks: entries in the entitlements and enable Settings > Developer > Associated Domains Development on the device.

  1. Android — previously unlisted NewDot path opens the app (standalone + HybridApp)

    1. Open a path that was not in the old allowlist, e.g. https://new.expensify.com/workspace/<policyID>/members.
    2. Verify that the app opens directly on the Workspace members page instead of the link opening in Chrome.
    3. Repeat with a path that was already in the allowlist, e.g. https://new.expensify.com/r/<reportID>.
    4. Verify that the report opens in the app exactly as before, i.e. no regression on the previously supported routes.
    5. Open a path that is not supported, e.g. https://new.expensify.com/some-unsupported-path
    6. Verify that app opens on the Not found page
  2. Android — Expensify Classic links are unchanged (HybridApp)

    1. Open https://www.expensify.com/inbox.
    2. Verify that the app opens on the OldDot Inbox, i.e. the OldDot deeplink behaviour is unchanged.
    3. Open https://www.expensify.com/expensify-classic/hubs/getting-started/, a Classic path that is not in the OldDot allowlist.
    4. Verify that the link opens in the browser and not in the app, i.e. the wildcard did not leak into the Expensify Classic domains.
  3. iOS — universal links resolve to the app (standalone + HybridApp)

    1. Build and install the app on an iOS device or simulator with the AASA wildcard available (see the note above).
    2. Send yourself a https://new.expensify.com/settings/profile link in Notes or Messages and long-press it.
    3. Verify that the "Open in New Expensify" option is offered and that tapping the link opens the Settings > Profile page in the app.
    4. Repeat with a previously unlisted path such as https://new.expensify.com/workspace/<policyID>/members.
    5. Verify that it also opens in the app rather than Safari.
    6. On the HybridApp build, tap https://www.expensify.com/inbox.
    7. Verify that it opens the OldDot Inbox as before, since www.expensify.com serves its own AASA which this PR does not change.

Offline tests

N/A — the change only touches native link-matching configuration and does not involve API calls or Onyx data.

QA Steps

Same as tests, but Android only

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.
  • 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
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

@war-in

war-in commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

@MelvinBot review

@MelvinBot

Copy link
Copy Markdown
Contributor

🤖 Review: looks safe to ship, with one verification gap to close.

The wildcard change is sound. The one real risk with collapsing the allowlists to * — hijacking a browser/SDK-only callback into the app — is already mitigated in code, and unknown paths degrade gracefully.

Key findings

  • Plaid OAuth is protected. The old iOS AASA deliberately listed only the single narrow path /partners/plaid/oauth_ios (never a broad /partners/*). Wildcarding now captures it, but both deeplink handlers already intercept it and hand it to the native Plaid SDK before navigation runs, so no regression: subscribe.ts:44-53 and Link.ts:519-521. These two guards are the load-bearing mitigation — keep them.
  • Unknown paths degrade gracefully. Any path that now opens the app but has no matching route lands on NotFound via the catch-all config.ts:40 ([SCREENS.NOT_FOUND]: '*'), matching the PR's stated "Not found" expectation.
  • Other callbacks are intended in-app routes, not browser-only: connection-complete / bank-connection-complete / sign-in-with-saml are real registered screens, and native accounting/bank OAuth completes inside an in-app WebView (matches on navState.url), so it never travels as a universal link. No regression there.

Please verify before merging

  1. Backend browser-only redirects. The App repo only surfaces partners/plaid/oauth_ios as a browser-only path on new.expensify.com. Confirm no server-served redirect endpoint on that host (outside this repo) needs to stay in the browser — anything like that would now be pulled into the app.
  2. Native QA is required and not yet evidenced. This is a native-only change (AASA + AndroidManifest.xml); universal links / app links can't be exercised by web automation, and the Screenshots section is still empty. It needs manual verification on iOS + Android, standalone + HybridApp, covering: a previously-unlisted path opening the app, a previously-listed path (no regression), an unsupported path → NotFound, and www.expensify.com Classic links unchanged (per your Test steps). The iOS AASA also only takes effect once this branch's .well-known/apple-app-site-association is deployed, so on-device testing needs the ?mode=developer entitlement workaround you documented.
Config correctness notes
  • iOS {"/": "*"} is valid AASA and matches every path on the domain.
  • Android host-only <data android:scheme="https" android:host="new.expensify.com" /> (+ staging) matches all paths on the host. Note the old Android manifest never listed /partners/* at all, so nothing regresses there; the Android Plaid flow doesn't rely on a partners/plaid/oauth_ios universal link.
  • CI is green; the only red check is Check independent approval, which is expected pre-approval.

view run

@github-actions

Copy link
Copy Markdown
Contributor

🚧 Julesssss has triggered a test Expensify/App build. You can view the workflow run here.

@github-actions

This comment has been minimized.

@war-in

war-in commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

I verified it works correctly on Android, unable to test on iOS

Screen_Recording_20260825_114729_Expensify.Adhoc.mp4

@war-in

war-in commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Regarding Melvin's review

Backend browser-only redirects.

This one is fine; we want to open all links in the app

@war-in
war-in marked this pull request as ready for review September 16, 2026 09:58
@war-in
war-in requested review from a team as code owners September 16, 2026 09:58
@melvin-bot
melvin-bot Bot requested review from JmillsExpensify and aimane-chnaif and removed request for a team September 16, 2026 09:58
@melvin-bot

melvin-bot Bot commented Sep 16, 2026

Copy link
Copy Markdown

@aimane-chnaif 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 09:58
# Conflicts:
#	.well-known/apple-app-site-association
@war-in

war-in commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Discussed this one with @Julesssss and we can thoroughly test Android before the rollout, but iOS will probably have to be tested on staging/prod (we need to deploy AASA file first)

@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: d06f18ba43

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread android/app/src/main/AndroidManifest.xml

@JmillsExpensify JmillsExpensify 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.

Product experience is largely the same. LGTM

@aimane-chnaif

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible 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 checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (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 verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • 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 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.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this 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.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
Android: mWeb Chrome
iOS: HybridApp
iOS: mWeb Safari
MacOS: Chrome / Safari

@MelvinBot

Copy link
Copy Markdown
Contributor

⚠️ A setup step failed before I could start. This is usually a temporary issue — try again in a few minutes, and if it persists post in #melvinbot-ai.

Run details

@aimane-chnaif

Copy link
Copy Markdown
Contributor

NewDot opens some of its own links outside the app on purpose, and on native some of those go through Linking.openURL (an OS link hand-off).
On Android HybridApp, the catch-all now makes the app the handler for those URLs:

  1. Linking.openURL sends a plain ACTION_VIEW intent, and HybridApp is now the handler.
  2. The activity is singleTask, so the intent goes to onNewIntent → checkForUniversalLink.
  3. DeepLinkManager.handleUniversalLink only checks the host (isNewDotEligibleURL), so it sends the URL back into NewDot with navigateToNewDot(url).

On main, none of the URLs below matched the old list, so they opened in the browser.

1. New Expensify links meant to open in the browser reopen the app (production Android HybridApp)

Code path: AnchorRenderer.tsx sets onLinkPress only when getInternalNewExpensifyPath returns a path. The site root returns '', and so do the paths in CONST.PATHS_TO_TREAT_AS_EXTERNAL (NewExpensify.dmg, docs/index.html). For those links, AnchorForCommentsOnly/index.native.tsx calls Linking.openURL(href) directly.

Repro steps (Android HybridApp):

  1. Open any chat, for example your self DM.
  2. Send the message https://new.expensify.com/NewExpensify.dmg (on a staging build, use https://staging.new.expensify.com/NewExpensify.dmg).
  3. Tap the link in the sent message.

Expected (main): The browser opens and downloads the file.
Actual (PR): The browser doesn't open. The app reloads to NewDot at NewExpensify.dmg and likely shows Not Found.

bug.mov

Variant: Send https://new.expensify.com and tap it. On main the site opens in the browser. With the PR, the app just comes back to the front.

2. The PDS/FSG "Download" button opens the app instead of the PDF (production Android HybridApp)

Code path: src/pages/ReimbursementAccount/EnterSignerInfo/subSteps/UploadDocuments.tsx:118 and src/pages/ReimbursementAccount/NonUSD/SignerInfo/subSteps/UploadDocuments.tsx:113 call openExternalLink(${environmentUrl}/pdfs/PDSAndFSG.pdf) → Linking.openURL.

Repro steps:

  1. Go to Workspaces → pick a workspace → Workflows → Connect bank account, and choose Australia (AUD).
  2. Continue to Signer info → Upload documents.
  3. Under "PDS, FSG + TMD disclosure paperwork", tap Download.

Expected: The PDF opens in the browser.
Actual: The app opens again at pdfs/PDSAndFSG.pdf and likely shows Not Found. The form still records the paperwork as downloaded.

3. The "Expensify web app" link on the Unsupported device screen loops back into the app (production Android HybridApp)

Code path: multifactorAuthentication.unsupportedDevice.pleaseUseWebApp links to the root of CONST.NEW_EXPENSIFY_URL. The path is empty, so the link goes through openExternalLink.

Repro steps:

  1. On an Android device or emulator with no screen lock or biometrics, open a flow that needs multifactor authentication, such as reviewing a transaction that needs authorization.
  2. On "Unsupported device", tap Expensify web app.

Expected: new.expensify.com opens in the browser.
Actual: The app comes back to the front. The only way out that the screen offers leads back into the app.

@MelvinBot

Copy link
Copy Markdown
Contributor

⚠️ A setup step failed before I could start. This is usually a temporary issue — try again in a few minutes, and if it persists post in #melvinbot-ai.

Run details

@war-in

war-in commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

@Julesssss

Copy link
Copy Markdown
Contributor

Sorry forgot to send my draft yesterday..


what do you think about those?

#99347 (comment)
#99347 (comment)

Sorry for delay.


A) AI feedback

I am not sure if this is a problem and whether that URL should do anything. It doesn't on prod as far as I can tell. If we can exclude these on either mobile platform great, but I don't think it's a blocker.

B) New Expensify links meant to open in the browser reopen the app

Expected (main): The browser opens and downloads the file.

Not the best example, as this is an odd case. But for https://new.expensify.com or linking to terms and conditions (example the links on the auth page) that is worth fixing. Perhaps an alternative solution here would be to open an on-app webview?

C) The PDS/FSG "Download" button opens the app instead of the PDF

Yeah that one seems problematic. Especially for some of the banking flows for which we are providing terms and conditions. @war-in any ideas how to resolve this one?

D) The "Expensify web app" link on the Unsupported device screen loops back into the app

On "Unsupported device", tap Expensify web app.

Are they able to workaround by navigating manually on browser? If so and there is no special URL speeding up their auth process I think it could be okay. Possibly fixed along with changes we'll make to resolve the above issues.

@war-in

war-in commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

I like the web view idea a lot!

But the real problem is that Android doesn't allow for easy opt-outs from deeplinks, so we need to find another solution or simply give up on the wildcard idea

I'll try to dig into this again 🤞

@war-in

war-in commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor Author

Ok, I think I found a way

iOS is simple, we can exclude some paths, but Android supports such a feature since Android 15.

@Julesssss could you verify in Google Play console how many Android devices are under v14? Sentry shows that this is ~20%, but this is from the traces and crashes, so that number can be inflated

@Julesssss

Copy link
Copy Markdown
Contributor

Unfortunately a reasonably high amount of users, close to 10% apparently.

Screenshot 2026-09-25 at 16 30 30

@war-in

war-in commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

@Julesssss what do you think about preserving the current behaviour (from main) for Android <15 users? I think that would be the cleanest approach without any workarounds 🤔

@Julesssss

Copy link
Copy Markdown
Contributor

@Julesssss what do you think about preserving the current behaviour (from main) for Android <15 users? I think that would be the cleanest approach without any workarounds 🤔

Yeah that should be okay. So I know we can use conditions to wrap by Android version, but does this require some other method? I can't remember but assume some OS linking into the app might be done prior to out SDK version checks.

@war-in

war-in commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

I can't remember but assume some OS linking into the app might be done prior to out SDK version checks.

Yeah, it'd definitely be too late. But we can use values and conditionally put the old and the new links during a build time. I already implemented that and pushed but it looks like GH is having some issues
I'll ping you tomorrow once it's resolved

@war-in

war-in commented Oct 12, 2026

Copy link
Copy Markdown
Contributor Author

@Julesssss all changes are in this and ME PRs so please let me know if that approach works for you 🙏

@github-actions

Copy link
Copy Markdown
Contributor

🚧 Julesssss has triggered a test Expensify/App build. You can view the workflow run here.

@github-actions

This comment has been minimized.

@war-in

war-in commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

@aimane-chnaif could you review? 🙏
@Julesssss can we get another adhoc build? 🙏

@github-actions

Copy link
Copy Markdown
Contributor

🚧 Julesssss has triggered a test Expensify/App build. You can view the workflow run here.

@Expensify Expensify deleted a comment from github-actions Bot Oct 20, 2026
@Expensify Expensify deleted a comment from github-actions Bot Oct 20, 2026
@Julesssss

Copy link
Copy Markdown
Contributor

@aimane-chnaif could you review? 🙏

Please also review the linked Mobile-Expensify PR too

@github-actions

Copy link
Copy Markdown
Contributor

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