Standalone: Android Facebook Auth never returns from com.facebook.CustomTabActivity

Please provide the following:

  1. SDK Version: 38
  2. Platforms(Android/iOS/web/all): Android

Using expo-auth-session, not expo-facebook.

Only happens on standalone build, on Android.

The Problem

What happens?

  1. When the facebook app is not installed, you’ll load m.facebook.com during the auth process, where you can press the login button.
  2. If the authorization is successful, the following url is pushed back to android:
fb<YOUR APP ID>://authorize?code=<authcode>&state=<state>
  1. The url is picked up by the standalone android app at <package.name>/com.facebook.CustomTabActivity.
  2. This activity is a blank screen with the app name as title.
  3. It’s supposed to “call back” into <package.name>/host.exp.exponent.MainActivity, but it never does. It’s just stuck on that screen.

How to reproduce

You can actually reproduce this without a Facebook App or anything.

  1. init a project
  2. add a package name, like “com.example.app”
  3. add facebookScheme to app.json, you can use a bogus one: fb1234567898765432
  4. build the android app
adb shell am start -W -a android.intent.action.VIEW -d "fb1234567898765432://authorize?code=invalid&state=a01bCdEFJK" com.example.app

I can’t seem to find issues about this, anywhere, so my guess is that I am doing something incorrectly, but it could also just be that it’s broken for everyone.

Here is a minimal repro. It will work on web, but it will open the “custom tabs activity” with a blank screen when coming back from auth on Android. Note: no facebook app installed.

APK download (available up to 29 days from this post).

// app.json
{
  "expo": {
    "name": "Sounders Music Facebook",
    "slug": "sounders-music-facebook",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "extra": {
      "facebook-client-id": "771225260278103"
    },
    "facebookScheme": "fb771225260278103",
    "facebookAppId": "771225260278103",
    "facebookDisplayName": "Sounders Music Integration Sample",
    "scheme": "com.soundersmusic.facebook",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "updates": {
      "fallbackToCacheTimeout": 0
    },
    "assetBundlePatterns": [
      "**/*"
    ],
    "ios": {
      "supportsTablet": true
    },
    "web": {
      "favicon": "./assets/favicon.png"
    },
    "android": {
      "package": "com.soundersmusic.facebook"
    }
  }
}
// package.json
{
  "main": "node_modules/expo/AppEntry.js",
  "dependencies": {
    "expo": "~38.0.8",
    "expo-auth-session": "~1.4.0",
    "expo-status-bar": "^1.0.2",
    "react": "~16.11.0",
    "react-dom": "~16.11.0",
    "react-native": "https://github.com/expo/react-native/archive/sdk-38.0.2.tar.gz",
    "react-native-web": "~0.11.7"
  },
  "devDependencies": {
    "@babel/core": "^7.8.6",
    "@types/react": "~16.9.41",
    "@types/react-native": "~0.62.13",
    "typescript": "~3.9.5"
  },
  "private": true
}
// App.tsx

import { makeRedirectUri, useAuthRequest } from 'expo-auth-session';
import Constants from 'expo-constants';
import { StatusBar } from 'expo-status-bar';
import * as WebBrowser from 'expo-web-browser';
import React, { useEffect, useMemo, useState } from 'react';
import { Button, Platform, StyleSheet, Text, View } from 'react-native';

WebBrowser.maybeCompleteAuthSession();

export default function App() {
  const [result, setResult] = useState('');
  return (
    <View style={styles.container}>
      <Facebook loading={false} attempt={setResult} />
      <Text>{JSON.stringify(result, undefined, 2)}</Text>
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

const FACEBOOK_ID = Constants.manifest.extra['facebook-client-id'];
const discovery = {
  authorizationEndpoint: 'https://www.facebook.com/v8.0/dialog/oauth',
  tokenEndpoint: 'https://graph.facebook.com/v8.0/oauth/access_token',
};

const useProxy = Constants.appOwnership === 'expo' && Platform.OS !== 'web';

function Facebook_(props: { loading: boolean; attempt: (prop: any) => void }) {
  if (!FACEBOOK_ID) {
    return null;
  }

  return <FacebookButton {...props} />;
}

function FacebookButton({
  loading,
  attempt,
}: {
  loading: boolean;
  attempt: (prop: any) => void;
}) {
  const [isAuthenticating, setAuthenticating] = useState(false);

  const config = useMemo(
    () => ({
      clientId: FACEBOOK_ID,
      scopes: ['public_profile', 'email'],
      // For usage in managed apps using the proxy
      redirectUri: makeRedirectUri({
        useProxy,
        // For usage in bare and standalone
        // Use your FBID here. The path MUST be `authorize`.
        native: `fb${FACEBOOK_ID}://authorize`,
      }),
      useProxy,
      extraParams: {
        // Use `popup` on web for a better experience
        display: Platform.select({ web: 'popup' }) as string,
        // Optionally you can use this to rerequest declined permissions
        // auth_type: 'rerequest',
      },
    }),
    []
  );

  const [request, response, promptAsync] = useAuthRequest(config, discovery);

  useEffect(() => {
    if (!response) {
      return;
    }

    setAuthenticating(false);

    if (response.type === 'success') {
      const { code, state } = response.params;

      const result = {
        type: 'facebook',
        success: true,
        redirect_uri: fixRedirectUri(config.redirectUri),
        code,
        state,
      };

      attempt(result);
    }
  }, [response, attempt, setAuthenticating]);

  return (
    <View
      style={{
        borderRadius: 30,
        backgroundColor: '#347AE5',
        marginHorizontal: 4,
        elevation: 2,
      }}
    >
      <Button
        disabled={!request || isAuthenticating || loading}
        color="#347AE5"
        title="Facebook Go"
        onPress={() => {
          setAuthenticating(true);
          promptAsync({
            useProxy,
            windowFeatures: { width: 700, height: 600 },
          });
        }}
      />
    </View>
  );
}

function fixRedirectUri(redirectUri: string, forProxy?: boolean) {
  if (forProxy) {
    return redirectUri;
  }

  if (Platform.OS !== 'web' && !redirectUri.startsWith('fb')) {
    return redirectUri;
  }

  if (Platform.OS === 'web' && redirectUri !== window.origin) {
    return redirectUri;
  }

  return redirectUri + '/';
}

export const Facebook = React.memo(Facebook_);

This is now tracked on GitHub: https://github.com/expo/expo/issues/9917