+ );
+}
diff --git a/ui/src/components/onboarding/GoogleOAuthButton.tsx b/ui/src/components/onboarding/GoogleOAuthButton.tsx
new file mode 100644
index 00000000..ac965ae2
--- /dev/null
+++ b/ui/src/components/onboarding/GoogleOAuthButton.tsx
@@ -0,0 +1,107 @@
+// [nexus] Google OAuth button — 3-second risk warning gate, opens popup, captures stateId
+import { useState, useEffect, useRef } from "react";
+import { Button } from "@/components/ui/button";
+import { CheckCircle } from "lucide-react";
+import { puterProxyApi } from "@/api/puter-proxy";
+
+interface GoogleOAuthButtonProps {
+ onSuccess: (stateId: string) => void;
+ onError: (msg: string) => void;
+}
+
+export function GoogleOAuthButton({ onSuccess, onError }: GoogleOAuthButtonProps) {
+ const [loading, setLoading] = useState(false);
+ const [warningTimer, setWarningTimer] = useState(false);
+ const [connected, setConnected] = useState(false);
+
+ // 3-second gate — button stays disabled until user has read the risk warning
+ useEffect(() => {
+ const id = setTimeout(() => setWarningTimer(true), 3000);
+ return () => clearTimeout(id);
+ }, []);
+
+ async function handleClick() {
+ setLoading(true);
+ try {
+ const { url, stateId } = await puterProxyApi.getAuthUrl();
+
+ const popup = window.open(url, "_blank", "popup,width=600,height=700");
+ if (!popup) {
+ throw new Error("Popup blocked");
+ }
+
+ // Poll for popup close — when closed, assume callback stored tokens server-side
+ await new Promise((resolve) => {
+ const interval = setInterval(() => {
+ if (popup.closed) {
+ clearInterval(interval);
+ resolve();
+ }
+ }, 500);
+ });
+
+ onSuccess(stateId);
+ setConnected(true);
+ } catch {
+ onError("Google sign-in failed. Try again or use an API key instead.");
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+ {/* Policy-risk warning — always visible when this component is rendered */}
+
+
Policy risk -- read before continuing
+
+ 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.
+
+
+
+
+
+ );
+}
diff --git a/ui/src/components/onboarding/PuterAuthButton.tsx b/ui/src/components/onboarding/PuterAuthButton.tsx
new file mode 100644
index 00000000..8ce26324
--- /dev/null
+++ b/ui/src/components/onboarding/PuterAuthButton.tsx
@@ -0,0 +1,105 @@
+// [nexus] Puter auth button — loads CDN script, triggers signIn popup, captures token
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { CheckCircle, LogIn } from "lucide-react";
+
+interface PuterAuthButtonProps {
+ onSuccess: (token: string) => void;
+ onError: (msg: string) => void;
+}
+
+function loadScript(): Promise {
+ return new Promise((resolve, reject) => {
+ if ((window as any).puter) {
+ resolve();
+ return;
+ }
+ const script = document.createElement("script");
+ script.src = "https://js.puter.com/v2/";
+ script.onload = () => resolve();
+ script.onerror = () => reject(new Error("Failed to load Puter SDK"));
+ document.head.appendChild(script);
+ });
+}
+
+export function PuterAuthButton({ onSuccess, onError }: PuterAuthButtonProps) {
+ const [loading, setLoading] = useState(false);
+ const [connected, setConnected] = useState(false);
+
+ async function handleClick() {
+ setLoading(true);
+ try {
+ await loadScript();
+
+ // signIn must be called in the same async chain as the click event for popup to work
+ await (window as any).puter.auth.signIn();
+
+ // Extract token — try multiple access patterns (Pitfall 1: token location varies)
+ let token: string | undefined =
+ (window as any).puter?.authToken ??
+ (window as any).puter?.auth?.token;
+
+ if (!token) {
+ const user = await (window as any).puter?.auth?.getUser?.();
+ token = user?.token;
+ }
+
+ if (!token) {
+ console.warn("[nexus] Puter token is undefined after signIn — user may not be authenticated");
+ }
+
+ onSuccess(token ?? "");
+ setConnected(true);
+ } catch {
+ onError("Puter sign-in failed. Check your Puter.com account and try again.");
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+ );
+}