Skip to content

Instantly share code, notes, and snippets.

@jonnyreeves
Created September 20, 2021 08:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jonnyreeves/3a051cee26a403827de5c6278af6cf78 to your computer and use it in GitHub Desktop.
Save jonnyreeves/3a051cee26a403827de5c6278af6cf78 to your computer and use it in GitHub Desktop.
import * as React from 'react';
import * as GoogleWebAuth from 'expo-auth-session/providers/google';
import * as GoogleAppAuth from "expo-google-app-auth";
import * as WebBrowser from 'expo-web-browser';
import * as AppAuth from 'expo-app-auth';
import { Platform } from 'react-native';
import Constants, {AppOwnership} from 'expo-constants';
// Call this function once in your Component which handles the Google authentication
// flow; typically done outside of the component decleration (ie: just after your
// import statements).
// Refer to the code example here: https://docs.expo.dev/guides/authentication/#google
export function maybeCompleteAuthSession() {
if (Platform.OS === 'web') {
WebBrowser.maybeCompleteAuthSession();
}
}
// Initialises the state required to perform a Google Authentication request
// (authRequest, and authResult) and returns a func which will initiate the request
// across both Android and Web (promptAsync).
// Refer to the code example here: https://docs.expo.dev/guides/authentication/#google
export function useGoogleSignIn(authConfig) {
if (Platform.OS === 'web') {
const [ authRequet, authResult, promptAsync ] = GoogleWebAuth.useAuthRequest({
webClientId: authConfig.webClientId,
scopes: authConfig.scopes
});
return [ authRequet, authResult, promptAsync ];
} else {
const [ authRequest, setAuthRequest ] = React.useState(true);
const [ authResult, setAuthResult ] = React.useState(null);
const promptAsync = () => {
setAuthRequest(false);
GoogleAppAuth.logInAsync(authConfig)
.then(authObject => {
setAuthRequest(true);
const type = authObject?.type;
if (type === 'cancel') {
setAuthResult({ type });
} else if (type === 'success') {
setAuthResult({
type,
authentication: authObject,
});
} else {
setAuthResult(null);
}
});
};
return [ authRequest, authResult, promptAsync ];
}
}
// Initialises the state required to perform a Google Refresh Token exchange
// (refreshRequest and refreshResult), and returns a func which will perform the
// refresh token exchange (refreshAsync).
export function useGoogleTokenRefresh(authConfig) {
const [ refreshRequest, setRefreshRequest ] = React.useState(true);
const [ refreshResult, setRefreshResult ] = React.useState(null);
const refreshAsync = (refreshToken) => {
if (!refreshToken) {
return setRefreshResult({
type: 'cancelled'
});
}
setRefreshRequest(false);
const clientId = getRefreshClientId(authConfig);
const config = {
issuer: 'https://accounts.google.com',
clientId,
scopes: authConfig.scopes,
};
AppAuth.refreshAsync(config, refreshToken)
.then(res => {
setRefreshResult({
type: 'success',
authentication: res
})
})
.catch(err => {
setRefreshResult({
type: 'failed'
});
})
.finally(() => setRefreshRequest(true))
};
return [ refreshRequest, refreshResult, refreshAsync ];
}
function getRefreshClientId(authConfig) {
const isStandalone = Constants.appOwnership === AppOwnership.Standalone;
const { androidStandaloneAppClientId, androidClientId } = authConfig;
if (Platform.OS === 'android') {
return (isStandalone) ? androidStandaloneAppClientId : androidClientId;
}
console.warn(`Could not determine refresh clientId for platform: ${Platform.OS}`)
return "";
}
@jonnyreeves
Copy link
Author

jonnyreeves commented Sep 20, 2021

As of September, 2021, the Expo Auth Session (expo-auth-session) provider for Google does not appear to support providing access to an OAuth Refresh Token (refreshToken). The legacy AppAuth (expo-app-auth) provider does, but does not work on the Web which slows app development.

This wrapper class presents a unified wrapper around both, providing a common interface based on React Hooks (following the interface of expo-auth-session).

Usage

The follow component will attempt to fetch a cached refresh token from secure storage and exchange it for a valid access token. If this exchange should fail (eg: no token in storage, or the refresh api call were to fail) the user will be prompted to authenticate via google. If the authentication completes successfully, and a refresh token is returned in the TokenResponse, this component will attempt to store the refresh token to skip future authentication prompts.

const SECURE_STORE_KEY = "my-storage-key";

GoogleAuthHelper.maybeCompleteAuthSession();

export default function GoogleSignIn({ onAccessToken }) {

    const authConfig = {
        webClientId: 'xxx-yyy.apps.googleusercontent.com',
        androidClientId: "xxx-yyy.apps.googleusercontent.com",
        androidStandaloneAppClientId: "xxx-yyy.apps.googleusercontent.com",
        scopes: [ 'https://www.googleapis.com/auth/drive' ],
    }

    const [ needsSignIn, setNeedsSignIn ] = React.useState(false);
    const [ refreshRequest, refreshResponse, doRefresh ] = GoogleAuthHelper.useGoogleTokenRefresh(authConfig);
    const [ authRequest, authResponse, doAuthPrompt] = GoogleAuthHelper.useGoogleSignIn(authConfig);

    // Handle refresh state changes.
    React.useEffect(() => {
        const type = refreshResponse?.type;
        if (type === 'success') {
            const { accessToken } = refreshResponse.authentication;
            onAccessToken(accessToken);
        } else if (type === 'failed' || type === 'cancelled') {
            setNeedsSignIn(true);
        }
    }, [ refreshResponse ]);

    // Handle authentication state changes.
    React.useEffect(() => {
        const type = authResponse?.type;
        if (type === 'success') {
            const { accessToken, refreshToken } = authResponse.authentication;  

            // If we have a refresh token in the response, persist it to keep the user signed in.
            if (refreshToken) {
                SecureStore.setItemAsync(SECURE_STORE_KEY, refreshToken)
                    .then(() => console.log("Refresh token persisted to secure storage"))
                    .catch((err) => console.warn("Failed to persist refresh token to secure storage: " + err))
                    .finally(() => onAccessToken(accessToken));
            } else {
                onAccessTokenSet();
            }
        }
    }, [ authResponse ])

    // On mount, try to fetch a cached refresh token from Secure Storage; this will either
    // complete in a succesul login flow, or fail and require us to prompt the user to sign in.
    React.useEffect(() => {
        SecureStore.getItemAsync(SECURE_STORE_KEY)
            .then(cachedRefreshToken => doRefresh(cachedRefreshToken))
            .catch(err => doRefresh(null));
    }, []);

    const loading = (
        <View>
              <Text/>Please wait...</Text>
        </View>
    )

    const signInOptions = (
        <View>
            <Button title="Sign in with Google" disabled={!authRequest} onPress={() => doAuthPrompt() } />
        </View>
    )

    return (
        <View style={styles.screen}>
            { !needsLogin && loading }
            { needsSignIn && signInOptions }
        </View>
    );
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment