> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useparagon.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Google Drive

> Connect to your users' Google Drive accounts.

export const IntegrationsCompatibility = ({workflows = true, actionkit = false, proxy = true, managedSync = false, authType = "Basic Auth", integrationName: integrationNameProp, integrationSlug: integrationSlugProp}) => {
  const FEATURE_REQUEST_ENDPOINT = "https://agosnlmllwykglihfhiw.supabase.co/functions/v1/handle-vote";
  const FEATURE_REQUEST_HEADERS = {
    "Content-Type": "application/json",
    "Authorization": `Bearer sb_publishable_DlIzCjWe8NiqjZnLziegbg_P-w5L9X4`,
    'apikey': `sb_pubishable_DlIzCjWe8NiqjZnLziegbg_P-w5L9X4`
  };
  const SESSION_VOTE_PREFIX = "paragon_compat_vote:";
  const PURPLE = "rgb(102, 69, 230)";
  const slugifyFeature = label => label.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
  const integrationKeyFromPathname = pathname => {
    if (!pathname) return "";
    const match = pathname.match(/\/(?:resources\/)?integrations\/([^/?#]+)/);
    return match ? decodeURIComponent(match[1]).replace(/\/$/, "") : "";
  };
  const voteStorageKey = (integrationKey, featureKey) => `${SESSION_VOTE_PREFIX}${integrationKey}:${featureKey}`;
  const readVoteFromStorage = (integrationKey, featureKey) => {
    if (typeof sessionStorage === "undefined") return false;
    try {
      return sessionStorage.getItem(voteStorageKey(integrationKey, featureKey)) === "1";
    } catch {
      return false;
    }
  };
  const writeVoteToStorage = (integrationKey, featureKey) => {
    try {
      sessionStorage.setItem(voteStorageKey(integrationKey, featureKey), "1");
    } catch {}
  };
  const readPageTitleFallback = () => {
    if (typeof document === "undefined") return "";
    const h1 = document.querySelector("article h1") || document.querySelector("main h1") || document.querySelector('[class*="title"] h1') || document.querySelector("h1");
    const text = h1?.textContent?.trim();
    return text || "";
  };
  const getRuntimeConfig = () => {
    if (typeof window === "undefined") return null;
    return window.__PARAGON_FEATURE_REQUEST__ ?? null;
  };
  const resolveEndpoint = () => {
    const rt = getRuntimeConfig();
    if (rt?.endpoint) return String(rt.endpoint).trim();
    return String(FEATURE_REQUEST_ENDPOINT || "").trim();
  };
  const resolveHeaders = () => {
    const rt = getRuntimeConfig();
    const base = {
      ...FEATURE_REQUEST_HEADERS,
      ...rt?.headers && typeof rt.headers === "object" ? rt.headers : {}
    };
    return base;
  };
  const isValidEmail = s => (/^[^\s@]+@[^\s@]+\.[^\s@]+$/).test(String(s).trim());
  const renderCompatCheckSvg = (sizePx = 18) => <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" fill={PURPLE} style={{
    width: `${sizePx}px`,
    height: `${sizePx}px`,
    flexShrink: 0,
    display: "block",
    verticalAlign: "middle",
    margin: "0 auto"
  }} aria-hidden>
      <path d="M320 576C178.6 576 64 461.4 64 320C64 178.6 178.6 64 320 64C461.4 64 576 178.6 576 320C576 461.4 461.4 576 320 576zM438 209.7C427.3 201.9 412.3 204.3 404.5 215L285.1 379.2L233 327.1C223.6 317.7 208.4 317.7 199.1 327.1C189.8 336.5 189.7 351.7 199.1 361L271.1 433C276.1 438 282.9 440.5 289.9 440C296.9 439.5 303.3 435.9 307.4 430.2L443.3 243.2C451.1 232.5 448.7 217.5 438 209.7z" />
    </svg>;
  const products = useMemo(() => [{
    label: "Managed Sync",
    value: managedSync
  }, {
    label: "ActionKit",
    value: actionkit
  }, {
    label: "Workflows",
    value: workflows
  }, {
    label: "Proxy API",
    value: proxy
  }, {
    label: "Auth Type",
    value: authType
  }], [actionkit, managedSync, workflows, proxy, authType]);
  const [mounted, setMounted] = useState(false);
  const [resolvedIntegrationKey, setResolvedIntegrationKey] = useState(() => integrationSlugProp?.trim() || "");
  const [resolvedDisplayName, setResolvedDisplayName] = useState(() => integrationNameProp?.trim() || "Integration");
  const [inlineFormOpen, setInlineFormOpen] = useState(false);
  const [formFeature, setFormFeature] = useState({
    featureLabel: "",
    featureKey: ""
  });
  const [email, setEmail] = useState("");
  const [description, setDescription] = useState("");
  const [submitting, setSubmitting] = useState(false);
  const [submitError, setSubmitError] = useState("");
  const [voteClickError, setVoteClickError] = useState("");
  const [voteLogging, setVoteLogging] = useState(false);
  const [loggingFeatureKey, setLoggingFeatureKey] = useState("");
  const [, bumpVoteUi] = useState(0);
  const inlineCardRef = useRef(null);
  useEffect(() => {
    setMounted(true);
    const path = typeof window !== "undefined" ? window.location.pathname : "";
    const fromPath = integrationSlugProp?.trim() || integrationKeyFromPathname(path);
    setResolvedIntegrationKey(fromPath);
    const fromDom = readPageTitleFallback();
    const name = integrationNameProp?.trim() || fromDom || (fromPath ? fromPath.replace(/-/g, " ") : "Integration");
    setResolvedDisplayName(name);
  }, [integrationNameProp, integrationSlugProp]);
  const dismissInlineForm = useCallback(() => {
    setInlineFormOpen(false);
    setSubmitError("");
  }, []);
  const handleRequestClick = useCallback(async ({featureLabel, featureKey}) => {
    setVoteClickError("");
    const integrationKey = resolvedIntegrationKey || integrationKeyFromPathname(typeof window !== "undefined" ? window.location.pathname : "");
    if (!integrationKey) {
      setVoteClickError("Could not determine integration. Set the integrationSlug prop on this page.");
      return;
    }
    if (readVoteFromStorage(integrationKey, featureKey)) {
      return;
    }
    setLoggingFeatureKey(featureKey);
    setVoteLogging(true);
    const votedAt = new Date().toISOString();
    const ctaPayload = {
      integration_key: integrationKey,
      integration_name: resolvedDisplayName,
      feature_key: featureKey,
      feature_label: featureLabel,
      vote_phase: "cta_click",
      voted_at: votedAt
    };
    try {
      const endpoint = resolveEndpoint();
      if (endpoint) {
        const res = await fetch(endpoint, {
          method: "POST",
          headers: resolveHeaders(),
          body: JSON.stringify(ctaPayload)
        });
        if (!res.ok) {
          const text = await res.text().catch(() => "");
          throw new Error(text || `Request failed (${res.status})`);
        }
      } else if (typeof console !== "undefined" && console.warn) {
        console.warn("[IntegrationsCompatibility] FEATURE_REQUEST_ENDPOINT is empty; vote not sent. Set endpoint or window.__PARAGON_FEATURE_REQUEST__.");
      }
      writeVoteToStorage(integrationKey, featureKey);
      bumpVoteUi(v => v + 1);
      setFormFeature({
        featureLabel,
        featureKey
      });
      setEmail("");
      setDescription("");
      setSubmitError("");
      setInlineFormOpen(true);
    } catch (err) {
      setVoteClickError(err instanceof Error ? err.message : "Could not log your vote. Please try again.");
    } finally {
      setVoteLogging(false);
      setLoggingFeatureKey("");
    }
  }, [resolvedIntegrationKey, resolvedDisplayName]);
  useEffect(() => {
    if (!inlineFormOpen) return undefined;
    const id = window.setTimeout(() => {
      inlineCardRef.current?.scrollIntoView?.({
        behavior: "smooth",
        block: "nearest"
      });
    }, 80);
    return () => window.clearTimeout(id);
  }, [inlineFormOpen]);
  const submitEnrichment = useCallback(async () => {
    const trimmed = email.trim();
    if (!isValidEmail(trimmed)) {
      setSubmitError("Please enter a valid work email.");
      return;
    }
    const integrationKey = resolvedIntegrationKey || integrationKeyFromPathname(typeof window !== "undefined" ? window.location.pathname : "");
    if (!integrationKey) {
      setSubmitError("Could not determine integration. Set the integrationSlug prop on this page.");
      return;
    }
    const {featureKey, featureLabel} = formFeature;
    const endpoint = resolveEndpoint();
    const enrichedAt = new Date().toISOString();
    const payload = {
      integration_key: integrationKey,
      integration_name: resolvedDisplayName,
      feature_key: featureKey,
      feature_label: featureLabel,
      vote_phase: "enrichment",
      email: trimmed,
      description: description.trim() || undefined,
      voted_at: enrichedAt,
      enriched_at: enrichedAt
    };
    setSubmitting(true);
    setSubmitError("");
    try {
      if (endpoint) {
        const res = await fetch(endpoint, {
          method: "POST",
          headers: resolveHeaders(),
          body: JSON.stringify(payload)
        });
        if (!res.ok) {
          const text = await res.text().catch(() => "");
          throw new Error(text || `Request failed (${res.status})`);
        }
      } else if (typeof console !== "undefined" && console.warn) {
        console.warn("[IntegrationsCompatibility] FEATURE_REQUEST_ENDPOINT is empty; enrichment not sent. Set endpoint or window.__PARAGON_FEATURE_REQUEST__.");
      }
      setInlineFormOpen(false);
      setEmail("");
      setDescription("");
    } catch (err) {
      setSubmitError(err instanceof Error ? err.message : "Something went wrong. Please try again.");
    } finally {
      setSubmitting(false);
    }
  }, [email, description, formFeature, resolvedDisplayName, resolvedIntegrationKey]);
  const renderCompatProductCell = ({product, integrationKey, onRequestClick, alignWide, voteLogging, loggingFeatureKey}) => {
    const isAuthType = product.label === "Auth Type";
    const href = typeof product.value === "string" && !isAuthType ? product.value : null;
    const featureKey = slugifyFeature(product.label);
    const alreadyVoted = !isAuthType && integrationKey && !href && !product.value ? readVoteFromStorage(integrationKey, featureKey) : false;
    const unsupported = !isAuthType && !href && !product.value;
    const cellInner = isAuthType ? product.value : href ? <div style={{
      display: "inline-flex",
      alignItems: "center",
      gap: "6px"
    }}>
        {renderCompatCheckSvg()}
        <a href={href} className="compat-doc-link">
          Docs
          <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M7 17l9.2-9.2M17 17V7H7" /></svg>
        </a>
      </div> : product.value ? renderCompatCheckSvg() : alreadyVoted ? <span style={{
      fontSize: "12px",
      fontWeight: 500,
      color: "rgba(55, 55, 58, 0.85)"
    }} className="compat-requested-text">Requested</span> : <button type="button" className="compat-pill" disabled={voteLogging && loggingFeatureKey === featureKey} onClick={() => onRequestClick({
      featureLabel: product.label,
      featureKey
    })} style={{
      cursor: voteLogging && loggingFeatureKey === featureKey ? "not-allowed" : "pointer",
      opacity: voteLogging && loggingFeatureKey === featureKey ? 0.65 : 1
    }}>
        <svg className="compat-pill-plus" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" style={{
      color: PURPLE
    }} aria-hidden>
          <line x1="12" y1="5" x2="12" y2="19"></line>
          <line x1="5" y1="12" x2="19" y2="12"></line>
        </svg>
        {voteLogging && loggingFeatureKey === featureKey ? "Submitting..." : "Request"}
      </button>;
    if (alignWide) {
      return <div style={{
        fontSize: "13px",
        padding: "8px 8px",
        minHeight: unsupported || alreadyVoted ? "40px" : undefined,
        flex: 1,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        textAlign: "center"
      }}>
          {cellInner}
        </div>;
    }
    return <span style={{
      fontSize: "13px",
      width: "80px",
      textAlign: "center",
      display: "inline-flex",
      justifyContent: "center"
    }}>
        {cellInner}
      </span>;
  };
  return <div style={{
    backgroundColor: "transparent",
    border: "0px solid rgba(225, 225, 229, 1)",
    borderRadius: "12px",
    padding: "0px 0px",
    overflow: "hidden"
  }}>
      <style>{`
        .compat-wide { display: flex; }
        .compat-narrow { display: none; }
        @media (max-width: 640px) {
          .compat-wide { display: none !important; }
          .compat-narrow { display: flex !important; }
        }
        
        .compat-doc-link {
          color: rgb(102, 69, 230);
          font-weight: 500;
          font-size: 13px;
          display: inline-flex;
          align-items: center;
          gap: 2px;
          text-decoration: none;
        }
        .compat-doc-link:hover {
          text-decoration: underline;
          text-underline-offset: 2px;
        }
        .dark .compat-doc-link {
          color: rgb(162, 140, 255);
        }

        .compat-pill {
          display: inline-flex;
          align-items: center;
          justify-content: center;
          gap: 4px;
          box-sizing: border-box;
          min-height: 32px;
          padding: 6px 12px;
          border-radius: 999px;
          border: 1px solid rgba(0, 0, 0, 0.12);
          background-color: rgba(250, 250, 249, 1);
          font-size: 12px;
          font-weight: 500;
          color: rgba(35, 35, 38, 1);
          font-family: inherit;
          line-height: 1.2;
          width: auto;
          text-decoration: none;
          margin: 0;
          transition: box-shadow 0.15s ease, background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
        }
        .compat-pill:hover:not(:disabled) {
          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
        }
        
        a.compat-doc-link {
          text-decoration: underline !important;
          text-underline-offset: 2px !important;
          border-bottom: none !important;
          box-shadow: none !important;
          background: none !important;
        }
        a.compat-doc-link:hover {
          opacity: 0.8;
        }

        .dark .compat-pill {
          background-color: rgba(255, 255, 255, 0.05);
          border-color: rgba(255, 255, 255, 0.1);
          color: rgba(255, 255, 255, 0.85);
        }
        .dark .compat-pill:hover:not(:disabled) {
          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
          background-color: rgba(255, 255, 255, 0.1);
        }
        .dark .compat-pill-plus {
          color: rgba(255, 255, 255, 0.85) !important;
        }
        .dark .compat-requested-text {
          color: rgba(255, 255, 255, 0.6) !important;
        }

        .compat-form-card {
          margin-top: 24px;
          width: 100%;
          box-sizing: border-box;
          background-color: #fff;
          border-radius: 16px;
          border: 1px solid rgba(0, 0, 0, 0.1);
          box-shadow: 0 8px 24px rgba(0,0,0,0.06);
          padding: 24px;
        }
        .dark .compat-form-card {
          background-color: #0f1114;
          border-color: rgba(255, 255, 255, 0.1);
          box-shadow: 0 8px 24px rgba(0,0,0,0.4);
        }

        .compat-form-close-btn {
          flex-shrink: 0;
          display: inline-flex;
          align-items: center;
          justify-content: center;
          width: 36px;
          height: 36px;
          margin: 0;
          padding: 0;
          border-radius: 8px;
          background: #fff;
          color: rgba(35, 35, 38, 0.75);
          font-size: 24px;
          line-height: 1;
          font-family: inherit;
          border: none;
        }
        .dark .compat-form-close-btn {
          background: rgba(255,255,255,0.05);
          color: rgba(255,255,255,0.7);
        }

        .compat-form-title {
          margin: 0 0 16px;
          font-size: 20px;
          font-weight: 700;
          color: #111;
        }
        .dark .compat-form-title {
          color: rgba(255, 255, 255, 0.95);
        }

        .compat-form-text {
          display: block;
          margin: 0 0 20px;
          font-size: 14px;
          color: rgba(55, 55, 58, 0.72);
          line-height: 1.5;
        }
        .dark .compat-form-text {
          color: rgba(255, 255, 255, 0.6);
        }

        .compat-form-label {
          display: flex;
          flex-direction: column;
          gap: 6px;
          font-size: 13px;
          font-weight: 500;
          color: #333;
        }
        .dark .compat-form-label {
          color: rgba(255, 255, 255, 0.85);
        }

        .compat-form-input-readonly {
          width: 100%;
          box-sizing: border-box;
          padding: 10px 12px;
          border-radius: 8px;
          border: 1px solid rgba(0, 0, 0, 0.08);
          background-color: rgba(250, 248, 245, 0.95);
          font-size: 14px;
          color: rgba(35, 35, 38, 0.95);
        }
        .dark .compat-form-input-readonly {
          background-color: rgba(255, 255, 255, 0.05);
          border-color: rgba(255, 255, 255, 0.1);
          color: rgba(255, 255, 255, 0.7);
        }

        .compat-form-input {
          width: 100%;
          box-sizing: border-box;
          padding: 10px 12px;
          border-radius: 8px;
          border: 1px solid rgba(0, 0, 0, 0.15);
          background-color: #fff;
          font-size: 14px;
          font-family: inherit;
          color: #111;
        }
        .dark .compat-form-input {
          background-color: rgba(255, 255, 255, 0.05);
          border-color: rgba(255, 255, 255, 0.15);
          color: rgba(255, 255, 255, 0.95);
        }

        .compat-form-submit {
          padding: 10px 16px;
          border-radius: 8px;
          border: 1px solid rgba(0, 0, 0, 0.15);
          background: #fff;
          font-size: 14px;
          font-weight: 700;
          font-family: inherit;
          color: #111;
        }
        .dark .compat-form-submit {
          background: rgba(255, 255, 255, 0.1);
          border-color: rgba(255, 255, 255, 0.2);
          color: rgba(255, 255, 255, 0.95);
        }
        .compat-form-tag {
          display: inline-flex;
          align-items: center;
          gap: 6px;
          padding: 6px 10px;
          border-radius: 8px;
          background-color: rgba(102, 69, 230, 0.12);
          border: 1px solid rgba(102, 69, 230, 0.35);
          color: rgb(102, 69, 230);
          font-size: 12px;
          font-weight: 600;
        }
        .dark .compat-form-tag {
          background-color: rgba(162, 140, 255, 0.15);
          border-color: rgba(162, 140, 255, 0.4);
          color: rgb(180, 160, 255);
        }
      `}</style>

      <div className="compat-wide" style={{
    flexDirection: "row",
    gap: "0px"
  }}>
        {products.map(f => <div key={f.label} style={{
    flex: 1,
    minWidth: 0,
    textAlign: "center",
    display: "flex",
    flexDirection: "column"
  }}>
            <div style={{
    fontSize: "13px",
    fontWeight: 500,
    padding: "6px 8px",
    borderBottom: "1px solid rgba(102, 69, 230, 0.15)",
    overflow: "hidden",
    textOverflow: "ellipsis",
    whiteSpace: "nowrap",
    textAlign: "center"
  }}>
              {f.label}
            </div>
            {renderCompatProductCell({
    product: f,
    integrationKey: mounted ? resolvedIntegrationKey : "",
    onRequestClick: handleRequestClick,
    alignWide: true,
    voteLogging,
    loggingFeatureKey
  })}
          </div>)}
      </div>

      <div className="compat-narrow" style={{
    flexDirection: "column",
    gap: "0px"
  }}>
        <div style={{
    display: "flex",
    justifyContent: "space-between",
    borderBottom: "1px solid rgba(102, 69, 230, 0.15)",
    padding: "6px 0"
  }}>
          <span style={{
    fontSize: "13px",
    fontWeight: 500
  }}>Product</span>
          <span style={{
    fontSize: "13px",
    fontWeight: 500,
    width: "80px",
    textAlign: "center"
  }}>Supported</span>
        </div>
        {products.map(f => <div key={f.label} style={{
    display: "flex",
    justifyContent: "space-between",
    alignItems: "center",
    padding: "5px 0"
  }}>
            <span style={{
    fontSize: "13px"
  }}>{f.label}</span>
            {renderCompatProductCell({
    product: f,
    integrationKey: mounted ? resolvedIntegrationKey : "",
    onRequestClick: handleRequestClick,
    alignWide: false,
    voteLogging,
    loggingFeatureKey
  })}
          </div>)}
      </div>

      {voteClickError ? <p style={{
    margin: "16px 0 0",
    fontSize: "13px",
    color: "#b42318"
  }} role="alert">
          {voteClickError}
        </p> : null}

      {inlineFormOpen ? <div ref={inlineCardRef} role="region" aria-labelledby="compat-feature-request-title" className="compat-form-card">
          <div style={{
    display: "flex",
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "space-between",
    gap: "12px",
    marginBottom: "16px"
  }}>
            <div className="compat-form-tag">
              Feature request
            </div>
            <button type="button" onClick={dismissInlineForm} disabled={submitting} aria-label="Close feature request form" className="compat-form-close-btn" style={{
    cursor: submitting ? "not-allowed" : "pointer"
  }}>
              <span aria-hidden>×</span>
            </button>
          </div>
          <h2 id="compat-feature-request-title" className="compat-form-title">
            Request support for {formFeature.featureLabel}
          </h2>
          <p className="compat-form-text">
            Your vote has been recorded. Add your email and an optional note and we'll notify you when support is added.
          </p>

          <div style={{
    display: "flex",
    flexDirection: "column",
    gap: "16px"
  }}>
            <label className="compat-form-label">
              Integration
              <input type="text" readOnly value={resolvedDisplayName} className="compat-form-input-readonly" tabIndex={-1} />
            </label>
            <label className="compat-form-label">
              Feature
              <input type="text" readOnly value={formFeature.featureLabel} className="compat-form-input-readonly" tabIndex={-1} />
            </label>
            <label className="compat-form-label">
              Work email
              <input type="email" name="email" autoComplete="email" placeholder="you@company.com" value={email} onChange={e => setEmail(e.target.value)} className="compat-form-input" disabled={submitting} />
            </label>
            <label className="compat-form-label">
              Describe your use case (optional)
              <textarea placeholder="What would you build if this were supported?" value={description} onChange={e => setDescription(e.target.value)} rows={4} className="compat-form-input" style={{
    resize: "vertical",
    minHeight: "96px"
  }} disabled={submitting} />
            </label>
          </div>

          {submitError ? <p style={{
    margin: "14px 0 0",
    fontSize: "13px",
    color: "#b42318"
  }} role="alert">
              {submitError}
            </p> : null}

          <div style={{
    display: "flex",
    justifyContent: "flex-end",
    gap: "10px",
    marginTop: "22px",
    flexWrap: "wrap"
  }}>
            <button type="button" onClick={submitEnrichment} disabled={submitting} className="compat-form-submit" style={{
    cursor: submitting ? "not-allowed" : "pointer"
  }}>
              {submitting ? "Submitting…" : "Submit details"}
            </button>
          </div>
        </div> : null}
    </div>;
};

