Krishna Nandakumar
Krishna Nandakumar

Reputation: 91

Google Picker + Google Drive API

As part of Project Strobe, Google is planning to move many of the Google Drive API scopes to "restricted" which means that you need an independent security verification.

In the same article, they suggest using the Google Picker and drive.file scope to obtain per file access. I've successfully implemented the Google Picker API to access files and retreive the meta data of the file. But how do I actually import the content of the file with this flow (for example, the data in an actual Google sheet)?

GOOGLE PICKER WRAPPER

import React from "react";
import PropTypes from "prop-types";
import loadScript from "load-script";

const GOOGLE_SDK_URL = "https://apis.google.com/js/api.js";

let scriptLoadingStarted = false;

class GoogleFilePicker extends React.Component {
  static propTypes = {
    children: PropTypes.node,
    clientId: PropTypes.string.isRequired,
    developerKey: PropTypes.string,
    scope: PropTypes.array,
    viewId: PropTypes.string,
    authImmediate: PropTypes.bool,
    origin: PropTypes.string,
    onChange: PropTypes.func,
    onAuthenticate: PropTypes.func,
    onAuthFailed: PropTypes.func,
    createPicker: PropTypes.func,
    multiselect: PropTypes.bool,
    navHidden: PropTypes.bool,
    disabled: PropTypes.bool,
    authToken: PropTypes.string,
  };

  static defaultProps = {
    onChange: () => {},
    onAuthenticate: () => {},
    onAuthFailed: () => {},
    scope: ["https://www.googleapis.com/auth/drive.file"],
    viewId: "DOCS",
    authImmediate: false,
    multiselect: false,
    navHidden: false,
    disabled: false,
    authToken: "",
  };

  constructor(props) {
    super(props);

    this.onApiLoad = this.onApiLoad.bind(this);
    this.onChoose = this.onChoose.bind(this);
  }

  componentDidMount() {
    if (this.isGoogleReady()) {
      // google api is already exists
      // init immediately
      this.onApiLoad();
    } else if (!scriptLoadingStarted) {
      // load google api and the init
      scriptLoadingStarted = true;
      loadScript(GOOGLE_SDK_URL, this.onApiLoad);
    } else {
      // is loading
    }
  }

  isGoogleReady() {
    return !!window.gapi;
  }

  isGoogleAuthReady() {
    return !!window.gapi.auth;
  }

  isGooglePickerReady() {
    return !!window.google.picker;
  }

  onApiLoad() {
    window.gapi.load("auth");
    window.gapi.load("picker");
  }

  doAuth(callback) {
    window.gapi.auth.authorize(
      {
        client_id: this.props.clientId,
        scope: this.props.scope,
        immediate: this.props.authImmediate,
      },
      callback
    );
  }

  onChoose() {
    if (
      !this.isGoogleReady() ||
      !this.isGoogleAuthReady() ||
      !this.isGooglePickerReady() ||
      this.props.disabled
    ) {
      return null;
    }

    // const token = window.gapi.auth.getToken();
    // const oauthToken = token && token.access_token;
    const oauthToken = this.props.authToken;

    if (oauthToken) {
      this.createPicker(oauthToken);
    } else {
      this.doAuth((response) => {
        if (response.access_token) {
          this.createPicker(response.access_token);
        } else {
          this.props.onAuthFailed(response);
        }
      });
    }
  }

  createPicker(oauthToken) {
    // this.props.onAuthenticate(oauthToken);

    if (this.props.createPicker) {
      return this.props.createPicker(window.google, oauthToken);
    }

    const googleViewId = window.google.picker.ViewId[this.props.viewId];
    const view = new window.google.picker.View(googleViewId);

    if (this.props.mimeTypes) {
      view.setMimeTypes(this.props.mimeTypes.join(","));
    }
    if (this.props.query) {
      view.setQuery(this.props.query);
    }

    if (!view) {
      throw new Error("Can't find view by viewId");
    }

    const picker = new window.google.picker.PickerBuilder()
      .addView(view)
      .setOAuthToken(oauthToken)
      .setDeveloperKey(this.props.developerKey)
      .setCallback(this.props.onChange);

    if (this.props.origin) {
      picker.setOrigin(this.props.origin);
    }

    if (this.props.navHidden) {
      picker.enableFeature(window.google.picker.Feature.NAV_HIDDEN);
    }

    if (this.props.multiselect) {
      picker.enableFeature(window.google.picker.Feature.MULTISELECT_ENABLED);
    }

    picker.build().setVisible(true);
  }

