From 157a3e149317024ccd28fb812debecde189267fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 Aug 2025 08:27:11 +0000 Subject: [PATCH 1/4] Initial plan From bddf182150a81e50417619cbee06517ea247cc38 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 Aug 2025 08:41:39 +0000 Subject: [PATCH 2/4] Implement modern passkey login UI with improved UX Co-authored-by: bradleyayers <105820+bradleyayers@users.noreply.github.com> --- projects/app/src/app/login.tsx | 342 ++++++++++++++++++++++----------- 1 file changed, 231 insertions(+), 111 deletions(-) diff --git a/projects/app/src/app/login.tsx b/projects/app/src/app/login.tsx index a09658fe51..01b9762e1f 100644 --- a/projects/app/src/app/login.tsx +++ b/projects/app/src/app/login.tsx @@ -12,98 +12,149 @@ import { Platform, Text, View } from "react-native"; import z from "zod/v4"; export default function LoginPage() { - const auth = useAuth(); + return ( + + {/* Header */} + + Welcome to Pinyinly + + Sign in securely with your passkey or create a new account + + - const [name, setName] = useState(``); + {/* Main Content */} + + {/* Passkey Authentication Section */} + - return ( - - Passkey - - - { - auth.logInWithPasskey().catch((error: unknown) => { - console.error(`failed to log in with passkey`, error); - }); - }} - > - Log in with Passkey - - - - { - auth.logInWithPasskey().catch((error: unknown) => { - console.error(`failed to log in with passkey`, error); - }); - }} - > - Log in with Passkey (conditional UI) - - - - - { - setName(text); - }} - value={name} - /> - { - auth.signUpWithPasskey({ name }).catch((error: unknown) => { - console.error(`failed to log in with passkey`, error); - }); - }} - > - Sign up with Passkey - - + {/* Alternative Sign-in Options */} + + + {/* Development/Debug Section */} + {__DEV__ && } - Login - - {auth.data?.allDeviceSessions.map((x, i) => ( - - - - - Skill count: - - Session ID: {x.serverSessionId} - DB name: {x.replicacheDbName} - - { - auth.logInToExistingDeviceSession( - (s) => s.replicacheDbName === x.replicacheDbName, - ); - }} - > - Log in - - - - ))} + {/* Back to App Button */} + + - - Session ID: {auth.data?.activeDeviceSession.serverSessionId} - - - DB name: {auth.data?.activeDeviceSession.replicacheDbName} + + ); +} + +function PasskeyAuthSection() { + const auth = useAuth(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [mode, setMode] = useState<`signin` | `signup`>(`signin`); + const [name, setName] = useState(``); + + const handlePasskeySignIn = async () => { + setIsLoading(true); + setError(null); + try { + await auth.logInWithPasskey(); + } catch (error_) { + console.error(`Passkey sign-in failed:`, error_); + setError(`Sign-in failed. Please try again or create an account.`); + } finally { + setIsLoading(false); + } + }; + + const handlePasskeySignUp = async () => { + if (!name.trim()) { + setError(`Please enter your name to create an account.`); + return; + } + + setIsLoading(true); + setError(null); + try { + await auth.signUpWithPasskey({ name: name.trim() }); + } catch (error_) { + console.error(`Passkey sign-up failed:`, error_); + setError(`Account creation failed. Please try again.`); + } finally { + setIsLoading(false); + } + }; + + return ( + + + {mode === `signin` ? `Sign In` : `Create Account`} + {error != null && error.length > 0 && ( + + {error} + + )} + + {mode === `signup` && ( + + )} + { - auth.signOut(); + variant="filled" + onPress={() => { + void (mode === `signin` ? handlePasskeySignIn() : handlePasskeySignUp()); }} + disabled={isLoading || (mode === `signup` && !name.trim())} > - Logout + {isLoading + ? (mode === `signin` ? `Signing in...` : `Creating account...`) + : (mode === `signin` ? `šŸ” Sign in with Passkey` : `šŸ” Create account with Passkey`) + } - {__DEV__ ? : null} + {Platform.OS === `web` && ( + + )} + + + + {mode === `signin` ? `Don't have an account?` : `Already have an account?`} + + { + setMode(mode === `signin` ? `signup` : `signin`); + setError(null); + setName(``); + }} + > + + {mode === `signin` ? `Create one` : `Sign in`} + + + + + ); +} + +function AlternativeSignInSection() { + const auth = useAuth(); + + return ( + + + + or + + + + {/* Apple Sign In */} {Platform.OS === `web` ? ( - ) : null} - - {Platform.OS === `ios` ? ( + ) : Platform.OS === `ios` ? ( { let credential; @@ -135,35 +184,90 @@ export default function LoginPage() { if (err.success) { switch (err.data.code) { case `ERR_REQUEST_CANCELED`: { - // handle that the user canceled the sign-in flow - console.error(`request canceled`); + console.error(`Apple sign-in canceled`); break; } default: { - console.error( - `unknown error code=${err.data.code}, error=`, - err.data, - ); + console.error(`Apple sign-in error:`, err.data); } } } else { - console.error(`unknown error (no code), error=`, error); + console.error(`Unknown Apple sign-in error:`, error); } - return; } invariant(credential.identityToken != null); - void auth.logInWithApple(credential.identityToken); }} /> ) : null} - - UI - + + ); +} - +function DeveloperDebugSection() { + const auth = useAuth(); + + return ( + + Development Tools + + {/* Existing Sessions */} + {auth.data?.allDeviceSessions && auth.data.allDeviceSessions.length > 0 && ( + + Existing Sessions + {auth.data.allDeviceSessions.map((session, i) => ( + + + + + Session: {session.serverSessionId ?? `Anonymous`} + + + Skills: + + + { + auth.logInToExistingDeviceSession( + (s) => s.replicacheDbName === session.replicacheDbName, + ); + }} + > + Use + + + + ))} + + )} + + {/* Current Session Info */} + + + Current Session: {auth.data?.activeDeviceSession.serverSessionId ?? `Anonymous`} + + + DB: {auth.data?.activeDeviceSession.replicacheDbName} + + + + {/* Dev Actions */} + + { auth.signOut(); }} + > + Sign Out + + + UI Demo + + + + ); } @@ -173,19 +277,35 @@ function ServerSessionIdLoginForm() { const [input, setInput] = useState(``); return ( - { - if (e.nativeEvent.key === `Enter`) { - auth.logInWithServerSessionId(input); - e.preventDefault(); - } - }} - value={input} - onChangeText={(text) => { - setInput(text); - }} - /> + + Session ID Login + + + { + if (e.nativeEvent.key === `Enter`) { + auth.logInWithServerSessionId(input); + e.preventDefault(); + } + }} + value={input} + onChangeText={(text) => { + setInput(text); + }} + /> + + { + auth.logInWithServerSessionId(input); + }} + disabled={!input.trim()} + > + Login + + + ); } From 9eef817ad950ab19789b86d6c106bbfe729a3062 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 05:00:08 +0000 Subject: [PATCH 3/4] Move development tools from login page to dedicated /dev/auth page - Created new /dev/auth.tsx page with all authentication development tools - Created /dev/index.tsx for improved dev tools navigation - Removed DeveloperDebugSection from login.tsx - Added small dev tools link to login page footer (dev mode only) - All linting and type checking passes Co-authored-by: bradleyayers <105820+bradleyayers@users.noreply.github.com> --- projects/app/src/app/dev/auth.tsx | 155 +++++++++++++++++++++++++++++ projects/app/src/app/dev/index.tsx | 53 ++++++++++ projects/app/src/app/login.tsx | 130 ++---------------------- 3 files changed, 219 insertions(+), 119 deletions(-) create mode 100644 projects/app/src/app/dev/auth.tsx create mode 100644 projects/app/src/app/dev/index.tsx diff --git a/projects/app/src/app/dev/auth.tsx b/projects/app/src/app/dev/auth.tsx new file mode 100644 index 0000000000..aeb9e17b7f --- /dev/null +++ b/projects/app/src/app/dev/auth.tsx @@ -0,0 +1,155 @@ +import { useAuth } from "@/client/auth"; +import { useRizzleQuery } from "@/client/hooks/useRizzleQuery"; +import { RectButton } from "@/client/ui/RectButton"; +import { SessionStoreProvider } from "@/client/ui/SessionStoreProvider"; +import { TextInputSingle } from "@/client/ui/TextInputSingle"; +import { Link } from "expo-router"; +import { useState } from "react"; +import { Text, View } from "react-native"; + +export default function DevAuthPage() { + return ( + + + Authentication Development Tools + + Manage sessions, debug authentication flows, and test login functionality. + + + + + + ); +} + +function DeveloperDebugSection() { + const auth = useAuth(); + + return ( + + {/* Existing Sessions */} + {auth.data?.allDeviceSessions && auth.data.allDeviceSessions.length > 0 && ( + + Existing Sessions + {auth.data.allDeviceSessions.map((session, i) => ( + + + + + Session: {session.serverSessionId ?? `Anonymous`} + + + Skills: + + + DB: {session.replicacheDbName} + + + { + auth.logInToExistingDeviceSession( + (s) => s.replicacheDbName === session.replicacheDbName, + ); + }} + > + Use Session + + + + ))} + + )} + + {/* Current Session Info */} + + Current Session + + + Session ID: {auth.data?.activeDeviceSession.serverSessionId ?? `Anonymous`} + + + Database: {auth.data?.activeDeviceSession.replicacheDbName} + + + + + {/* Session ID Login */} + + Session ID Login + + + + {/* Dev Actions */} + + Development Actions + + { auth.signOut(); }} + > + Sign Out + + + UI Components + + + API Debug + + + + + ); +} + +function ServerSessionIdLoginForm() { + const auth = useAuth(); + const [input, setInput] = useState(``); + + return ( + + + Enter a server session ID to log in directly + + + + { + if (e.nativeEvent.key === `Enter`) { + auth.logInWithServerSessionId(input); + e.preventDefault(); + } + }} + value={input} + onChangeText={(text) => { + setInput(text); + }} + /> + + { + auth.logInWithServerSessionId(input); + }} + disabled={!input.trim()} + > + Login + + + + ); +} + +function SkillCount() { + const result = useRizzleQuery([`wordCount`], async (r, tx) => { + const skillStates = await r.query.skillState.scan(tx).toArray(); + return skillStates.length; + }); + + return result.isPending ? ( + Loading… + ) : ( + {result.data} words + ); +} \ No newline at end of file diff --git a/projects/app/src/app/dev/index.tsx b/projects/app/src/app/dev/index.tsx new file mode 100644 index 0000000000..82a1a6f7a5 --- /dev/null +++ b/projects/app/src/app/dev/index.tsx @@ -0,0 +1,53 @@ +import { RectButton } from "@/client/ui/RectButton"; +import { Link } from "expo-router"; +import { Text, View } from "react-native"; + +export default function DevIndexPage() { + return ( + + + Development Tools + + Access development utilities, debugging tools, and component demos. + + + + + + + + + + + + ); +} + +function DevToolCard({ title, description, href }: { + title: string; + description: string; + href: `/dev/auth` | `/dev/ui` | `/dev/api`; +}) { + return ( + + + + {title} + {description} + + + + ); +} \ No newline at end of file diff --git a/projects/app/src/app/login.tsx b/projects/app/src/app/login.tsx index 01b9762e1f..972ca3b419 100644 --- a/projects/app/src/app/login.tsx +++ b/projects/app/src/app/login.tsx @@ -1,7 +1,5 @@ import { useAuth } from "@/client/auth"; -import { useRizzleQuery } from "@/client/hooks/useRizzleQuery"; import { RectButton } from "@/client/ui/RectButton"; -import { SessionStoreProvider } from "@/client/ui/SessionStoreProvider"; import { SignInWithAppleButton } from "@/client/ui/SignInWithAppleButton"; import { TextInputSingle } from "@/client/ui/TextInputSingle"; import { invariant } from "@pinyinly/lib/invariant"; @@ -29,14 +27,22 @@ export default function LoginPage() { {/* Alternative Sign-in Options */} - - {/* Development/Debug Section */} - {__DEV__ && } {/* Back to App Button */} + {__DEV__ && ( + + + + + Development Tools + + + + + )} ); @@ -206,109 +212,6 @@ function AlternativeSignInSection() { ); } -function DeveloperDebugSection() { - const auth = useAuth(); - - return ( - - Development Tools - - {/* Existing Sessions */} - {auth.data?.allDeviceSessions && auth.data.allDeviceSessions.length > 0 && ( - - Existing Sessions - {auth.data.allDeviceSessions.map((session, i) => ( - - - - - Session: {session.serverSessionId ?? `Anonymous`} - - - Skills: - - - { - auth.logInToExistingDeviceSession( - (s) => s.replicacheDbName === session.replicacheDbName, - ); - }} - > - Use - - - - ))} - - )} - - {/* Current Session Info */} - - - Current Session: {auth.data?.activeDeviceSession.serverSessionId ?? `Anonymous`} - - - DB: {auth.data?.activeDeviceSession.replicacheDbName} - - - - {/* Dev Actions */} - - { auth.signOut(); }} - > - Sign Out - - - UI Demo - - - - - - ); -} - -function ServerSessionIdLoginForm() { - const auth = useAuth(); - const [input, setInput] = useState(``); - - return ( - - Session ID Login - - - { - if (e.nativeEvent.key === `Enter`) { - auth.logInWithServerSessionId(input); - e.preventDefault(); - } - }} - value={input} - onChangeText={(text) => { - setInput(text); - }} - /> - - { - auth.logInWithServerSessionId(input); - }} - disabled={!input.trim()} - > - Login - - - - ); -} - const GoHomeButton = () => ( @@ -317,15 +220,4 @@ const GoHomeButton = () => ( ); -function SkillCount() { - const result = useRizzleQuery([`wordCount`], async (r, tx) => { - const skillStates = await r.query.skillState.scan(tx).toArray(); - return skillStates.length; - }); - return result.isPending ? ( - Loading… - ) : ( - {result.data} words - ); -} From 43f0dce946009008ec62eb3aad2435befb546d7e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 05:08:45 +0000 Subject: [PATCH 4/4] Move Apple Sign-in to dev tools and auto-switch to signup on auth failure Co-authored-by: bradleyayers <105820+bradleyayers@users.noreply.github.com> --- projects/app/src/app/dev/auth.tsx | 76 ++++++++++++++++++++++++++++++- projects/app/src/app/login.tsx | 74 ++---------------------------- 2 files changed, 78 insertions(+), 72 deletions(-) diff --git a/projects/app/src/app/dev/auth.tsx b/projects/app/src/app/dev/auth.tsx index aeb9e17b7f..653575ef31 100644 --- a/projects/app/src/app/dev/auth.tsx +++ b/projects/app/src/app/dev/auth.tsx @@ -2,10 +2,14 @@ import { useAuth } from "@/client/auth"; import { useRizzleQuery } from "@/client/hooks/useRizzleQuery"; import { RectButton } from "@/client/ui/RectButton"; import { SessionStoreProvider } from "@/client/ui/SessionStoreProvider"; +import { SignInWithAppleButton } from "@/client/ui/SignInWithAppleButton"; import { TextInputSingle } from "@/client/ui/TextInputSingle"; +import { invariant } from "@pinyinly/lib/invariant"; +import * as AppleAuthentication from "expo-apple-authentication"; import { Link } from "expo-router"; import { useState } from "react"; -import { Text, View } from "react-native"; +import { Platform, Text, View } from "react-native"; +import z from "zod/v4"; export default function DevAuthPage() { return ( @@ -98,6 +102,15 @@ function DeveloperDebugSection() { + + {/* Deprecated Apple Sign-in */} + + Deprecated Features + + Apple Sign-in (deprecated - for development/testing only) + + + ); } @@ -152,4 +165,65 @@ function SkillCount() { ) : ( {result.data} words ); +} + +function AppleSignInSection() { + const auth = useAuth(); + + return ( + + {/* Apple Sign In */} + {Platform.OS === `web` ? ( + { + void auth.logInWithApple(data.authorization.id_token); + }} + redirectUri={`https://${location.hostname}/api/auth/login/apple/callback`} + /> + ) : Platform.OS === `ios` ? ( + { + let credential; + try { + credential = await AppleAuthentication.signInAsync({ + requestedScopes: [ + AppleAuthentication.AppleAuthenticationScope.FULL_NAME, + AppleAuthentication.AppleAuthenticationScope.EMAIL, + ], + }); + } catch (error) { + const err = z.object({ code: z.string() }).safeParse(error); + if (err.success) { + switch (err.data.code) { + case `ERR_REQUEST_CANCELED`: { + console.error(`Apple sign-in canceled`); + break; + } + default: { + console.error(`Apple sign-in error:`, err.data); + } + } + } else { + console.error(`Unknown Apple sign-in error:`, error); + } + return; + } + + invariant(credential.identityToken != null); + void auth.logInWithApple(credential.identityToken); + }} + /> + ) : Platform.OS === `android` ? ( + Apple Sign-in not available on Android + ) : ( + Apple Sign-in not available on this platform + )} + + ); } \ No newline at end of file diff --git a/projects/app/src/app/login.tsx b/projects/app/src/app/login.tsx index 972ca3b419..133c574835 100644 --- a/projects/app/src/app/login.tsx +++ b/projects/app/src/app/login.tsx @@ -1,13 +1,9 @@ import { useAuth } from "@/client/auth"; import { RectButton } from "@/client/ui/RectButton"; -import { SignInWithAppleButton } from "@/client/ui/SignInWithAppleButton"; import { TextInputSingle } from "@/client/ui/TextInputSingle"; -import { invariant } from "@pinyinly/lib/invariant"; -import * as AppleAuthentication from "expo-apple-authentication"; import { Link } from "expo-router"; import { useState } from "react"; import { Platform, Text, View } from "react-native"; -import z from "zod/v4"; export default function LoginPage() { return ( @@ -24,9 +20,6 @@ export default function LoginPage() { {/* Passkey Authentication Section */} - - {/* Alternative Sign-in Options */} - {/* Back to App Button */} @@ -62,7 +55,9 @@ function PasskeyAuthSection() { await auth.logInWithPasskey(); } catch (error_) { console.error(`Passkey sign-in failed:`, error_); - setError(`Sign-in failed. Please try again or create an account.`); + // Automatically switch to signup mode instead of showing an error + setMode(`signup`); + setError(null); } finally { setIsLoading(false); } @@ -149,69 +144,6 @@ function PasskeyAuthSection() { ); } -function AlternativeSignInSection() { - const auth = useAuth(); - - return ( - - - - or - - - - {/* Apple Sign In */} - {Platform.OS === `web` ? ( - { - void auth.logInWithApple(data.authorization.id_token); - }} - redirectUri={`https://${location.hostname}/api/auth/login/apple/callback`} - /> - ) : Platform.OS === `ios` ? ( - { - let credential; - try { - credential = await AppleAuthentication.signInAsync({ - requestedScopes: [ - AppleAuthentication.AppleAuthenticationScope.FULL_NAME, - AppleAuthentication.AppleAuthenticationScope.EMAIL, - ], - }); - } catch (error) { - const err = z.object({ code: z.string() }).safeParse(error); - if (err.success) { - switch (err.data.code) { - case `ERR_REQUEST_CANCELED`: { - console.error(`Apple sign-in canceled`); - break; - } - default: { - console.error(`Apple sign-in error:`, err.data); - } - } - } else { - console.error(`Unknown Apple sign-in error:`, error); - } - return; - } - - invariant(credential.identityToken != null); - void auth.logInWithApple(credential.identityToken); - }} - /> - ) : null} - - ); -} - const GoHomeButton = () => (