expo: Some schemes might break Android verified app links

Summary

One of our partners reported issues in verifying app links on Android. They added the expo.android.intentFilters, with the autoVerify property set to true. We found out that the added scheme by the dev client (<data android:scheme="exp+<slug>" />), was also added to these intent filters. That scheme broke the app link verification process on Android.

Unfortunately, starting from Android 12, verifying your app links seems required to even be listed in the “Open with app” list.

Looking at the dev client’s config plugin:

// Generate a cross-platform scheme used to launch the dev client.
const scheme = getDefaultScheme(config);
if (!AndroidConfig.Scheme.hasScheme(scheme, androidManifest)) {
  androidManifest = AndroidConfig.Scheme.appendScheme(scheme, androidManifest);
}
return androidManifest;

There is no great way for plugins to not add a scheme to custom intent filters, or to remove them specifically from the autoVerify-ed intent filters.

AndroidManifest.xml activity examples

Example activity intent filters in AndroidManifest.xml that breaks
<activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:screenOrientation="portrait">
  <intent-filter>
    <action android:name="android.intent.action.MAIN"/>
    <category android:name="android.intent.category.LAUNCHER"/>
  </intent-filter>
  <intent-filter>
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <data android:scheme="<slug>"/>
    <data android:scheme="<package>"/>
    <data android:scheme="exp+<slug>"/>
  </intent-filter>
  <intent-filter android:autoVerify="true" data-generated="true">
    <action android:name="android.intent.action.VIEW"/>
    <data android:scheme="https" android:host="<name>.onelink.me" android:pathPrefix="/XXXX"/>
    <data android:scheme="https" android:host="<name>.onelink.me" android:pathPrefix="/XXXX"/>
    <data android:scheme="https" android:host="<name>.onelink.me" android:pathPrefix="/XXXX"/>
    <data android:scheme="exp+<slug>"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <category android:name="android.intent.category.DEFAULT"/>
  </intent-filter>
</activity>
Example activity intent filters in AndroidManifest.xml that works
<activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:screenOrientation="portrait">
  <intent-filter>
    <action android:name="android.intent.action.MAIN"/>
    <category android:name="android.intent.category.LAUNCHER"/>
  </intent-filter>
  <intent-filter>
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <data android:scheme="<slug>"/>
    <data android:scheme="<package>"/>
    <data android:scheme="exp+<slug>"/>
  </intent-filter>
  <intent-filter android:autoVerify="true" data-generated="true">
    <action android:name="android.intent.action.VIEW"/>
    <data android:scheme="https" android:host="<name>.onelink.me" android:pathPrefix="/XXXX"/>
    <data android:scheme="https" android:host="<name>.onelink.me" android:pathPrefix="/XXXX"/>
    <data android:scheme="https" android:host="<name>.onelink.me" android:pathPrefix="/XXXX"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <category android:name="android.intent.category.DEFAULT"/>
  </intent-filter>
</activity>

Workaround plugin that pulls the exp+ schemes from auto verifying intent filters

withAndroidVerifiedLinksWorkaround.js
const { createRunOncePlugin, withAndroidManifest } = require('@expo/config-plugins');

/**
 * @typedef {import('@expo/config-plugins').ConfigPlugin} ConfigPlugin
 * @typedef {import('@expo/config-plugins').AndroidManifest} AndroidManifest
 */

/**
 * Remove the custom Expo dev client scheme from intent filters, which are set to `autoVerify=true`.
 * The custom scheme `<data android:scheme="exp+<slug>"/>` seems to block verification for these intent filters.
 * This plugin makes sure there is no scheme in the autoVerify intent filters, that starts with `exp+`.
 * 
 * @type {ConfigPlugin}
 */
const withAndroidVerfiedLinksWorkaround = (config) => (
  withAndroidManifest(config, (config) => {
    config.modResults = removeExpoSchemaFromVerifiedIntentFilters(config.modResults);
    return config;
  })
);

