Skip to content

Commit c1a53ff

Browse files
committed
feat: GitHub PAT input + secure token persistence on iOS (localStorage fallback)
1 parent c9cb475 commit c1a53ff

3 files changed

Lines changed: 95 additions & 12 deletions

File tree

components/chat-home.tsx

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ export const ChatHome = memo(function ChatHome({
120120
verificationUri: string
121121
} | null>(null)
122122
const [authLoading, setAuthLoading] = useState(false)
123+
const [showPatInput, setShowPatInput] = useState(false)
124+
const [patInput, setPatInput] = useState('')
125+
const patInputRef = useRef<HTMLInputElement>(null)
123126
const deviceFlowAbort = useRef<AbortController | null>(null)
124127

125128
const isMobile = typeof window !== 'undefined' && window.innerWidth <= 768
@@ -164,6 +167,18 @@ export const ChatHome = memo(function ChatHome({
164167
setAuthLoading(false)
165168
}, [])
166169

170+
const handlePatSubmit = useCallback(() => {
171+
const t = patInput.trim()
172+
if (!t) return
173+
setManualToken(t)
174+
setPatInput('')
175+
setShowPatInput(false)
176+
}, [patInput, setManualToken])
177+
178+
useEffect(() => {
179+
if (showPatInput) setTimeout(() => patInputRef.current?.focus(), 100)
180+
}, [showPatInput])
181+
167182
const handleRepoConnect = useCallback(async () => {
168183
const val = repoInput
169184
.trim()
@@ -330,15 +345,66 @@ export const ChatHome = memo(function ChatHome({
330345
)}
331346

332347
{/* Sign in with GitHub — when no token */}
333-
{!ghAuthenticated && !deviceFlow && (
334-
<button
335-
onClick={startGitHubSignIn}
336-
disabled={authLoading}
337-
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-[var(--border)] bg-[color-mix(in_srgb,var(--bg-elevated)_80%,transparent)] text-[12px] text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:border-[var(--text-disabled)] transition-all cursor-pointer disabled:opacity-50"
338-
>
339-
<Icon icon="lucide:github" width={14} height={14} />
340-
{authLoading ? 'Signing in…' : 'Sign in with GitHub'}
341-
</button>
348+
{!ghAuthenticated && !deviceFlow && !showPatInput && (
349+
<div className="flex flex-col items-center gap-2">
350+
<button
351+
onClick={startGitHubSignIn}
352+
disabled={authLoading}
353+
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-[var(--border)] bg-[color-mix(in_srgb,var(--bg-elevated)_80%,transparent)] text-[12px] text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:border-[var(--text-disabled)] transition-all cursor-pointer disabled:opacity-50"
354+
>
355+
<Icon icon="lucide:github" width={14} height={14} />
356+
{authLoading ? 'Signing in…' : 'Sign in with GitHub'}
357+
</button>
358+
<button
359+
onClick={() => setShowPatInput(true)}
360+
className="text-[11px] text-[var(--text-disabled)] hover:text-[var(--text-secondary)] transition-colors cursor-pointer"
361+
>
362+
or use a personal access token
363+
</button>
364+
</div>
365+
)}
366+
367+
{/* PAT input */}
368+
{!ghAuthenticated && showPatInput && !deviceFlow && (
369+
<div className="w-full space-y-2">
370+
<div className="flex items-center gap-1.5">
371+
<div className="flex-1 relative">
372+
<Icon icon="lucide:key-round" width={14} height={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-[var(--text-disabled)]" />
373+
<input
374+
ref={patInputRef}
375+
type="password"
376+
value={patInput}
377+
onChange={(e) => setPatInput(e.target.value)}
378+
onKeyDown={(e) => { if (e.key === 'Enter') handlePatSubmit(); if (e.key === 'Escape') setShowPatInput(false) }}
379+
placeholder="ghp_xxxx..."
380+
autoCapitalize="off"
381+
autoCorrect="off"
382+
spellCheck={false}
383+
className="w-full pl-8 pr-3 py-2 rounded-lg border border-[var(--border)] bg-[color-mix(in_srgb,var(--bg-elevated)_80%,transparent)] text-[13px] text-[var(--text-primary)] placeholder:text-[var(--text-disabled)] outline-none focus:border-[var(--brand)] transition-colors"
384+
/>
385+
</div>
386+
<button
387+
onClick={handlePatSubmit}
388+
disabled={!patInput.trim()}
389+
className="shrink-0 px-3 py-2 rounded-lg text-[12px] font-medium transition-all cursor-pointer disabled:opacity-40 disabled:cursor-default bg-[var(--brand)] text-[var(--brand-contrast,#fff)]"
390+
>
391+
Save
392+
</button>
393+
</div>
394+
<p className="text-[10px] text-[var(--text-disabled)] leading-relaxed">
395+
Create a token at{' '}
396+
<a href="https://github.com/settings/tokens" target="_blank" rel="noopener noreferrer" className="text-[var(--brand)] underline">
397+
github.com/settings/tokens
398+
</a>
399+
{' '}with <span className="font-mono">repo</span> scope. Stored securely on device.
400+
</p>
401+
<button
402+
onClick={() => { setShowPatInput(false); setPatInput('') }}
403+
className="text-[11px] text-[var(--text-disabled)] hover:text-[var(--text-secondary)] cursor-pointer"
404+
>
405+
← back to sign in
406+
</button>
407+
</div>
342408
)}
343409

344410
{/* Device flow — show code */}

context/github-auth-context.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,19 @@ export function GitHubAuthProvider({ children }: { children: ReactNode }) {
5858

5959
const persistToken = useCallback(async (t: string, src: TokenSource) => {
6060
if (isTauri()) {
61+
let stored = false
6162
try {
6263
await tauriInvoke('local_secret_set', {
6364
service: KEYCHAIN_SERVICE,
6465
account: KEYCHAIN_ACCOUNT,
6566
secret: t,
6667
})
68+
stored = true
6769
} catch {
68-
// Keep token in-memory even if keychain write fails.
70+
// Keychain unavailable (iOS) — fall back to localStorage
71+
}
72+
if (!stored) {
73+
try { localStorage.setItem('code-editor:gh-token-fallback', btoa(t)) } catch {}
6974
}
7075
localStorage.setItem(STORAGE_SOURCE_KEY, src)
7176
}
@@ -94,7 +99,18 @@ export function GitHubAuthProvider({ children }: { children: ReactNode }) {
9499
return
95100
}
96101
} catch {
97-
// Continue to migration fallback.
102+
// Keychain unavailable (iOS) — try localStorage fallback
103+
try {
104+
const fallback = localStorage.getItem('code-editor:gh-token-fallback')
105+
if (fallback && !cancelled) {
106+
const decoded = atob(fallback)
107+
setToken(decoded)
108+
setSource('manual')
109+
setGithubToken(decoded)
110+
setLoading(false)
111+
return
112+
}
113+
} catch {}
98114
}
99115
}
100116

@@ -167,6 +183,7 @@ export function GitHubAuthProvider({ children }: { children: ReactNode }) {
167183
}
168184
localStorage.removeItem(LEGACY_STORAGE_KEY)
169185
localStorage.removeItem(STORAGE_SOURCE_KEY)
186+
localStorage.removeItem('code-editor:gh-token-fallback')
170187
setToken('')
171188
setSource('none')
172189
setGithubToken('')

next-env.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference types="next" />
22
/// <reference types="next/image-types/global" />
3-
import "./.next/dev/types/routes.d.ts";
3+
import "./.next/types/routes.d.ts";
44

55
// NOTE: This file should not be edited
66
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

0 commit comments

Comments
 (0)