Skip to content

Commit

Permalink
Merge branch 'main' into fix/remove-mobile-files
Browse files Browse the repository at this point in the history
  • Loading branch information
paribaker authored Aug 30, 2024
2 parents 2371fc6 + 99eb831 commit f99f599
Show file tree
Hide file tree
Showing 18 changed files with 581 additions and 23 deletions.
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
4 changes: 2 additions & 2 deletions {{cookiecutter.project_slug}}/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -151,5 +151,5 @@ DEFAULT_FROM_EMAIL='{{ cookiecutter.project_name }} <noreply@{{ cookiecutter.pro

# Testing (NOTE: Heroku and Github Actions will need to have matching values for some of these)
DJANGO_SUPERUSER_PASSWORD='!!!DJANGO_SECRET_KEY!!!'
CYPRESS_TEST_USER_PASS='!!!DJANGO_SECRET_KEY!!!'
CYPRESS_baseUrl='http://localhost:8080'
PLAYWRIGHT_TEST_USER_PASS='!!!DJANGO_SECRET_KEY!!!'
PLAYWRIGHT_TEST_BASE_URL='http://localhost:8080'
8 changes: 7 additions & 1 deletion {{cookiecutter.project_slug}}/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,11 @@ See the [frontend README](client/README.md)
1. `pipenv run pytest server`
1. `pipenv run black server`
1. `pipenv run isort server --diff` (shows you what isort is expecting)

## Frontend E2E Testing with Playwright

1. `cd client`
1. `npx playwright install` - Installs browser driver
1. `npx playwright install-deps` - Install system-level dependencies
1. `npx playwright test`
1. `npx playwright codegen localhost:8080` (generate your tests through manual testing)
1. `npx playwright codegen localhost:8080` - Generate your tests through manual testing
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>
)
}
Loading

0 comments on commit f99f599

Please sign in to comment.