diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 414719c..ec24db4 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -46,4 +46,4 @@ jobs:
- name: Install Dependencies
run: yarn install --frozen-lockfile
- name: Run Prettier through format
- run: yarn format-check
+ run: yarn format:check
diff --git a/.prettierignore b/.prettierignore
deleted file mode 100644
index d201650..0000000
--- a/.prettierignore
+++ /dev/null
@@ -1 +0,0 @@
-zodios-client-template.hbs
diff --git a/README.md b/README.md
index 5110099..766c317 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ Welcome to `react-native-template` 👋, the go-to template for building mobile
- [Nativewind Integration](#3-nativewind-integration)
- [Full Localization Support](#4-full-localization-support)
- [Typed Expo Router Setup](#5-typed-expo-router-setup)
- - [Zodius API Client Setup](#6-zodius-api-client-setup)
+ - [Orval API Client Setup](#6-orval-api-client-setup)
- [Custom Utility Hooks](#7-custom-utility-hooks)
- [Zustand State Management](#8-zustand-state-management)
- [CI/CD Workflow Configuration](#9-cicd-workflow-configuration)
@@ -30,7 +30,6 @@ Welcome to `react-native-template` 👋, the go-to template for building mobile
- [Loading](#loading)
- [FormTextInput](#formtextinput)
- [ValidationError](#validationerror)
- - [Toaster](#toaster)
7. [Using the Template Effectively](#using-the-template-effectively)
- [Recommended Folder Structure](#recommended-folder-structure)
- [Development Decision Flow Chart](#development-decision-flow-chart)
@@ -186,30 +185,39 @@ export default Routes;
// router.push(Routes.artists.artist('1').songs.song('2'));
```
-
+
-### 6. Zodius API Client Setup 📡
+### 6. Orval API Client Setup 📡
-A pre-configured Zodius API client with Tenstack Query for managing API calls. The `./api` folder includes a fully set up example for GET and POST requests, complete with schemas, definitions, and global error handling through a custom Zodius plugin.
+A pre-configured Orval setup generates a typed API client and TanStack Query hooks from your OpenAPI spec. The `api/generated` folder contains endpoints, models, and optional MSW mocks, powered by a custom Axios mutator and React Query.
+
+```bash
+# Generate the client from your OpenAPI schema
+yarn gen-api
+```
```typescript
-import { Zodios } from '@zodios/core';
-import { ZodiosHooks } from '@zodios/react';
-import apiErrorPlugin from './api-error-plugin';
-import exampleApi from './example';
+// Use generated React Query hooks
+import { useGetRandomFact, useGetFacts } from 'api/generated/endpoints';
-const API_URL = process.env.EXPO_PUBLIC_API_URL || '';
+const { data, isLoading, error } = useGetRandomFact({ max_length: 140 });
+```
-// Zodios API client
-const apiClient = new Zodios(API_URL, [...exampleApi]);
+```typescript
+// Imperative request (without a hook)
+import { getRandomFact } from 'api/generated/endpoints';
-// Apply global error handling
-apiClient.use(apiErrorPlugin);
+const { data } = await getRandomFact({ max_length: 140 });
+```
-// Zodios hooks for react
-const api = new ZodiosHooks('exampleApi', apiClient);
+```typescript
+// Global headers and base URL are configured via Axios
+// Base URL: env.EXPO_PUBLIC_API_URL (see api/axios-instance.ts)
+// Headers: injected by ApiProvider (see utils/providers/api-provider.ts)
+import { ApiProvider } from '@utils/providers/api-provider';
-export { api, apiClient };
+// Wrap your app (e.g., in your root layout)
+{children};
```
@@ -623,55 +631,6 @@ import { WithValidationError } from '@components/ValidationError';
The `ValidationError` and `WithValidationError` components help maintain a clean UI by only showing error messages when necessary, enhancing the user experience with clear feedback.
-
-
-## Toaster 🍞
-
-The `Toaster` component is a dynamic and interactive toast notification system designed to provide immediate feedback to users. It's connected to a store for global state management and comes with an API plugin for automatic display on API events.
-
-### Component Features
-
-- **Gesture Support**: Users can dismiss the toast by dragging it down, thanks to the integrated gesture handler.
-- **Animated Visibility**: Uses `react-native-reanimated` for smooth show and hide animations.
-- **Safe Area Handling**: Accounts for device safe areas, ensuring the toast is always visible and accessible.
-- **Custom Icons**: Displays icons for error, success, or information based on the toast type.
-
-### How It Works
-
-The `Toaster` component listens to the toast state from `useToastStore`. When a toast is set, it animates into view. It can be dismissed with a drag gesture or by pressing the 'Dismiss' button.
-
-### Usage
-
-The `Toaster` component does not need to be manually managed; it works by setting the toast state through the `useToastStore` actions:
-
-```javascript
-useToastStore.getState().setToast({
- type: 'success',
- message: 'Your changes have been saved!',
-});
-```
-
-### Customizing the Toaster
-
-While the `Toaster` itself does not require props, you can customize the animations and styles directly within the component's file if needed.
-
-### API Integration
-
-`apiToastPlugin` is set up to automatically display toasts in response to API calls, making use of the `ZodiosPlugin` system. It provides feedback for errors and successes, skipping certain URLs or GET requests as configured.
-
-### Example of Plugin Usage
-
-Simply add the `apiToastPlugin` to your Zodios API client configuration:
-
-```javascript
-const apiClient = new Zodios(API_URL, [
- /* ...endpoints */
-]);
-apiClient.use(apiToastPlugin);
-```
-
-The `Toaster` provides a smooth, user-friendly notification mechanism that enhances the interactivity of the application, keeping users informed with minimal disruption.
-
## More Components Comming Soon... 🎉
Stay tuned for more components and features that will be added to the template in the future. We're committed to providing a comprehensive set of tools and solutions to help you build your mobile applications with ease.
diff --git a/api/api-error-plugin.ts b/api/api-error-plugin.ts
deleted file mode 100644
index 073827a..0000000
--- a/api/api-error-plugin.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { ZodiosPlugin } from '@zodios/core';
-import { AxiosError } from 'axios';
-
-const SKIP_ERROR_HANDLING_URLS = ['/example/skip-error-handling'];
-
-const errorErrorPlugin: ZodiosPlugin = {
- name: 'errorErrorPlugin',
- error: async (api, config, err) => {
- if (SKIP_ERROR_HANDLING_URLS.includes(config.url)) {
- console.log('Skipping error handling for', config.url);
- throw err;
- }
-
- if (err instanceof AxiosError) {
- console.error('AxiosError', err);
- }
-
- throw err;
- },
-};
-
-export default errorErrorPlugin;
diff --git a/api/api-toast-plugin.ts b/api/api-toast-plugin.ts
deleted file mode 100644
index d3681f0..0000000
--- a/api/api-toast-plugin.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import useToastStore from '@utils/stores/toast-store';
-import { ZodiosPlugin } from '@zodios/core';
-import { AxiosError } from 'axios';
-import i18n from 'i18next';
-
-const SKIP_ERROR_HANDLING_URLS = [''];
-const SKIP_SUCCESS_HANDLING_URLS = [''];
-
-const apiToastPlugin: ZodiosPlugin = {
- name: 'apiToastPlugin',
- error: async (api, config, err) => {
- if (SKIP_ERROR_HANDLING_URLS.includes(config.url)) {
- console.log('Skipping error handling for', config.url);
- throw err;
- }
-
- if (err instanceof AxiosError) {
- useToastStore.getState().setToast({
- type: 'error',
- message:
- err.response?.data?.message || i18n.t('common:apiErrorDescription'),
- });
- }
-
- throw err;
- },
- response: async (api, config, response) => {
- if (SKIP_SUCCESS_HANDLING_URLS.includes(config.url)) {
- console.log('Skipping success handling for', config.url);
- return response;
- }
-
- // Skip handling GET requests
- if (config.method?.toUpperCase() === 'GET') {
- return response;
- }
-
- const getMessage = () => {
- let message = '';
- switch (config.method?.toUpperCase()) {
- case 'POST':
- message = i18n.t('common:createSuccess');
- break;
- case 'PUT':
- message = i18n.t('common:updateSuccess');
- break;
- case 'DELETE':
- message = i18n.t('common:deleteSuccess');
- break;
- }
-
- return message;
- };
-
- if (response.status >= 200 && response.status < 300) {
- useToastStore.getState().setToast({
- type: 'success',
- message: getMessage(),
- });
- }
-
- return response;
- },
-};
-
-export default apiToastPlugin;
diff --git a/api/api-token-plugin.ts b/api/api-token-plugin.ts
deleted file mode 100644
index 1dbe9bb..0000000
--- a/api/api-token-plugin.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { ZodiosPlugin } from '@zodios/core';
-
-/**
- * Custom plugin for Zodios to inject API key into request headers
- */
-const apiTokenPlugin = (): ZodiosPlugin => {
- return {
- request: async (_, config) => {
- /**
- * You should implement your own logic to get the auth token
- * @example const { data } = await supabase.auth.getSession();
- * @example const authToken = data.session?.access_token;
- */
- const authToken = 'fake-token';
-
- return {
- ...config,
- headers: {
- ...config.headers,
- ...(authToken && { Authorization: `Bearer ${authToken}` }),
- },
- };
- },
- };
-};
-
-export default apiTokenPlugin;
diff --git a/api/api.ts b/api/api.ts
deleted file mode 100644
index 692493e..0000000
--- a/api/api.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { Zodios } from '@zodios/core';
-import { ZodiosHooks } from '@zodios/react';
-import apiErrorPlugin from './api-error-plugin';
-import exampleApi from './example';
-
-const API_URL = process.env.EXPO_PUBLIC_API_URL || '';
-
-// Zodios API client
-const apiClient = new Zodios(API_URL, [...exampleApi]);
-
-apiClient.use(apiErrorPlugin);
-const api = new ZodiosHooks('exampleApi', apiClient);
-
-export { api, apiClient };
diff --git a/api/axios-instance.ts b/api/axios-instance.ts
new file mode 100644
index 0000000..1c154bf
--- /dev/null
+++ b/api/axios-instance.ts
@@ -0,0 +1,35 @@
+import Axios, {
+ type AxiosError,
+ type AxiosRequestConfig,
+ type AxiosResponse,
+} from 'axios';
+import env from '../env';
+
+export const AXIOS_INSTANCE = Axios.create({
+ baseURL: env.EXPO_PUBLIC_API_URL,
+});
+
+// add a second `options` argument here if you want to pass extra options to each generated query
+export const customAxios = (
+ config: AxiosRequestConfig,
+ options?: AxiosRequestConfig,
+): Promise> => {
+ const source = Axios.CancelToken.source();
+ const promise = AXIOS_INSTANCE({
+ ...config,
+ ...options,
+ cancelToken: source.token,
+ }).then((data) => data);
+
+ // @ts-expect-error: The cancel method is not typed.
+ promise.cancel = () => {
+ source.cancel('Query was cancelled');
+ };
+
+ return promise;
+};
+
+// In some case with react-query and swr you want to be able to override the return error type so you can also do it here like this
+export type ErrorType = AxiosError;
+
+export type BodyType = BodyData;
diff --git a/api/example/index.ts b/api/example/index.ts
deleted file mode 100644
index 98a2c91..0000000
--- a/api/example/index.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { apiBuilder } from '@zodios/core';
-import { z } from 'zod';
-
-// Endpoints for Example API - Example Endpoints.
-const exampleApi = apiBuilder({
- method: 'get',
- path: '/example',
- alias: 'getExample',
- description: 'Get example',
- response: z.object({
- text: z.string(),
- }),
- parameters: [
- {
- type: 'Query',
- name: 'name',
- description: 'User name',
- schema: z.string().optional(),
- },
- ],
- errors: [{ status: 'default', schema: z.object({ message: z.string() }) }],
-})
- .addEndpoint({
- method: 'post',
- path: '/example/:exampleId',
- description: 'Add example',
- alias: 'addExample',
- response: z.object({}),
- parameters: [
- {
- name: 'exampleId',
- type: 'Path',
- schema: z.string(),
- },
- {
- name: 'body',
- type: 'Body',
- schema: z.object({
- name: z.string(),
- }),
- },
- ],
- errors: [{ status: 'default', schema: z.object({ message: z.string() }) }],
- })
- .build();
-
-export default exampleApi;
diff --git a/api/generated/endpoints.ts b/api/generated/endpoints.ts
new file mode 100644
index 0000000..1d8c775
--- /dev/null
+++ b/api/generated/endpoints.ts
@@ -0,0 +1,443 @@
+/**
+ * Generated by orval v7.10.0 🍺
+ * Do not edit manually.
+ * Cat Fact API
+ * An API for facts about cats
+ * OpenAPI spec version: 1.0.0
+ */
+import type {
+ DataTag,
+ DefinedInitialDataOptions,
+ DefinedUseQueryResult,
+ QueryClient,
+ QueryFunction,
+ QueryKey,
+ UndefinedInitialDataOptions,
+ UseQueryOptions,
+ UseQueryResult,
+} from '@tanstack/react-query';
+import { useQuery } from '@tanstack/react-query';
+
+import type {
+ Breed,
+ CatFact,
+ GetBreedsParams,
+ GetFactsParams,
+ GetRandomFactParams,
+} from './model';
+
+import type { ErrorType } from '../axios-instance';
+import { customAxios } from '../axios-instance';
+
+type SecondParameter unknown> = Parameters[1];
+
+/**
+ * Returns a a list of breeds
+ * @summary Get a list of breeds
+ */
+export const getBreeds = (
+ params?: GetBreedsParams,
+ options?: SecondParameter,
+ signal?: AbortSignal,
+) => {
+ return customAxios(
+ { url: `/breeds`, method: 'GET', params, signal },
+ options,
+ );
+};
+
+export const getGetBreedsQueryKey = (params?: GetBreedsParams) => {
+ return [`/breeds`, ...(params ? [params] : [])] as const;
+};
+
+export const getGetBreedsQueryOptions = <
+ TData = Awaited>,
+ TError = ErrorType,
+>(
+ params?: GetBreedsParams,
+ options?: {
+ query?: Partial<
+ UseQueryOptions>, TError, TData>
+ >;
+ request?: SecondParameter;
+ },
+) => {
+ const { query: queryOptions, request: requestOptions } = options ?? {};
+
+ const queryKey = queryOptions?.queryKey ?? getGetBreedsQueryKey(params);
+
+ const queryFn: QueryFunction>> = ({
+ signal,
+ }) => getBreeds(params, requestOptions, signal);
+
+ return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
+ Awaited>,
+ TError,
+ TData
+ > & { queryKey: DataTag };
+};
+
+export type GetBreedsQueryResult = NonNullable<
+ Awaited>
+>;
+export type GetBreedsQueryError = ErrorType;
+
+export function useGetBreeds<
+ TData = Awaited>,
+ TError = ErrorType,
+>(
+ params: undefined | GetBreedsParams,
+ options: {
+ query: Partial<
+ UseQueryOptions>, TError, TData>
+ > &
+ Pick<
+ DefinedInitialDataOptions<
+ Awaited>,
+ TError,
+ Awaited>
+ >,
+ 'initialData'
+ >;
+ request?: SecondParameter;
+ },
+ queryClient?: QueryClient,
+): DefinedUseQueryResult & {
+ queryKey: DataTag;
+};
+export function useGetBreeds<
+ TData = Awaited>,
+ TError = ErrorType,
+>(
+ params?: GetBreedsParams,
+ options?: {
+ query?: Partial<
+ UseQueryOptions>, TError, TData>
+ > &
+ Pick<
+ UndefinedInitialDataOptions<
+ Awaited>,
+ TError,
+ Awaited>
+ >,
+ 'initialData'
+ >;
+ request?: SecondParameter;
+ },
+ queryClient?: QueryClient,
+): UseQueryResult & { queryKey: DataTag };
+export function useGetBreeds<
+ TData = Awaited>,
+ TError = ErrorType,
+>(
+ params?: GetBreedsParams,
+ options?: {
+ query?: Partial<
+ UseQueryOptions>, TError, TData>
+ >;
+ request?: SecondParameter;
+ },
+ queryClient?: QueryClient,
+): UseQueryResult & { queryKey: DataTag };
+/**
+ * @summary Get a list of breeds
+ */
+
+export function useGetBreeds<
+ TData = Awaited>,
+ TError = ErrorType,
+>(
+ params?: GetBreedsParams,
+ options?: {
+ query?: Partial<
+ UseQueryOptions>, TError, TData>
+ >;
+ request?: SecondParameter;
+ },
+ queryClient?: QueryClient,
+): UseQueryResult & { queryKey: DataTag } {
+ const queryOptions = getGetBreedsQueryOptions(params, options);
+
+ const query = useQuery(queryOptions, queryClient) as UseQueryResult<
+ TData,
+ TError
+ > & { queryKey: DataTag };
+
+ query.queryKey = queryOptions.queryKey;
+
+ return query;
+}
+
+/**
+ * Returns a random fact
+ * @summary Get Random Fact
+ */
+export const getRandomFact = (
+ params?: GetRandomFactParams,
+ options?: SecondParameter,
+ signal?: AbortSignal,
+) => {
+ return customAxios(
+ { url: `/fact`, method: 'GET', params, signal },
+ options,
+ );
+};
+
+export const getGetRandomFactQueryKey = (params?: GetRandomFactParams) => {
+ return [`/fact`, ...(params ? [params] : [])] as const;
+};
+
+export const getGetRandomFactQueryOptions = <
+ TData = Awaited>,
+ TError = ErrorType,
+>(
+ params?: GetRandomFactParams,
+ options?: {
+ query?: Partial<
+ UseQueryOptions>, TError, TData>
+ >;
+ request?: SecondParameter;
+ },
+) => {
+ const { query: queryOptions, request: requestOptions } = options ?? {};
+
+ const queryKey = queryOptions?.queryKey ?? getGetRandomFactQueryKey(params);
+
+ const queryFn: QueryFunction>> = ({
+ signal,
+ }) => getRandomFact(params, requestOptions, signal);
+
+ return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
+ Awaited>,
+ TError,
+ TData
+ > & { queryKey: DataTag };
+};
+
+export type GetRandomFactQueryResult = NonNullable<
+ Awaited>
+>;
+export type GetRandomFactQueryError = ErrorType;
+
+export function useGetRandomFact<
+ TData = Awaited>,
+ TError = ErrorType,
+>(
+ params: undefined | GetRandomFactParams,
+ options: {
+ query: Partial<
+ UseQueryOptions>, TError, TData>
+ > &
+ Pick<
+ DefinedInitialDataOptions<
+ Awaited>,
+ TError,
+ Awaited>
+ >,
+ 'initialData'
+ >;
+ request?: SecondParameter;
+ },
+ queryClient?: QueryClient,
+): DefinedUseQueryResult & {
+ queryKey: DataTag;
+};
+export function useGetRandomFact<
+ TData = Awaited>,
+ TError = ErrorType,
+>(
+ params?: GetRandomFactParams,
+ options?: {
+ query?: Partial<
+ UseQueryOptions>, TError, TData>
+ > &
+ Pick<
+ UndefinedInitialDataOptions<
+ Awaited>,
+ TError,
+ Awaited>
+ >,
+ 'initialData'
+ >;
+ request?: SecondParameter;
+ },
+ queryClient?: QueryClient,
+): UseQueryResult & { queryKey: DataTag };
+export function useGetRandomFact<
+ TData = Awaited>,
+ TError = ErrorType,
+>(
+ params?: GetRandomFactParams,
+ options?: {
+ query?: Partial<
+ UseQueryOptions>, TError, TData>
+ >;
+ request?: SecondParameter;
+ },
+ queryClient?: QueryClient,
+): UseQueryResult & { queryKey: DataTag };
+/**
+ * @summary Get Random Fact
+ */
+
+export function useGetRandomFact<
+ TData = Awaited>,
+ TError = ErrorType,
+>(
+ params?: GetRandomFactParams,
+ options?: {
+ query?: Partial<
+ UseQueryOptions>, TError, TData>
+ >;
+ request?: SecondParameter;
+ },
+ queryClient?: QueryClient,
+): UseQueryResult & { queryKey: DataTag } {
+ const queryOptions = getGetRandomFactQueryOptions(params, options);
+
+ const query = useQuery(queryOptions, queryClient) as UseQueryResult<
+ TData,
+ TError
+ > & { queryKey: DataTag };
+
+ query.queryKey = queryOptions.queryKey;
+
+ return query;
+}
+
+/**
+ * Returns a a list of facts
+ * @summary Get a list of facts
+ */
+export const getFacts = (
+ params?: GetFactsParams,
+ options?: SecondParameter,
+ signal?: AbortSignal,
+) => {
+ return customAxios(
+ { url: `/facts`, method: 'GET', params, signal },
+ options,
+ );
+};
+
+export const getGetFactsQueryKey = (params?: GetFactsParams) => {
+ return [`/facts`, ...(params ? [params] : [])] as const;
+};
+
+export const getGetFactsQueryOptions = <
+ TData = Awaited>,
+ TError = ErrorType,
+>(
+ params?: GetFactsParams,
+ options?: {
+ query?: Partial<
+ UseQueryOptions>, TError, TData>
+ >;
+ request?: SecondParameter;
+ },
+) => {
+ const { query: queryOptions, request: requestOptions } = options ?? {};
+
+ const queryKey = queryOptions?.queryKey ?? getGetFactsQueryKey(params);
+
+ const queryFn: QueryFunction>> = ({
+ signal,
+ }) => getFacts(params, requestOptions, signal);
+
+ return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
+ Awaited>,
+ TError,
+ TData
+ > & { queryKey: DataTag };
+};
+
+export type GetFactsQueryResult = NonNullable<
+ Awaited>
+>;
+export type GetFactsQueryError = ErrorType;
+
+export function useGetFacts<
+ TData = Awaited>,
+ TError = ErrorType,
+>(
+ params: undefined | GetFactsParams,
+ options: {
+ query: Partial<
+ UseQueryOptions>, TError, TData>
+ > &
+ Pick<
+ DefinedInitialDataOptions<
+ Awaited>,
+ TError,
+ Awaited>
+ >,
+ 'initialData'
+ >;
+ request?: SecondParameter;
+ },
+ queryClient?: QueryClient,
+): DefinedUseQueryResult & {
+ queryKey: DataTag;
+};
+export function useGetFacts<
+ TData = Awaited>,
+ TError = ErrorType,
+>(
+ params?: GetFactsParams,
+ options?: {
+ query?: Partial<
+ UseQueryOptions>, TError, TData>
+ > &
+ Pick<
+ UndefinedInitialDataOptions<
+ Awaited>,
+ TError,
+ Awaited>
+ >,
+ 'initialData'
+ >;
+ request?: SecondParameter;
+ },
+ queryClient?: QueryClient,
+): UseQueryResult & { queryKey: DataTag };
+export function useGetFacts<
+ TData = Awaited>,
+ TError = ErrorType,
+>(
+ params?: GetFactsParams,
+ options?: {
+ query?: Partial<
+ UseQueryOptions>, TError, TData>
+ >;
+ request?: SecondParameter;
+ },
+ queryClient?: QueryClient,
+): UseQueryResult & { queryKey: DataTag };
+/**
+ * @summary Get a list of facts
+ */
+
+export function useGetFacts<
+ TData = Awaited>,
+ TError = ErrorType,
+>(
+ params?: GetFactsParams,
+ options?: {
+ query?: Partial<
+ UseQueryOptions>, TError, TData>
+ >;
+ request?: SecondParameter;
+ },
+ queryClient?: QueryClient,
+): UseQueryResult & { queryKey: DataTag } {
+ const queryOptions = getGetFactsQueryOptions(params, options);
+
+ const query = useQuery(queryOptions, queryClient) as UseQueryResult<
+ TData,
+ TError
+ > & { queryKey: DataTag };
+
+ query.queryKey = queryOptions.queryKey;
+
+ return query;
+}
diff --git a/api/generated/model/breed.ts b/api/generated/model/breed.ts
new file mode 100644
index 0000000..cd4a140
--- /dev/null
+++ b/api/generated/model/breed.ts
@@ -0,0 +1,23 @@
+/**
+ * Generated by orval v7.10.0 🍺
+ * Do not edit manually.
+ * Cat Fact API
+ * An API for facts about cats
+ * OpenAPI spec version: 1.0.0
+ */
+
+/**
+ * Breed
+ */
+export interface Breed {
+ /** Breed */
+ breed?: string;
+ /** Country */
+ country?: string;
+ /** Origin */
+ origin?: string;
+ /** Coat */
+ coat?: string;
+ /** Pattern */
+ pattern?: string;
+}
diff --git a/api/generated/model/catFact.ts b/api/generated/model/catFact.ts
new file mode 100644
index 0000000..2da3f3a
--- /dev/null
+++ b/api/generated/model/catFact.ts
@@ -0,0 +1,17 @@
+/**
+ * Generated by orval v7.10.0 🍺
+ * Do not edit manually.
+ * Cat Fact API
+ * An API for facts about cats
+ * OpenAPI spec version: 1.0.0
+ */
+
+/**
+ * CatFact
+ */
+export interface CatFact {
+ /** Fact */
+ fact?: string;
+ /** Length */
+ length?: number;
+}
diff --git a/api/generated/model/getBreedsParams.ts b/api/generated/model/getBreedsParams.ts
new file mode 100644
index 0000000..238a417
--- /dev/null
+++ b/api/generated/model/getBreedsParams.ts
@@ -0,0 +1,14 @@
+/**
+ * Generated by orval v7.10.0 🍺
+ * Do not edit manually.
+ * Cat Fact API
+ * An API for facts about cats
+ * OpenAPI spec version: 1.0.0
+ */
+
+export type GetBreedsParams = {
+ /**
+ * limit the amount of results returned
+ */
+ limit?: number;
+};
diff --git a/api/generated/model/getFactsParams.ts b/api/generated/model/getFactsParams.ts
new file mode 100644
index 0000000..d5eee93
--- /dev/null
+++ b/api/generated/model/getFactsParams.ts
@@ -0,0 +1,18 @@
+/**
+ * Generated by orval v7.10.0 🍺
+ * Do not edit manually.
+ * Cat Fact API
+ * An API for facts about cats
+ * OpenAPI spec version: 1.0.0
+ */
+
+export type GetFactsParams = {
+ /**
+ * maximum length of returned fact
+ */
+ max_length?: number;
+ /**
+ * limit the amount of results returned
+ */
+ limit?: number;
+};
diff --git a/api/generated/model/getRandomFactParams.ts b/api/generated/model/getRandomFactParams.ts
new file mode 100644
index 0000000..b7d1b7d
--- /dev/null
+++ b/api/generated/model/getRandomFactParams.ts
@@ -0,0 +1,14 @@
+/**
+ * Generated by orval v7.10.0 🍺
+ * Do not edit manually.
+ * Cat Fact API
+ * An API for facts about cats
+ * OpenAPI spec version: 1.0.0
+ */
+
+export type GetRandomFactParams = {
+ /**
+ * maximum length of returned fact
+ */
+ max_length?: number;
+};
diff --git a/api/generated/model/index.ts b/api/generated/model/index.ts
new file mode 100644
index 0000000..e0281d2
--- /dev/null
+++ b/api/generated/model/index.ts
@@ -0,0 +1,13 @@
+/**
+ * Generated by orval v7.10.0 🍺
+ * Do not edit manually.
+ * Cat Fact API
+ * An API for facts about cats
+ * OpenAPI spec version: 1.0.0
+ */
+
+export * from './breed';
+export * from './catFact';
+export * from './getBreedsParams';
+export * from './getFactsParams';
+export * from './getRandomFactParams';
diff --git a/api/generated/types.ts b/api/generated/types.ts
new file mode 100644
index 0000000..e88d66b
--- /dev/null
+++ b/api/generated/types.ts
@@ -0,0 +1,71 @@
+/**
+ * Generated by orval v7.10.0 🍺
+ * Do not edit manually.
+ * Cat Fact API
+ * An API for facts about cats
+ * OpenAPI spec version: 1.0.0
+ */
+import { z as zod } from 'zod';
+
+/**
+ * Returns a a list of breeds
+ * @summary Get a list of breeds
+ */
+export const getBreedsQueryParams = zod.object({
+ limit: zod
+ .number()
+ .optional()
+ .describe('limit the amount of results returned'),
+});
+
+export const getBreedsResponseItem = zod
+ .object({
+ breed: zod.string().optional().describe('Breed'),
+ country: zod.string().optional().describe('Country'),
+ origin: zod.string().optional().describe('Origin'),
+ coat: zod.string().optional().describe('Coat'),
+ pattern: zod.string().optional().describe('Pattern'),
+ })
+ .describe('Breed');
+export const getBreedsResponse = zod.array(getBreedsResponseItem);
+
+/**
+ * Returns a random fact
+ * @summary Get Random Fact
+ */
+export const getRandomFactQueryParams = zod.object({
+ max_length: zod
+ .number()
+ .optional()
+ .describe('maximum length of returned fact'),
+});
+
+export const getRandomFactResponse = zod
+ .object({
+ fact: zod.string().optional().describe('Fact'),
+ length: zod.number().optional().describe('Length'),
+ })
+ .describe('CatFact');
+
+/**
+ * Returns a a list of facts
+ * @summary Get a list of facts
+ */
+export const getFactsQueryParams = zod.object({
+ max_length: zod
+ .number()
+ .optional()
+ .describe('maximum length of returned fact'),
+ limit: zod
+ .number()
+ .optional()
+ .describe('limit the amount of results returned'),
+});
+
+export const getFactsResponseItem = zod
+ .object({
+ fact: zod.string().optional().describe('Fact'),
+ length: zod.number().optional().describe('Length'),
+ })
+ .describe('CatFact');
+export const getFactsResponse = zod.array(getFactsResponseItem);
diff --git a/app/(authenticated)/index.tsx b/app/(authenticated)/index.tsx
index ba44100..aec17aa 100644
--- a/app/(authenticated)/index.tsx
+++ b/app/(authenticated)/index.tsx
@@ -5,6 +5,7 @@ import Routes from '@utils/routes';
import { useExampleStore } from '@utils/stores/example-store';
import useToastStore from '@utils/stores/toast-store';
import theme from '@utils/theme';
+import { useGetRandomFact } from 'api/generated/endpoints';
import { router } from 'expo-router';
import { MinusIcon, PlusIcon } from 'lucide-react-native';
import { Text, View } from 'react-native';
@@ -13,6 +14,10 @@ const AuthHomeScreen = () => {
const { value, increment, decrement } = useExampleStore();
const { setToast } = useToastStore();
+ const { data, isLoading, error, refetch, isFetching } = useGetRandomFact({
+ max_length: 140,
+ });
+
return (
{
}}>
Authenticated Home
- {value}
-
-
+
+
+ {value}
+
+
+
+ Random Cat Fact
+ {isLoading ? (
+
+ ) : error ? (
+ Unable to load a cat fact.
+ ) : (
+ {data?.data?.fact}
+ )}
+
+
-
diff --git a/app/_layout.tsx b/app/_layout.tsx
index fbdabd7..94e97ee 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -3,6 +3,7 @@ import * as Sentry from '@sentry/react-native';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import useCustomFonts from '@utils/hooks/use-custom-fonts';
import '@utils/i18n/config';
+import { ApiProvider } from '@utils/providers/api-provider';
import { isRunningInExpoGo } from 'expo';
import { Slot, SplashScreen, useNavigationContainerRef } from 'expo-router';
import { useEffect } from 'react';
@@ -47,8 +48,10 @@ const RootLayout = () => {
return (
-
-
+
+
+
+
);
};
diff --git a/orval.config.ts b/orval.config.ts
index 3b81f0c..a8750d7 100644
--- a/orval.config.ts
+++ b/orval.config.ts
@@ -8,7 +8,7 @@ export default defineConfig({
schemas: 'api/generated/model',
client: 'react-query',
clean: true,
- mock: true,
+ mock: false,
prettier: true,
override: {
mutator: {
@@ -22,4 +22,16 @@ export default defineConfig({
target: './placeholder.yaml',
},
},
+ apiZod: {
+ output: {
+ mode: 'split',
+ client: 'zod',
+ prettier: true,
+ target: 'api/generated/types.ts',
+ },
+ input: {
+ // This will get overridden by /scripts/generate-api.ts
+ target: './placeholder.yaml',
+ },
+ },
});
diff --git a/package.json b/package.json
index 76d757e..b72c6a4 100644
--- a/package.json
+++ b/package.json
@@ -4,13 +4,17 @@
"main": "index.js",
"scripts": {
"start": "infisical run -- expo start -c",
- "android": "infisical run -- expo start --android",
- "ios": "infisical run -- expo start --ios",
+ "android": "infisical run -- expo run:android --device",
+ "android:production": "expo prebuild --clean && eas build --platform android --profile production --local",
+ "android:preview": "expo prebuild && eas build --platform android --profile preview --local",
+ "ios": "infisical run -- expo run:ios --device",
+ "ios:production": "expo prebuild --clean && eas build --platform ios --profile production --local",
+ "ios:preview": "expo prebuild && eas build --platform ios --profile preview --local",
"web": "infisical run -- expo start --web -c",
"gen-api": "infisical run --command 'tsx ./scripts/generate-api.ts'",
"lint": "eslint .",
- "format-check": "prettier --check .",
- "format-fix": "prettier --write .",
+ "format:check": "prettier --check .",
+ "format:fix": "prettier --write .",
"prepare": "husky",
"eas-build-pre-install": "./scripts/infisical.sh"
},
@@ -19,12 +23,11 @@
"@sentry/react-native": "~6.14.0",
"@t3-oss/env-core": "^0.13.8",
"@tanstack/react-query": "^5.29.2",
- "@zodios/core": "^10.9.6",
- "@zodios/react": "^10.4.5",
"axios": "^1.6.8",
"clsx": "^2.1.0",
"exp": "^57.2.1",
"expo": "^53.0.13",
+ "expo-application": "~6.1.5",
"expo-constants": "~17.1.6",
"expo-font": "~13.3.1",
"expo-linear-gradient": "~14.1.5",
diff --git a/scripts/generate-api.ts b/scripts/generate-api.ts
new file mode 100644
index 0000000..8daa92f
--- /dev/null
+++ b/scripts/generate-api.ts
@@ -0,0 +1,8 @@
+import orval from 'orval';
+import env from '../env';
+
+const SCHEMA_NAME = '/docs?api-docs.json'; // Usually it's '/openapi.json';
+
+orval('orval.config.ts', 'react-native-template', {
+ input: env.EXPO_PUBLIC_API_URL + SCHEMA_NAME,
+});
diff --git a/scripts/zodios-client-template.hbs b/scripts/zodios-client-template.hbs
deleted file mode 100644
index 4f519fa..0000000
--- a/scripts/zodios-client-template.hbs
+++ /dev/null
@@ -1,88 +0,0 @@
-import { makeApi, Zodios } from "@zodios/core";
-import { z } from "zod";
-import apiErrorPlugin from './api-error-plugin';
-import apiTokenPlugin from './api-token-plugin';
-import apiToastPlugin from './api-toast-plugin';
-import { ZodiosHooks } from '@zodios/react';
-
-
-{{#each types}}
-{{{this}}};
-{{/each}}
-
-{{#each schemas}}
-const {{@key}}{{#if (lookup ../emittedType @key)}}: z.ZodType<{{@key}}>{{/if}} = {{{this}}};
-export type {{@key}} = z.infer;
-{{/each}}
-
-{{#ifNotEmptyObj schemas}}
-export const schemas = {
-{{#each schemas}}
- {{@key}},
-{{/each}}
-};
-{{/ifNotEmptyObj}}
-
-const endpoints = makeApi([
-{{#each endpoints}}
- {
- method: "{{method}}",
- path: "{{path}}",
- {{#if @root.options.withAlias}}
- {{#if alias}}
- alias: "{{alias}}",
- {{/if}}
- {{/if}}
- {{#if description}}
- description: `{{description}}`,
- {{/if}}
- {{#if requestFormat}}
- requestFormat: "{{requestFormat}}",
- {{/if}}
- {{#if parameters}}
- parameters: [
- {{#each parameters}}
- {
- name: "{{name}}",
- {{#if description}}
- description: `{{description}}`,
- {{/if}}
- {{#if type}}
- type: "{{type}}",
- {{/if}}
- schema: {{{schema}}}
- },
- {{/each}}
- ],
- {{/if}}
- response: {{{response}}},
- {{#if errors.length}}
- errors: [
- {{#each errors}}
- {
- {{#ifeq status "default" }}
- status: "default",
- {{else}}
- status: {{status}},
- {{/ifeq}}
- {{#if description}}
- description: `{{description}}`,
- {{/if}}
- schema: {{{schema}}}
- },
- {{/each}}
- ]
- {{/if}}
- },
-{{/each}}
-]);
-
-const API_URL = process.env.EXPO_PUBLIC_API_URL || '';
-
-const apiClient = new Zodios(API_URL, endpoints);
-apiClient.use(apiErrorPlugin);
-apiClient.use(apiTokenPlugin());
-apiClient.use(apiToastPlugin);
-const api = new ZodiosHooks('endpoints', apiClient);
-
-export { api, apiClient };
\ No newline at end of file
diff --git a/utils/providers/api-provider.ts b/utils/providers/api-provider.ts
new file mode 100644
index 0000000..8df6bcb
--- /dev/null
+++ b/utils/providers/api-provider.ts
@@ -0,0 +1,90 @@
+import { AXIOS_INSTANCE } from 'api/axios-instance';
+import { AxiosHeaders } from 'axios';
+import * as Application from 'expo-application';
+import { useRouter } from 'expo-router';
+import { PropsWithChildren, useEffect, useMemo } from 'react';
+import { Platform } from 'react-native';
+
+export const getApiHeaders = (accessToken?: string | null) => {
+ const headers = {
+ 'X-App-Version': Application.nativeApplicationVersion,
+ 'X-OS': Platform.OS,
+ };
+
+ if (accessToken) {
+ return {
+ Authorization: `Bearer ${accessToken}`,
+ ...headers,
+ };
+ }
+
+ return headers;
+};
+
+/**
+ * ApiProvider is a context wrapper that injects authentication and versioning
+ * headers into all outgoing API requests made through Axios.
+ *
+ * - Listens for Supabase auth state changes and updates the session token.
+ * - Automatically attaches the `Authorization` header (with the Supabase
+ * session access token) to outgoing requests.
+ * - Adds app metadata headers (`X-App-Version` and `X-OS`) to help the backend
+ * enforce version control and platform-specific behavior.
+ * - Intercepts API responses to handle specific status codes (e.g., HTTP 426
+ * for forced app updates), and redirects the user to the appropriate screen
+ * if necessary.
+ */
+export const ApiProvider = ({ children }: PropsWithChildren) => {
+ const router = useRouter();
+
+ /**
+ * You should implement your own logic to get the auth token
+ * @example const { data } = await supabase.auth.getSession();
+ * @example const accessToken = data.session?.access_token;
+ */
+ const accessToken = 'fake-token';
+
+ const authorizationHeaders = useMemo(
+ () => getApiHeaders(accessToken),
+ [accessToken],
+ );
+
+ // Add the authorization token and the app version to the API requests.
+ useEffect(() => {
+ const requestInterceptor = AXIOS_INSTANCE.interceptors.request.use(
+ async (config) => ({
+ ...config,
+ headers: new AxiosHeaders({
+ ...config.headers,
+ ...authorizationHeaders,
+ }),
+ }),
+ );
+
+ /**
+ * You should implement your own logic to handle errors here.
+ * @example
+ * // If the backend responds with a status code of 426, the app needs to
+ * // be updated. Here we redirect the user to the update required screen.
+ * if (error.response?.status === 426) {
+ * while (router.canGoBack()) {
+ * router.back();
+ * }
+ * router.replace('/update-required');
+ * }
+ */
+ const responseInterceptor = AXIOS_INSTANCE.interceptors.response.use(
+ (response) => response,
+ async (error) => {
+ return Promise.reject(error);
+ },
+ );
+
+ return () => {
+ AXIOS_INSTANCE.interceptors.request.eject(requestInterceptor);
+ AXIOS_INSTANCE.interceptors.response.eject(responseInterceptor);
+ };
+ }, [router, authorizationHeaders]);
+
+ return children;
+};
diff --git a/yarn.lock b/yarn.lock
index 554859c..433ef52 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3912,7 +3912,7 @@ __metadata:
languageName: node
linkType: hard
-"@zodios/core@npm:^10.3.1, @zodios/core@npm:^10.9.6":
+"@zodios/core@npm:^10.3.1":
version: 10.9.6
resolution: "@zodios/core@npm:10.9.6"
peerDependencies:
@@ -3922,17 +3922,6 @@ __metadata:
languageName: node
linkType: hard
-"@zodios/react@npm:^10.4.5":
- version: 10.4.5
- resolution: "@zodios/react@npm:10.4.5"
- peerDependencies:
- "@tanstack/react-query": 4.x
- "@zodios/core": ">=10.2.0 <11.0.0"
- react: ">=16.8.0"
- checksum: 10c0/782f7ad907c6737aa28c0aac53743239eb3a09f0e3921217195a612e80b7d53809e88eca464665748128613861e06a92c5130c1971901e56057f9aa05f6e56e4
- languageName: node
- linkType: hard
-
"JSONStream@npm:^1.3.4, JSONStream@npm:^1.3.5":
version: 1.3.5
resolution: "JSONStream@npm:1.3.5"
@@ -6772,6 +6761,15 @@ __metadata:
languageName: node
linkType: hard
+"expo-application@npm:~6.1.5":
+ version: 6.1.5
+ resolution: "expo-application@npm:6.1.5"
+ peerDependencies:
+ expo: "*"
+ checksum: 10c0/c4fa0bddfc911af17055334558314d819d403efa5db22b05cffc44c91eef38e9fb57b4a5aae35378523c59847189a7ca09ad9e5370ee5a3b0f23c1c5146c8683
+ languageName: node
+ linkType: hard
+
"expo-asset@npm:~11.1.5":
version: 11.1.5
resolution: "expo-asset@npm:11.1.5"
@@ -12571,8 +12569,6 @@ __metadata:
"@types/aes-js": "npm:^3.1.4"
"@types/react": "npm:~19.0.10"
"@types/react-dom": "npm:~18.2.25"
- "@zodios/core": "npm:^10.9.6"
- "@zodios/react": "npm:^10.4.5"
axios: "npm:^1.6.8"
clsx: "npm:^2.1.0"
eslint: "npm:8"
@@ -12580,6 +12576,7 @@ __metadata:
eslint-plugin-sonarjs: "npm:^1.0.3"
exp: "npm:^57.2.1"
expo: "npm:^53.0.13"
+ expo-application: "npm:~6.1.5"
expo-constants: "npm:~17.1.6"
expo-font: "npm:~13.3.1"
expo-linear-gradient: "npm:~14.1.5"