Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React Native - Add Settings Stack #347

Merged
merged 30 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
62d1868
chore: Update user hooks and queries in React Native client
noriega2112 Aug 28, 2024
4220c07
feat: Add Container component for React Native client
noriega2112 Aug 28, 2024
2b732d1
feat: Add ContactEmailButton component for React Native client
noriega2112 Aug 28, 2024
41404f0
feat: Add settings screens to React Native client
noriega2112 Aug 28, 2024
166e0ed
feat: Add settings screens and related components for React Native cl…
noriega2112 Aug 28, 2024
c75605b
feat: Add Ionicons and BounceableWind to DashboardScreen
noriega2112 Aug 28, 2024
7c1e91e
feat: Refactor settings screen in React Native client
noriega2112 Aug 28, 2024
ac95488
Refactor buttonProps in Settings screen
noriega2112 Aug 28, 2024
8882e57
Refactor buttonProps in Settings screen
noriega2112 Aug 28, 2024
8d653d4
Refactor paddingBottom style in Settings screen
noriega2112 Aug 28, 2024
de537a9
feat: Add React Native settings screens and related components
noriega2112 Aug 29, 2024
d3d4142
feat: Add React Native settings screens and related components
noriega2112 Aug 29, 2024
0403034
Refactor text styling in Settings screen
noriega2112 Aug 29, 2024
a2a40e7
Refactor component import casing for consistency
noriega2112 Aug 29, 2024
4146161
feat: Update user API to support partial updates
noriega2112 Aug 29, 2024
09a5bc8
Refactor text styling in Settings screen
noriega2112 Aug 29, 2024
933b8e2
Refactor component import casing for consistency
noriega2112 Aug 29, 2024
eb9f51d
feat: Add fullName field to userShape
noriega2112 Aug 29, 2024
c0ca3dc
feat: Update Terms of Service and Privacy Policy links
noriega2112 Aug 29, 2024
49cc454
feat: Update StatusBar style in App.tsx
noriega2112 Aug 29, 2024
7a9b1ed
Merge remote-tracking branch 'origin/main' into feature/settings-stack
noriega2112 Aug 29, 2024
5a2d197
feat: Improve save button behavior in EditProfile screen
noriega2112 Aug 29, 2024
e1db08b
feat: Add DestroyModelMixin to UserViewSet
noriega2112 Aug 29, 2024
fd08597
feat: Update dashboard screen layout and add settings button
noriega2112 Aug 29, 2024
5b1a4f8
feat: Add warning alert for account deletion in EditProfile screen
noriega2112 Aug 29, 2024
89d2580
feat: Update logout behavior and navigation in EditProfile screen
noriega2112 Aug 29, 2024
8355dbe
feat: Add cancel option to delete account alert in EditProfile screen
noriega2112 Aug 29, 2024
65974df
feat: Remove unused patchUser service call in user API
noriega2112 Aug 30, 2024
411bfd6
feat: Add useAuth hook to EditProfile screen
noriega2112 Aug 30, 2024
4724cc8
feat: Update user hooks import in user service
noriega2112 Aug 30, 2024
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
4 changes: 3 additions & 1 deletion cookiecutter.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
"clients/web/react/src/pages/app-or-auth.tsx",
"clients/web/react/src/pages/index.ts",
"clients/web/react/src/pages/layout.tsx",
"*/swagger-ui.html"
"*/swagger-ui.html",
"clients/mobile/react-native/src/screens/settings/edit-profile.tsx",
"clients/mobile/react-native/src/screens/settings/main-settings.tsx"
],
"mail_service": [
"Mailgun",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export default Sentry.wrap((): JSX.Element => {
<GestureHandlerRootView style={styles.flex}>
<QueryClientProvider client={queryClient}>
<SheetProvider>
<StatusBar />
<StatusBar style="dark" />
<AppRoot />
</SheetProvider>
</QueryClientProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Linking, Text } from 'react-native'
import { Bounceable } from 'rn-bounceable'
import { useConstants } from '@utils/constants'

export const ContactEmailButton = () => {
const { supportEmail } = useConstants()
const handleSendMail = async () => {
const mailtoUrl = `mailto:${supportEmail}`
const canOpen = await Linking.canOpenURL(mailtoUrl)
if (canOpen) {
Linking.openURL(mailtoUrl)
} else {
console.error('could not open this url: ', mailtoUrl)
}
}
return (
<Bounceable onPress={handleSendMail}>
<Text className="text-lg text-center font-primary-bold">
{supportEmail}
</Text>
</Bounceable>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { FC, ReactNode } from 'react'
import { View } from 'react-native'
import { MultiPlatformSafeAreaView } from './multi-platform-safe-area-view'

export const Container: FC<{
children: ReactNode
containerClassName?: string
innerContainerClassName?: string
hasHorizontalPadding?: boolean
}> = ({ children, containerClassName, innerContainerClassName, hasHorizontalPadding = true }) => {
return (
<MultiPlatformSafeAreaView safeAreaClassName={`h-full flex-1 flex-grow ${containerClassName}`}>
<View
className={`flex-1 flex-grow ${
hasHorizontalPadding ? 'px-4' : ''
} ${innerContainerClassName}`}
>
{children}
</View>
</MultiPlatformSafeAreaView>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { useAtomValue } from 'jotai'
import React from 'react'
import { Text, View } from 'react-native'
import { SheetManager } from 'react-native-actions-sheet'
import Ionicons from '@expo/vector-icons/Ionicons'
import { BounceableWind } from '@components/styled'

export const DashboardScreen = () => {
const navio = useAtomValue(navioAtom)
Expand All @@ -22,8 +24,19 @@ export const DashboardScreen = () => {

return (
<MultiPlatformSafeAreaView safeAreaClassName="flex-1">
<View className="flex-grow items-center justify-center">
<Text className="text-xl font-primary-bold">Welcome to the Dashboard</Text>
<View className="flex-grow items-center">
<View className="flex-row justify-end items-center w-full px-10">
<BounceableWind
contentContainerClassName="flex-row items-center gap-2"
onPress={() => navio?.stacks.push('SettingsStack')}
>
<Text>Settings</Text>
<Ionicons name="settings-outline" size={32} color="black" />
</BounceableWind>
</View>
<View className="flex-1 justify-center">
<Text className="text-xl font-primary-bold">Welcome to the Dashboard</Text>
</View>
</View>
<View className="w-full p-3">
<View className="pb-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Main } from '@screens/main'
import { Auth } from '@screens/auth/auth'
import { DashboardScreen } from '@screens/dashboard'
import { ComponentsPreview } from '@screens/ComponentsPreview'
import { ContactUs, EditProfile, Settings } from '@screens/settings'

// Default options - forcing a mobile trigger
export const screenDefaultOptions = (): NativeStackNavigationOptions => ({
Expand All @@ -29,10 +30,11 @@ export const tabDefaultOptions = (): BottomTabNavigationOptions => ({
})
// NAVIO
export const navio = Navio.build({
screens: { Auth, Login, SignUp, Main, DashboardScreen, ComponentsPreview },
screens: { Auth, Login, SignUp, Main, DashboardScreen, ComponentsPreview, Settings, ContactUs, EditProfile },
stacks: {
AuthStack: ['Auth'],
MainStack: ['DashboardScreen'],
SettingsStack: ['Settings', 'ContactUs', 'EditProfile'],
/**
* Set me as the root to see the components preview
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Text, View } from 'react-native'
import { BButton } from '@components/Button'
import { Container } from '@components/container'
import { getNavio } from '..'
import { ContactEmailButton } from '@components/contact-email-button'
import { Ionicons } from '@expo/vector-icons'
import colors from '@utils/colors'
import { BounceableWind } from '@components/styled'

export const ContactUs = () => {
const navio = getNavio()

return (
<Container>
<View className="items-center">
<BounceableWind
onPress={() => {
navio.goBack()
}}
contentContainerClassName="absolute left-0 top-0"
>
<Ionicons size={26} name="chevron-back" color={colors.grey[280]} />
</BounceableWind>
<Text className="text-xl font-primary-medium">
Contact Us
</Text>
</View>
<View className="flex-grow items-center justify-center">
<View className="pt-10">
<Text className="text-grey-280 text-2xl text-center font-primary-bold">
Needing help?
</Text>
</View>
<View className="pt-3 justify-center item-center">
<Text className="text-grey-280 text-lg text-center">Reach out to us at</Text>
<ContactEmailButton />
</View>
</View>
<View>
<BButton
label="BACK TO SETTINGS"
variant="primary"
onPress={() => {
navio.goBack()
}}
/>
</View>
</Container>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { ActivityIndicator, Alert, ScrollView, TextInput, Text, View } from 'react-native'
import { Bounceable } from 'rn-bounceable'
import { Container } from '@components/container'
import { UserShape, fullNameZod, useLogout, useUser, userApi } from '@services/user'
import { userQueries } from '@services/user/queries'
import { getNavio } from '..'
import { ErrorMessage } from '@components/errors'
import { Ionicons } from '@expo/vector-icons'
import colors from '@utils/colors'
import { BButton } from '@components/Button'
import { isAxiosError } from 'axios'
import { useAuth } from '@stores/auth'

const Separator = () => {
return (
<View className="py-3">
<View className="w-full h-[0.5px] bg-grey-160 rounded-full" />
</View>
)
}

export const EditProfile = () => {
const navio = getNavio()
const onCancel = () => {
navio.goBack()
}
const { data: user } = useUser()
const { userId } = useAuth()
const [fullName, setFullName] = useState(user?.fullName ?? '')
const [errors, setErrors] = useState<string[] | undefined>()

const handleFullNameChange = (name: string) => {
setFullName(name)
}
const unsavedChanges = user && user.fullName !== fullName

const parsedName = fullNameZod.safeParse(fullName)
const isValid = parsedName.success
const qClient = useQueryClient()

const { mutate: save, isPending: isSaving } = useMutation({
mutationFn: userApi.update,
onMutate: async () => {
const userSnapshot = qClient.getQueryData<UserShape>(
userQueries.retrieve(userId).queryKey,
)?.fullName
await qClient.cancelQueries({ queryKey: userQueries.retrieve(userId).queryKey })
qClient.setQueryData(userQueries.retrieve(userId).queryKey, (input?: UserShape) => {
return input ? { ...input } : undefined
})
return { userSnapshot }
},
onSuccess: () => {
qClient.invalidateQueries({ queryKey: userQueries.all() })
},
onError: (e, _, context) => {
//rollback update
qClient.setQueryData(userQueries.retrieve(userId).queryKey, (input?: UserShape) => {
return input
? { ...input, fullName: context?.userSnapshot ?? user?.fullName ?? '' }
: undefined
})

if (isAxiosError(e)) {
const { data } = e?.response ?? {}
if (data) {
const isArrayOfStrings = Array.isArray(data) && data.length && typeof data[0] === 'string'
const isObjectOfErrors = Object.keys(data).every((key) => Array.isArray(data[key]))
setErrors(
(isArrayOfStrings
? data
: isObjectOfErrors
? Object.keys(data).map((key) => data[key])
: ['Something went wrong']) as string[],
)
}
}
},
})

const { mutate: logout, isPending: isLoggingOut } = useLogout()

const { mutate: deleteUser, isPending: isDeleting } = useMutation({
mutationFn: userApi.remove,
onSuccess: () => {
logout()
navio.stacks.setRoot('AuthStack')
},
onError: () => {
Alert.alert('Error', "Couldn't delete your account. Please try again later.", [
{
text: 'Ok',
},
])
},
})

const handleSave = () => {
if (!user) return
const [firstName, lastName] = fullName.split(' ')
save({ id: user.id, firstName, lastName })
}

const showWarningAlert = () => {
Alert.alert(
'WARNING',
'Deleting your account is permanent and cannot be undone. If you would like to use this app again, you will need to create a new account.',
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Delete',
onPress: () => {
if (!user) return
deleteUser(user?.id)
},
},
],
)
}

if (!user) return <></> //never

return (
<Container>
<View className="items-center justify-between flex-row">
<Bounceable onPress={onCancel} disabled={isDeleting || isLoggingOut || isSaving}>
<Ionicons size={26} name="chevron-back" color={colors.grey[280]} />
</Bounceable>
<Text className="text-xl">Edit Profile</Text>
<Bounceable
onPress={handleSave}
disabled={isDeleting || isLoggingOut || isSaving || (unsavedChanges && !isValid)}
>
{isSaving ? (
<ActivityIndicator />
) : (
<Text
className={
'text-xl ' + (unsavedChanges && isValid ? 'text-grey-280' : 'text-grey-180')
}
>
Save
</Text>
)}
</Bounceable>
</View>
<ScrollView>
<View className="pt-10">
<View>
<View className="justify-center flex-1">
<Text className="text-grey-280 text-lg font-primary-bold">Full Name</Text>
</View>
<Separator />
<View className="justify-center flex-3">
<View>
<TextInput
className="text-grey-280 text-lg pb-2.5"
value={fullName}
onChangeText={handleFullNameChange}
/>
</View>
</View>
</View>
{fullName && !isValid ? (
<View>
<ErrorMessage>
{parsedName.error.issues.map((i) => i.message).join(', ')}
</ErrorMessage>
</View>
) : null}
<Separator />
<View>
<View className="flex">
<Text className="text-grey-280 text-lg font-primary-bold">Email</Text>
</View>
<Separator />
<View className="flex-3">
<Text className="text-disabled-gray text-lg">{user.email}</Text>
</View>
<Separator />
{errors?.map((error, idx) => (
<View className="py-3">
<ErrorMessage key={idx}>{error}</ErrorMessage>
</View>
))}
</View>
</View>
</ScrollView>

<BButton
label="DELETE ACCOUNT"
variant="primary"
onPress={showWarningAlert}
containerClassName="mb-7"
isLoading={isDeleting || isLoggingOut || isSaving}
buttonProps={{
disabled: isDeleting || isLoggingOut || isSaving,
}}
/>
</Container>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { Settings } from './main-settings'
export { ContactUs } from './contact-us'
export { EditProfile } from './edit-profile'
Loading
Loading