Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Build_plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ All Jules prompts are written by the Orchestrator. Copilot reviews every PR. Orc
| 1.6 | Business logic hooks | ✅ Done | useAuth, useLogs (offline queue + AppState listener), useStats (DST-safe), useProfile |
| 1.7 | Dev auth bypass | ✅ Done | `src/lib/devConfig.ts` — `DEV_BYPASS_AUTH = __DEV__ && true` |
| 1.8 | Google Sign In stub | ✅ Done | `src/lib/googleSignIn.ts` — for Expo Go compatibility |
| 1.9 | Email/password auth | ✅ Done | Added to AuthScreen for dev testing and as production fallback |

---

Expand Down Expand Up @@ -179,6 +180,7 @@ Do not build any of the following until explicitly added to the build plan:
| 07 | Business logic hooks | ✅ Merged — Copilot fixed DST-safe dates, `isNetworkError`, AppState listener, UUID fallback |
| 07b | DEV_BYPASS_AUTH | ✅ Merged via Antigravity — `__DEV__ && true` pattern |
| 07c | Google Sign In stub | ✅ Merged via Antigravity — Expo Go compatibility |
| 07d | Email/password auth | ✅ Merged |
| 08 | Onboarding screen | ✅ Merged |
| 09 | Home screen + LogModal | ✅ Merged |
| 10 | History screen | 🔲 Pending — prompt ready in handoff document |
Expand Down
18 changes: 18 additions & 0 deletions src/constants/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export const Colors = {
success: '#34C759',
black: '#000000',
tabInactive: '#C8C4BE',
googleBlue: '#4285F4',
googleGreen: '#34A853',
googleYellow: '#FBBC05',
googleRed: '#EA4335',
overlay: 'rgba(0, 0, 0, 0.3)',
}

