diff --git a/src/generators.js b/src/generators.js
index 91247c1d..6e6cbdf4 100644
--- a/src/generators.js
+++ b/src/generators.js
@@ -3,6 +3,7 @@ import NextGenerator from "./generators/NextGenerator.js";
import NuxtGenerator from "./generators/NuxtGenerator.js";
import ReactGenerator from "./generators/ReactGenerator.js";
import ReactNativeGenerator from "./generators/ReactNativeGenerator.js";
+import ReactNativeGeneratorV2 from "./generators/ReactNativeGeneratorV2.js";
import TypescriptInterfaceGenerator from "./generators/TypescriptInterfaceGenerator.js";
import VueGenerator from "./generators/VueGenerator.js";
import VuetifyGenerator from "./generators/VuetifyGenerator.js";
@@ -28,6 +29,8 @@ export default async function generators(generator = "react") {
return wrap(ReactGenerator);
case "react-native":
return wrap(ReactNativeGenerator);
+ case "react-native-v2":
+ return wrap(ReactNativeGeneratorV2);
case "typescript":
return wrap(TypescriptInterfaceGenerator);
case "vue":
diff --git a/src/generators/ReactNativeGeneratorV2.js b/src/generators/ReactNativeGeneratorV2.js
new file mode 100644
index 00000000..fdfd9d7c
--- /dev/null
+++ b/src/generators/ReactNativeGeneratorV2.js
@@ -0,0 +1,188 @@
+import chalk from "chalk";
+import handlebars from "handlebars";
+import BaseGenerator from "./BaseGenerator.js";
+import hbhComparison from "handlebars-helpers/lib/comparison.js";
+
+export default class extends BaseGenerator {
+ constructor(params) {
+ super(params);
+
+ handlebars.registerHelper("ifNotResource", function (item, options) {
+ if (item === null) {
+ return options.fn(this);
+ }
+ return options.inverse(this);
+ });
+
+ this.registerTemplates(`react-native-v2/`, [
+ "app/(tabs)/foos.tsx",
+ "app/_layout.tsx.dist",
+ "lib/hooks.ts",
+ "lib/store.ts",
+ "lib/types/ApiResource.ts",
+ "lib/types/HydraView.ts",
+ "lib/types/Logs.ts",
+ "lib/types/foo.ts",
+ "lib/factory/logFactory.ts",
+ "lib/slices/fooSlice.ts",
+ "lib/api/fooApi.ts",
+ "components/Main.tsx",
+ "components/Navigation.tsx",
+ "components/StoreProvider.tsx",
+ "components/foo/CreateEditModal.tsx",
+ "components/foo/Form.tsx",
+ "components/foo/LogsRenderer.tsx",
+ ]);
+
+ handlebars.registerHelper("compare", hbhComparison.compare);
+ }
+
+ help(resource) {
+ const titleLc = resource.title.toLowerCase();
+
+ console.log(
+ 'Code for the "%s" resource type has been generated!',
+ resource.title
+ );
+
+ console.log("You must now configure the lib/store.ts");
+ console.log(
+ chalk.green(`
+ // imports for ${titleLc}
+ import ${titleLc}Slice from './slices/${titleLc}Slice';
+ import { ${titleLc}Api } from './api/${titleLc}Api';
+
+ // reducer for ${titleLc}
+ reducer: {
+ ...
+ ${titleLc}: ${titleLc}Slice,
+ [${titleLc}Api.reducerPath]: ${titleLc}Api.reducer,
+ }
+
+ // middleware for ${titleLc}
+ getDefaultMiddleware().concat(..., ${titleLc}Api.middleware)
+ `)
+ );
+
+ console.log(
+ "You should replace app/_layout.tsx by the generated one and add the following route:"
+ );
+ console.log(
+ chalk.green(`
+
+
+ tabs: {
+ ...
+ ${titleLc}: {
+ title: '${titleLc}',
+ headerShown: false,
+ tabBarIcon: ({ color }) => ,
+ },
+ }
+ `)
+ );
+ }
+
+ generate(api, resource, dir) {
+ const lc = resource.title.toLowerCase();
+ const titleUcFirst =
+ resource.title.charAt(0).toUpperCase() + resource.title.slice(1);
+ const fields = this.parseFields(resource);
+
+ const context = {
+ title: resource.title,
+ name: resource.name,
+ lc,
+ uc: resource.title.toUpperCase(),
+ fields,
+ formFields: this.buildFields(fields),
+ hydraPrefix: this.hydraPrefix,
+ ucf: titleUcFirst,
+ };
+
+ // Create directories
+ // These directories may already exist
+ [
+ `${dir}/app/(tabs)`,
+ `${dir}/config`,
+ `${dir}/components`,
+ `${dir}/components/${lc}`,
+ `${dir}/lib`,
+ `${dir}/lib/api`,
+ `${dir}/lib/factory`,
+ `${dir}/lib/slices`,
+ `${dir}/lib/types`,
+ ].forEach((dir) => this.createDir(dir, false));
+
+ // static files
+ [
+ "lib/hooks.ts",
+ "lib/store.ts",
+ "lib/types/ApiResource.ts",
+ "lib/types/HydraView.ts",
+ "lib/types/Logs.ts",
+ "lib/factory/logFactory.ts",
+ "components/Main.tsx",
+ "components/Navigation.tsx",
+ "components/StoreProvider.tsx",
+ ].forEach((file) => this.createFile(file, `${dir}/${file}`));
+
+ // templated files ucFirst
+ ["lib/types/%s.ts"].forEach((pattern) =>
+ this.createFileFromPattern(pattern, dir, [titleUcFirst], context)
+ );
+
+ // templated files lc
+ [
+ "app/(tabs)/%ss.tsx",
+ "app/_layout.tsx.dist",
+ "lib/slices/%sSlice.ts",
+ "lib/api/%sApi.ts",
+ "components/%s/CreateEditModal.tsx",
+ "components/%s/Form.tsx",
+ "components/%s/LogsRenderer.tsx",
+ ].forEach((pattern) =>
+ this.createFileFromPattern(pattern, dir, [lc], context)
+ );
+
+ this.createEntrypoint(api.entrypoint, `${dir}/config/entrypoint.js`);
+ }
+
+ getDescription(field) {
+ return field.description ? field.description.replace(/"/g, "'") : "";
+ }
+
+ parseFields(resource) {
+ const fields = [
+ ...resource.writableFields,
+ ...resource.readableFields,
+ ].reduce((list, field) => {
+ if (list[field.name]) {
+ return list;
+ }
+
+ const isReferences = Boolean(
+ field.reference && field.maxCardinality !== 1
+ );
+ const isEmbeddeds = Boolean(field.embedded && field.maxCardinality !== 1);
+
+ return {
+ ...list,
+ [field.name]: {
+ ...field,
+ type: this.getType(field),
+ description: this.getDescription(field),
+ readonly: false,
+ isReferences,
+ isEmbeddeds,
+ isRelations: isEmbeddeds || isReferences,
+ },
+ };
+ }, {});
+
+ return Object.values(fields);
+ }
+}
diff --git a/src/generators/ReactNativeGeneratorV2.test.js b/src/generators/ReactNativeGeneratorV2.test.js
new file mode 100644
index 00000000..2981cf64
--- /dev/null
+++ b/src/generators/ReactNativeGeneratorV2.test.js
@@ -0,0 +1,60 @@
+import { Api, Resource, Field } from "@api-platform/api-doc-parser";
+import path from "path";
+import { fileURLToPath } from "url";
+import fs from "fs";
+import tmp from "tmp";
+import ReactNativeGeneratorV2 from "./ReactNativeGeneratorV2.js";
+
+const dirname = path.dirname(fileURLToPath(import.meta.url));
+
+test("Generate a React Native V2 app", () => {
+ const generator = new ReactNativeGeneratorV2({
+ hydraPrefix: "hydra:",
+ templateDirectory: `${dirname}/../../templates`,
+ });
+ const tmpobj = tmp.dirSync({ unsafeCleanup: true });
+
+ const fields = [
+ new Field("bar", {
+ id: "http://schema.org/url",
+ range: "http://www.w3.org/2001/XMLSchema#string",
+ reference: null,
+ required: true,
+ description: "An URL",
+ }),
+ ];
+ const resource = new Resource("abc", "http://example.com/foos", {
+ id: "abc",
+ title: "abc",
+ readableFields: fields,
+ writableFields: fields,
+ });
+ const api = new Api("http://example.com", {
+ entrypoint: "http://example.com:8080",
+ title: "My API",
+ resources: [resource],
+ });
+ generator.generate(api, resource, tmpobj.name);
+
+ [
+ "/lib/hooks.ts",
+ "/lib/store.ts",
+ "/lib/types/ApiResource.ts",
+ "/lib/types/HydraView.ts",
+ "/lib/types/Logs.ts",
+ "/lib/factory/logFactory.ts",
+ "/components/Main.tsx",
+ "/components/Navigation.tsx",
+ "/components/StoreProvider.tsx",
+ "/app/_layout.tsx.dist",
+
+ "/app/(tabs)/abcs.tsx",
+ "/lib/slices/abcSlice.ts",
+ "/lib/api/abcApi.ts",
+ "/components/abc/CreateEditModal.tsx",
+ "/components/abc/Form.tsx",
+ "/components/abc/LogsRenderer.tsx",
+ ].forEach((file) => expect(fs.existsSync(tmpobj.name + file)).toBe(true));
+
+ tmpobj.removeCallback();
+});
diff --git a/templates/react-native-v2/app/(tabs)/foos.tsx b/templates/react-native-v2/app/(tabs)/foos.tsx
new file mode 100644
index 00000000..a65beceb
--- /dev/null
+++ b/templates/react-native-v2/app/(tabs)/foos.tsx
@@ -0,0 +1,73 @@
+import Main from "@/components/Main";
+import Navigation from "@/components/Navigation";
+import CreateEditModal from "@/components/{{{lc}}}/CreateEditModal";
+import LogsRenderer from "@/components/{{{lc}}}/LogsRenderer";
+import { useLazyGetAllQuery } from "@/lib/api/{{{lc}}}Api";
+import { useAppDispatch, useAppSelector } from "@/lib/hooks";
+import { setCurrentData, setData, setModalIsEdit, setModalIsVisible, setPage, setView } from "@/lib/slices/{{{lc}}}Slice";
+import {{{ucf}}} from "@/lib/types/{{{ucf}}}";
+import { useLocalSearchParams } from "expo-router";
+import { useEffect } from "react";
+import { Pressable, ScrollView, Text, View } from "react-native";
+
+export default function {{{ucf}}}s() {
+ const datas = useAppSelector(state => state.{{lc}}.data);
+ const view = useAppSelector(state => state.{{{lc}}}.view);
+ const { page = '1' } = useLocalSearchParams<{ page: string }>();
+
+ const dispatch = useAppDispatch();
+ const [getAll] = useLazyGetAllQuery();
+
+ const toggleEditModal = (data: {{{ucf}}}) => {
+ dispatch(setCurrentData(data));
+ dispatch(setModalIsVisible(true));
+ dispatch(setModalIsEdit(true));
+ };
+
+ const toggleCreateModal = () => {
+ dispatch(setModalIsVisible(true));
+ dispatch(setModalIsEdit(false));
+ }
+
+ useEffect(() => {
+ const intPage = parseInt(page);
+ if (intPage < 0) return;
+ dispatch(setPage(intPage));
+ getAll(intPage)
+ .unwrap()
+ .then(fulfilled => {
+ dispatch(setView(fulfilled["hydra:view"]));
+ dispatch(setData(fulfilled["hydra:member"]));
+ })
+ }, [page]);
+
+ return (
+
+
+ {{{ucf}}}s List
+ toggleCreateModal()}>
+ Create
+
+
+
+
+
+ {
+ datas.map(data => (
+ toggleEditModal(data)} key={data["@id"]}>
+
+ ID: {data['@id']}
+ {{#each fields}}
+ {{{name}}}: {data["{{{name}}}"]}
+ {{/each}}
+
+
+ ))
+ }
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/templates/react-native-v2/app/_layout.tsx.dist b/templates/react-native-v2/app/_layout.tsx.dist
new file mode 100644
index 00000000..aa787aec
--- /dev/null
+++ b/templates/react-native-v2/app/_layout.tsx.dist
@@ -0,0 +1,39 @@
+import React from 'react';
+import { FontAwesome } from "@expo/vector-icons";
+import "../global.css";
+import { Tabs } from "expo-router";
+import StoreProvider from '@/components/StoreProvider';
+
+function TabBarIcon(props: {
+ name: React.ComponentProps['name'];
+ color: string;
+}) {
+ return ;
+}
+const iconMargin = { marginBottom: -3 }
+
+export default function Layout() {
+ return (
+
+
+
+
+
+ )
+}
+
+const options = {
+ tabsContainer: {
+ headerShown: false,
+ tabBarShowLabel: false,
+ },
+ tabs: {
+ home: {
+ title: 'Accueil',
+ tabBarIcon: ({ color }) => ,
+ },
+ }
+};
diff --git a/templates/react-native-v2/components/Main.tsx b/templates/react-native-v2/components/Main.tsx
new file mode 100644
index 00000000..fa5fcb14
--- /dev/null
+++ b/templates/react-native-v2/components/Main.tsx
@@ -0,0 +1,16 @@
+import { View } from "react-native";
+
+export default function Main({ children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+const styles = {
+ container: {
+ position: 'relative',
+ marginHorizontal: '3%',
+ }
+}
\ No newline at end of file
diff --git a/templates/react-native-v2/components/Navigation.tsx b/templates/react-native-v2/components/Navigation.tsx
new file mode 100644
index 00000000..0332f5cb
--- /dev/null
+++ b/templates/react-native-v2/components/Navigation.tsx
@@ -0,0 +1,64 @@
+import { HydraView } from "@/lib/types/HydraView";
+import { FontAwesome } from "@expo/vector-icons";
+import { useRouter } from "expo-router";
+import { Pressable, View } from "react-native";
+
+function NavigationIcon(props: {
+ name: React.ComponentProps['name'];
+ color: string;
+}) {
+ return ;
+}
+const iconMargin = { marginBottom: -3 }
+
+export default function Navigation(props: { view: HydraView }) {
+ const view = props.view;
+ if (!view) {
+ return null;
+ }
+
+ const router = useRouter();
+
+ const {
+ "hydra:first": first,
+ "hydra:previous": previous,
+ "hydra:next": next,
+ "hydra:last": last,
+ } = view;
+
+ return (
+
+ {
+ if (first) router.navigate(first)
+ }}>
+
+
+
+ {
+ if (previous) router.navigate(previous)
+ }}>
+
+
+
+ {
+ if (next) router.navigate(next)
+ }}>
+
+
+
+ {
+ if (last) router.navigate(last)
+ }}>
+
+
+
+ );
+}
+
+const styles = {
+ container: {
+ position: 'absolute',
+ bottom: 5,
+ minWidth: '100%'
+ }
+}
\ No newline at end of file
diff --git a/templates/react-native-v2/components/StoreProvider.tsx b/templates/react-native-v2/components/StoreProvider.tsx
new file mode 100644
index 00000000..92936376
--- /dev/null
+++ b/templates/react-native-v2/components/StoreProvider.tsx
@@ -0,0 +1,17 @@
+import { useRef } from 'react'
+import { Provider } from 'react-redux'
+import { makeStore, AppStore } from '@/lib/store'
+
+export default function StoreProvider({
+ children
+}: {
+ children: React.ReactNode
+}) {
+ const storeRef = useRef()
+
+ if (!storeRef.current) {
+ storeRef.current = makeStore()
+ }
+
+ return {children}
+}
diff --git a/templates/react-native-v2/components/foo/CreateEditModal.tsx b/templates/react-native-v2/components/foo/CreateEditModal.tsx
new file mode 100644
index 00000000..b5fed4bb
--- /dev/null
+++ b/templates/react-native-v2/components/foo/CreateEditModal.tsx
@@ -0,0 +1,67 @@
+import { useAppSelector } from "@/lib/hooks";
+import { Modal, Pressable, Text, View } from "react-native";
+import { useDispatch } from "react-redux";
+import Form from "./Form";
+import { addLog, setData, setModalIsVisible, setView } from "@/lib/slices/{{{lc}}}Slice";
+import { useDeleteMutation, useLazyGetAllQuery } from "@/lib/api/{{{lc}}}Api";
+import { createErrorLog, createSuccessLog } from "@/lib/factory/logFactory";
+
+export default function CreateEditModal() {
+ const {{{lc}}}State = useAppSelector(state => state.{{{lc}}});
+ const { modalState, currentData, page } = {{{lc}}}State;
+ const dispatch = useDispatch();
+ const [deleteMutation] = useDeleteMutation();
+ const [getAll] = useLazyGetAllQuery();
+
+ function handleDelete() {
+ deleteMutation(currentData['@id'])
+ .unwrap()
+ .then(() => {
+ dispatch(addLog(createSuccessLog(`{{{ucf}}} ${currentData['@id']} has been deleted successfully.`)))
+ getAll(page)
+ .unwrap()
+ .then(fulfilled => {
+ dispatch(setModalIsVisible(false));
+ dispatch(setView(fulfilled["hydra:view"]));
+ dispatch(setData(fulfilled["hydra:member"]));
+ });
+ })
+ .catch(error => {
+ if (error.data) {
+ dispatch(addLog(createErrorLog(`Error: ${error.data["hydra:description"]}`)))
+ }
+ });
+ }
+
+ return (
+
+
+
+ {modalState.edit ? `Edit {{{ucf}}}` : 'Create a new {{{ucf}}}'}
+
+ {
+ modalState.edit &&
+ handleDelete()}>
+ Delete
+
+ }
+ dispatch(setModalIsVisible(false))}>
+ Close
+
+
+
+
+ )
+}
+
+const styles = {
+ container: { height: '80%', width: '100%', backgroundColor: '#e3e9e5' },
+ closeButton: { position: 'absolute', right: 5, top: 5 }
+}
\ No newline at end of file
diff --git a/templates/react-native-v2/components/foo/Form.tsx b/templates/react-native-v2/components/foo/Form.tsx
new file mode 100644
index 00000000..424b0dea
--- /dev/null
+++ b/templates/react-native-v2/components/foo/Form.tsx
@@ -0,0 +1,120 @@
+import { useCreateMutation, useLazyGetAllQuery, useUpdateMutation } from "@/lib/api/{{{lc}}}Api";
+import { createErrorLog, createSuccessLog } from "@/lib/factory/logFactory";
+import { useAppSelector } from "@/lib/hooks";
+import { addLog, setData, setModalIsVisible, setView } from "@/lib/slices/{{{lc}}}Slice";
+import {{{ucf}}} from "@/lib/types/{{{ucf}}}";
+import { useState } from "react";
+import { Controller, SubmitErrorHandler, useForm } from "react-hook-form";
+import { Pressable, Text, TextInput, View } from "react-native";
+import { SafeAreaView } from "react-native-safe-area-context";
+import { useDispatch } from "react-redux";
+
+export default function Form() {
+ const [update] = useUpdateMutation();
+ const [create] = useCreateMutation();
+ const [getAll] = useLazyGetAllQuery();
+ const {{{lc}}}Data = useAppSelector(state => state.{{{lc}}});
+ const { page, currentData, modalState } = {{{lc}}}Data;
+ const dispatch = useDispatch();
+ const [errors, setErrors] = useState([]);
+ const submitQuery = modalState.edit ? update : create;
+
+ const initValues: {{{ucf}}} = modalState.edit ? currentData : {
+ '@id': '',
+ {{#each formFields}}
+ {{{name}}}: {{#if (compare type "==" "number")}}0{{else}}''{{/if}},
+ {{/each}}
+ }
+
+ const { control, handleSubmit, reset } = useForm<{{{ucf}}}>({
+ defaultValues: initValues
+ });
+
+
+ const onSubmit = (data: {{{ucf}}}) => {
+ intParser(data);
+ submitQuery(data)
+ .unwrap()
+ .then(() => {
+ dispatch(addLog(createSuccessLog(`The {{{lc}}} has been ${modalState.edit ? 'edited' : 'created'} successfully.`)))
+ getAll(page)
+ .unwrap()
+ .then(fulfilled => {
+ reset();
+ dispatch(setView(fulfilled["hydra:view"]));
+ dispatch(setData(fulfilled["hydra:member"]));
+ dispatch(setModalIsVisible(false));
+ });
+ })
+ .catch(error => {
+ if (error.data) {
+ dispatch(
+ addLog(createErrorLog(`Error : ${error.data["hydra:description"]}`))
+ )
+ }
+ });
+ return;
+ };
+
+ const intParser = (data: {{{ucf}}}) => {
+ Object.keys(data).forEach(key => {
+ if ((typeof initValues[key] == "number") && !isNaN(parseInt(data[key]))) {
+ data[key] = parseInt(data[key]);
+ }
+ });
+ return;
+ }
+
+ const onError: SubmitErrorHandler<{{{ucf}}}> = (errors, e) => {
+ setErrors(Object.keys(errors));
+ return;
+ }
+
+
+ return (
+
+
+ {errors.length > 0 &&
+
+
+ Field{errors.length > 1 ? "s" : ""} "{errors.join(', ')}" {errors.length > 1 ? "are" : "is"} empty
+
+
+ }
+ {{#each formFields}}
+ (
+
+ {{{name}}} :
+
+
+ )}
+ name="{{{name}}}"
+ {{#if required}}rules={fieldRequired}{{/if}}
+ />
+ {{/each}}
+
+
+ {modalState.edit ? 'Edit' : 'Create'}
+
+
+
+ );
+}
+
+const fieldRequired = {
+ required: true
+}
+
+const styles = {
+ textInput: { minWidth: 200 }
+}
\ No newline at end of file
diff --git a/templates/react-native-v2/components/foo/LogsRenderer.tsx b/templates/react-native-v2/components/foo/LogsRenderer.tsx
new file mode 100644
index 00000000..4419732c
--- /dev/null
+++ b/templates/react-native-v2/components/foo/LogsRenderer.tsx
@@ -0,0 +1,47 @@
+import { useAppSelector } from "@/lib/hooks";
+import { cleanLogs } from "@/lib/slices/{{{lc}}}Slice";
+import { Log } from "@/lib/types/Logs";
+import { Pressable, Text, View } from "react-native";
+import { useDispatch } from "react-redux";
+
+export default function LogsRenderer() {
+ const logs = useAppSelector(state => state.{{{lc}}}.logs);
+ const { errors, successes } = logs;
+
+ return (
+
+ {
+ errors.length > 0 &&
+
+
+ {errors.map((error: string, index: number) => (
+ - {error}
+ ))}
+
+
+
+ }
+ {
+ successes.length > 0 &&
+
+
+ {successes.map((success: string, index: number) => (
+ - {success}
+ ))}
+
+
+
+ }
+
+ )
+}
+
+const CloseButton = (props: { type: keyof Log }) => {
+ const dispatch = useDispatch();
+
+ return (
+ dispatch(cleanLogs(props.type))}>
+ X
+
+ )
+}
\ No newline at end of file
diff --git a/templates/react-native-v2/lib/api/fooApi.ts b/templates/react-native-v2/lib/api/fooApi.ts
new file mode 100644
index 00000000..6fceb81e
--- /dev/null
+++ b/templates/react-native-v2/lib/api/fooApi.ts
@@ -0,0 +1,43 @@
+import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
+import {{{ucf}}} from '../types/{{{ucf}}}';
+import { ENTRYPOINT } from '@/config/entrypoint';
+
+export const {{{lc}}}Api = createApi({
+ reducerPath: '{{{lc}}}Api',
+ baseQuery: fetchBaseQuery({ baseUrl: ENTRYPOINT }),
+ endpoints: builder => ({
+ getAll: builder.query({
+ query: (page) => {
+ return `/{{{lc}}}s?page=${page}`
+ }
+ }),
+ delete: builder.mutation({
+ query: (id) => {
+ return {
+ url: `${id}`,
+ method: 'DELETE'
+ }
+ }
+ }),
+ create: builder.mutation({
+ query: ({{{lc}}}) => {
+ return {
+ url: `/{{{lc}}}s`,
+ method: 'POST',
+ body: {{{lc}}},
+ }
+ }
+ }),
+ update: builder.mutation({
+ query: ({{{lc}}}) => {
+ return {
+ url: {{{lc}}}['@id'],
+ method: 'PUT',
+ body: {{{lc}}},
+ }
+ }
+ }),
+ })
+});
+
+export const { useLazyGetAllQuery, useDeleteMutation, useCreateMutation, useUpdateMutation } = {{{lc}}}Api;
\ No newline at end of file
diff --git a/templates/react-native-v2/lib/factory/logFactory.ts b/templates/react-native-v2/lib/factory/logFactory.ts
new file mode 100644
index 00000000..69af26ee
--- /dev/null
+++ b/templates/react-native-v2/lib/factory/logFactory.ts
@@ -0,0 +1,16 @@
+import { Log, NewLog } from "../types/Logs";
+
+function createLog(type: keyof Log, message: string): NewLog {
+ return {
+ type,
+ message
+ }
+}
+
+export function createSuccessLog(message: string): NewLog {
+ return createLog("successes", message);
+}
+
+export function createErrorLog(message: string): NewLog {
+ return createLog("errors", message);
+}
\ No newline at end of file
diff --git a/templates/react-native-v2/lib/hooks.ts b/templates/react-native-v2/lib/hooks.ts
new file mode 100644
index 00000000..5c8ff39c
--- /dev/null
+++ b/templates/react-native-v2/lib/hooks.ts
@@ -0,0 +1,7 @@
+import { useDispatch, useSelector, useStore } from 'react-redux'
+import type { TypedUseSelectorHook } from 'react-redux'
+import type { RootState, AppDispatch, AppStore } from './store'
+
+export const useAppDispatch: () => AppDispatch = useDispatch
+export const useAppSelector: TypedUseSelectorHook = useSelector
+export const useAppStore: () => AppStore = useStore
\ No newline at end of file
diff --git a/templates/react-native-v2/lib/slices/fooSlice.ts b/templates/react-native-v2/lib/slices/fooSlice.ts
new file mode 100644
index 00000000..068480da
--- /dev/null
+++ b/templates/react-native-v2/lib/slices/fooSlice.ts
@@ -0,0 +1,69 @@
+import { createSlice } from '@reduxjs/toolkit'
+import type { PayloadAction } from '@reduxjs/toolkit'
+import {{{ucf}}} from '../types/{{{ucf}}}';
+import { HydraView } from '../types/HydraView';
+import { Log, NewLog } from '../types/Logs';
+
+interface {{{ucf}}}SliceState {
+ page: number;
+ data: {{{ucf}}}[];
+ currentData?: {{{ucf}}};
+ modalState: ModalState;
+ view: HydraView;
+ logs: Log;
+}
+
+interface ModalState {
+ open: boolean;
+ edit: boolean;
+}
+
+const initialState: {{{ucf}}}SliceState = {
+ page: 1,
+ data: [],
+ currentData: undefined,
+ modalState: {
+ open: false,
+ edit: false,
+ },
+ view: {},
+ logs: {
+ errors: [],
+ successes: [],
+ }
+}
+
+export const {{{lc}}}Slice = createSlice({
+ name: '{{{lc}}}Slice',
+ initialState,
+ reducers: {
+ setPage: (state, action: PayloadAction) => {
+ state.page = action.payload;
+ },
+ setData: (state, action: PayloadAction<{{{ucf}}}[]>) => {
+ state.data = action.payload;
+ },
+ setView: (state, action: PayloadAction) => {
+ state.view = action.payload;
+ },
+ setCurrentData: (state, action: PayloadAction<{{{ucf}}}>) => {
+ state.currentData = action.payload;
+ },
+ setModalIsVisible: (state, action: PayloadAction) => {
+ state.modalState.open = action.payload;
+ },
+ setModalIsEdit: (state, action: PayloadAction) => {
+ state.modalState.edit = action.payload;
+ },
+ addLog: (state, action: PayloadAction) => {
+ state.logs[action.payload.type] = [...state.logs[action.payload.type], action.payload.message];
+ },
+ cleanLogs: (state, action: PayloadAction) => {
+ state.logs[action.payload] = [];
+ },
+ }
+})
+
+export const { setPage, setData, setView, setCurrentData, setModalIsEdit, setModalIsVisible, addLog, cleanLogs } = {{{lc}}}Slice.actions;
+
+export default {{{lc}}}Slice.reducer;
\ No newline at end of file
diff --git a/templates/react-native-v2/lib/store.ts b/templates/react-native-v2/lib/store.ts
new file mode 100644
index 00000000..7d829bc0
--- /dev/null
+++ b/templates/react-native-v2/lib/store.ts
@@ -0,0 +1,16 @@
+import { configureStore } from '@reduxjs/toolkit';
+
+export const makeStore = () => {
+ return configureStore({
+ reducer: {
+ },
+ middleware: getDefaultMiddleware =>
+ getDefaultMiddleware()
+ .concat(),
+ });
+};
+
+export type AppStore = ReturnType;
+export type RootState = ReturnType;
+export type AppDispatch = AppStore['dispatch'];
+
diff --git a/templates/react-native-v2/lib/types/ApiResource.ts b/templates/react-native-v2/lib/types/ApiResource.ts
new file mode 100644
index 00000000..217fe654
--- /dev/null
+++ b/templates/react-native-v2/lib/types/ApiResource.ts
@@ -0,0 +1,3 @@
+export default interface ApiResource {
+ "@id": string;
+}
\ No newline at end of file
diff --git a/templates/react-native-v2/lib/types/HydraView.ts b/templates/react-native-v2/lib/types/HydraView.ts
new file mode 100644
index 00000000..c4b9e82d
--- /dev/null
+++ b/templates/react-native-v2/lib/types/HydraView.ts
@@ -0,0 +1,6 @@
+export interface HydraView {
+ 'hydra:first'?: string;
+ 'hydra:last'?: string;
+ 'hydra:previous'?: string;
+ 'hydra:next'?: string;
+}
\ No newline at end of file
diff --git a/templates/react-native-v2/lib/types/Logs.ts b/templates/react-native-v2/lib/types/Logs.ts
new file mode 100644
index 00000000..46a49352
--- /dev/null
+++ b/templates/react-native-v2/lib/types/Logs.ts
@@ -0,0 +1,9 @@
+export interface Log {
+ errors: Array;
+ successes: Array;
+}
+
+export interface NewLog {
+ type: keyof Log;
+ message: string;
+}
\ No newline at end of file
diff --git a/templates/react-native-v2/lib/types/foo.ts b/templates/react-native-v2/lib/types/foo.ts
new file mode 100644
index 00000000..d5f67698
--- /dev/null
+++ b/templates/react-native-v2/lib/types/foo.ts
@@ -0,0 +1,7 @@
+import ApiResource from "./ApiResource";
+
+export default interface {{{ ucf }}} extends ApiResource {
+ {{#each fields}}
+ {{#if readonly}}readonly{{/if}} {{{name}}}?: {{#if (compare type "==" "Date")}}string{{else}}{{{type}}}{{/if}};
+ {{/each}}
+}
\ No newline at end of file