  render() {
    console.log(window.gapi);
    return (
      <div onClick={this.onChoose}>
        {this.props.children ? (
          this.props.children
        ) : (
          <button>Open google chooser</button>
        )}
      </div>
    );
  }
}

export default GoogleFilePicker;

CALLING THE WRAPPER

I'm calling the component above in the code below. Once the document is retrieved I call the copyFile function. This function works as expected when the document has been created by the app, but not when the file has been created by the user outside of the app. I know that this intended by Google for the drive.google.file scope but the documentation implies that using the Google Picker should solve this.

  <GoogleFilePicker
    clientId="CLIENT ID"
    developerKey="DEVELOPER KEY"
    scope={["https://www.googleapis.com/auth/drive.file"]}
    onChange={(data) => {
      if (data.docs) {
        copyFile(data.docs[0].id, `Template: ${data.docs[0].name}`, "");
      }
    }}
    onAuthenticate={(token) => console.log("oauth token:", token)}
    onAuthFailed={(data) => console.log("on auth failed:", data)}
    multiselect={true}
    navHidden={true}
    authImmediate={false}
    // mimeTypes={["image/png", "image/jpeg", "image/jpg"]}
    viewId={"DOCS"}
    authToken={auth.state.user.googleToken}
  >

Upvotes: 3

Views: 2361

Answers (2)

Maplefury
Maplefury

Reputation: 63

I recently had this issue and I needed to get the app id from the google cloud console and add it to the picker with the .setAppId(ID_HERE). This sets it up so Google can set the app and file link, and then when you make API calls with the app it will know it has access to that file. You should be able to make whichever API request setup in the same project in the cloud console; for me I had Google Drive, Docs and Picker APIs enabled in the same project. I used the picker to allow the user to give access to the app for a template file, then the app can copy that template to the users drive then using the Doc API to inject their data into it.

You may also need to setup a callback function to execute after the user has made a selection/cancelled the picker. For example this is how I implemented this:

  var self = this;
  var filePickerCallback = (data) => {
    if (data[google.picker.Response.ACTION] == google.picker.Action.PICKED) {
      var doc = data[google.picker.Response.DOCUMENTS][0];
      if (doc["id"] === getTemplateToFindInPicker()){
        self.startGoogleDocumentBuild();
      } else {
        self.showDocsDialog_(ERROR, WRONG FILE SELECTED);
      }
    }
    if(data[google.picker.Response.ACTION] == google.picker.Action.CANCEL){
      self.showPickerFlowErrorDialog();
    }
  }
  filePickerCallback.bind(this);

  let token =  getPickerAccessToken();
  var isMCAV = CODE_TO_CHECK_MCAVNESS;
  let name = NAME_BASED_ON_CONTEXT;

  // let's us choose the query string, what to search based on file name, so its very clear what the user needs to choose
  let view = new google.picker.View(google.picker.ViewId.DOCUMENTS).setQuery(name);
  var picker = new google.picker.PickerBuilder()
    .addView(view)
    .setTitle("Please choose: " + name)
    .setAppId(getAppIDs())
    .setOAuthToken(token)
    .setDeveloperKey(getDeveloperKey())
    .setCallback(filePickerCallback)
    .build();
  picker.setVisible(true);

Upvotes: 1

DHuang
DHuang

Reputation: 25

I believe you need to add the

.setAppId("x")

To the picker builder

Upvotes: 0

Related Questions