Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(auth, iOS): Expo plugin: skip expo-router for auth URLs #8203

Merged
merged 19 commits into from
Jan 2, 2025

Conversation

jey
Copy link
Contributor

@jey jey commented Dec 26, 2024

Description

Summary:
Fix #7258: @react-native-firebase/auth (phone auth) + Expo Router - Redirected to appScheme://firebaseuth/link after resolving recaptcha in ios simulator

Problem:
When using @react-native-firebase/auth and expo-router in the same application, the phone number login method on iOS fails in some scenarios. In particular, when Firebase uses its ReCAPTCHA verifier (e.g. when running in the iOS simulator), the application ends up navigating away from the login page to a new route /firebaseauth/link.

Cause:
After the user has completed the captcha challenge, the WebView redirects back to the application using an iOS deep link of the form ${googleServiceJson.REVERSED_CLIENT_ID}://firebaseauth/link?${queryParams}. This inadvertently gets handled by Expo Router, which treats it as a navigation event to a route called /firebaseauth/link.

The root cause of this is a bad interaction between a feature in the Firebase iOS SDK and one in Expo Router:

  • Firebase uses a technique called "swizzling" to automatically intercept the openURL call and do its business, but then it also passes along the same openURL call to our application so we can handle it too.
  • Our iOS app's AppDelegate is supposed to return TRUE if it can handle the provided URL, and FALSE if it can't.
  • When using Expo Router, our AppDelegate is configured to just pass the URL along onto the router.
  • However, Expo Router always answers "yes I can handle this URL" -- even if it's going to "handle" it by displaying a "Not Found" error.

This interacts badly because it's effectively like Expo Router is hijacking the call that's intended for Firebase Auth. Yet, this situation only occurs because Firebase Auth is forwarding the openURL call to our app even after it has already handled it.

Solution:
This PR automatically updates the AppDelegate's openURL method to not handle URLs for the hostname "firebaseauth", under the assumption that it's already being handled by Firebase.

  • Adds a configuration option to @react-native-firebase/auth Expo plugin called captchaOpenUrlFix.
  • When captchaOpenUrlFix option is unset or "default", automatically applies patch if expo-router plugin is detected in Expo config.
  • Robust error handling and reporting

TODO:

  • Documentation updates

CC: @mikehardy

Related issues

Fixes #7258, #7953

Release Summary

Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
    • Yes
  • My change supports the following platforms;
    • Android
    • iOS
  • My change includes tests;
    • e2e tests added or updated in packages/\*\*/e2e
    • jest tests added or updated in packages/\*\*/__tests__
  • I have updated TypeScript types that are affected by my change.
  • This is a breaking change;
    • Yes
    • No

Test Plan

Comprehensive unit tests have been added:

  • plugin configurations
  • positive and negative examples of patching
  • tests for each error handling condition

Copy link

vercel bot commented Dec 26, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
react-native-firebase ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jan 2, 2025 5:13am

@CLAassistant
Copy link

CLAassistant commented Dec 26, 2024

CLA assistant check
All committers have signed the CLA.

@jey jey changed the title DRAFT: fix(auth, iOS): Expo plugin: don't invoke expo-router for openURL DRAFT: fix(auth, iOS): Expo plugin: don't invoke expo-router for auth URLs Dec 26, 2024
@jey jey changed the title DRAFT: fix(auth, iOS): Expo plugin: don't invoke expo-router for auth URLs DRAFT: fix(auth, iOS): Expo plugin: skip expo-router for auth URLs Dec 26, 2024
Copy link
Collaborator

@mikehardy mikehardy left a comment

Choose a reason for hiding this comment

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

Oh wow - you are quick with this one!

As mentioned in the related issue, it's a bit delicate because it has to do regex-matching so it will likely break across some future version, but also, I'm not aware of any other way to do this

Given the fragility I think it has to be default-off and have a parameter that turns on this logic (named anything reasonable, like maybe "expoRouterCompatibilityPatch" ... or something)

If it had that, then most people could use the plugin perfectly safely and never break, and those that needed this would be the only ones affected

But in general: thanks so much for posting up a PR to help folks, we rely on community submissions for Expo stuff so it's a huge help here

@jey jey marked this pull request as draft December 28, 2024 05:50
@jey jey changed the title DRAFT: fix(auth, iOS): Expo plugin: skip expo-router for auth URLs fix(auth, iOS): Expo plugin: skip expo-router for auth URLs Dec 28, 2024
@jey
Copy link
Contributor Author

jey commented Dec 28, 2024

@mikehardy This is now ready for review, but I'm also hoping to push some documentation updates later, within the next few days.

I made the plugin configurable as you suggested, but I also made it so that in the default configuration, it will automatically apply the patch only if "expo-router" is also detected (by looking in the Expo Plugins list). I think this is a happy medium because it addresses the edge cases of projects where it's not applicable, but also makes it easy for the median Expo user. My impression is that the typical Expo user is not comfortable dealing with native code and having even more manual configuration changes to apply and figure out on top of the already complex configuration required for RNFirebase Phone Auth to work.

