Skip to content

Commit

Permalink
feat(mobile): beautify auth screen (#58)
Browse files Browse the repository at this point in the history
  • Loading branch information
bkdev98 committed Jun 9, 2024
1 parent fc20266 commit 4c55bf0
Show file tree
Hide file tree
Showing 21 changed files with 636 additions and 86 deletions.
12 changes: 7 additions & 5 deletions apps/mobile/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { useAuth } from '@clerk/clerk-expo'
import { Redirect, Stack } from 'expo-router'
import { Text } from 'react-native'
import { Redirect, SplashScreen, Stack } from 'expo-router'
import { useEffect } from 'react'

export default function AuthenticatedLayout() {
const { isLoaded, isSignedIn } = useAuth()

if (!isLoaded) {
return <Text>Loading...</Text>
}
useEffect(() => {
if (isLoaded) {
SplashScreen.hideAsync()
}
}, [isLoaded])

if (!isSignedIn) {
return <Redirect href={'/login'} />
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/app/(auth)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default function UnAuthenticatedLayout() {
}

return (
<SafeAreaView className="flex-1">
<SafeAreaView className="flex-1 bg-card">
<Stack screenOptions={{ headerShown: false }} />
</SafeAreaView>
)
Expand Down
44 changes: 41 additions & 3 deletions apps/mobile/app/(auth)/login.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,54 @@
import { Button } from '@/components/Button'
import { Separator } from '@/components/Separator'
import { AuthEmail } from '@/components/auth/auth-email'
import { ScrollView, Text } from 'react-native'
import {
AppleAuthButton,
GoogleAuthButton,
} from '@/components/auth/auth-social'
import { AuthIllustration } from '@/components/svg-assets/auth-illustration'
import { Link } from 'expo-router'
import { MailIcon } from 'lucide-react-native'
import { useState } from 'react'
import { ScrollView, Text, View } from 'react-native'

export default function LoginScreen() {
const [withEmail, setWithEmail] = useState(false)
return (
<ScrollView className="bg-card p-6" contentContainerClassName="gap-4">
<ScrollView
className="bg-card"
contentContainerClassName="gap-4 p-8"
automaticallyAdjustKeyboardInsets
keyboardShouldPersistTaps="handled"
>
<Text className="text-3xl font-semibold font-sans">
Manage your expense seamlessly
</Text>
<Text className="text-muted-foreground font-sans">
Let <Text className="text-primary">6pm</Text> a good time to spend
</Text>
<AuthEmail />
<AuthIllustration className="h-[326px] my-16" />
<View className="flex flex-col gap-3">
<AppleAuthButton />
<GoogleAuthButton />
<Button
label="Continue with Email"
leftIcon={MailIcon}
variant="outline"
onPress={() => setWithEmail(true)}
/>
<Separator className="w-[70%] mx-auto my-3" />
{withEmail && <AuthEmail />}
</View>
<Text className="font-sans text-muted-foreground text-xs text-center mx-auto px-4 mt-4">
By continuing, you acknowledge that you understand and agree to the{' '}
<Link href="/terms-of-service" asChild className="text-primary">
<Text>Terms & Conditions</Text>
</Link>{' '}
and{' '}
<Link href="/privacy-policy" asChild className="text-primary">
<Text>Privacy Policy</Text>
</Link>
</Text>
</ScrollView>
)
}
5 changes: 5 additions & 0 deletions apps/mobile/app/(aux)/privacy-policy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Text } from 'react-native'

export default function PrivacyScreen() {
return <Text className="font-sans m-4 mx-auto">Privacy Policy</Text>
}
7 changes: 7 additions & 0 deletions apps/mobile/app/(aux)/terms-of-service.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Text } from 'react-native'

export default function TermsScreen() {
return (
<Text className="font-sans m-4 mx-auto">Terms & Conditions</Text>
)
}
33 changes: 24 additions & 9 deletions apps/mobile/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ import {
BeVietnamPro_700Bold,
useFonts,
} from '@expo-google-fonts/be-vietnam-pro'
import { Slot } from 'expo-router'
import { Stack } from 'expo-router'
import * as SplashScreen from 'expo-splash-screen'
import { useEffect } from 'react'

import 'react-native-reanimated'
import { useColorScheme } from '@/hooks/useColorScheme'
Expand All @@ -23,7 +22,16 @@ import {
ThemeProvider,
} from '@react-navigation/native'
import { QueryClientProvider } from '@tanstack/react-query'
import { cssInterop } from 'nativewind'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { Svg } from 'react-native-svg'

cssInterop(Svg, {
className: {
target: 'style',
nativeStyleToProp: { width: true, height: true },
},
})

// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync()
Expand All @@ -45,12 +53,6 @@ export default function RootLayout() {
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
})

useEffect(() => {
if (fontsLoaded) {
SplashScreen.hideAsync()
}
}, [fontsLoaded])

if (!fontsLoaded) {
return null
}
Expand All @@ -65,7 +67,20 @@ export default function RootLayout() {
value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}
>
<SafeAreaProvider>
<Slot />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen
name="(aux)/privacy-policy"
options={{
presentation: 'modal',
}}
/>
<Stack.Screen
name="(aux)/terms-of-service"
options={{
presentation: 'modal',
}}
/>
</Stack>
</SafeAreaProvider>
</ThemeProvider>
</ClerkProvider>
Expand Down
41 changes: 35 additions & 6 deletions apps/mobile/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import { type VariantProps, cva } from 'class-variance-authority'
import { Text, TouchableOpacity } from 'react-native'

