Skip to content

Commit

Permalink
PR: Add full identity handling to the frontend #32 (#33)
Browse files Browse the repository at this point in the history
**Notes for reviewers:**
The diff has definitely too many files, since I moved one folder 😠
The files, that were actually meaningfully changed:
- `/common/identity/*.ts` - **entire new folder, main thing to review**
- `/tests/common/identity.test.tsx` - **tests for new functionality**
- `/app/auth/(login|register).tsx` - **communication with backend,
redirects, error handling**
- `/app/dashboard.tsx` - stub file for dashboard
- `/app/profiles/list.tsx` - stub file for profile selection, to be
merged with #34
- `/common/util/mock-router.tsx` - self-explainatory

All of the other changes are just renames, import fixes, etc.

**Notes for all contributors:**
Familiarize yourself with the functions exported from
`@/common/identity`, example usage is show on the `login` and `register`
pages.
  • Loading branch information
tchojnacki authored Oct 15, 2023
1 parent d46412e commit d3e6129
Show file tree
Hide file tree
Showing 42 changed files with 777 additions and 215 deletions.
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
android/** linguist-generated=true
util/api/schema.d.ts linguist-generated=true
common/api/schema.d.ts linguist-generated=true
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
*.md
expo-env.d.ts
util/api/schema.d.ts
common/api/schema.d.ts
11 changes: 5 additions & 6 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { SplashScreen, Stack } from "expo-router";
import { PaperProvider } from "react-native-paper";

import { AuthProvider } from "@/util/api/auth";
import { I18nProvider } from "@/util/i18n";
import { ProviderList } from "@/util/provider-list";
import { ThemeProvider } from "@/util/theme-provider";
import { useFontLoader } from "@/util/use-font-loader";
import { I18nProvider } from "@/common/i18n";
import { IdentityProvider } from "@/common/identity";
import { ProviderList, useFontLoader } from "@/common/util";
import { ThemeProvider } from "@/components/ThemeProvider";

export { ErrorBoundary } from "expo-router";

Expand All @@ -19,7 +18,7 @@ export default function RootLayout() {

return (
<ProviderList
providers={[I18nProvider, AuthProvider, PaperProvider, ThemeProvider]}
providers={[I18nProvider, IdentityProvider, PaperProvider, ThemeProvider]}
>
<Stack />
</ProviderList>
Expand Down
34 changes: 27 additions & 7 deletions app/auth/login.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { router, Link, Stack } from "expo-router";
import { Link, Stack } from "expo-router";
import { Formik } from "formik";
import { useState } from "react";
import {
Keyboard,
StyleSheet,
Expand All @@ -14,22 +15,35 @@ import {
useTheme,
} from "react-native-paper";

import { useI18n } from "@/util/i18n";
import { POST } from "@/common/api";
import { useI18n } from "@/common/i18n";
import { useIdentity, RedirectIfLoggedIn } from "@/common/identity";

const Page = () => {
const identity = useIdentity();
const theme = useTheme();
const { t } = useI18n();

const handleFormSubmit = (values: { email: string; password: string }) => {
// TODO: Handle form submission, send data to backend API
console.log(values);
router.replace("/profile/list");
const [status, setStatus] = useState<"idle" | "pending" | "error">("idle");

if (identity.isLoggedIn) {
return <RedirectIfLoggedIn identity={identity} />;
}

const onSubmit = async (body: { email: string; password: string }) => {
setStatus("pending");
const res = await POST("/api/v1/token", { body });
if (res.error) {
setStatus("error");
} else {
identity.logIn(res.data.token);
}
};

return (
<Formik
initialValues={{ email: "", password: "" }}
onSubmit={handleFormSubmit}
onSubmit={onSubmit}
validate={(values) => {
const errors: { email?: string; password?: string } = {};
if (!values.email) {
Expand Down Expand Up @@ -90,7 +104,13 @@ const Page = () => {
{errors.password}
</HelperText>
)}
{status === "error" ? (
<HelperText type="error" style={styles.errorMessage}>
{t("login.badCredentials")}
</HelperText>
) : null}
<Button
disabled={status === "pending"}
mode="contained"
onPress={() => handleSubmit()}
style={styles.submit}
Expand Down
42 changes: 34 additions & 8 deletions app/auth/register.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Link, Stack, router } from "expo-router";
import { Link, Stack } from "expo-router";
import { Formik } from "formik";
import { useState } from "react";
import {
Keyboard,
KeyboardAvoidingView,
Expand All @@ -16,7 +17,9 @@ import {
useTheme,
} from "react-native-paper";

import { useI18n } from "@/util/i18n";
import { POST } from "@/common/api";
import { useI18n } from "@/common/i18n";
import { useIdentity, RedirectIfLoggedIn } from "@/common/identity";

interface IRegisterForm {
email: string;
Expand All @@ -26,13 +29,30 @@ interface IRegisterForm {
}

const Page = () => {
const identity = useIdentity();
const theme = useTheme();
const { t } = useI18n();

const handleFormSubmit = (values: IRegisterForm) => {
// TODO: Handle form submission, send data to backend API
console.log({ submittingValues: values });
router.replace("/profile/list");
const [status, setStatus] = useState<"idle" | "pending" | "error">("idle");

if (identity.isLoggedIn) {
return <RedirectIfLoggedIn identity={identity} />;
}

const onSubmit = async (values: IRegisterForm) => {
const body = {
...values,
// Empty string is not a valid value for phoneNumber, but null is.
phoneNumber: values.phoneNumber ? values.phoneNumber : null,
};
setStatus("pending");
const accountRes = await POST("/api/v1/account", { body });
const tokenRes = await POST("/api/v1/token", { body });
if (accountRes.error || tokenRes.error) {
setStatus("error");
} else {
identity.logIn(tokenRes.data.token);
}
};

return (
Expand All @@ -45,7 +65,7 @@ const Page = () => {
phoneNumber: "",
} as IRegisterForm
}
onSubmit={handleFormSubmit}
onSubmit={onSubmit}
validate={(values) => {
const errors: {
email?: string;
Expand All @@ -68,7 +88,7 @@ const Page = () => {
} else if (values.confirmPassword !== values.password) {
errors.confirmPassword = t("register.passwordMismatch");
}
if (!/^[0-9]{9}$/.test(values.phoneNumber) && values.phoneNumber) {
if (values.phoneNumber && !/^[0-9]{9}$/.test(values.phoneNumber)) {
errors.phoneNumber = t("register.badPhoneNumber");
}
return errors;
Expand Down Expand Up @@ -160,7 +180,13 @@ const Page = () => {
{errors.phoneNumber}
</HelperText>
)}
{status === "error" ? (
<HelperText type="error" style={styles.errorMessage}>
{t("register.serverError")}
</HelperText>
) : null}
<Button
disabled={status === "pending"}
mode="contained"
onPress={() => handleSubmit()}
style={styles.submit}
Expand Down
23 changes: 23 additions & 0 deletions app/dashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Button } from "react-native-paper";

import { useIdentity, RedirectIfNoProfile } from "@/common/identity";
import { MonoText } from "@/components/StyledText";
import { View } from "@/components/Themed";

const Page = () => {
const identity = useIdentity();

if (!identity.hasProfile) {
return <RedirectIfNoProfile identity={identity} />;
}

return (
<View>
{/* TODO: This shouldn't be here */}
<MonoText>{JSON.stringify(identity.profile, null, 2)}</MonoText>
<Button onPress={() => identity.logOut()}>LOG OUT</Button>
</View>
);
};

