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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ jobs:

- name: Typecheck
run: npx tsc --noEmit

- name: Production readiness checks
run: bash scripts/check-prod-ready.sh
11 changes: 11 additions & 0 deletions app.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ export default {
color: '#FCF150',
},
],
// Mic + speech recognition for talking to Kaori on the 3D stage
[
'expo-speech-recognition',
{
microphonePermission:
'Allow $(PRODUCT_NAME) to access the microphone so you can talk to Kaori.',
speechRecognitionPermission:
'Allow $(PRODUCT_NAME) to use speech recognition to turn your voice into text.',
androidSpeechServicePackages: ['com.google.android.googlequicksearchbox'],
},
],
// Add `use_frameworks! :linkage => :static` to the iOS Podfile. Required
// because @react-native-google-signin/google-signin transitively pulls in
// AppCheckCore (Swift), whose Obj-C deps (GoogleUtilities, RecaptchaInterop)
Expand Down
94 changes: 48 additions & 46 deletions app/(auth)/welcome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@
*/

import { Ionicons } from '@expo/vector-icons';
import {
GoogleSignin,
isErrorWithCode,
statusCodes,
} from '@react-native-google-signin/google-signin';
import * as AppleAuthentication from 'expo-apple-authentication';
import { router } from 'expo-router';
import { useEffect, useState } from 'react';
Expand All @@ -30,11 +25,19 @@ import { getUserCount } from '@/lib/api/user';
import { useThemeContext } from '@/lib/providers/ThemeProvider';
import { useAuthStore } from '@/lib/stores/authStore';

// Configure Google Sign-In with the web client ID (used for ID token audience)
GoogleSignin.configure({
iosClientId: '624774098704-r7eqvb0jc4i3or885fk3k1u3l5uqlqmd.apps.googleusercontent.com',
webClientId: '624774098704-j2q01j9g7pj41f8poqbvkvho9f7v3mco.apps.googleusercontent.com',
});
// Conditionally load native Google Sign-In (unavailable in Expo Go)
let GoogleSigninModule: any = null;
try {
const mod = require('@react-native-google-signin/google-signin');
GoogleSigninModule = mod.GoogleSignin;
GoogleSigninModule.configure({
iosClientId: '624774098704-r7eqvb0jc4i3or885fk3k1u3l5uqlqmd.apps.googleusercontent.com',
webClientId: '624774098704-j2q01j9g7pj41f8poqbvkvho9f7v3mco.apps.googleusercontent.com',
});
} catch {
// Native module not available (Expo Go) -- Google Sign-In button will be hidden
}
const googleSignInAvailable = GoogleSigninModule != null;

// Format number with K/M suffix
function formatCount(count: number): string {
Expand All @@ -56,10 +59,11 @@ export default function WelcomeScreen() {

// Trigger native Google sign-in
const onGoogleSignIn = async () => {
if (!GoogleSigninModule) return;
setIsGoogleLoading(true);
try {
await GoogleSignin.hasPlayServices();
const response = await GoogleSignin.signIn();
await GoogleSigninModule.hasPlayServices();
const response = await GoogleSigninModule.signIn();

if (response.type === 'success' && response.data.idToken) {
// Send ID token to backend for verification
Expand All @@ -71,16 +75,12 @@ export default function WelcomeScreen() {
Alert.alert('Error', 'Google sign-in failed. Please try again.');
}
} catch (error: any) {
if (isErrorWithCode(error)) {
if (error.code === statusCodes.SIGN_IN_CANCELLED) {
// User cancelled — do nothing
} else if (error.code === statusCodes.IN_PROGRESS) {
// Sign-in already in progress
} else if (error.code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) {
Alert.alert('Error', 'Google Play Services is not available on this device.');
} else {
Alert.alert('Error', error.message || 'Failed to sign in with Google');
}
if (error.code === 'SIGN_IN_CANCELLED') {
// User cancelled — do nothing
} else if (error.code === 'IN_PROGRESS') {
// Sign-in already in progress
} else if (error.code === 'PLAY_SERVICES_NOT_AVAILABLE') {
Alert.alert('Error', 'Google Play Services is not available on this device.');
} else {
Alert.alert('Error', error.message || 'Failed to sign in with Google');
}
Expand Down Expand Up @@ -165,30 +165,32 @@ export default function WelcomeScreen() {
Get Started
</Button>

{/* Google Sign-In Button */}
<Pressable
style={[
styles.ssoButton,
{
backgroundColor: colors.surface,
borderColor: theme.border,
opacity: isGoogleLoading ? 0.6 : 1,
},
]}
onPress={onGoogleSignIn}
disabled={isGoogleLoading}
>
{isGoogleLoading ? (
<ActivityIndicator size="small" color={theme.text} />
) : (
<>
<Ionicons name="logo-google" size={20} color={theme.text} />
<Text style={[styles.ssoButtonText, { color: theme.text }]}>
Continue with Google
</Text>
</>
)}
</Pressable>
{/* Google Sign-In Button - hidden when native module unavailable (Expo Go) */}
{googleSignInAvailable && (
<Pressable
style={[
styles.ssoButton,
{
backgroundColor: theme.surface,
borderColor: theme.border,
opacity: isGoogleLoading ? 0.6 : 1,
},
]}
onPress={onGoogleSignIn}
disabled={isGoogleLoading}
>
{isGoogleLoading ? (
<ActivityIndicator size="small" color={theme.text} />
) : (
<>
<Ionicons name="logo-google" size={20} color={theme.text} />
<Text style={[styles.ssoButtonText, { color: theme.text }]}>
Continue with Google
</Text>
</>
)}
</Pressable>
)}

{/* Apple Sign-In Button - iOS only */}
{Platform.OS === 'ios' && (
Expand Down
7 changes: 5 additions & 2 deletions app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { Ionicons } from '@expo/vector-icons';
import { Tabs } from 'expo-router';
import { Platform } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { colors } from '@/constants/colors';
import { useThemeContext } from '@/lib/providers/ThemeProvider';

Expand All @@ -26,6 +27,8 @@ function TabBarIcon({ focused, color, size, name, focusedName }: TabBarIconProps

export default function TabsLayout() {
const { isDark, theme } = useThemeContext();
const insets = useSafeAreaInsets();
const bottomPadding = Platform.OS === 'ios' ? 28 : Math.max(insets.bottom, 8);

return (
<Tabs
Expand All @@ -38,9 +41,9 @@ export default function TabsLayout() {
backgroundColor: isDark ? '#000000' : theme.surface,
borderTopColor: isDark ? '#1A1A1A' : theme.border,
borderTopWidth: 1,
height: Platform.OS === 'ios' ? 88 : 64,
height: (Platform.OS === 'ios' ? 60 : 56) + bottomPadding,
paddingTop: 8,
paddingBottom: Platform.OS === 'ios' ? 28 : 8,
paddingBottom: bottomPadding,
},
tabBarLabelStyle: {
fontSize: 11,
Expand Down
5 changes: 5 additions & 0 deletions app/(tabs)/homies/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export default function HomiesLayout() {
<Stack.Screen name="conversations" />
<Stack.Screen name="[userId]" />
<Stack.Screen name="chat/[conversationId]" />
<Stack.Screen name="bot-chat/[botId]" />
<Stack.Screen
name="companion-stage/[botId]"
options={{ contentStyle: { backgroundColor: '#0b0e17' } }}
/>
</Stack>
);
}
Loading
Loading