export const Fonts = {
Expand All @@ -50,13 +55,26 @@ export const FontSizes = {
export const Spacing = {
xs: 4,
sm: 8,
smLg: 10,
md: 12,
lg: 16,
xl: 20,
xxl: 24,
xxxl: 48,
screen: 22,
}

export const Sizes = {
buttonHeight: 52,
iconSm: 18,
}

export const BorderWidths = {
sm: 1,
md: 1.5,
lg: 2,
}

export const Radius = {
sm: 8,
md: 12,
Expand Down
201 changes: 184 additions & 17 deletions src/screens/AuthScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Platform } from 'react-native';
import { Colors, Fonts, FontSizes, Spacing, Radius } from '../constants/theme';
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Platform, TextInput, Alert, KeyboardAvoidingView, ScrollView } from 'react-native';
import { Colors, Fonts, FontSizes, Spacing, Radius, Sizes, BorderWidths } from '../constants/theme';
import * as AppleAuthentication from 'expo-apple-authentication';
import { GoogleSignin } from '../lib/googleSignIn';
import { supabase } from '../lib/supabase';
import Svg, { Path } from 'react-native-svg';

const GoogleIcon = () => (
<Svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<Svg width={Sizes.iconSm} height={Sizes.iconSm} viewBox="0 0 18 18" fill="none">
<Path
d="M17.64 9.20455C17.64 8.56636 17.5827 7.95273 17.4764 7.36364H9V10.845H13.8436C13.635 11.97 13.0009 12.9232 12.0477 13.5614V15.8195H14.9564C16.6582 14.2527 17.64 11.9455 17.64 9.20455Z"
fill="#4285F4"
fill={Colors.googleBlue}
/>
<Path
d="M9 18C11.43 18 13.4673 17.1941 14.9564 15.8195L12.0477 13.5614C11.2418 14.1014 10.2109 14.4205 9 14.4205C6.65591 14.4205 4.67182 12.8373 3.96409 10.71H0.957275V13.0418C2.43818 15.9832 5.48182 18 9 18Z"
fill="#34A853"
fill={Colors.googleGreen}
/>
<Path
d="M3.96409 10.71C3.78409 10.17 3.68182 9.59318 3.68182 9C3.68182 8.40682 3.78409 7.83 3.96409 7.29H0.957275V9.62182C0.347727 8.42318 0 7.07318 0 5.65C0 4.22682 0.347727 2.87682 0.957275 1.67818L3.96409 4.01C4.67182 1.88273 6.65591 0.299545 9 0.299545C10.2109 0.299545 11.2418 0.618636 12.0477 1.15864L14.9564 -1.10045C13.4673 -2.475 11.43 -3.28091 9 -3.28091C5.48182 -3.28091 2.43818 -1.26409 0.957275 1.67818H3.96409Z"
fill="#FBBC05"
fill={Colors.googleYellow}
transform="translate(0, 3.3218)"
/>
<Path
d="M9 3.57955C10.3214 3.57955 11.5077 4.03364 12.4405 4.92545L15.0218 2.34409C13.4632 0.891818 11.4259 0 9 0C5.48182 0 2.43818 2.01682 0.957275 4.95818L3.96409 7.29C4.67182 5.16273 6.65591 3.57955 9 3.57955Z"
fill="#EA4335"
fill={Colors.googleRed}
/>
</Svg>
);
Expand All @@ -34,13 +34,19 @@ export default function AuthScreen() {
const [appleAvailable, setAppleAvailable] = useState(false);
const [googleConfigError, setGoogleConfigError] = useState<string | null>(null);

const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [authMode, setAuthMode] = useState<'signin' | 'signup'>('signin');
const [emailLoading, setEmailLoading] = useState(false);

useEffect(() => {
if (Platform.OS === 'ios' || Platform.OS === 'macos') {
AppleAuthentication.isAvailableAsync()
.then(setAppleAvailable)
.catch((err) => console.error('Apple authentication availability check failed:', err));
}

// Intentionally public for client-side OAuth
const webClientId = process.env.EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID;
const iosClientId = process.env.EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID;
if (!webClientId || !iosClientId) {
Expand Down Expand Up @@ -83,6 +89,43 @@ export default function AuthScreen() {
}
};

const handleEmailAuth = async () => {
const trimmedEmail = email.trim();

if (!trimmedEmail || !password) return;
if (password.length < 6) {
Alert.alert('Password too short', 'Password must be at least 6 characters.');
return;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

setEmailLoading(true);
try {
if (authMode === 'signin') {
const { error } = await supabase.auth.signInWithPassword({
email: trimmedEmail,
password: password,
});
if (error) throw error;
} else {
const { error } = await supabase.auth.signUp({
email: trimmedEmail,
password: password,
});
if (error) throw error;
Alert.alert(
'Check your email',
'We sent you a confirmation link. Click it to activate your account, then sign in.'
);
setAuthMode('signin');
}
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Something went wrong.';
Alert.alert('Sign in failed', message);
} finally {
Comment on lines +121 to +124

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use mode-specific failure copy for email auth errors.

At Line 122, the alert title is always “Sign in failed”, which is misleading when authMode === 'signup'. Use a conditional title so sign-up failures are labelled correctly.

Suggested patch
-      Alert.alert('Sign in failed', message);
+      Alert.alert(authMode === 'signin' ? 'Sign in failed' : 'Sign up failed', message);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Something went wrong.';
Alert.alert('Sign in failed', message);
} finally {
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Something went wrong.';
Alert.alert(authMode === 'signin' ? 'Sign in failed' : 'Sign up failed', message);
} finally {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/screens/AuthScreen.tsx` around lines 120 - 123, The alert title is
hard-coded to "Sign in failed" inside the catch block in AuthScreen.tsx; update
that Alert.alert call to use a mode-specific title based on the local authMode
(e.g., use "Sign up failed" when authMode === 'signup' and "Sign in failed"
otherwise) so failures reflect the current flow; ensure you reference the same
authMode variable in scope where the catch runs (e.g., in the email
submit/handleEmailAuth function) and keep the existing error message logic
unchanged.

setEmailLoading(false);
}
};

const handleGoogleSignIn = async () => {
setGoogleLoading(true);
try {
Expand Down Expand Up @@ -163,6 +206,71 @@ export default function AuthScreen() {
</>
)}
</TouchableOpacity>

<View style={styles.dividerRow}>
<View style={styles.dividerLine} />
<Text style={styles.dividerText}>or</Text>
<View style={styles.dividerLine} />
</View>

{/* Requires: Supabase dashboard -> Authentication -> Providers -> Email -> Enable */}
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={64}
style={{ width: '100%' }}
>
<ScrollView
contentContainerStyle={styles.emailForm}
keyboardShouldPersistTaps="handled"
>
<TextInput
value={email}
onChangeText={setEmail}
placeholder="Email address"
placeholderTextColor={Colors.textHint}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
style={styles.input}
/>
<TextInput
value={password}
onChangeText={setPassword}
placeholder="Password"
placeholderTextColor={Colors.textHint}
secureTextEntry={true}
autoCapitalize="none"
style={styles.input}
/>
<TouchableOpacity
style={styles.primaryButton}
onPress={handleEmailAuth}
disabled={emailLoading}
activeOpacity={0.8}
>
{emailLoading ? (
<ActivityIndicator color={Colors.white} />
) : (
<Text style={styles.primaryButtonText}>
{authMode === 'signin' ? 'Sign in' : 'Create account'}
</Text>
)}
</TouchableOpacity>

<View style={styles.toggleRow}>
<Text style={styles.toggleTextMuted}>
{authMode === 'signin'
? "Don't have an account? "
: "Already have an account? "}
</Text>
<TouchableOpacity onPress={() => setAuthMode(authMode === 'signin' ? 'signup' : 'signin')}>
<Text style={styles.toggleTextAction}>
{authMode === 'signin' ? 'Create one' : 'Sign in'}
</Text>
</TouchableOpacity>
</View>
</ScrollView>
</KeyboardAvoidingView>
</View>

<Text style={styles.finePrint}>
Expand All @@ -186,7 +294,7 @@ const styles = StyleSheet.create({
},
header: {
alignItems: 'center',
marginBottom: 48,
marginBottom: Spacing.xxxl,
},
logoContainer: {
flexDirection: 'row',
Expand All @@ -209,7 +317,7 @@ const styles = StyleSheet.create({
fontWeight: '300', // sansLight mapping
fontSize: FontSizes.md,
color: Colors.textMuted,
marginTop: 10,
marginTop: Spacing.smLg,
},
buttonContainer: {
width: '100%',
Expand All @@ -222,21 +330,21 @@ const styles = StyleSheet.create({
},
appleButton: {
width: '100%',
height: 52,
height: Sizes.buttonHeight,
},
googleButton: {
width: '100%',
height: 52,
height: Sizes.buttonHeight,
backgroundColor: Colors.white,
borderColor: Colors.border,
borderWidth: 1,
borderRadius: 14,
borderWidth: BorderWidths.sm,
borderRadius: Radius.lg,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
googleIconContainer: {
marginRight: 10,
marginRight: Spacing.smLg,
},
googleButtonText: {
fontFamily: Fonts.sansMedium,
Expand All @@ -253,17 +361,76 @@ const styles = StyleSheet.create({
},
loadingOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.3)',
backgroundColor: Colors.overlay,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 14,
borderRadius: Radius.lg,
zIndex: 1,
},
finePrint: {
fontFamily: Fonts.sans,
fontSize: FontSizes.sm,
color: Colors.textHint,
textAlign: 'center',
marginTop: 16,
marginTop: Spacing.lg,
},
dividerRow: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.md,
marginVertical: Spacing.xl,
},
dividerLine: {
height: BorderWidths.sm,
flex: 1,
backgroundColor: Colors.border,
},
dividerText: {
fontFamily: Fonts.sans,
fontSize: FontSizes.sm,
color: Colors.textHint,
},
emailForm: {
width: '100%',
gap: Spacing.md,
},
input: {
backgroundColor: Colors.cardBg,
borderWidth: BorderWidths.md,
borderColor: Colors.inputBorder,
borderRadius: Radius.pill,
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.md,
fontFamily: Fonts.sans,
fontSize: FontSizes.md,
color: Colors.textPrimary,
},
primaryButton: {
width: '100%',
padding: Spacing.lg,
borderRadius: Radius.pill,
backgroundColor: Colors.primary,
alignItems: 'center',
},
primaryButtonText: {
fontFamily: Fonts.sansMedium,
fontSize: FontSizes.md,
color: Colors.white,
},
toggleRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginTop: Spacing.xs,
},
toggleTextMuted: {
fontFamily: Fonts.sans,
fontSize: FontSizes.sm,
color: Colors.textMuted,
},
toggleTextAction: {
fontFamily: Fonts.sansMedium,
fontSize: FontSizes.sm,
color: Colors.primary,
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});