/**
 * Iterate over all `autoVerify=true` intent filters, and pull out schemes starting with `exp+`.
 * 
 * @param {AndroidManifest} androidManifest
 */
function removeExpoSchemaFromVerifiedIntentFilters(androidManifest) {
  // see: https://github.com/expo/expo-cli/blob/f1624c75b52cc1c4f99354ec4021494e0eff74aa/packages/config-plugins/src/android/Scheme.ts#L164-L179
  for (const application of androidManifest.manifest.application || []) {
    for (const activity of application.activity || []) {
      if (activityHasSingleTaskLaunchMode(activity)) {
        for (const intentFilter of activity['intent-filter'] || []) {
          if (intentFilterHasAutoVerification(intentFilter) && intentFilter?.data) {
            intentFilter.data = intentFilterRemoveSchemeFromData(intentFilter, (scheme) => scheme?.startsWith('exp+'));
          }
        }
        break;
      }
    }
  }

  return androidManifest;
}

/**
 * Determine if the activity should contain the intent filters to clean.
 * 
 * @see https://github.com/expo/expo-cli/blob/f1624c75b52cc1c4f99354ec4021494e0eff74aa/packages/config-plugins/src/android/Scheme.ts#L166
 */
function activityHasSingleTaskLaunchMode(activity) {
  return activity?.$?.['android:launchMode'] === 'singleTask';
}

/**
 * Determine if the intent filter has `autoVerify=true`.
 */
function intentFilterHasAutoVerification(intentFilter) {
  return intentFilter?.$?.['android:autoVerify'] === 'true';
}

/**
 * Remove schemes from the intent filter that matches the function.
 */
function intentFilterRemoveSchemeFromData(intentFilter, schemeMatcher) {
  return intentFilter?.data?.filter(entry => !schemeMatcher(entry?.$['android:scheme'] || ''));
}

module.exports = createRunOncePlugin(withAndroidVerfiedLinksWorkaround, 'withAndroidVerfiedLinksWorkaround', '1.0.0');

Environment

  Expo CLI 5.0.5 environment info:
    System:
      OS: Windows 10 10.0.22000
    Binaries:
      Node: 16.13.1 - C:\Program Files\nodejs\node.EXE
      Yarn: 1.22.4 - C:\Program Files\nodejs\yarn.CMD
      npm: 8.1.2 - C:\Program Files\nodejs\npm.CMD
    IDEs:
      Android Studio: Version     2020.3.0.0 AI-203.7717.56.2031.7784292
    npmPackages:
      expo: ~44.0.0 => 44.0.6
      react: 17.0.1 => 17.0.1
      react-dom: 17.0.1 => 17.0.1
      react-native: 0.64.3 => 0.64.3
      react-native-web: 0.17.1 => 0.17.1
    Expo Workflow: managed

Please specify your device/emulator/simulator platform, model and version

Android 12

Error output

none

Reproducible demo or steps to reproduce from a blank project

Verifying app links is a delicate process, you can find some debugging options here: https://developer.android.com/training/app-links/verify-site-associations#check-link-policies

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 21
  • Comments: 15 (6 by maintainers)

Most upvoted comments

@mguyard Create a new file for the plugin e.g. plugins/withAndroidVerifiedLinksWorkaround.js. Then add the plugin to your app.json under expo.plugins e.g.

{
  "expo": {
     ...
    "plugins": [
      "./plugins/withAndroidVerifiedLinksWorkaround"
    ]
  }
}

Yes, works with eas build. You have to create a new build for the changes to take effect.

We are facing this issue currently with RN-Firebase and dynamic links, the same setup works fine in iOS and pre Android 12 builds. We have tried the config plugin route without success yet. Would be good to have an update if this issue is being worked on and or of it was tied to the Expo dev client or other libraries.