<IntegrationsCompatibility workflows={true} actionkit="/actionkit/integrations/googledrive" proxy={true} managedSync="/managed-sync/integrations/googledrive" authType="OAuth2" />

## Setup Guide

You can find your Google app credentials by visiting your [Google Cloud Console dashboard](https://console.cloud.google.com/projectselector2/home/dashboard?supportedpurview=project).

You'll need the following information to set up your Google App with Paragon Connect:

* Client ID
* Client Secret
* Scopes Requested

<Warning>
  Google Applications using scopes that permit access to certain user data must complete a verification process. [Read more](https://developers.google.com/identity/protocols/oauth2/scopes) about Google's various API scopes and processes to verify your usage.
</Warning>

### Add the Redirect URL to your Google app

Paragon provides a redirect URL to send information to your Google app. To add the redirect URL to your Google app:

1. Copy the link under "**Redirect URL"** in your integration settings in Paragon. The Redirect URL is:

```
https://passport.useparagon.com/oauth
```

2. In your [Google Cloud Console dashboard](https://console.cloud.google.com/projectselector2/home/dashboard?supportedpurview=project), navigate to **APIs & Services > Credentials** in the sidebar.

3. Press "**+ Create Credentials**", then select **OAuth client ID.**

4. Select "**Web application**" from the Application type drop-down menu.

<Info>
  **Note:** You'll need to configure Google's [consent screen](https://console.developers.google.com/apis/credentials) for access to **Client ID** and **Client Secret** if you haven't already.
</Info>

<Frame>
  <img src="https://mintcdn.com/paragon/cqJFSKyZXJDp3p3z/assets/Selecting%20Web%20Application%20in%20Google%20OAuth.png?fit=max&auto=format&n=cqJFSKyZXJDp3p3z&q=85&s=c3d99d48bd87c1ca7ed8945d5ada4d20" alt="" width="1100" height="460" data-path="assets/Selecting Web Application in Google OAuth.png" />
</Frame>

5. Under **Authorized redirect URIs**, press the "**+ Add URI**" button.

6. Paste-in the redirect URL from Paragon.

7. Press the blue "**Create**" button.

<Frame>
  <img src="https://mintcdn.com/paragon/XNDe-bFT_2yOxy1A/assets/Connect%20-%20Adding%20Google%20redirect%20URI%20for%20OAuth.gif?s=d645ae8275601193c745916527ad8fd1" alt="" width="548" height="257" data-path="assets/Connect - Adding Google redirect URI for OAuth.gif" />
</Frame>

Google provides you with the **Client ID** and **Client Secret** needed for the next steps after adding the redirect URL to your project.

### Enable Google Drive API in Google Cloud Console Dashboard

1. In your [Google Cloud Console dashboard,](https://console.cloud.google.com/projectselector2/home/dashboard?supportedpurview=project) navigate to **APIs & Services > Library** in the sidebar.

2. Search for "**Google Drive API**" from the API Library.

3. Select the "**Google Drive API**".

4. Press the blue "**Enable**" button to enable the API for your application.

<Frame>
  <img src="https://mintcdn.com/paragon/jCM_Y_j0HttScr1R/assets/Enabling%20Google%20Drive%20API.png?fit=max&auto=format&n=jCM_Y_j0HttScr1R&q=85&s=86357e070566e4cefbe4c28fbc9638e4" alt="" width="1318" height="474" data-path="assets/Enabling Google Drive API.png" />
</Frame>

### Add your Google app to Paragon

1. Select **Google Drive** from the **Integrations Catalog**.

2. Under **Integrations > Connected Integrations > Google Drive > App Configuration > Configure**, fill out your credentials from the end of [Step 1](/resources/integrations/google-drive#1-add-the-redirect-url-to-your-google-app) in their respective sections:

* **Client ID:** Found at the end of Step 1.
* **Client Secret:** Found at the end of Step 1.
* **Permissions:** Select the scopes you've requested for your application. For a list of recommended scopes, please view this integration within your Paragon dashboard. [View dashboard.](https://dashboard.useparagon.com) They should begin with `drive`.

Press the purple "**Save Changes**" button to save your credentials.

<Info>
  **Note:** Leaving the Client ID and Client Secret blank will use Paragon development keys.
</Info>

<Frame>
  <img src="https://mintcdn.com/paragon/XNDe-bFT_2yOxy1A/assets/Connecting%20a%20new%20Google%20app%20to%20Paragon%20Connect.png?fit=max&auto=format&n=XNDe-bFT_2yOxy1A&q=85&s=6308a617fd7eb7ebf9731f5fe9b0796a" alt="" width="1764" height="1106" data-path="assets/Connecting a new Google app to Paragon Connect.png" />
</Frame>

## Connecting to Google Drive

Once your users have connected their Google Drive account, you can use the Paragon SDK to access the Google Drive API on behalf of connected users.

See the Google Drive [REST API documentation](https://developers.google.com/drive/api/v3/reference) for their full API reference.

Any Google Drive API endpoints can be accessed with the Paragon SDK as shown in this example.

```js theme={null}
// You can find your project ID in the Overview tab of any Integration

// Authenticate the user
paragon.authenticate(<ProjectId>, <UserToken>);
            
// Query Files
await paragon.request("googledrive", `/files?q=${
  encodeURIComponent('name="test"')
}`, {
  method: "GET"
});
```

## Building Google Drive workflows

Once your Google Drive account is connected, you can add steps to perform the following actions:

* Get File Metadata
* Upload File
* Download Google Doc
* Create Folder
* Delete Folder
* Get Folder by ID
* Move Folder
* Get Files
* Search Folders

You can also use the [Google Drive Request](/workflows/requests#making-integration-requests) step to access any of Google Drive's API endpoints without the authentication piece.

When saving or getting files in Google Drive, you can reference data from previous steps by typing `{{` to invoke the variable menu.

## Using Webhook Triggers

Webhook triggers can be used to run workflows based on events in your users' Google Drive account. For example, you might want to trigger a workflow whenever new files are created in Google Drive to sync your users' Google Drive files to your application in real-time.

<Frame>
  <img src="https://mintcdn.com/paragon/7RZyQGncIlY8Xl4A/assets/Google%20Drive%20Triggers%20in%20Paragon%20Connect.png?fit=max&auto=format&n=7RZyQGncIlY8Xl4A&q=85&s=1672f8753edc372bab2664e01bd32f48" alt="" width="1004" height="618" data-path="assets/Google Drive Triggers in Paragon Connect.png" />
</Frame>

You can find the full list of Webhook Triggers for Google Drive below:

* **File Created**
* **File Updated**
* **File Deleted**

## Using the Google Drive File Picker

You can allow your user to select files from their Google Drive account in your app with the Google Drive File Picker provided by the Paragon SDK.

<Frame>
  <img src="https://mintcdn.com/paragon/fdJ50ZEE8l2RCUKL/assets/using-the-google-drive-file-picker-in-paragon-connect.gif?s=b666b304bd41ec0d47e30b5270f3b4ba" alt="" width="563" data-path="assets/using-the-google-drive-file-picker-in-paragon-connect.gif" />
</Frame>

**Enable the Google Picker API**

You will need to enable the Google Picker API for your application in order to access the File Picker.

1. In your [Google Cloud Console dashboard,](https://console.cloud.google.com/projectselector2/home/dashboard?supportedpurview=project) navigate to **APIs & Services > Library** in the sidebar for your app.

2. Search for "**Google Picker API**" from the API Library.

3. Select the "**Google Picker API**" and press the blue "**Enable**" button to enable the API for your application.

<Frame>
  <img src="https://mintcdn.com/paragon/jCM_Y_j0HttScr1R/assets/Enabling%20the%20Google%20Picker%20API%20for%20a%20Google%20Drive%20integraiton%20for%20Paragon%20Connect.png?fit=max&auto=format&n=jCM_Y_j0HttScr1R&q=85&s=e01db9255a3a68ae699a37be6d8608f7" alt="" width="1384" height="506" data-path="assets/Enabling the Google Picker API for a Google Drive integraiton for Paragon Connect.png" />
</Frame>

**Creating a Google Drive API Key**

To show the Google Drive File Picker, you will need a **Google Drive API Key**. This key is a separate key from the Client ID you provided to Paragon during integration setup.

1. Navigate to [Google Cloud Console > APIs & Services > Credentials](https://console.cloud.google.com/apis/credentials). Make sure the selected project in the header is your app.

2. Click **Create Credentials** and select **API key**.

3. An API key value will appear. Copy this value to use in [Showing the File Picker](#showing-the-file-picker).

<Frame>
  <img src="https://mintcdn.com/paragon/p_CuCy_equ6Xxvlm/assets/credentials_APIs_Services_example_Google_Cloud_console.png?fit=max&auto=format&n=p_CuCy_equ6Xxvlm&q=85&s=7984f759b1623faaa563ca1a712a99e7" alt="" width="2304" height="804" data-path="assets/credentials_APIs_Services_example_Google_Cloud_console.png" />
</Frame>

**Note**: While the API Key value is not sensitive and can safely be used in your public application, we recommend restricting the API Key with the following settings:

* Application restrictions: Websites with your origin/domain
* API restrictions: Google Drive API

[Read more](https://cloud.google.com/docs/authentication/api-keys#api_key_restrictions) in Google's docs for API Key restrictions.

#### Showing the File Picker

Use the Paragon SDK in your frontend application to show the File Picker in your app.

The SDK provides an `ExternalFilePicker` class to load Google's JavaScript into your page and authenticate with your user's connected Google Drive account.

To initialize the picker you will need your Google Drive API key and your Google Cloud project number. The Google Cloud project number is a unique, numerical identifier for your project. You can find it within the Google Cloud Console from the sidebar navigation under [**IAM & Admin section > Settings**](https://console.cloud.google.com/iam-admin/settings).

```js theme={null}
let picker = new paragon.ExternalFilePicker("googledrive", {
    onFileSelect: (files) => {
        // Handle file selection
    },

    // Optional settings:
    integrationOptions: {
        googledrive: {
            // If true, a tab in the Picker appears for browsing Shared Drives
            enableSharedDrives: true,

            // If true, the full folder hierarchy appears in the Picker. Otherwise,
            // all files will appear as a flat list. Defaults to `false`.
            includeFolders: false

            // Set the display mode of the Picker as a thumbnail grid or list of file titles.
            // Defaults to "grid". Thumbnail grid requires either `https://www.googleapis.com/auth/drive` or
            // `https://www.googleapis.com/auth/drive.readonly` scope to appear correctly.
            viewMode: "grid" || "list",
        }
    }
});

// Loads external dependencies and user's access token
await picker.init({ developerKey: "YOUR_GOOGLE_API_KEY", "appId": "YOUR_GOOGLE_PROJECT_NUMBER" });

// Open the File Picker
picker.open();
```

You can configure the File Picker to listen for additional callbacks or to restrict allowed file types. Learn more about configuring File Picker options in the [SDK Reference](/apis/api-reference#externalfilepicker).

#### Downloading the Selected File

The Google File Picker callback will return a `Response` object describing the user's file picker interaction including an array of any files selected. Using this array of `fileIds`, you can use the [Proxy API](/apis/proxy) to perform an authenticated proxy requests to download the files.

**Note:** Files containing binary content, including photos and videos, should be downloaded using the `files.get` endpoint whereas Google Workspace Documents can be downloaded using `files.export.`

<CodeGroup>
  ```javascript Downloadable files, e.g. images, videos, PDFs theme={null}
  // Downloading a non-Google Workspace File like images and videos
  await paragon.request('googledrive', '/files/<fileId>?alt=media', {
  	method: 'GET'
  });
  ```

  ```javascript Google Workspace files, e.g. Google Docs, Slides, Sheets theme={null}
  // Exports a Google Workspace document to the requested MIME type
  await paragon.request('googledrive', '/files/<fileId>/export?mimeType=<supportedMimeType>/pdf', {
  	method: 'GET'
  });
  ```

  ```plain REST API theme={null}
  POST https://proxy.useparagon.com/projects/19d...012/sdk/proxy/googledrive/files/<fileId>/export?mimeType=<supportedMimeType>/pdf

  Authorization: Bearer eyJ...
  Content-Type: application/json
  ```
</CodeGroup>

## Publishing your Google Drive app

**Required for publishing**: In order to complete the Google application verification process, you must implement the following feature in your integration:

* [Setting up Redirect Pages](/resources/integrations/google-drive#setting-up-a-redirect-page-in-your-app)

For more information, see [Google's documentation on validation requirements](https://support.google.com/cloud/answer/13463073).

### Setting up a Redirect Page in your app

<Frame>
  <iframe width="768" height="432" src="https://www.youtube.com/embed/W8OGUn4ye3E" title="[Tutorial] Implementing the Google Redirect Page install flow with Paragon" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen />
</Frame>

Your Google Drive integration requires a Redirect Page hosted in your application to support verification of your application by Google.

The Redirect Page should be implemented as follows:

* Able to receive a `GET` request with a number of query parameters.
* Redirect to `https://passport.useparagon.com/oauth` with the same query parameters.

```js theme={null}
paragon.connect("googledrive", {
  overrideRedirectUrl: "https://your-app.url/google-drive-redirect"
});
```

### Updating the allowed Redirect URL

If you were previously testing with `https://passport.useparagon.com/oauth` as your Google Drive Redirect URL, you will need to update this value after implementing a Redirect Page:

1. Log into your [Google Cloud Console dashboard](https://console.cloud.google.com/projectselector2/home/dashboard?supportedpurview=project) and select your application.

2. Navigate to **APIs & Services > Credentials** and select the credentials you use with Paragon.

3. Under **Authorized redirect URIs**, provide the URL of your Redirect Page.
