Repository navigation
STU-384: Implement the account connection and single provider-key editor #92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d27ade3
3f865c9
30d28b5
d4306b3
26c2afe
2720c4a
f4b5e0f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -925,6 +925,7 @@ function getFlutterFlowEndpoint() { | |
|
|
||
| function setFlutterFlowEndpoint(endpoint) { | ||
| localStorage.setItem("flutterflow_api_endpoint", endpoint); | ||
| invalidateStaleProjectSelection(); | ||
| return true; | ||
| } | ||
|
|
||
|
|
@@ -949,6 +950,15 @@ let commitTargetProjectId = null; | |
| // modal's dropdown when the modal is closed/reopened before the fetch returns. | ||
| let confirmProjectToken = 0; | ||
|
|
||
| // Canonical account-connection state for the account connection card. It is | ||
| // derived from the outcome of a real FlutterFlow projects fetch — never from | ||
| // the presence of stored bytes alone and never from the placeholder token | ||
| // "set". 'connected' is only reachable from an actual listProjects response | ||
| // that returned at least one project. | ||
| // kind: 'not-configured' | 'validating' | 'connected' | 'no-projects' | ||
| // | 'unauthorized' | 'network-error' | ||
| let ffConnectionState = "not-configured"; | ||
|
|
||
| async function initializeApiKeys() { | ||
| // Remove an exportable key left by an earlier version even if its encrypted | ||
| // credentials were already cleared. | ||
|
|
@@ -959,6 +969,16 @@ async function initializeApiKeys() { | |
| flutterflowProjectId = await getApiKey("flutterflow_project_id"); | ||
| updateApiKeyStatusIndicators(); | ||
| updateDeployButtonVisibility(); | ||
|
|
||
| // If a FlutterFlow key is stored, confirm the real connection even before | ||
| // the settings modal is opened, so the account connection card is truthful | ||
| // on load. Non-blocking; runs once per initialize. | ||
| if (flutterflowApiKey) { | ||
| validateFlutterFlowConnection(); | ||
| } else { | ||
| ffConnectionState = "not-configured"; | ||
| renderApiKeyConnection(); | ||
| } | ||
| } | ||
|
|
||
| // --- API KEY UI FUNCTIONS --- | ||
|
|
@@ -1265,6 +1285,139 @@ function validateFlutterFlowProjectId(projectId) { | |
| return /^[a-zA-Z0-9-]+$/.test(projectId); | ||
| } | ||
|
|
||
| /** | ||
| * Renders the account connection card from the canonical connection state. | ||
| * Not-config/validating/connected/no-projects/unauthorized/network-error are | ||
| * derived from ffConnectionState, which is only ever set from a real | ||
| * listProjects outcome. No raw or masked API key is ever written into a status | ||
| * label here. | ||
| */ | ||
| function renderApiKeyConnection() { | ||
| const dot = document.getElementById("acct-ff-dot"); | ||
| const status = document.getElementById("acct-ff-status"); | ||
| const endpoint = document.getElementById("acct-ff-endpoint"); | ||
| const project = document.getElementById("acct-ff-project"); | ||
|
|
||
| // Endpoint and default project always mirror the stored configuration (the | ||
| // same value the deploy confirmation pre-selects). | ||
| if (endpoint) { | ||
| const ep = getFlutterFlowEndpoint(); | ||
| endpoint.textContent = ep && ep.includes("staging") ? "Staging" : "Production"; | ||
| } | ||
| if (project) { | ||
| project.textContent = flutterflowProjectId || "—"; | ||
| } | ||
| if (!status) return; | ||
|
|
||
| let label; | ||
| let tone; | ||
| if (!hasStoredKey("flutterflow")) { | ||
| label = "Not connected — add your FlutterFlow API key"; | ||
| tone = ""; | ||
| } else { | ||
| switch (ffConnectionState) { | ||
| case "connected": | ||
| label = "Connected to FlutterFlow"; | ||
| tone = "ok"; | ||
| break; | ||
| case "no-projects": | ||
| label = "Key accepted — no projects found"; | ||
| tone = "warn"; | ||
| break; | ||
| case "unauthorized": | ||
| label = "API key rejected (401/403) — re-enter your key"; | ||
| tone = "bad"; | ||
| break; | ||
| case "network-error": | ||
| label = "Could not reach FlutterFlow — check your network"; | ||
| tone = "bad"; | ||
| break; | ||
| case "validating": | ||
| default: | ||
| label = "Checking connection…"; | ||
| tone = "warn"; | ||
| break; | ||
| } | ||
| } | ||
| status.textContent = label; | ||
| if (dot) dot.className = `acct-dot${tone ? " " + tone : ""}`; | ||
| } | ||
|
|
||
| /** | ||
| * Refreshes the canonical connection state by calling the real FlutterFlow | ||
| * projects endpoint with the stored key. A returned empty list and a 401/403 | ||
| * are distinct outcomes; both differ from a network failure. Used at startup | ||
| * and after saving/clearing so the account connection card is truthful. | ||
| * @returns {Promise<Array|null>} the loaded projects, or null on error. | ||
| */ | ||
| async function validateFlutterFlowConnection() { | ||
| const apiKey = await getApiKey("flutterflow"); | ||
| if (!apiKey || !hasStoredKey("flutterflow")) { | ||
| ffConnectionState = "not-configured"; | ||
| renderApiKeyConnection(); | ||
| return null; | ||
| } | ||
|
|
||
| ffConnectionState = "validating"; | ||
| renderApiKeyConnection(); | ||
|
|
||
| try { | ||
| const client = new FlutterFlowApiClient( | ||
| apiKey, | ||
| "", | ||
| "main", | ||
| getFlutterFlowEndpoint(), | ||
| ); | ||
| const projects = await client.listProjects(); | ||
| ffConnectionState = projects && projects.length > 0 | ||
| ? "connected" | ||
| : "no-projects"; | ||
| renderApiKeyConnection(); | ||
| return projects; | ||
| } catch (error) { | ||
| const msg = String((error && error.message) || ""); | ||
| ffConnectionState = /(401|403)|denied|unauthorized|scoped|list permission/i.test(msg) | ||
| ? "unauthorized" | ||
| : "network-error"; | ||
|
Comment on lines
+1377
to
+1381
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| renderApiKeyConnection(); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Clears the currently selected FlutterFlow project so a stale selection | ||
| * (belonging to a different endpoint, or a removed key) is never reused as a | ||
| * deploy/sync default. Storage, the in-memory value and the dropdown mirror | ||
| * each other: the account connection card and the deploy confirmation read the | ||
| * same stored value, so invalidating it keeps them consistent. | ||
| */ | ||
| function clearStoredProjectSelection() { | ||
| flutterflowProjectId = ""; | ||
| localStorage.removeItem(STORAGE_KEY_PREFIX + "flutterflow_project_id"); | ||
| sessionStorage.removeItem(SESSION_STORAGE_KEY_PREFIX + "flutterflow_project_id"); | ||
| } | ||
|
Comment on lines
+1394
to
+1398
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Replacement keys retain stale projects Saving a replacement key without selecting a project retains the old default. Learn moreThe project ID is stored independently from the API key. The new helper clears it only when the endpoint changes, while saveApiKeys leaves it untouched when a replacement key produces no selected project. Deployment later falls back to that stored ID when the confirmation dropdown has no selection. Example: Key A stores project Recommended fix: When a non-empty replacement API key is saved, clear the stored project unless the current dropdown supplies a valid project from that replacement key. Keep the key and project update ordered so a failed save cannot leave a partially updated pair. Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| /** | ||
| * Endpoint changes serve a different project set, so the previously selected | ||
| * project is no longer a trustworthy target. Invalidate the stale selection and | ||
| * re-fetch the project list for the new endpoint when a key is configured. | ||
| */ | ||
| function invalidateStaleProjectSelection() { | ||
| clearStoredProjectSelection(); | ||
| const select = document.getElementById("flutterflow-projects-select"); | ||
| if (select) { | ||
| select.innerHTML = '<option value="">Enter your API key to load projects</option>'; | ||
| select.value = ""; | ||
| } | ||
| if (hasStoredKey("flutterflow")) { | ||
| ffConnectionState = "validating"; | ||
| fetchProjects(flutterflowApiKey); | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Changing the endpoint clears the selected project and calls Greptile automatically discovered a related ticket stating that endpoint changes must invalidate stale projects and reload projects for the selected endpoint, which informed this comment. Source Used: Linear — Implement the account connection and single provider-key editor Prompt To Fix With AIThis is a comment left during a code review.
Path: app.js
Line: 1403
Comment:
**Staging Reloads Production Projects**
Changing the endpoint clears the selected project and calls `fetchProjects`, but that function creates `FlutterFlowApiClient` without passing `getFlutterFlowEndpoint()`, so the client defaults to Production. Selecting Staging therefore repopulates the dropdown and connection state with Production projects while the account card says “Staging,” allowing users to select a Production project for a Staging configuration.
Greptile automatically discovered a related ticket stating that endpoint changes must invalidate stale projects and reload projects for the selected endpoint, which informed this comment.
**Source Used:** Linear — [Implement the account connection and single provider-key editor](https://linear.app/stuart-gardoll/issue/STU-384/implement-the-account-connection-and-single-provider-key-editor)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| } else { | ||
| ffConnectionState = "not-configured"; | ||
| } | ||
| renderApiKeyConnection(); | ||
| } | ||
|
|
||
| function updateInputValidationState(inputId, isValid) { | ||
| const input = document.getElementById(inputId); | ||
| if (!input) return; | ||
|
|
@@ -1334,16 +1487,33 @@ async function fetchProjects(apiKey) { | |
| select.innerHTML = '<option value="">Loading projects...</option>'; | ||
| if (errorElement) errorElement.classList.add("hidden"); | ||
|
|
||
| // A real request is in flight; the account connection card shows validating | ||
| // until the outcome is known (never "connected" from storage alone). | ||
| if (hasStoredKey("flutterflow") || apiKey) { | ||
| ffConnectionState = "validating"; | ||
| } | ||
| renderApiKeyConnection(); | ||
|
Comment on lines
+1490
to
+1495
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Unsaved keys replace connection status Blurring an unsaved replacement key makes Learn moreThe account card is gated by the stored in-memory key, but Example: A valid stored key shows Connected. The user types an invalid replacement, blurs the field, then closes without saving. The card reports that the API key was rejected even though the valid stored key remains active. Recommended fix: Separate candidate-key validation state from the canonical stored-key connection state. Only update Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| try { | ||
| // Create temporary client instance (no project ID needed) | ||
| const client = new FlutterFlowApiClient(apiKey, ""); | ||
| const client = new FlutterFlowApiClient( | ||
| apiKey, | ||
| "", | ||
| "main", | ||
| getFlutterFlowEndpoint(), | ||
| ); | ||
| const projects = await client.listProjects(); | ||
|
|
||
| if (!projects || projects.length === 0) { | ||
| ffConnectionState = "no-projects"; | ||
| renderApiKeyConnection(); | ||
| select.innerHTML = '<option value="">No projects found</option>'; | ||
| return; | ||
| } | ||
|
|
||
| ffConnectionState = "connected"; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blurring a replacement key validates the unsaved input through Greptile automatically discovered a related ticket stating that the account card and editor must share one canonical credential value and only show connected after validating that value, which informed this comment. Source Used: Linear — Implement the account connection and single provider-key editor Prompt To Fix With AIThis is a comment left during a code review.
Path: app.js
Line: 1498
Comment:
**Unsaved Keys Become Canonical**
Blurring a replacement key validates the unsaved input through `fetchProjects`, and any successful request unconditionally sets the global connection state to `connected`. A user can therefore close the modal without saving and still see the account card report that the persisted credential is connected. Because requests have no identity or stale-result guard, an older request can also overwrite the result for a newer credential.
Greptile automatically discovered a related ticket stating that the account card and editor must share one canonical credential value and only show connected after validating that value, which informed this comment.
**Source Used:** Linear — [Implement the account connection and single provider-key editor](https://linear.app/stuart-gardoll/issue/STU-384/implement-the-account-connection-and-single-provider-key-editor)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| renderApiKeyConnection(); | ||
|
Comment on lines
1507
to
+1515
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Stale project requests win races Starting another Learn moreOpening the modal starts a request for the stored key. Blurring a replacement key or changing the endpoint can start another request before the first finishes. Unlike the deploy confirmation loader, Example: The production request starts first. The user switches to staging, whose request finishes quickly. The slower production response then marks the card connected and replaces the dropdown with production projects. Recommended fix: Add a monotonically increasing request token or abort controller to Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| // Populate dropdown | ||
| select.innerHTML = '<option value="">Select a project...</option>'; | ||
| projects.forEach((project) => { | ||
|
|
@@ -1360,6 +1530,11 @@ async function fetchProjects(apiKey) { | |
| } | ||
| } catch (error) { | ||
| console.error("Failed to fetch projects:", error); | ||
| const msg = String((error && error.message) || ""); | ||
| ffConnectionState = /(401|403)|denied|unauthorized|scoped|list permission/i.test(msg) | ||
| ? "unauthorized" | ||
| : "network-error"; | ||
| renderApiKeyConnection(); | ||
| select.innerHTML = '<option value="">Error loading projects</option>'; | ||
| if (errorElement) { | ||
| errorElement.textContent = `Failed to load projects: ${error.message}`; | ||
|
|
@@ -5330,30 +5505,8 @@ function renderAccountOverview() { | |
| else meterTrack.setAttribute("aria-label", `${count} of ${limit} generations used`); | ||
| } | ||
|
|
||
| // --- FlutterFlow connection (real stored credential state) --- | ||
| const connKeyConfigured = hasStoredKey("flutterflow"); | ||
| const connProjectConfigured = hasStoredKey("flutterflow_project_id"); | ||
| const ffDot = document.getElementById("acct-ff-dot"); | ||
| const ffStatus = document.getElementById("acct-ff-status"); | ||
| const ffEndpoint = document.getElementById("acct-ff-endpoint"); | ||
| const ffProject = document.getElementById("acct-ff-project"); | ||
| if (ffStatus) { | ||
| if (connKeyConfigured && connProjectConfigured) { | ||
| ffStatus.textContent = "Connected to FlutterFlow"; | ||
| if (ffDot) { ffDot.className = "acct-dot ok"; } | ||
| } else if (connKeyConfigured) { | ||
| ffStatus.textContent = "API key set — pick a project"; | ||
| if (ffDot) { ffDot.className = "acct-dot warn"; } | ||
| } else { | ||
| ffStatus.textContent = "Not connected — add your FlutterFlow API key"; | ||
| if (ffDot) { ffDot.className = "acct-dot"; } | ||
| } | ||
| } | ||
| if (ffEndpoint) { | ||
| const endpoint = getFlutterFlowEndpoint(); | ||
| ffEndpoint.textContent = endpoint && endpoint.includes("staging") ? "Staging" : "Production"; | ||
| } | ||
| if (ffProject) ffProject.textContent = flutterflowProjectId || "—"; | ||
| // --- FlutterFlow connection (real response-driven state) --- | ||
| renderApiKeyConnection(); | ||
| } | ||
|
|
||
| function updateUsageDisplay() { | ||
|
|
@@ -5961,7 +6114,12 @@ async function populateConfirmProjectSelect() { | |
| select.disabled = false; | ||
| select.innerHTML = '<option value="">Loading projects…</option>'; | ||
| try { | ||
| const client = new FlutterFlowApiClient(apiKey, ""); | ||
| const client = new FlutterFlowApiClient( | ||
| apiKey, | ||
| "", | ||
| "main", | ||
| getFlutterFlowEndpoint(), | ||
| ); | ||
| const projects = await client.listProjects(); | ||
| if (!isCurrent()) return; | ||
|
|
||
|
|
@@ -6678,6 +6836,7 @@ window.openModelSelector = openModelSelector; | |
| window.handlePromptImageSelect = handlePromptImageSelect; | ||
| window.removePromptImage = removePromptImage; | ||
| window.saveApiKeys = saveApiKeys; | ||
| window.validateFlutterFlowConnection = validateFlutterFlowConnection; | ||
| window.clearAllApiKeys = clearAllApiKeys; | ||
| window.toggleKeyVisibility = toggleKeyVisibility; | ||
| window.handleWelcomeVideoEnd = handleWelcomeVideoEnd; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Staging fallback loads production projects
When staging returns 404,
listProjectsretries its hard-coded production legacy URL. Connection, settings, and deploy selectors can accept production projects while later operations target staging.Learn more
The configured endpoint now reaches
listProjects, but the fallback URL remains fixed to production. A 404 from the staging convention path therefore changes environments rather than trying a staging-compatible path or preserving the failure. Every changed caller uses the same method, so the account check, API-key project selector, and deploy selector all inherit this behavior.Example: With staging selected,
https://api.flutterflow.io/v2-staging/listProjectsreturns 404. The client then requestshttps://api.flutterflow.io/v2/l/listProjects, accepts its production project list, and lets the user select a project unavailable on staging.Recommended fix: Derive any legacy fallback from the configured endpoint, or disable the production legacy fallback whenever
baseUrlis not the production endpoint. Add a staging-404 test that ensures no production request occurs.Was this helpful? React with 👍 or 👎 to provide feedback.