Repository navigation
STU-384: Implement the account connection and single provider-key editor - #92
Conversation
…invalidation - Add canonical ffConnectionState driven by real listProjects outcomes (not-configured/validating/connected/no-projects/unauthorized/network-error). - Render account connection card from that state; 'Connected' only after an actual projects response. Masked/raw keys never appear in status labels. - Endpoint change invalidates stale project selection and re-fetches. - Account card default project mirrors the deploy-confirmation stored value. - Startup validates a stored key so the card is truthful on load.
7 hermetic browser tests covering not-configured, validating->connected, no-projects vs 401/403 vs network-error, endpoint-change invalidation, save/reload/remove single-value, and signed-out entry. Expose validateFlutterFlowConnection for a visible retry path.
1440x900 and 360x800 captures of the account connection card showing a response-driven Connected state, default project, and endpoint.
|
| function clearStoredProjectSelection() { | ||
| flutterflowProjectId = ""; | ||
| localStorage.removeItem(STORAGE_KEY_PREFIX + "flutterflow_project_id"); | ||
| sessionStorage.removeItem(SESSION_STORAGE_KEY_PREFIX + "flutterflow_project_id"); | ||
| } |
There was a problem hiding this comment.
🟡 Replacement keys retain stale projects
Saving a replacement key without selecting a project retains the old default. saveApiKeys then displays and deploys with a project unrelated to the new key.
Learn more
The 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 alpha. The user enters key B, whose project list does not contain alpha, and saves without choosing another project. The account card still shows alpha, and a deployment can submit key B with project alpha.
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.
| // 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(); |
There was a problem hiding this comment.
🟡 Unsaved keys replace connection status
Blurring an unsaved replacement key makes fetchProjects overwrite the stored key's status. Canceling leaves the account card describing the discarded key.
Learn more
The account card is gated by the stored in-memory key, but fetchProjects validates either that key or a newly typed candidate. Its result always writes the shared ffConnectionState. Closing the modal does not revalidate the stored key, so the candidate result persists.
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 ffConnectionState when the request validates the currently stored key; use modal-local state for unsaved input.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if (!projects || projects.length === 0) { | ||
| ffConnectionState = "no-projects"; | ||
| renderApiKeyConnection(); | ||
| select.innerHTML = '<option value="">No projects found</option>'; | ||
| return; | ||
| } | ||
|
|
||
| ffConnectionState = "connected"; | ||
| renderApiKeyConnection(); |
There was a problem hiding this comment.
🟡 Stale project requests win races
Starting another fetchProjects request does not invalidate the first. A late response can replace the newer connection state and dropdown with stale projects.
Learn more
Opening 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, fetchProjects has no token check before any state or dropdown write.
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 fetchProjects. Check it before every ffConnectionState, error, and dropdown update, including catch handling.
Was this helpful? React with 👍 or 👎 to provide feedback.
| } catch (error) { | ||
| const msg = String((error && error.message) || ""); | ||
| ffConnectionState = /(401|403)|denied|unauthorized|scoped|list permission/i.test(msg) | ||
| ? "unauthorized" | ||
| : "network-error"; |
There was a problem hiding this comment.
| } | ||
| if (hasStoredKey("flutterflow")) { | ||
| ffConnectionState = "validating"; | ||
| fetchProjects(flutterflowApiKey); |
There was a problem hiding this 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
Prompt To Fix With AI
This 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.| return; | ||
| } | ||
|
|
||
| ffConnectionState = "connected"; |
There was a problem hiding this comment.
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
Prompt To Fix With AI
This 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.…; add remove-leg browser test - validateFlutterFlowConnection, fetchProjects and populateConfirmProjectSelect now pass getFlutterFlowEndpoint() into FlutterFlowApiClient instead of relying on the production default, so the account card and project dropdown reflect the configured (staging) server like the deploy flow does. - Endpoint-change test now feeds DIFFERENT lists to production vs staging and asserts the staging-only project appears, so it fails if the client falls back to production. - New browser test removes the stored key and asserts the connection card returns to not-configured with no raw/masked key material remaining.
| apiKey, | ||
| "", | ||
| "main", | ||
| getFlutterFlowEndpoint(), |
There was a problem hiding this comment.
🟡 Staging fallback loads production projects
When staging returns 404, listProjects retries 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/listProjects returns 404. The client then requests https://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 baseUrl is not the production endpoint. Add a staging-404 test that ensures no production request occurs.
Was this helpful? React with 👍 or 👎 to provide feedback.
The pull_request CI run for the preceding commit was never created (no 'CI' check-run exists for it, while the sibling Code scanning job did run). Re-pushing to obtain a CI verdict; no source change. Unrelated: the 'Code scanning AI findings' job fails with GitHub's own 'CAPIError: 400 The requested model is not supported' from the Copilot autofind agent. It fails identically on other PRs and on this branch's earlier sha, so it is not caused by this change.
…ui-ux' into sgardoll/stu-384-implement-the-account-connection-and-single-provider-key # Conflicts: # dist/assets/index-CleimGxe.css # dist/assets/index-D6csqwUm.js # dist/index.html
ba53422
into
milestone/stu-implement-updated-ui-ux
STU-384 — Implement the account connection and single provider-key editor
Linear: https://linear.app/stuart-gardoll/issue/STU-384/implement-the-account-connection-and-single-provider-key-editor
What changed (my slice)
The account connection card is now driven by the canonical connection state derived from a real
listProjectsresponse — never from stored bytes alone and never the placeholder token"set".Connected to FlutterFlowis only reachable after a genuine projects endpoint response.app.js: canonicalffConnectionState(not-configured | validating | connected | no-projects | unauthorized | network-error);renderApiKeyConnection()renders the card + a.acct-dot.badtone;validateFlutterFlowConnection()re-confirms on startup/after save/clear;fetchProjectsreports outcomes; endpoint change (setFlutterFlowEndpoint) invalidates the stale stored project + dropdown and re-fetches; account default-project mirrors the same stored value the deploy confirmation pre-selects. ExposedvalidateFlutterFlowConnectionfor a visible retry path.index.html: added.acct-dot.badstate token (the only html change).e2e/stu384-connection.spec.js: 7 hermetic browser tests.Acceptance criteria
••••fragment; connected/reload/signed-out tests exercise the single shared value.flutterflowProjectId=getApiKey("flutterflow_project_id"), the exact valuepopulateConfirmProjectSelectpre-selects; verified by the connected/endpoint tests. (Full deploy-confirm modal cross-assertion is not an automated e2e because it needs a generated candidate; source-level consistency is shared.)npm run test:key-storagepasses; browser fixtures exercise valid/invalid keys, failure retry and signed-out entry.Full CI sequence (all green on final commit)
npm testnpm run buildnpm run test:ui(full suite)npm run test:key-storagenpm run test:accessibilitynpm run test:deploy-guardnpm run test:proxiesFalsification evidence (break -> FAIL -> restore -> PASS)
unauthorizedrender branch to labelConnected to FlutterFlow. The 401 spec FAILED ate2e/stu384-connection.spec.js:182(expect(...).toHaveText("API key rejected (401/403) ...")). Restored -> 2 passed.clearStoredProjectSelection()frominvalidateStaleProjectSelection(). The endpoint spec FAILED at:252(expect(storedAfter).toBeNull()). Restored -> passed.await saveApiKey("flutterflow", ...)insaveApiKeys(). The signed-out spec FAILED at:281(reload placeholder was notKey saved). Restored -> passed.validateFlutterFlowConnectionsuccess branch to not flip state. The network-retry spec FAILED at:211(finalConnected to FlutterFlownever appeared). Restored -> passed.All four restored edits are byte-identical to the green baseline (
grep -c "#BROKE#" app.js-> 0).Reference screenshots (changed surface)
screenshots/stu384-connected-1440x900.pngscreenshots/stu384-connected-360x800.pngGaps / not verified
flutterflow_project_id); if final acceptance requires a candidate-generation e2e it should be added by the reviewer.'set'state was rendered for them.