import type { SvgProps } from 'react-native-svg'
import { cn } from '../lib/utils'

const buttonVariants = cva(
'flex flex-row items-center justify-center rounded-md',
'flex flex-row items-center gap-2 justify-center rounded-md',
{
variants: {
variant: {
default: 'bg-primary',
secondary: 'bg-secondary',
outline: 'border-border border',
destructive: 'bg-destructive',
ghost: 'bg-slate-700',
link: 'text-primary underline-offset-4',
},
size: {
default: 'h-10 px-4',
default: 'h-12 px-4',
sm: 'h-8 px-2',
lg: 'h-12 px-8',
lg: 'h-14 px-8',
},
},
defaultVariants: {
Expand All @@ -32,6 +34,7 @@ const buttonTextVariants = cva('text-center font-medium font-sans', {
variant: {
default: 'text-primary-foreground',
secondary: 'text-secondary-foreground',
outline: 'text-primary',
destructive: 'text-destructive-foreground',
ghost: 'text-primary-foreground',
link: 'text-primary-foreground underline',
Expand All @@ -48,32 +51,58 @@ const buttonTextVariants = cva('text-center font-medium font-sans', {
},
})

interface ButtonProps
export interface ButtonProps
extends React.ComponentPropsWithoutRef<typeof TouchableOpacity>,
VariantProps<typeof buttonVariants> {
VariantProps<typeof buttonVariants> {
label: string
labelClasses?: string
leftIcon?: React.ComponentType<SvgProps>
rightIcon?: React.ComponentType<SvgProps>
}
function Button({
label,
labelClasses,
className,
variant,
size,
leftIcon: LeftIcon,
rightIcon: RightIcon,
disabled,
...props
}: ButtonProps) {
return (
<TouchableOpacity
className={cn(buttonVariants({ variant, size, className }))}
activeOpacity={0.8}
className={cn(
buttonVariants({ variant, size, className }),
{ 'opacity-50': disabled },
)}
disabled={disabled}
{...props}
>
{LeftIcon && (
<LeftIcon
className={cn(
'w-5 h-5',
buttonTextVariants({ variant, size, className: labelClasses }),
)}
/>
)}
<Text
className={cn(
buttonTextVariants({ variant, size, className: labelClasses }),
)}
>
{label}
</Text>
{RightIcon && (
<RightIcon
className={cn(
'w-5 h-5',
buttonTextVariants({ variant, size, className: labelClasses }),
)}
/>
)}
</TouchableOpacity>
)
}
Expand Down
85 changes: 85 additions & 0 deletions apps/mobile/components/IconButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { type VariantProps, cva } from 'class-variance-authority'
import { TouchableOpacity } from 'react-native'

import type { SvgProps } from 'react-native-svg'
import { cn } from '../lib/utils'

const buttonVariants = cva(
'flex flex-row items-center gap-2 justify-center rounded-md',
{
variants: {
variant: {
default: 'bg-primary',
secondary: 'bg-secondary',
outline: 'border-border border',
destructive: 'bg-destructive',
ghost: 'bg-transparent',
link: 'text-primary underline-offset-4',
},
size: {
default: 'h-12 w-12',
sm: 'h-8 w-8',
lg: 'h-14 w-14',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)

const iconVariants = cva('text-center font-medium font-sans', {
variants: {
variant: {
default: 'text-primary-foreground',
secondary: 'text-secondary-foreground',
outline: 'text-primary',
destructive: 'text-destructive-foreground',
ghost: 'text-primary',
link: 'text-primary-foreground underline',
},
size: {
default: 'text-base',
sm: 'w-5 h-5',
lg: 'text-xl',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
})

export interface IconButtonProps
extends React.ComponentPropsWithoutRef<typeof TouchableOpacity>,
VariantProps<typeof buttonVariants> {
icon: React.ComponentType<SvgProps>
iconClasses?: string
}
function IconButton({
icon: Icon,
iconClasses,
className,
variant,
size,
disabled,
...props
}: IconButtonProps) {
return (
<TouchableOpacity
activeOpacity={0.8}
className={cn(buttonVariants({ variant, size, className }), {
'opacity-50': disabled,
})}
disabled={disabled}
{...props}
>
<Icon
className={cn(iconVariants({ variant, size, className: iconClasses }))}
/>
</TouchableOpacity>
)
}

export { IconButton, buttonVariants, iconVariants }
23 changes: 16 additions & 7 deletions apps/mobile/components/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,28 @@ export interface InputProps
label?: string
labelClasses?: string
inputClasses?: string
leftSection?: React.ReactNode
rightSection?: React.ReactNode
}
const Input = forwardRef<React.ElementRef<typeof TextInput>, InputProps>(
// biome-ignore lint/correctness/noUnusedVariables: <explanation>
({ className, label, labelClasses, inputClasses, ...props }, ref) => (
({ className, label, labelClasses, inputClasses, leftSection, rightSection, ...props }, ref) => (
<View className={cn('flex flex-col gap-1.5', className)}>
{label && <Text className={cn('text-base', labelClasses)}>{label}</Text>}
<TextInput
className={cn(
inputClasses,
'border border-input py-2.5 px-4 rounded-lg font-sans',
<View>
<TextInput
className={cn(
inputClasses,
'border border-border placeholder-input py-2.5 px-4 rounded-lg font-sans',
)}
{...props}
/>
{rightSection && (
<View className="absolute right-2 top-1/2 transform -translate-y-1/2">
{rightSection}
</View>
)}
{...props}
/>
</View>
</View>
),
)
Expand Down
Loading

0 comments on commit 4c55bf0

Please sign in to comment.