Same issue happens with the expo plugin for react-native-fbsdk-next. It appends this to the intent filters which breaks autoVerify in the same way, e.g:

<data android:scheme="fbXXX"/>

See: https://github.com/thebergamo/react-native-fbsdk-next/issues/329

Since I’m dubious about other plugins breaking this, I modified @byCedric’s solution to be a general purpose validator. For app links which use auto verification, it will only ever include entries which use a http or https scheme. As far as I can tell this is in alignment with the Android docs, since auto verification applies to website links only.

withAndroidVerifiedLinksWorkaround.js
const {
  createRunOncePlugin,
  withAndroidManifest,
} = require('@expo/config-plugins');

/**
 * @typedef {import('@expo/config-plugins').ConfigPlugin} ConfigPlugin
 * @typedef {import('@expo/config-plugins').AndroidManifest} AndroidManifest
 */

/**
 * Remove any non 'http' or 'https' entries in the intent filters which are set to `autoVerify=true`.
 *
 * This stops some plugins like the expo-dev-client and react-native-fbsdk-next adding their own custom schemes which break auto verification.
 *
 * See: https://github.com/expo/expo/issues/19745 and https://github.com/thebergamo/react-native-fbsdk-next/issues/329
 *
 * Important: Expo seems to run plugins in the reverse order that they are listed in your config. So place this plugin
 * at the top of your plugin config so it runs last.
 *
 * @type {ConfigPlugin}
 */
const withAndroidVerifiedLinksWorkaround = (config) =>
  withAndroidManifest(config, (config) => {
    config.modResults = removeInvalidIntentFiltersFromIntentFilters(
      config.modResults
    );
    return config;
  });

/**
 * Iterate over all `autoVerify=true` intent filters and only keep ones which begin
 * with 'http' or 'https'.
 *
 * @param {AndroidManifest} androidManifest
 */
function removeInvalidIntentFiltersFromIntentFilters(androidManifest) {
  for (const application of androidManifest.manifest.application || []) {
    for (const activity of application.activity || []) {
      if (activityHasSingleTaskLaunchMode(activity)) {
        for (const intentFilter of activity['intent-filter'] || []) {
          if (
            intentFilterHasAutoVerification(intentFilter) &&
            intentFilter?.data
          ) {
            intentFilter.data = intentFilterRemoveSchemeFromData(
              intentFilter,
              (scheme) =>
                !scheme?.startsWith('https') || !scheme?.startsWith('http')
            );
          }
        }
        break;
      }
    }
  }

  return androidManifest;
}

/**
 * Determine if the activity should contain the intent filters to clean.
 *
 */
function activityHasSingleTaskLaunchMode(activity) {
  return activity?.$?.['android:launchMode'] === 'singleTask';
}

/**
 * Determine if the intent filter has `autoVerify=true`.
 */
function intentFilterHasAutoVerification(intentFilter) {
  return intentFilter?.$?.['android:autoVerify'] === 'true';
}

/**
 * Remove schemes from the intent filter that matches the function.
 */
function intentFilterRemoveSchemeFromData(intentFilter, schemeMatcher) {
  return intentFilter?.data?.filter(
    (entry) => !schemeMatcher(entry?.$['android:scheme'] || '')
  );
}

module.exports = createRunOncePlugin(
  withAndroidVerifiedLinksWorkaround,
  'withAndroidVerifiedLinksWorkaround',
  '1.0.0'
);

Hello,

I’ve exactly the same issue. I see that this bug isn’t already assigned. Is it plan to resolve this bug in a new version ?

As i’m pretty new expo user, i don’t understand everything and how to use the workaround purpose. I someone can help me to understand how to apply the workaround (where, how, etc…) this will be very appreciate.

Another question, does this workaround works if i use “eas build” ?

Thanks by advance

@byCedric Thank you for writing up this issue and sharing your workaround!

I can confirm that we ran into the same problem and the above workaround resolved it for us.