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 = () => (