# Mobile Application Development Tutorial - React Native
## About This Tutorial
- This tutorial assumes that you have completed the [Web Application Development tutorial](../../book-store/part-01.md) and built an ABP based application named `Acme.BookStore` with [React Native](../../../framework/ui/react-native) as the mobile option.. Therefore, if you haven't completed the [Web Application Development tutorial](../../book-store/part-01.md), you either need to complete it or download the source code from down below and follow this tutorial.
- In this tutorial, we will only focus on the UI side of the `Acme.BookStore` application and will implement the CRUD operations.
- Before starting, please make sure that the [React Native Development Environment](https://abp.io/docs/latest/framework/ui/react-native?Tiered=No) is ready on your machine.
## Download the Source Code
You can use the following link to download the source code of the application described in this article:
- [Acme.BookStore](https://abp.io/Account/Login?returnUrl=/api/download/samples/bookstore-react-native-mongodb)
> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../../../kb/windows-path-too-long-fix.md).
## The Book List Page
In react native there is no dynamic proxy generation, that's why we need to create the BookAPI proxy manually under the `./src/api` folder.
```js
import api from "./API";
export const getList = () => api.get("/api/app/book").then(({ data }) => data);
export const get = (id) =>
api.get(`/api/app/book/${id}`).then(({ data }) => data);
export const create = (input) =>
api.post("/api/app/book", input).then(({ data }) => data);
export const update = (input, id) =>
api.put(`/api/app/book/${id}`, input).then(({ data }) => data);
export const remove = (id) =>
api.delete(`/api/app/book/${id}`).then(({ data }) => data);
```
### Add the `Book Store` menu item to the navigation
For the create menu item, navigate to `./src/navigators/DrawerNavigator.js` file and add `BookStoreStack` to `Drawer.Navigator` component.
```js
//Other imports..
import BookStoreStackNavigator from "./BookStoreNavigator";
const Drawer = createDrawerNavigator();
export default function DrawerNavigator() {
return (
{/*Added Screen*/}
null }}}%}
/>
{/*Added Screen*/}
);
}
```
Create the `BookStoreStackNavigator` in `./src/navigators/BookStoreNavigator.js`, this navigator will be used for the BookStore menu item.
```js
import React from "react";
import { SafeAreaView } from "react-native-safe-area-context";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import i18n from "i18n-js";
import HamburgerIcon from "../components/HamburgerIcon/HamburgerIcon";
import BookStoreScreen from "../screens/Books/BookStoreScreen";
const Stack = createNativeStackNavigator();
export default function BookStoreStackNavigator() {
return (
({
title: i18n.t("BookStore::Menu:BookStore"),
headerLeft: () => ,
})}
/>
);
}
```
- BookStoreScreen will be used to store the `books` and `authors` page
Add the `BookStoreStack` to the screens object in the `./src/components/DrawerContent/DrawerContent.js` file. The DrawerContent component will be used to render the menu items.
```js
// Imports..
const screens = {
HomeStack: { label: "::Menu:Home", iconName: "home" },
DashboardStack: {
label: "::Menu:Dashboard",
requiredPolicy: "BookStore.Dashboard",
iconName: "chart-areaspline",
},
UsersStack: {
label: "AbpIdentity::Users",
iconName: "account-supervisor",
requiredPolicy: "AbpIdentity.Users",
},
//Add this property
BookStoreStack: {
label: "BookStore::Menu:BookStore",
iconName: "book",
},
//Add this property
TenantsStack: {
label: "Saas::Tenants",
iconName: "book-outline",
requiredPolicy: "Saas.Tenants",
},
SettingsStack: {
label: "AbpSettingManagement::Settings",
iconName: "cog",
navigation: null,
},
};
// Other codes..
```

### Create Book List page
Before creating the book list page, we need to create the `BookStoreScreen.js` file under the `./src/screens/BookStore` folder. This file will be used to store the `books` and `authors` page.
```js
import React from "react";
import i18n from "i18n-js";
import { BottomNavigation } from "react-native-paper";
import BooksScreen from "./Books/BooksScreen";
const BooksRoute = () => ;
function BookStoreScreen({ navigation }) {
const [index, setIndex] = React.useState(0);
const [routes] = React.useState([
{
key: "books",
title: i18n.t("BookStore::Menu:Books"),
focusedIcon: "book",
unfocusedIcon: "book-outline",
},
]);
const renderScene = BottomNavigation.SceneMap({
books: BooksRoute,
});
return (
);
}
export default BookStoreScreen;
```
Create the `BooksScreen.js` file under the `./src/screens/BookStore/Books` folder.
```js
import React from "react";
import { useSelector } from "react-redux";
import { View } from "react-native";
import { useTheme, List } from "react-native-paper";
import { getBooks } from "../../api/BookAPI";
import i18n from "i18n-js";
import DataList from "../../components/DataList/DataList";
import { createAppConfigSelector } from "../../store/selectors/AppSelectors";
function BooksScreen({ navigation }) {
const theme = useTheme();
const currentUser = useSelector(createAppConfigSelector())?.currentUser;
return (
{currentUser?.isAuthenticated && (
);
}
export default BooksScreen;
```
- `getBooks` function is used to fetch the books from the server.
- `i18n` API to localize the given key. It uses the incoming resource from the `application-localization` endpoint.
- `DataList` component takes the `fetchFn` property that we'll give to the API request function, it's used to fetch data and maintain the logic of lazy loading etc.

## Creating a New Book
### Add the `@react-native-community/datetimepicker` package for the date functionality.
```bash
yarn expo install @react-native-community/datetimepicker
//or
npx expo install @react-native-community/datetimepicker
```
### Add the `CreateUpdateBook` Screen to the BookStoreNavigator
Like the `BookStoreScreen` we need to add the `CreateUpdateBookScreen` to the `./src/navigators/BookStoreNavigator.js` file.
```js
//Other codes
import { Button } from "react-native-paper"; //Added this line
import CreateUpdateBookScreen from "../screens/BookStore/Books/CreateUpdateBook/CreateUpdateBookScreen"; //Added this line
//Other codes
export default function BookStoreStackNavigator() {
return (
{/*Other screens*/}
{/* Added this screen */}
({
title: i18n.t(
route.params?.bookId ? "BookStore::Edit" : "BookStore::NewBook"
),
headerRight: () => (
),
})}
/>
);
}
```
To navigate to the `CreateUpdateBookScreen`, we need to add the `CreateUpdateBook` button to the `BooksScreen.js` file.
```js
//Other imports..
import {
// rest imports..,
StyleSheet,
} from "react-native";
import {
// rest imports..,
AnimatedFAB,
} from "react-native-paper";
function BooksScreen({ navigation }) {
//Other codes..
return (
{/* Other codes..*/}
{/* Included Code */}
{currentUser?.isAuthenticated && (
navigation.navigate("CreateUpdateBook")}
visible={true}
animateFrom={"right"}
iconMode={"static"}
style={[styles.fabStyle, { backgroundColor: theme.colors.primary }]}
/>
)}
{/* Included Code */}
);
}
//Added lines
const styles = StyleSheet.create({
container: {
flexGrow: 1,
},
fabStyle: {
bottom: 16,
right: 16,
position: "absolute",
},
});
//Added lines
export default BooksScreen;
```
After adding the `CreateUpdateBook` button, we need to add the `CreateUpdateBookScreen.js` file under the `./src/screens/BookStore/Books/CreateUpdateBook` folder.
```js
import PropTypes from "prop-types";
import React from "react";
import { create } from "../../../../api/BookAPI";
import LoadingActions from "../../../../store/actions/LoadingActions";
import { createLoadingSelector } from "../../../../store/selectors/LoadingSelectors";
import { connectToRedux } from "../../../../utils/ReduxConnect";
import CreateUpdateBookForm from "./CreateUpdateBookForm";
function CreateUpdateBookScreen({ navigation, startLoading, clearLoading }) {
const submit = (data) => {
startLoading({ key: "save" });
create(data)
.then(() => navigation.goBack())
.finally(() => clearLoading());
};
return ;
}
CreateUpdateBookScreen.propTypes = {
startLoading: PropTypes.func.isRequired,
clearLoading: PropTypes.func.isRequired,
};
export default connectToRedux({
component: CreateUpdateBookScreen,
stateProps: (state) => ({ loading: createLoadingSelector()(state) }),
dispatchProps: {
startLoading: LoadingActions.start,
clearLoading: LoadingActions.clear,
},
});
```
- In this page we'll store logic, send post/put requests, get the selected book data and etc.
- This page will wrap the `CreateUpdateBookFrom` component and pass the submit function with other properties.
Create a `CreateUpdateBookForm.js` file under the `./src/screens/BookStore/Books/CreateUpdateBook` folder and add the following code to it.
```js
import React, { useRef, useState } from "react";
import {
Platform,
KeyboardAvoidingView,
StyleSheet,
View,
ScrollView,
} from "react-native";
import { useFormik } from "formik";
import i18n from "i18n-js";
import PropTypes from "prop-types";
import * as Yup from "yup";
import { useTheme, TextInput } from "react-native-paper";
import DateTimePicker from "@react-native-community/datetimepicker";
import { FormButtons } from "../../../../components/FormButtons";
import ValidationMessage from "../../../../components/ValidationMessage/ValidationMessage";
import AbpSelect from "../../../../components/Select/Select";
const validations = {
name: Yup.string().required("AbpValidation::ThisFieldIsRequired."),
price: Yup.number().required("AbpValidation::ThisFieldIsRequired."),
type: Yup.string().nullable().required("AbpValidation::ThisFieldIsRequired."),
publishDate: Yup.string()
.nullable()
.required("AbpValidation::ThisFieldIsRequired."),
};
const props = {
underlineStyle: { backgroundColor: "transparent" },
underlineColor: "#333333bf",
};
function CreateUpdateBookForm({ submit }) {
const theme = useTheme();
const [bookTypeVisible, setBookTypeVisible] = useState(false);
const [publishDateVisible, setPublishDateVisible] = useState(false);
const nameRef = useRef();
const priceRef = useRef();
const typeRef = useRef();
const publishDateRef = useRef();
const inputStyle = {
...styles.input,
backgroundColor: theme.colors.primaryContainer,
};
const bookTypes = new Array(8).fill(0).map((_, i) => ({
id: i + 1,
displayName: i18n.t(`BookStore::Enum:BookType.${i + 1}`),
}));
const onSubmit = (values) => {
if (!bookForm.isValid) {
return;
}
submit({ ...values });
};
const bookForm = useFormik({
enableReinitialize: true,
validateOnBlur: true,
validationSchema: Yup.object().shape({
...validations,
}),
initialValues: {
name: "",
price: "",
type: "",
publishDate: null,
},
onSubmit,
});
const isInvalidControl = (controlName = null) => {
if (!controlName) {
return;
}
return (
((!!bookForm.touched[controlName] && bookForm.submitCount > 0) ||
bookForm.submitCount > 0) &&
!!bookForm.errors[controlName]
);
};
const onChange = (event, selectedDate) => {
if (!selectedDate) {
return;
}
setPublishDateVisible(false);
if (event && event.type !== "dismissed") {
bookForm.setFieldValue("publishDate", selectedDate, true);
}
};
return (
setBookTypeVisible(false)}
selectedItem={bookForm.values.type}
setSelectedItem={(id) => {
bookForm.setFieldValue("type", id, true);
bookForm.setFieldValue(
"typeDisplayName",
bookTypes.find((f) => f.id === id)?.displayName || null,
false
);
}}
/>
{publishDateVisible && (
)}
priceRef.current.focus()}
returnKeyType="next"
onChangeText={bookForm.handleChange("name")}
onBlur={bookForm.handleBlur("name")}
value={bookForm.values.name}
autoCapitalize="none"
label={i18n.t("BookStore::Name")}
style={inputStyle}
{...props}
/>
{isInvalidControl("name") && (
{bookForm.errors.name}
)}
typeRef.current.focus()}
returnKeyType="next"
onChangeText={bookForm.handleChange("price")}
onBlur={bookForm.handleBlur("price")}
value={bookForm.values.price}
autoCapitalize="none"
label={i18n.t("BookStore::Price")}
style={inputStyle}
{...props}
/>
{isInvalidControl("price") && (
{bookForm.errors.price}
)}
setBookTypeVisible(true)}
icon="menu-down"
/>
}
style={inputStyle}
editable={false}
value={bookForm.values.typeDisplayName}
{...props}
/>
{isInvalidControl("type") && (
{bookForm.errors.type}
)}
setPublishDateVisible(true)}
icon="menu-down"
/>
}
style={inputStyle}
editable={false}
value={bookForm.values.publishDate?.toLocaleDateString()}
{...props}
/>
{isInvalidControl("publishDate") && (
{bookForm.errors.publishDate}
)}
);
}
const styles = StyleSheet.create({
input: {
container: {
margin: 8,
marginLeft: 16,
marginRight: 16,
},
borderRadius: 8,
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
},
button: {
marginLeft: 16,
marginRight: 16,
},
});
CreateUpdateBookForm.propTypes = {
submit: PropTypes.func.isRequired,
};
export default CreateUpdateBookForm;
```
- `formik` will manage the form state, validation and value changes.
- `Yup` allows for the build validation schema.
- `AbpSelect` component is used to select the book type.
- `submit` method will pass the form values to the `CreateUpdateBookScreen` component.


## Update a Book
We need the navigation parameter for the get bookId and then navigate it again after the Create & Update operation. That's why we'll pass the navigation parameter to the `BooksScreen` component.
```js
//Imports..
//Add navigation parameter
const BooksRoute = (nav) => ;
function BookStoreScreen({ navigation }) {
//Other codes..
const renderScene = BottomNavigation.SceneMap({
books: () => BooksRoute(navigation), //Use this way
});
//Other codes..
}
export default BookStoreScreen;
```
Replace the code below in the `BookScreen.js` file under the `./src/screens/BookStore/Books` folder.
```js
import React from "react";
import { useSelector } from "react-redux";
import { Alert, View, StyleSheet } from "react-native";
import { useTheme, List, IconButton, AnimatedFAB } from "react-native-paper";
import { useActionSheet } from "@expo/react-native-action-sheet";
import i18n from "i18n-js";
import { getList } from "../../../api/BookAPI";
import DataList from "../../../components/DataList/DataList";
import { createAppConfigSelector } from "../../../store/selectors/AppSelectors";
function BooksScreen({ navigation }) {
const theme = useTheme();
const currentUser = useSelector(createAppConfigSelector())?.currentUser;
const { showActionSheetWithOptions } = useActionSheet();
const openContextMenu = (item) => {
const options = [];
options.push(i18n.t("AbpUi::Edit"));
options.push(i18n.t("AbpUi::Cancel"));
showActionSheetWithOptions(
{
options,
cancelButtonIndex: options.length - 1,
},
(index) => {
switch (options[index]) {
case i18n.t("AbpUi::Edit"):
edit(item);
break;
}
}
);
};
const edit = (item) => {
navigation.navigate("CreateUpdateBook", { bookId: item.id });
};
return (
{currentUser?.isAuthenticated && (
);
}
const styles = StyleSheet.create({
container: {
flexGrow: 1,
},
fabStyle: {
bottom: 16,
right: 16,
position: "absolute",
},
});
export default BooksScreen;
```
Replace code below for `CreateUpdateBookScreen.js` file under the `./src/screens/BookStore/Books/CreateUpdateBook/`
```js
import PropTypes from "prop-types";
import React, { useEffect, useState } from "react";
import { get, create, update } from "../../../../api/BookAPI";
import LoadingActions from "../../../../store/actions/LoadingActions";
import { createLoadingSelector } from "../../../../store/selectors/LoadingSelectors";
import { connectToRedux } from "../../../../utils/ReduxConnect";
import CreateUpdateBookForm from "./CreateUpdateBookForm";
function CreateUpdateBookScreen({
navigation,
route,
startLoading,
clearLoading,
}) {
const { bookId } = route.params || {};
const [book, setBook] = useState(null);
const submit = (data) => {
startLoading({ key: "save" });
(data.id ? update(data, data.id) : create(data))
.then(() => navigation.goBack())
.finally(() => clearLoading());
};
useEffect(() => {
if (bookId) {
startLoading({ key: "fetchBookDetail" });
get(bookId)
.then((response) => setBook(response))
.finally(() => clearLoading());
}
}, [bookId]);
return ;
}
CreateUpdateBookScreen.propTypes = {
startLoading: PropTypes.func.isRequired,
clearLoading: PropTypes.func.isRequired,
};
export default connectToRedux({
component: CreateUpdateBookScreen,
stateProps: (state) => ({ loading: createLoadingSelector()(state) }),
dispatchProps: {
startLoading: LoadingActions.start,
clearLoading: LoadingActions.clear,
},
});
```
- `get` method is used to fetch the book details from the server.
- `update` method is used to update the book on the server.
- `route` parameter will be used to get the bookId from the navigation.
Replace the `CreateUpdateBookForm.js` file with the code below. We'll use this file for the create and update operations.
```js
//Imports..
//validateSchema
//props
function CreateUpdateBookForm({
submit,
book = null, //Add book parameter with default value
}) {
//Other codes..
const bookForm = useFormik({
enableReinitialize: true,
validateOnBlur: true,
validationSchema: Yup.object().shape({
...validations,
}),
initialValues: {
//Update initialValues
...book,
name: book?.name || "",
price: book?.price.toString() || "",
type: book?.type || "",
typeDisplayName:
book?.type && i18n.t("BookStore::Enum:BookType." + book.type),
publishDate: (book?.publishDate && new Date(book?.publishDate)) || null,
//Update initialValues
},
onSubmit,
});
//Others codes..
}
//Other codes..
```
- `book` is a nullable property. It'll store the selected book, if the book parameter is null then we'll create a new book.


## Delete a Book
Replace the code below in the `BooksScreen.js` file under the `./src/screens/BookStore/Books` folder.
```js
import React, { useState } from "react";
import { useSelector } from "react-redux";
import { Alert, View, StyleSheet } from "react-native";
import { useTheme, List, IconButton, AnimatedFAB } from "react-native-paper";
import { useActionSheet } from "@expo/react-native-action-sheet";
import i18n from "i18n-js";
import { getList, remove } from "../../../api/BookAPI";
import DataList from "../../../components/DataList/DataList";
import { createAppConfigSelector } from "../../../store/selectors/AppSelectors";
function BooksScreen({ navigation }) {
const theme = useTheme();
const currentUser = useSelector(createAppConfigSelector())?.currentUser;
const [refresh, setRefresh] = useState(null);
const { showActionSheetWithOptions } = useActionSheet();
const openContextMenu = (item) => {
const options = [];
options.push(i18n.t("AbpUi::Delete"));
options.push(i18n.t("AbpUi::Edit"));
options.push(i18n.t("AbpUi::Cancel"));
showActionSheetWithOptions(
{
options,
cancelButtonIndex: options.length - 1,
destructiveButtonIndex: options.indexOf(i18n.t("AbpUi::Delete")),
},
(index) => {
switch (options[index]) {
case i18n.t("AbpUi::Edit"):
edit(item);
break;
case i18n.t("AbpUi::Delete"):
removeOnClick(item);
break;
}
}
);
};
const removeOnClick = (item) => {
Alert.alert("Warning", i18n.t("BookStore::AreYouSureToDelete"), [
{
text: i18n.t("AbpUi::Cancel"),
style: "cancel",
},
{
style: "default",
text: i18n.t("AbpUi::Ok"),
onPress: () => {
remove(item.id).then(() => {
setRefresh((refresh ?? 0) + 1);
});
},
},
]);
};
const edit = (item) => {
navigation.navigate("CreateUpdateBook", { bookId: item.id });
};
return (
{currentUser?.isAuthenticated && (
);
}
const styles = StyleSheet.create({
container: {
flexGrow: 1,
},
fabStyle: {
bottom: 16,
right: 16,
position: "absolute",
},
});
export default BooksScreen;
```
- `Delete` option is added to context menu list
- `removeOnClick` method will handle the delete process. It'll show an alert before the delete operation.


## Authorization
### Hide Books item in tab
Add `grantedPolicies` to the policies variable from the `appConfig` store
```js
//Other imports..
import { useSelector } from "react-redux";
function BookStoreScreen({ navigation }) {
const [index, setIndex] = React.useState(0);
const [routes, setRoutes] = React.useState([]);
const currentUser = useSelector((state) => state.app.appConfig.currentUser);
const policies = useSelector(
(state) => state.app.appConfig.auth.grantedPolicies
);
const renderScene = BottomNavigation.SceneMap({
books: () => BooksRoute(navigation),
});
React.useEffect(() => {
if (!currentUser?.isAuthenticated || !policies) {
setRoutes([]);
return;
}
let _routes = [];
if (!!policies["BookStore.Books"]) {
_routes.push({
key: "books",
title: i18n.t("BookStore::Menu:Books"),
focusedIcon: "book",
unfocusedIcon: "book-outline",
});
}
setRoutes([..._routes]);
}, [Object.keys(policies)?.filter((f) => f.startsWith("BookStore")).length]);
return (
routes?.length > 0 && (
)
);
}
export default BookStoreScreen;
```
- In the `useEffect` function we'll check the `currentUser` and `policies` variables.
- useEffect's conditions will be the policies of the `BookStore` permission group.
- `Books` tab will be shown if the user has the `BookStore.Books` permission

### Hide the New Book Button
`New Book` button is placed in the BooksScreen as a `+` icon button. For the toggle visibility of the button, we need to add the `policies` variable to the `BooksScreen` component like the `BookStoreScreen` component. Open the `BooksScreen.js` file in the `./src/screens/BookStore/Books` folder and include the code below.
```js
//Imports..
function BooksScreen({ navigation }) {
const policies = useSelector(createAppConfigSelector())?.auth?.grantedPolicies;
//Other codes..
return (
{/*Other codes..*/}
{currentUser?.isAuthenticated &&
!!policies['BookStore.Books.Create'] && //Add this line
(
navigation.navigate('CreateUpdateBook')}
visible={true}
animateFrom={'right'}
iconMode={'static'}
style={[styles.fabStyle, { backgroundColor: theme.colors.primary }]}
/>
)
}
)
}
```
- Now the `+` icon button will be shown if the user has the `BookStore.Books.Create` permission.

### Hide the Edit and Delete Actions
Update your code as below in the `./src/screens/BookStore/Books/BooksScreen.js` file. We'll check the `policies` variables for the `Edit` and `Delete` actions.
```js
function BooksScreen() {
//...
const openContextMenu = (item) => {
const options = [];
if (policies["BookStore.Books.Delete"]) {
options.push(i18n.t("AbpUi::Delete"));
}
if (policies["BookStore.Books.Update"]) {
options.push(i18n.t("AbpUi::Edit"));
}
options.push(i18n.t("AbpUi::Cancel"));
};
//...
}
```

## Author
### Create API Proxy
```js
./src/api/AuthorAPI.js
import api from './API';
export const getList = () => api.get('/api/app/author').then(({ data }) => data);
export const get = id => api.get(`/api/app/author/${id}`).then(({ data }) => data);
export const create = input => api.post('/api/app/author', input).then(({ data }) => data);
export const update = (input, id) => api.put(`/api/app/author/${id}`, input).then(({ data }) => data);
export const remove = id => api.delete(`/api/app/author/${id}`).then(({ data }) => data);
```
## The Author List Page
### Add Authors Tab to BookStoreScreen
Open the `./src/screens/BookStore/BookStoreScreen.js` file and update it with the code below.
```js
//Other imports
import AuthorsScreen from "./Authors/AuthorsScreen";
//Other Routes..
const AuthorsRoute = (nav) => ;
function BookStoreScreen({ navigation }) {
//Other codes..
const renderScene = BottomNavigation.SceneMap({
books: () => BooksRoute(navigation),
authors: () => AuthorsRoute(navigation), //Added this line
});
//Added this
if (!!policies["BookStore.Authors"]) {
_routes.push({
key: "authors",
title: i18n.t("BookStore::Menu:Authors"),
focusedIcon: "account-supervisor",
unfocusedIcon: "account-supervisor-outline",
});
}
//Added this
}
export default BookStoreScreen;
```
Create a `AuthorsScreen.js` file under the `./src/screens/BookStore/Authors` folder and add the code below to it.
```js
import React, { useState } from "react";
import { useSelector } from "react-redux";
import { Alert, View, StyleSheet } from "react-native";
import { useTheme, List, IconButton, AnimatedFAB } from "react-native-paper";
import { useActionSheet } from "@expo/react-native-action-sheet";
import i18n from "i18n-js";
import { getList, remove } from "../../../api/AuthorAPI";
import DataList from "../../../components/DataList/DataList";
import { createAppConfigSelector } from "../../../store/selectors/AppSelectors";
function AuthorsScreen({ navigation }) {
const theme = useTheme();
const currentUser = useSelector(createAppConfigSelector())?.currentUser;
const policies = useSelector(createAppConfigSelector())?.auth
?.grantedPolicies;
const [refresh, setRefresh] = useState(null);
const { showActionSheetWithOptions } = useActionSheet();
const openContextMenu = (item) => {
const options = [];
if (policies["BookStore.Authors.Delete"]) {
options.push(i18n.t("AbpUi::Delete"));
}
if (policies["BookStore.Authors.Edit"]) {
options.push(i18n.t("AbpUi::Edit"));
}
options.push(i18n.t("AbpUi::Cancel"));
showActionSheetWithOptions(
{
options,
cancelButtonIndex: options.length - 1,
destructiveButtonIndex: options.indexOf(i18n.t("AbpUi::Delete")),
},
(index) => {
switch (options[index]) {
case i18n.t("AbpUi::Edit"):
edit(item);
break;
case i18n.t("AbpUi::Delete"):
removeOnClick(item);
break;
}
}
);
};
const removeOnClick = ({ id } = {}) => {
Alert.alert("Warning", i18n.t("BookStore::AreYouSureToDelete"), [
{
text: i18n.t("AbpUi::Cancel"),
style: "cancel",
},
{
style: "default",
text: i18n.t("AbpUi::Ok"),
onPress: () => {
remove(id).then(() => {
setRefresh((refresh ?? 0) + 1);
});
},
},
]);
};
const edit = ({ id } = {}) => {
navigation.navigate("CreateUpdateAuthor", { authorId: id });
};
return (
{currentUser?.isAuthenticated && (
);
}
const styles = StyleSheet.create({
container: {
flexGrow: 1,
},
fabStyle: {
bottom: 16,
right: 16,
position: "absolute",
},
});
export default AuthorsScreen;
```
Create a `CreateUpdateAuthorScreen.js` file under the `./src/screens/BookStore/Authors/CreateUpdateAuthor` folder and add the code below to it.
```js
import PropTypes from "prop-types";
import React, { useEffect, useState } from "react";
import { get, create, update } from "../../../../api/AuthorAPI";
import LoadingActions from "../../../../store/actions/LoadingActions";
import { createLoadingSelector } from "../../../../store/selectors/LoadingSelectors";
import { connectToRedux } from "../../../../utils/ReduxConnect";
import CreateUpdateAuthorForm from "./CreateUpdateAuthorForm";
function CreateUpdateAuthorScreen({
navigation,
route,
startLoading,
clearLoading,
}) {
const { authorId } = route.params || {};
const [author, setAuthor] = useState(null);
const submit = (data) => {
startLoading({ key: "save" });
(data.id ? update(data, data.id) : create(data))
.then(() => navigation.goBack())
.finally(() => clearLoading());
};
useEffect(() => {
if (authorId) {
startLoading({ key: "fetchAuthorDetail" });
get(authorId)
.then((response) => setAuthor(response))
.finally(() => clearLoading());
}
}, [authorId]);
return ;
}
CreateUpdateAuthorScreen.propTypes = {
startLoading: PropTypes.func.isRequired,
clearLoading: PropTypes.func.isRequired,
};
export default connectToRedux({
component: CreateUpdateAuthorScreen,
stateProps: (state) => ({ loading: createLoadingSelector()(state) }),
dispatchProps: {
startLoading: LoadingActions.start,
clearLoading: LoadingActions.clear,
},
});
```
Create a `CreateUpdateAuthorForm.js` file under the `./src/screens/BookStore/Authors/CreateUpdateAuthor` folder and add the code below to it.
```js
import React, { useRef, useState } from "react";
import {
Platform,
KeyboardAvoidingView,
StyleSheet,
View,
ScrollView,
} from "react-native";
import { useFormik } from "formik";
import i18n from "i18n-js";
import PropTypes from "prop-types";
import * as Yup from "yup";
import { useTheme, TextInput } from "react-native-paper";
import DateTimePicker from "@react-native-community/datetimepicker";
import { FormButtons } from "../../../../components/FormButtons";
import ValidationMessage from "../../../../components/ValidationMessage/ValidationMessage";
const validations = {
name: Yup.string().required("AbpValidation::ThisFieldIsRequired."),
birthDate: Yup.string()
.nullable()
.required("AbpValidation::ThisFieldIsRequired."),
};
const props = {
underlineStyle: { backgroundColor: "transparent" },
underlineColor: "#333333bf",
};
function CreateUpdateAuthorForm({ submit, author = null }) {
const theme = useTheme();
const [birthDateVisible, setPublishDateVisible] = useState(false);
const nameRef = useRef();
const birthDateRef = useRef();
const shortBioRef = useRef();
const inputStyle = {
...styles.input,
backgroundColor: theme.colors.primaryContainer,
};
const onSubmit = (values) => {
if (!authorForm.isValid) {
return;
}
submit({ ...values });
};
const authorForm = useFormik({
enableReinitialize: true,
validateOnBlur: true,
validationSchema: Yup.object().shape({
...validations,
}),
initialValues: {
...author,
name: author?.name || "",
birthDate: (author?.birthDate && new Date(author?.birthDate)) || null,
shortBio: author?.shortBio || "",
},
onSubmit,
});
const isInvalidControl = (controlName = null) => {
if (!controlName) {
return;
}
return (
((!!authorForm.touched[controlName] && authorForm.submitCount > 0) ||
authorForm.submitCount > 0) &&
!!authorForm.errors[controlName]
);
};
const onChange = (event, selectedDate) => {
if (!selectedDate) {
return;
}
setPublishDateVisible(false);
if (event && event.type !== "dismissed") {
authorForm.setFieldValue("birthDate", selectedDate, true);
}
};
return (
{birthDateVisible && (
)}
birthDateRef.current.focus()}
returnKeyType="next"
onChangeText={authorForm.handleChange("name")}
onBlur={authorForm.handleBlur("name")}
value={authorForm.values.name}
autoCapitalize="none"
label={i18n.t("BookStore::Name")}
style={inputStyle}
{...props}
/>
{isInvalidControl("name") && (
{authorForm.errors.name}
)}
shortBioRef.current.focus()}
right={
setPublishDateVisible(true)}
icon="menu-down"
/>
}
style={inputStyle}
editable={false}
value={authorForm.values.birthDate?.toLocaleDateString()}
{...props}
/>
{isInvalidControl("birthDate") && (
{authorForm.errors.birthDate}
)}
authorForm.handleSubmit()}
returnKeyType="next"
onChangeText={authorForm.handleChange("shortBio")}
onBlur={authorForm.handleBlur("shortBio")}
value={authorForm.values.shortBio}
autoCapitalize="none"
label={i18n.t("BookStore::ShortBio")}
style={inputStyle}
{...props}
/>
);
}
const styles = StyleSheet.create({
input: {
container: {
margin: 8,
marginLeft: 16,
marginRight: 16,
},
borderRadius: 8,
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
},
button: {
marginLeft: 16,
marginRight: 16,
},
});
CreateUpdateAuthorForm.propTypes = {
author: PropTypes.object,
submit: PropTypes.func.isRequired,
};
export default CreateUpdateAuthorForm;
```





## Add `Author` Relation To Book
Update BookAPI proxy file and include `getAuthorLookup` method
```js
import api from "./API";
export const getList = () => api.get("/api/app/book").then(({ data }) => data);
//Add this
export const getAuthorLookup = () =>
api.get("/api/app/book/author-lookup").then(({ data }) => data);
//Add this
export const get = (id) =>
api.get(`/api/app/book/${id}`).then(({ data }) => data);
export const create = (input) =>
api.post("/api/app/book", input).then(({ data }) => data);
export const update = (input, id) =>
api.put(`/api/app/book/${id}`, input).then(({ data }) => data);
export const remove = (id) =>
api.delete(`/api/app/book/${id}`).then(({ data }) => data);
```
### Add `AuthorName` to the Book List
Open `BooksScreen.js` file under the `./src/screens/BookStore/Books` and update code below.
```js
//Improts
function BooksScreen({ navigation }) {
//Other codes..
return (
//Other codes