export default Page;
15 changes: 9 additions & 6 deletions app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import { Link, Stack } from "expo-router";
import { StyleSheet, View } from "react-native";
import { ActivityIndicator, Button, Text } from "react-native-paper";

import { BASE_URL, useApi } from "@/common/api";
import { useI18n } from "@/common/i18n";
import { useIdentity, RedirectIfLoggedIn } from "@/common/identity";
import { MonoText } from "@/components/StyledText";
import { BASE_URL, useApi, useAuth } from "@/util/api";
import { useI18n } from "@/util/i18n";

export const Landing = ({ debug = false }: { debug?: boolean }) => {
const { data, isLoading, error, mutate } = useApi({ url: "/healthz" });
const { token } = useAuth();
const { data, isLoading, error, mutate } = useApi({
url: "/api/v1/healthz",
});
const { t } = useI18n();
const identity = useIdentity();

if (isLoading) {
return <ActivityIndicator size="large" />;
Expand Down Expand Up @@ -47,8 +50,8 @@ export const Landing = ({ debug = false }: { debug?: boolean }) => {
);
}

if (token) {
// TODO: redirect if logged in
if (identity.isLoggedIn) {
return <RedirectIfLoggedIn identity={identity} />;
}

return (
Expand Down
7 changes: 0 additions & 7 deletions app/profile/interfaces.ts

This file was deleted.

46 changes: 29 additions & 17 deletions app/profile/list.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,39 @@
import { Link, Stack } from "expo-router";
import { Link, Stack, router } from "expo-router";
import { ScrollView, StyleSheet, View } from "react-native";
import { Avatar, Button, List } from "react-native-paper";

import { IProfile } from "./interfaces";

import { useI18n } from "@/util/i18n";
import { useI18n } from "@/common/i18n";
import {
Profile,
isSenior,
isCaretaker,
useIdentity,
RedirectIfLoggedOut,
} from "@/common/identity";

const mockApiResponse = {
profiles: [
{ type: "caretaker", seniorId: "2137", seniorAlias: "Jan Kowalski" },
{ type: "caretaker", seniorId: "123", seniorAlias: "Grzegorz Floryda" },
{ type: "senior", seniorId: "789" },
] as IProfile[],
{ type: "caretaker", seniorId: 2137, seniorAlias: "Jan Kowalski" },
{ type: "caretaker", seniorId: 123, seniorAlias: "Grzegorz Floryda" },
{ type: "senior", seniorId: 789 },
] as Profile[],
};

const ProfilesList = () => {
const { t } = useI18n();
const identity = useIdentity();

if (!identity.isLoggedIn) {
return <RedirectIfLoggedOut identity={identity} />;
}

const profiles = mockApiResponse.profiles;
const seniorProfile = profiles.find(({ type }) => type === "senior");
const caretakerProfiles = profiles.filter(({ type }) => type === "caretaker");
const seniorProfile = profiles.find(isSenior);
const caretakerProfiles = profiles.filter(isCaretaker);

const handleItemPress = (seniorId: string) => {
console.log(seniorId); // TODO: navigate to profile
const handleItemPress = (profile: Profile) => {
identity.selectProfile(profile);
router.push("/dashboard");
};

return (
Expand All @@ -34,13 +46,13 @@ const ProfilesList = () => {
</List.Subheader>
)}
<ScrollView style={styles.scrollView}>
{caretakerProfiles.map(({ seniorId, seniorAlias }) => {
{caretakerProfiles.map((p) => {
return (
<List.Item
key={seniorId}
key={p.seniorId}
title={t("profileList.caretakerNameFallback")}
description={`Senior: ${seniorAlias || seniorId}`}
onPress={() => handleItemPress(seniorId)}
description={`Senior: ${p.seniorAlias}`}
onPress={() => handleItemPress(p)}
style={styles.listItem}
titleStyle={styles.listItemTitle}
descriptionStyle={styles.listItemDescription}
Expand All @@ -63,7 +75,7 @@ const ProfilesList = () => {
<List.Item
title={t("profileList.seniorNameFallback")}
description={t("profileList.seniorDescription")}
onPress={() => handleItemPress(seniorProfile.seniorId)}
onPress={() => handleItemPress(seniorProfile)}
style={styles.listItem}
titleStyle={styles.listItemTitle}
descriptionStyle={styles.listItemDescription}
Expand Down
10 changes: 9 additions & 1 deletion assets/i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@
"en": "Register",
"pl": "Zarejestruj się"
},
"login.badCredentials": {
"en": "The credentials are incorrect",
"pl": "Podane dane są nieprawidłowe"
},
"register.pageTitle": {
"en": "Register",
"pl": "Rejestracja"
Expand Down Expand Up @@ -83,7 +87,7 @@
"pl": "Zaloguj się"
},
"register.badPasswordLength": {
"en": "Password must have at least {length} letters",
"en": "Password must have at least {length} characters",
"pl": "Hasło musi mieć co najmniej {length} znaków"
},
"register.passwordMismatch": {
Expand All @@ -94,6 +98,10 @@
"en": "Invalid phone number",
"pl": "Błędny numer telefonu"
},
"register.serverError": {
"en": "Could not create the account",
"pl": "Nie udało się utworzyć konta"
},
"landing.pageTitle": {
"en": "Start",
"pl": "Start"
Expand Down
File renamed without changes.
File renamed without changes.
1 change: 0 additions & 1 deletion util/api/index.tsx → common/api/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export { useAuth, AuthProvider } from "./auth";
export { POST, PUT, DELETE } from "./client";
export { BASE_URL } from "./consts";
export { useApi } from "./swr";
File renamed without changes.
Loading

0 comments on commit d3e6129

Please sign in to comment.