@jey
Copy link
Contributor Author

jey commented Dec 30, 2024

CC: @Brandon-Perry

Copy link
Collaborator

@mikehardy mikehardy left a comment

Choose a reason for hiding this comment

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

This looks amazing, and amazingly robust.
Would be nice to have some docs though I'm not sure exactly where they'd go - perhaps there is an expo section in the auth module already, if so there would work

Left one note you can handle however you see fit about perhaps emitting an info message on successful execution

Default approve at this point, pending whatever you'd like to consider it done, but all okay by me

Cheers!

packages/auth/plugin/src/ios/openUrlFix.ts Show resolved Hide resolved
packages/auth/plugin/src/ios/openUrlFix.ts Show resolved Hide resolved
@mikehardy mikehardy added Workflow: Waiting for User Response Blocked waiting for user response. Workflow: Pending Merge Waiting on CI or similar and removed Needs Attention labels Dec 31, 2024
Copy link
Collaborator

@mikehardy mikehardy left a comment

Choose a reason for hiding this comment

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

awesome

@mikehardy mikehardy removed the Workflow: Waiting for User Response Blocked waiting for user response. label Jan 2, 2025
@mikehardy
Copy link
Collaborator

The patch-package failure is not related to this PR and is non-blocking here. It was a scheduled deprecation of npx react-native init that I was aware of from other usages but I forgot it was used here so I haven't forward-ported yet.

@mikehardy mikehardy merged commit 33744da into invertase:main Jan 2, 2025
16 of 17 checks passed
@mikehardy mikehardy removed the Workflow: Pending Merge Waiting on CI or similar label Jan 2, 2025
const props = undefined;
const spy = jest
.spyOn(WarningAggregator, 'addWarningIOS')
.mockImplementation(() => undefined);
Copy link
Contributor Author

@jey jey Jan 4, 2025

Choose a reason for hiding this comment

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

@mikehardy btw for some reason this call to mockImplementation is required so we don't get get spurious warning messages on jest's console, yet the one at Line 249 is not required. do you know why?

In particular here's the output when I remove both calls to mockImplementation:

 PASS  packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts
  ● Console

    console.warn
      » ios: @react-native-firebase/auth: modifying iOS AppDelegate openURL method to ignore firebaseauth reCAPTCHA redirect URLs

      77 |     } else {
      78 |       if (configValue === 'default') {
    > 79 |         WarningAggregator.addWarningIOS(
         |                           ^
      80 |           '@react-native-firebase/auth',
      81 |           'modifying iOS AppDelegate openURL method to ignore firebaseauth reCAPTCHA redirect URLs',
      82 |         );

      at Object.warn (packages/auth/node_modules/@expo/config-plugins/src/utils/warnings.ts:32:11)
      at withOpenUrlFixForAppDelegate (packages/auth/plugin/src/ios/openUrlFix.ts:79:27)
      at packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts:221:35
      at step (packages/auth/node_modules/tslib/tslib.js:195:27)
      at Object.next (packages/auth/node_modules/tslib/tslib.js:176:57)
      at fulfilled (packages/auth/node_modules/tslib/tslib.js:166:62)

 PASS  packages/auth/plugin/__tests__/iosPlugin_urlTypes.test.ts

Test Suites: 2 passed, 2 total
Tests:       26 passed, 26 total
Snapshots:   6 passed, 6 total
Time:        2.235 s
Ran all test suites matching /packages\/auth\/plugin\/__tests__\//i.

Copy link
Collaborator

Choose a reason for hiding this comment

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

If you don't mock it, it will print the warning as requested in code, so seeing the warning if you remove the mock seems to me to make sense here, and then...

I wonder if the second one is behaving seemingly-strangely is because the spy is one thing but the mocked implementation is separate - such that the mental model is 2 attempts to spy but only one object that is mocked or not, and if you mock it once it stays mocked. Test of hypothesis would be to reset mocks after test ? Then following my hypothesis the second test should behave as expected if mocked or not (silent if mocked, producing the warn output if not mocked)

const spy = jest.spyOn(WarningAggregator, 'addWarningIOS');
const spy = jest
.spyOn(WarningAggregator, 'addWarningIOS')
.mockImplementation(() => undefined);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mikehardy related to Line 220, when commenting this line mockImplementation() , why doesn't jest print a warning message with something like:

» ios: @react-native-firebase/auth: Skipping iOS openURL fix because no 'openURL' method was found

@jey
Copy link
Contributor Author

jey commented Jan 4, 2025

@mikehardy thanks for accepting and merging this PR! Just now I've I left a comment about a lingering mystery that came up when writing the tests

@jey jey deleted the jey/expo-router-auth-url-fix branch January 4, 2025 10:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants