feat: Google Drive tab on upload form#2306
Merged
Merged
Conversation
Adds a third "Google Drive" tab to the upload form alongside "Your computer" and "Dropbox". Clicking "Choose from Google Drive" opens the Google Picker (loaded on demand from Google's CDN — no npm dep), requests an access token scoped to drive.file (per-file authorization, narrowest possible), and POSTs the picked file + bearer token to the existing POST /api/upload/google_drive endpoint. The same conversion + downloads flow as Dropbox runs from there. The new useGooglePicker hook lazy-loads both apis.google.com/js/api.js (gapi/Picker) and accounts.google.com/gsi/client (Google Identity Services token model), then opens the Picker inside the token callback — opening it outside would race the token initialization and silently fail. Tab is gated on REACT_APP_GOOGLE_CLIENT_ID and REACT_APP_GOOGLE_API_KEY being present. When either is missing the tab is hidden, so deploys without the env vars degrade gracefully to the existing two-tab layout. Server side needs no changes — the endpoint, repository, and migration shipped in #2300 and #2305. Closes the empty "From Google Drive" section on Downloads that #2305 added — without this PR, nothing populates the google_drive_uploads table from the web app. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Every user-visible PR ships its What's New line in the same PR so the page is current the moment the feature lands. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
1 task
aalemayhu
added a commit
that referenced
this pull request
May 15, 2026
## What Adds \`https://apis.google.com\` and \`https://accounts.google.com\` to the \`script-src\` directive of the CSP meta tag in \`web/index.html\`. Required for the Google Drive upload tab from #2306 to function. ## Why Browser console on prod after deploying #2306 showed: \`\`\` Loading the script 'https://apis.google.com/js/api.js' violates the following Content Security Policy directive: "script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com https://static.hotjar.com https://script.hotjar.com https://www.dropbox.com" \`\`\` Same violation for \`accounts.google.com/gsi/client\`. Both are required by the Picker / GIS flow that \`useGooglePicker\` lazy-loads. ## How One-line addition to the existing CSP meta tag's \`script-src\`. No other directives needed — frame/connect/img directives are not set on this site, so the Picker iframe and XHR calls were never blocked. ## Testing - Local: rebuild + smoke-test the upload form. Tab opens the Picker. - Prod (after rebuild): browser console shows no CSP violations for the two Google origins. ## Risks - Two new third-party script origins in script-src — both are Google-owned and serve the official Picker SDK; this is the same trust boundary as the existing Dropbox SDK allowance (\`www.dropbox.com\`). - Rollback: revert this commit. The CSP returns to its prior shape; the Drive tab silently fails to load Picker, same as before this fix. ## Goal alignment Unblocks #2306, which closes the empty "From Google Drive" history section #2305 introduced. <!-- codesmith:footer --> --- <a href="https://app.blacksmith.sh/2anki/codesmith/server/pr/2307"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-in-codesmith-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-in-codesmith-light.svg"><img alt="View in Codesmith" src="https://pr-comments-assets.blacksmith.sh/codesmith/view-in-codesmith-dark.svg"></picture></a> <sup>Need help on this PR? Tag <code>@codesmith</code> with what you need.</sup> - [ ] Let Codesmith autofix CI failures and bot reviews <!-- /codesmith:footer --> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 task
aalemayhu
added a commit
that referenced
this pull request
May 15, 2026
## What Adds \`.setAppId(projectNumber)\` to the Picker builder chain in \`useGooglePicker.ts\`. \`projectNumber\` is derived at runtime from the existing \`REACT_APP_GOOGLE_CLIENT_ID\` (numeric prefix of the client ID), so no new env var is needed. ## Why With the \`drive.file\` OAuth scope (the narrowest, picked-files-only scope we chose in #2306), Google Picker only binds the picked file to *our* OAuth client when \`setAppId\` is called. Without it, the user can pick a file in the UI but the server's \`GET https://www.googleapis.com/drive/v3/files/<id>?alt=media\` with the bearer token returns **404 File not found**. Every Drive pick on prod was failing with "Error handling Google Drive files" until this fix. Verified on prod logs: \`\`\` 2|server | google drive upload success 2|server | POST /api/upload/google_drive 400 ... status: 404, message: "File not found: 1Gxh9TSApii4ErELl0jePIGUVMldsgSRe" \`\`\` ## How - One-line change in the builder chain: \`.setAppId(projectNumber)\` between the constructor and the rest of the chain. - \`projectNumber\` is derived as \`clientId().split('-')[0]\` — the OAuth client ID has the shape \`<projectNumber>-<hash>.apps.googleusercontent.com\`. Engineer trio call: derive instead of adding a third env var, because two vars that must stay in sync is a deployment footgun. - Test mock updated with a \`setAppId\` stub. Added an assertion that the derived project number reaches \`setAppId\`. - Changelog entry added for the user-visible fix. ## Testing - 6/6 \`useGooglePicker\` unit tests pass (was 5; +1 for the setAppId assertion). - \`/check\`: server tsc clean, web typecheck clean, 484 Vitest tests pass, Biome lint clean. ## Risks - Rollback: revert this commit. The tab returns to the broken state from before this PR, no other surface affected. - The \`drive.file\` scope still requires the Picker API enabled on the GCP project (already enabled — Picker UI opens on prod). ## Trio synthesis - **PM:** Ship the 1-line fix now. Severity high — 100% of Drive clicks fail post-#2306. Forward fix is cleaner than rolling back. - **Designer:** Hide tab if env vars missing. No copy changes (generic error state is fine for a hotfix). - **Engineer:** Derive project number from the OAuth client ID; no new env var. Insert \`setAppId\` in the chain. Mock needs the stub. - **Conflict:** PM and Designer both assumed a new env var \`REACT_APP_GOOGLE_PROJECT_NUMBER\`. Engineer pointed out it's derivable. **Resolution:** derive it, no new env var, no drift risk. ## Goal alignment Closes the deploy-blocking bug from #2306 so the Drive tab actually works for the user base that needs it. Critical for the 300K-user goal — without this fix, the Drive tab is visible but every click fails. <!-- codesmith:footer --> --- <a href="https://app.blacksmith.sh/2anki/codesmith/server/pr/2308"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-in-codesmith-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-in-codesmith-light.svg"><img alt="View in Codesmith" src="https://pr-comments-assets.blacksmith.sh/codesmith/view-in-codesmith-dark.svg"></picture></a> <sup>Need help on this PR? Tag <code>@codesmith</code> with what you need.</sup> - [ ] Let Codesmith autofix CI failures and bot reviews <!-- /codesmith:footer --> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


What
Adds a third "Google Drive" tab to the upload form alongside "Your computer" and "Dropbox". Clicking "Choose from Google Drive" opens the Google Picker, lets the user pick one file, and POSTs the picked file metadata plus an OAuth bearer token to the existing
POST /api/upload/google_driveendpoint. The same conversion + downloads flow that Dropbox uses runs from there.The tab is gated on
REACT_APP_GOOGLE_CLIENT_IDandREACT_APP_GOOGLE_API_KEY. When either is missing, the tab is hidden — deploys without the env vars degrade gracefully to the two-tab layout. The prod build needs both env vars set before users will see the new tab.Why
The
google_drive_uploadstable has been around since August 2024 but nothing in the web app has populated it for a while. #2305 added a "From Google Drive" section on Downloads — without this PR that section sits empty forever. Drive is the largest unaddressed file source for student users (Notion + Dropbox + Drive ≈ all of them), so closing the loop is the single biggest conversion-surface gap on the upload form.How
useGooglePickerhook (web/src/pages/UploadPage/components/UploadForm/hooks/useGooglePicker.ts) — lazy-loadsapis.google.com/js/api.js(gapi/Picker) andaccounts.google.com/gsi/client(Google Identity Services token client) on demand. No npm package — the official scripts are the only sanctioned path Google supports for Picker, and the equivalent npm packages pull in heavy server-side SDKs that are wrong for the browser.https://www.googleapis.com/auth/drive.file— Google's narrowest Picker scope. Per-file authorization (only the file the user picked is readable by our OAuth client); cannot enumerate the user's other Drive files. This was an active trio resolution — PM wanteddrive.file, engineer initially saiddrive.readonly. Drive.file wins because Picker authorizes those specific files for us and the existing server-sideAuthorization: Bearerdownload call still works.requestAccessTokencallback is fire-and-forget — it does not return a Promise. The Picker must be opened inside that callback or it races the token init and silently no-ops. The hook does this.UploadSourceTabs.tsx(new entrant at the end, per designer call).handleGoogleDriveClick+handleGoogleDriveFilesinUploadForm.tsxmirroring the Dropbox handlers. POSTsfiles(JSON-stringifiedGoogleDriveFile[]) +googleDriveAuth(bearer) — the exact body shapesrc/controllers/Upload/helpers/handleGoogleDrive.tsexpects.converting/success/emptyDeck/error/limitReached). The converting status line now reads "Fetching {filename} from Google Drive" when Drive is the source — same pattern as Dropbox.currentColorDrive triangle glyph (no full-color Google mark — full-color marks have stricter brand-guideline rules and fight the Stripe-class restraint baseline).Measuring success
Leading indicator: Drive-sourced conversions per week. Phase-2 gate: 25+ successful Drive uploads in the first 14 days post-launch unlocks investment in folder-pick + multi-file. Below 10 → leave as-is. Secondary: rows in
google_drive_uploadsstart increasing immediately after deploy, which surfaces the "From Google Drive" history section on/downloadsfor real users.Testing
useGooglePicker.test.ts— 5 tests (loading-state guards, picked-files outcome with access token, cancelled outcome, token-error rejection, missing-env-var rejection). Mockswindow.gapi,window.google.picker, andwindow.google.accounts.oauth2to drive the callbacks deterministically without loading the real scripts.UploadForm.test.tsxextended with 2 tests for tab visibility (Drive tab appears iff both env vars are present)./check: server tsc clean, web typecheck clean, 483 Vitest tests pass (was 481 before; +5 hook tests, +2 form tests minus 2 reused existing setup), Biome lint clean.Risks
drive.filescope inline. If they decline, the token callback getserror: 'access_denied'and we surface "Couldn't reach Google Drive. Sign in again and retry." Worth watching for the first week.apis.google.comandaccounts.google.com. Script blockers or corporate proxies will fail to load them and the user sees "Couldn't load Google Drive. Check your connection or disable script blockers and try again." Same behavior as the Dropbox tab on a Dropbox-blocked network.Out of scope (deferred to phase 2)
/api/upload/google_drivealready enforces.Sonar
Sonar scanner not run locally (token not configured in this environment) — flagging for reviewers so a Sonar bounce on the new hook isn't a surprise. The hook has nested callbacks which can trigger cognitive complexity warnings.
Goal alignment
Trio synthesis
currentColor— no full-color Google mark.<script>tags for both Google scripts (no npm pkg). Gotcha: open Picker inside the token callback. Hook unit tests mock both globals.drive.filescope; Engineer saiddrive.readonly. Resolution:drive.file— narrower, Google-recommended for Picker workflows, server-side download still works because Picker authorizes those specific files for the OAuth client.Need help on this PR? Tag
@codesmithwith what you need.