I finally abandoned the idea of a service account and used the oauth client.
I relied on the following React Native library: https://github.com/joonhocho/react-native-google-sign-in
Here is my authentication service (simplified, abbreviated + based reduction):
// <project_folder>/src/services/auth.js import GoogleSignIn from 'react-native-google-sign-in'; import { store } from '../store'; import auth from '../actions/auth'; export default () => { GoogleSignIn.configure({ // https://developers.google.com/identity/protocols/googlescopes scopes: ['https://www.googleapis.com/auth/spreadsheets'], clientID: 'XXXXXXXXXXXXXXXXX.apps.googleusercontent.com', }).then(() => { GoogleSignIn.signInPromise().then((user) => { store.dispatch(auth(user.accessToken)); }).catch((err) => { console.log(err); }); }).catch((err) => { console.log(err); }); };
So, I have user.accessToken
in my redux store and you can use it elsewhere to create REST requests for google APIs.
Here is a simple example of a component that processes auth and then extracts some data from a sheet:
// <project_folder>/src/main.js import React, { Component } from 'react'; import { ActivityIndicator, Text, View, StyleSheet, } from 'react-native'; import { connect } from 'react-redux'; import auth from './services/auth'; import Sheet from './services/sheet'; const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5FCFF', }, }); const sheetId = 'XX-XXXXXX_XXX_XXXXXXXXXXXXXXXXXXXXXX'; class Main extends Component { constructor(props) { super(props); this.state = { animating: true, }; } componentDidMount() { auth(); } componentWillUpdate(nextProps) { if (this.props.token !== nextProps.token) this.setState({ animating: false }); } componentDidUpdate(nextProps) { this.sheet = new Sheet(id, this.props.token); this.sheet.getDoc(); } render() { return ( <View style={styles.container}> <ActivityIndicator animating={this.state.animating} style={{ height: 80 }} size="large" /> {!this.state.animating && <Text> {this.props.name} </Text> } </View> ); } } const mapStateToProps = (state) => { return { token: state.auth.get('token'), name: state.sheet.get('name'), }; }; export default connect(mapStateToProps)(Main);
And here is the main service for reading / writing a sheet:
// <project_folder>/services/sheet.js import { store } from '../store'; import { nameGet, valueGet, valuePost, } from '../actions/sheet'; class Sheet { constructor(id, token) { this.id = id; this.token = token; this.endPoint = `https://sheets.googleapis.com/v4/spreadsheets/${this.id}`; } getDoc() { fetch(`${this.endPoint}?access_token=${this.token}`).then((response) => { response.json().then((data) => { console.log(data); store.dispatch(nameGet(data.properties.title)); }); }); } getCell(sheet, cell) { const range = `${sheet}!${cell}`; fetch(`${this.endPoint}/values/${range}?access_token=${this.token}`) .then((response) => { response.json() .then((data) => { console.log(data); store.dispatch(valueGet(data.values[0][0])); }) .catch((err) => { console.log(err); }); }) .catch((err) => { console.log(err); }); } writeCell(sheet, cell, value) { const range = `${sheet}!${cell}`; const body = JSON.stringify({ values: [[value]] }); fetch( `${this.endPoint}/values/${range}?valueInputOption=USER_ENTERED&access_token=${this.token}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body, } ) .then((response) => { response.json() .then((data) => { console.log(data); store.dispatch(valuePost('OK')); }) .catch((err) => { console.log(err); }); }) .catch((err) => { console.log(err); }); } } export default Sheet;
If there is a better way to do this, please let me know.