372 lines
21 KiB
Markdown
372 lines
21 KiB
Markdown
---
|
|
phase: 31-puter.js-zero-config-cloud
|
|
plan: "03"
|
|
type: execute
|
|
wave: 2
|
|
depends_on: ["31-01", "31-02"]
|
|
files_modified:
|
|
- ui/src/components/onboarding/ProviderSelectionStep.tsx
|
|
- ui/src/components/onboarding/PuterAuthButton.tsx
|
|
- ui/src/components/onboarding/GoogleOAuthButton.tsx
|
|
- ui/src/components/onboarding/ApiKeyEntryForm.tsx
|
|
- ui/src/components/NexusOnboardingWizard.tsx
|
|
- ui/src/api/puter-proxy.ts
|
|
autonomous: true
|
|
requirements: [CLOUD-01, CLOUD-03, CLOUD-04, CLOUD-05]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "User sees a Provider Selection step (Step 3 of 4) in the onboarding wizard with Puter, Google, and API Key options"
|
|
- "Clicking 'Continue with Puter' triggers Puter auth popup, stores token in React state for later server submission"
|
|
- "Google option shows policy-risk warning before enabling the Sign in button (3-second gate)"
|
|
- "Google OAuth opens popup, callback stores tokens by stateId, stateId captured in React state for later claim"
|
|
- "API key option shows inline form with provider dropdown and key input"
|
|
- "Skip for now button is always visible and advances to root directory step"
|
|
- "Pre-installed tools (Hermes, Claude Code, OpenClaw) show detected badges via adapter probe"
|
|
- "Step indicator shows 'Step N of 4' (updated from 3)"
|
|
- "handleSubmit posts collected credentials to server AFTER company creation (Puter token, Google claim, API key)"
|
|
artifacts:
|
|
- path: "ui/src/components/onboarding/ProviderSelectionStep.tsx"
|
|
provides: "Provider selection UI with three option cards and skip button"
|
|
exports: ["ProviderSelectionStep"]
|
|
- path: "ui/src/components/onboarding/PuterAuthButton.tsx"
|
|
provides: "Puter auth button that loads CDN script, calls signIn, captures token in state"
|
|
exports: ["PuterAuthButton"]
|
|
- path: "ui/src/components/onboarding/GoogleOAuthButton.tsx"
|
|
provides: "Google OAuth button with 3-second risk warning gate, opens popup, captures stateId"
|
|
exports: ["GoogleOAuthButton"]
|
|
- path: "ui/src/components/onboarding/ApiKeyEntryForm.tsx"
|
|
provides: "API key entry form with provider dropdown"
|
|
exports: ["ApiKeyEntryForm"]
|
|
- path: "ui/src/api/puter-proxy.ts"
|
|
provides: "API client for puter-proxy, oauth, and api-keys endpoints"
|
|
exports: ["puterProxyApi"]
|
|
- path: "ui/src/components/NexusOnboardingWizard.tsx"
|
|
provides: "4-step wizard with provider selection step inserted"
|
|
key_links:
|
|
- from: "ui/src/components/onboarding/PuterAuthButton.tsx"
|
|
to: "POST /api/puter-proxy/token"
|
|
via: "puterProxyApi.storeToken called in wizard handleSubmit after company creation"
|
|
pattern: "puter-proxy/token"
|
|
- from: "ui/src/components/onboarding/GoogleOAuthButton.tsx"
|
|
to: "POST /api/oauth/google/authorize"
|
|
via: "fetch call to get auth URL + stateId, then window.open"
|
|
pattern: "oauth/google/authorize"
|
|
- from: "ui/src/components/NexusOnboardingWizard.tsx"
|
|
to: "POST /api/oauth/google/claim"
|
|
via: "claim call in handleSubmit with stateId + companyId after company creation"
|
|
pattern: "oauth/google/claim"
|
|
- from: "ui/src/components/onboarding/ApiKeyEntryForm.tsx"
|
|
to: "POST /api/api-keys/store"
|
|
via: "puterProxyApi.storeApiKey called in wizard handleSubmit after company creation"
|
|
pattern: "api-keys/store"
|
|
- from: "ui/src/components/NexusOnboardingWizard.tsx"
|
|
to: "ui/src/components/onboarding/ProviderSelectionStep.tsx"
|
|
via: "import and render in step 3"
|
|
pattern: "ProviderSelectionStep"
|
|
- from: "ui/src/components/NexusOnboardingWizard.tsx"
|
|
to: "GET /api/adapters/:type/probe"
|
|
via: "probeAdapter calls for hermes_local, claude_local, openclaw_gateway on mount"
|
|
pattern: "probeAdapter|adapters.*probe"
|
|
---
|
|
|
|
<objective>
|
|
Build the Provider Selection step UI components and wire them into the existing 3-step onboarding wizard (making it 4 steps). This includes the Puter auth button, Google OAuth button with risk warning, API key entry form, and adapter auto-detection badges.
|
|
|
|
Purpose: CLOUD-01 (Puter zero-config UI), CLOUD-03 (Google OAuth UI), CLOUD-04 (auto-detect tools), CLOUD-05 (API key entry UI). This is the user-facing surface for all cloud provider paths.
|
|
Output: ProviderSelectionStep, PuterAuthButton, GoogleOAuthButton, ApiKeyEntryForm, updated wizard
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/home/mikkel/.claude/get-shit-done/workflows/execute-plan.md
|
|
@/home/mikkel/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/31-puter.js-zero-config-cloud/31-RESEARCH.md
|
|
@.planning/phases/31-puter.js-zero-config-cloud/31-UI-SPEC.md
|
|
@.planning/phases/30-hardware-detection-mode-selection/30-02-SUMMARY.md
|
|
|
|
<interfaces>
|
|
From ui/src/components/NexusOnboardingWizard.tsx (current state):
|
|
```typescript
|
|
// 3-step wizard: step 1 = hardware, step 2 = mode, step 3 = root directory
|
|
// Step indicator: "Step {step} of 3"
|
|
// defaultAdapter state: "claude_local" | "hermes_local"
|
|
// probing state for adapter detection
|
|
// handleSubmit creates company + agents on step 3 form submit
|
|
```
|
|
|
|
From ui/src/components/onboarding/ModeSelector.tsx (selected state pattern):
|
|
```typescript
|
|
// Cards use: border-primary bg-primary/5 when selected
|
|
// Vertical stack with gap-3
|
|
// Each card is a <button type="button"> element
|
|
```
|
|
|
|
From ui/src/api/agents.ts:
|
|
```typescript
|
|
probeAdapter: (type: string) =>
|
|
api.get<{ available: boolean; status: string }>(`/adapters/${encodeURIComponent(type)}/probe`),
|
|
```
|
|
|
|
From 31-UI-SPEC.md (copywriting):
|
|
```
|
|
Step heading: "Choose a provider"
|
|
Step subheading: "No API keys needed for the zero-config path."
|
|
Puter card label: "Puter -- free, zero-config"
|
|
Puter card description: "Free AI powered by your Puter.com account. No API key needed."
|
|
Google card label: "Google -- Gemini free tier"
|
|
Google risk warning body: "Google has suspended accounts that used third-party apps with Gemini credentials. This may affect your Gmail and Workspace access. Use a Google AI Studio API key instead if you want to avoid this risk."
|
|
API key card label: "API key -- subscription provider"
|
|
```
|
|
|
|
From Plan 02 routes (pendingTokens pattern):
|
|
```typescript
|
|
// POST /oauth/google/authorize returns { url, stateId }
|
|
// GET /oauth/google/callback stores tokens by stateId, redirects to /?google_oauth=success&state={stateId}
|
|
// POST /oauth/google/claim { stateId, companyId } -> moves tokens to secretService
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: API client + PuterAuthButton + GoogleOAuthButton + ApiKeyEntryForm</name>
|
|
<files>
|
|
ui/src/api/puter-proxy.ts,
|
|
ui/src/components/onboarding/PuterAuthButton.tsx,
|
|
ui/src/components/onboarding/GoogleOAuthButton.tsx,
|
|
ui/src/components/onboarding/ApiKeyEntryForm.tsx
|
|
</files>
|
|
<read_first>
|
|
ui/src/api/agents.ts,
|
|
ui/src/api/client.ts,
|
|
ui/src/components/onboarding/ModeSelector.tsx,
|
|
ui/src/components/NexusOnboardingWizard.tsx,
|
|
.planning/phases/31-puter.js-zero-config-cloud/31-UI-SPEC.md
|
|
</read_first>
|
|
<action>
|
|
**Create ui/src/api/puter-proxy.ts:**
|
|
|
|
Import `api` from `./client`. Export `puterProxyApi` with:
|
|
- `storeToken: (companyId: string, token: string) => api.post("/puter-proxy/token", { companyId, token })`
|
|
- `getAuthUrl: () => api.post<{ url: string; stateId: string }>("/oauth/google/authorize", {})` — no companyId needed
|
|
- `claimGoogleTokens: (stateId: string, companyId: string) => api.post("/oauth/google/claim", { stateId, companyId })`
|
|
- `storeApiKey: (companyId: string, provider: string, apiKey: string) => api.post("/api-keys/store", { companyId, provider, apiKey })`
|
|
|
|
**Create ui/src/components/onboarding/PuterAuthButton.tsx:**
|
|
|
|
Props: `{ onSuccess: (token: string) => void; onError: (msg: string) => void }`.
|
|
|
|
State: `loading` boolean, `connected` boolean.
|
|
|
|
`loadScript` helper: check if `window.puter` exists; if not, create a `<script>` tag with `src="https://js.puter.com/v2/"`, append to `<head>`, wait for `load` event. Return a promise.
|
|
|
|
`handleClick` (the onClick handler — MUST be synchronous entry point for popup to work):
|
|
1. Set `loading(true)`.
|
|
2. Call `loadScript()` to ensure Puter SDK is loaded.
|
|
3. Call `(window as any).puter.auth.signIn()` — this opens the popup. Must be in the same synchronous-ish call chain from the click event.
|
|
4. After signIn resolves, extract token: try `(window as any).puter.authToken` first, then `(window as any).puter.auth?.token`, then call `(window as any).puter.auth?.getUser?.()` and check for token on the returned object. Log a warning if token is undefined (per Pitfall 1 in RESEARCH.md).
|
|
5. Call `onSuccess(token)` — parent stores the token in React state for later submission to server after company creation.
|
|
6. Set `connected(true)`, `loading(false)`.
|
|
7. On any error: `onError("Puter sign-in failed. Check your Puter.com account and try again.")`.
|
|
|
|
Render a `<Button>` (from @/components/ui/button). When `loading`, show spinner (h-4 w-4 animate-spin SVG) and text "Connecting to Puter...". When `connected`, show CheckCircle icon (from lucide-react) and text "Puter connected". Otherwise show LogIn icon and text "Continue with Puter". Use `aria-busy={loading}`.
|
|
|
|
**Create ui/src/components/onboarding/GoogleOAuthButton.tsx:**
|
|
|
|
Props: `{ onSuccess: (stateId: string) => void; onError: (msg: string) => void }`.
|
|
|
|
State: `loading`, `warningTimer` (boolean, starts false — becomes true after 3 seconds), `connected` boolean.
|
|
|
|
On mount, start a 3-second timeout that sets `warningTimer = true`. Clean up timeout on unmount.
|
|
|
|
Render:
|
|
1. Policy-risk warning (always visible when this component is rendered):
|
|
```
|
|
<div role="alert" className="text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">
|
|
<p className="font-medium">Policy risk -- read before continuing</p>
|
|
<p className="mt-1">Google has suspended accounts that used third-party apps with Gemini credentials. This may affect your Gmail and Workspace access. Use a Google AI Studio API key instead if you want to avoid this risk.</p>
|
|
</div>
|
|
```
|
|
2. Button: "Sign in with Google" (disabled until `warningTimer === true`). On click:
|
|
a. Set `loading(true)`.
|
|
b. Call `puterProxyApi.getAuthUrl()` to get `{ url, stateId }`.
|
|
c. Open popup: `window.open(url, "_blank", "popup,width=600,height=700")`.
|
|
d. Poll for popup close or listen for `window.addEventListener("message", ...)`. Simpler: poll every 500ms checking if the popup is closed (`popup.closed`). When popup closes, check `window.location.search` for `google_oauth=success` query param, OR just trust that the callback happened server-side and the tokens are parked.
|
|
e. Actually, the simplest approach: after opening the popup, the Google OAuth callback redirects the popup to `/?google_oauth=success&state={stateId}`. The popup lands on that URL. Use `window.addEventListener("message")` — but since the popup is our own origin, we can poll it. Simplest: after popup opens, poll `popup.closed` every 500ms. When closed, assume success (the callback stored tokens server-side). Call `onSuccess(stateId)`.
|
|
f. Set `connected(true)`, `loading(false)`.
|
|
3. On error: call `onError(msg)`.
|
|
|
|
**Create ui/src/components/onboarding/ApiKeyEntryForm.tsx:**
|
|
|
|
Props: `{ onSave: (provider: string, apiKey: string) => void; onError: (msg: string) => void }`.
|
|
|
|
State: `provider` (default "openai"), `apiKey` (string), `saved` (boolean).
|
|
|
|
Render:
|
|
1. `<Label>` + select dropdown with options: OpenAI, Anthropic, Groq.
|
|
2. `<Label>` + `<Input>` for API key, type="password".
|
|
3. `<Button>` "Save API key". On click: validate apiKey not empty, call `onSave(provider, apiKey)`. Set `saved(true)`.
|
|
4. When `saved`, show CheckCircle icon and "API key saved" text.
|
|
5. Use shadcn `Label`, `Input`, `Button` components. Inline form (no page navigation).
|
|
</action>
|
|
<verify>
|
|
<automated>cd /opt/nexus && ls ui/src/api/puter-proxy.ts ui/src/components/onboarding/PuterAuthButton.tsx ui/src/components/onboarding/GoogleOAuthButton.tsx ui/src/components/onboarding/ApiKeyEntryForm.tsx</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- grep -q "puterProxyApi" ui/src/api/puter-proxy.ts
|
|
- grep -q "puter-proxy/token" ui/src/api/puter-proxy.ts
|
|
- grep -q "oauth/google/claim" ui/src/api/puter-proxy.ts
|
|
- grep -q "stateId" ui/src/api/puter-proxy.ts
|
|
- grep -q "js.puter.com/v2" ui/src/components/onboarding/PuterAuthButton.tsx
|
|
- grep -q "puter.auth.signIn" ui/src/components/onboarding/PuterAuthButton.tsx
|
|
- grep -q "Continue with Puter" ui/src/components/onboarding/PuterAuthButton.tsx
|
|
- grep -q "Connecting to Puter" ui/src/components/onboarding/PuterAuthButton.tsx
|
|
- grep -q "Policy risk" ui/src/components/onboarding/GoogleOAuthButton.tsx
|
|
- grep -q "suspended accounts" ui/src/components/onboarding/GoogleOAuthButton.tsx
|
|
- grep -q "Sign in with Google" ui/src/components/onboarding/GoogleOAuthButton.tsx
|
|
- grep -q "3000\|3 .* second\|setTimeout" ui/src/components/onboarding/GoogleOAuthButton.tsx
|
|
- grep -q "openai\|anthropic\|groq" ui/src/components/onboarding/ApiKeyEntryForm.tsx
|
|
- grep -q "Save API key" ui/src/components/onboarding/ApiKeyEntryForm.tsx
|
|
- grep -q 'type="password"' ui/src/components/onboarding/ApiKeyEntryForm.tsx
|
|
</acceptance_criteria>
|
|
<done>PuterAuthButton loads CDN and triggers signIn popup (token captured in React state), GoogleOAuthButton shows risk warning with 3s gate and opens OAuth popup (stateId captured), ApiKeyEntryForm captures provider/key pairs — all pass data back to parent via callbacks for post-company-creation submission</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: ProviderSelectionStep + wire 4-step wizard</name>
|
|
<files>
|
|
ui/src/components/onboarding/ProviderSelectionStep.tsx,
|
|
ui/src/components/NexusOnboardingWizard.tsx
|
|
</files>
|
|
<read_first>
|
|
ui/src/components/NexusOnboardingWizard.tsx,
|
|
ui/src/components/onboarding/ModeSelector.tsx,
|
|
ui/src/components/onboarding/PuterAuthButton.tsx,
|
|
ui/src/components/onboarding/GoogleOAuthButton.tsx,
|
|
ui/src/components/onboarding/ApiKeyEntryForm.tsx,
|
|
ui/src/api/agents.ts,
|
|
.planning/phases/31-puter.js-zero-config-cloud/31-UI-SPEC.md
|
|
</read_first>
|
|
<action>
|
|
**Create ui/src/components/onboarding/ProviderSelectionStep.tsx:**
|
|
|
|
Props:
|
|
```typescript
|
|
interface ProviderSelectionStepProps {
|
|
onPuterToken: (token: string) => void;
|
|
onGoogleOAuthState: (stateId: string) => void;
|
|
onApiKey: (provider: string, apiKey: string) => void;
|
|
onSkip: () => void;
|
|
onContinue: () => void;
|
|
detectedAdapters: Record<string, boolean>; // e.g., { hermes_local: true, claude_local: false }
|
|
}
|
|
```
|
|
|
|
State: `selectedProvider` ("puter" | "google" | "apikey" | null), `providerReady` boolean (true when auth is complete for selected provider), `error` string | null.
|
|
|
|
Layout (following ModeSelector card pattern from 30-02):
|
|
1. Three vertical cards in a stack (`flex flex-col gap-3`):
|
|
- **Puter card**: `<button type="button">` with `border-primary bg-primary/5` when selected. Label: "Puter -- free, zero-config". Description: "Free AI powered by your Puter.com account. No API key needed." If any adapters detected, show badges inside card: `<span className="text-xs text-primary">{Name} detected</span>`.
|
|
- **Google card**: Same pattern. Label: "Google -- Gemini free tier". Description: "Sign in with Google to access Gemini via your Google account."
|
|
- **API key card**: Same pattern. Label: "API key -- subscription provider". Description: "Use your own OpenAI, Anthropic, or Groq API key."
|
|
|
|
2. Below cards, conditionally render the action component for the selected provider:
|
|
- Puter selected: `<PuterAuthButton onSuccess={(token) => { onPuterToken(token); setProviderReady(true); }} onError={setError} />`
|
|
- Google selected: `<GoogleOAuthButton onSuccess={(stateId) => { onGoogleOAuthState(stateId); setProviderReady(true); }} onError={setError} />`
|
|
- API key selected: `<ApiKeyEntryForm onSave={(prov, key) => { onApiKey(prov, key); setProviderReady(true); }} onError={setError} />`
|
|
|
|
3. If `providerReady`: show a "Continue" button calling `onContinue()`.
|
|
|
|
4. Always show: `<Button variant="ghost" onClick={onSkip}>Skip for now</Button>` with `aria-label="Skip provider setup for now"`.
|
|
|
|
5. Show detected adapter badges on each card:
|
|
For each adapter in `detectedAdapters` where value is true, show `<span className="text-xs text-primary">Hermes detected</span>` (or "Claude Code detected", "OpenClaw detected") inside the relevant card area.
|
|
|
|
6. Error display: if `error` is set, show `<p className="text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">{error}</p>`.
|
|
|
|
**Update ui/src/components/NexusOnboardingWizard.tsx:**
|
|
|
|
Changes:
|
|
1. Import `ProviderSelectionStep` from `./onboarding/ProviderSelectionStep`.
|
|
2. Import `puterProxyApi` from `../api/puter-proxy`.
|
|
3. Change step count from 3 to 4. Step indicator: `"Step {step} of 4"`.
|
|
4. Add state: `puterToken` (string | null), `googleOAuthStateId` (string | null), `apiKeyData` ({ provider: string; apiKey: string } | null).
|
|
5. Add state: `detectedAdapters` (Record<string, boolean>, default `{}`).
|
|
6. Update the adapter probe useEffect to also probe `claude_local` and `openclaw_gateway` (in parallel with existing `hermes_local` probe). Store results in `detectedAdapters`.
|
|
7. Renumber steps:
|
|
- Step 1: Hardware Detection (unchanged)
|
|
- Step 2: Mode Selection (unchanged)
|
|
- Step 3: Provider Selection (NEW)
|
|
- Step 4: Root Directory (was step 3)
|
|
8. Step 2 "Continue" now goes to step 3 (not step 3/root dir).
|
|
9. Step 3 renders `<ProviderSelectionStep>`. "Skip" and "Continue" both advance to step 4. Back goes to step 2.
|
|
10. Step 4 is the existing root directory form. Back goes to step 3.
|
|
11. In `handleSubmit` (after company creation, before navigate), add credential storage:
|
|
```typescript
|
|
// Store collected provider credentials after company creation
|
|
if (puterToken) {
|
|
await puterProxyApi.storeToken(company.id, puterToken).catch(() => {});
|
|
}
|
|
if (googleOAuthStateId) {
|
|
// Claim Google OAuth tokens for this company
|
|
await puterProxyApi.claimGoogleTokens(googleOAuthStateId, company.id).catch(() => {});
|
|
}
|
|
if (apiKeyData) {
|
|
await puterProxyApi.storeApiKey(company.id, apiKeyData.provider, apiKeyData.apiKey).catch(() => {});
|
|
}
|
|
```
|
|
All credential storage is non-blocking (catch(() => {})) — same pattern as mode save.
|
|
12. Reset new state variables in the reset effect (when wizard closes): `setPuterToken(null)`, `setGoogleOAuthStateId(null)`, `setApiKeyData(null)`.
|
|
13. Update heading for step 3: "Choose a provider" with subheading "No API keys needed for the zero-config path."
|
|
</action>
|
|
<verify>
|
|
<automated>cd /opt/nexus && pnpm --filter @paperclipai/ui exec tsc --noEmit 2>&1 | head -20</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- grep -q "ProviderSelectionStep" ui/src/components/onboarding/ProviderSelectionStep.tsx
|
|
- grep -q "Choose a provider" ui/src/components/onboarding/ProviderSelectionStep.tsx
|
|
- grep -q "Skip for now" ui/src/components/onboarding/ProviderSelectionStep.tsx
|
|
- grep -q "border-primary bg-primary/5" ui/src/components/onboarding/ProviderSelectionStep.tsx
|
|
- grep -q "detected" ui/src/components/onboarding/ProviderSelectionStep.tsx
|
|
- grep -q "Step 3.*Provider\|ProviderSelectionStep" ui/src/components/NexusOnboardingWizard.tsx
|
|
- grep -q "Step {step} of 4\|of 4" ui/src/components/NexusOnboardingWizard.tsx
|
|
- grep -q "puterToken" ui/src/components/NexusOnboardingWizard.tsx
|
|
- grep -q "googleOAuthStateId" ui/src/components/NexusOnboardingWizard.tsx
|
|
- grep -q "claimGoogleTokens" ui/src/components/NexusOnboardingWizard.tsx
|
|
- grep -q "openclaw_gateway\|claude_local" ui/src/components/NexusOnboardingWizard.tsx
|
|
- TypeScript compilation passes (tsc --noEmit exits 0)
|
|
</acceptance_criteria>
|
|
<done>4-step wizard with ProviderSelectionStep at step 3, adapter auto-detection badges for hermes/claude/openclaw, Puter token captured in state then posted after company creation, Google OAuth stateId captured then claimed after company creation, API key captured then stored after company creation, Skip always available</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `cd /opt/nexus && pnpm --filter @paperclipai/ui exec tsc --noEmit` — no TypeScript errors
|
|
- All new components exist with correct exports
|
|
- Wizard shows 4 steps with provider selection at step 3
|
|
- Provider cards match ModeSelector visual pattern
|
|
- All UI copy matches 31-UI-SPEC.md copywriting contract
|
|
- Google OAuth flow uses stateId/claim pattern (no companyId before company creation)
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- Provider Selection step renders three provider cards with correct copy
|
|
- Puter auth triggers popup and captures token in React state
|
|
- Google OAuth shows risk warning with 3-second gate, opens popup, captures stateId
|
|
- API key form captures provider/key pairs
|
|
- Auto-detected adapters show badges on cards
|
|
- Skip for now always works
|
|
- Wizard is 4 steps, all credentials stored after company creation via handleSubmit
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/31-puter.js-zero-config-cloud/31-03-SUMMARY.md`
|
|
</output>
|