├── nativewind-env.d.ts ├── .example.env ├── app ├── assets │ ├── icon.png │ ├── splash.png │ ├── favicon.png │ ├── adaptive-icon.png │ └── images │ │ └── banner.png ├── components │ ├── screens │ │ ├── search │ │ │ ├── search.interface.ts │ │ │ ├── useSearchForm.ts │ │ │ ├── useSearch.ts │ │ │ └── Search.tsx │ │ ├── product │ │ │ ├── product-page.interface.ts │ │ │ ├── useProduct.ts │ │ │ ├── product-info │ │ │ │ ├── ProductInfo.tsx │ │ │ │ └── AddToCartButton.tsx │ │ │ ├── ProductHeader.tsx │ │ │ ├── ProductButton.tsx │ │ │ ├── Product.tsx │ │ │ └── favorite-button │ │ │ │ └── FavoriteButton.tsx │ │ ├── auth │ │ │ ├── email.regex.ts │ │ │ ├── AuthFields.tsx │ │ │ ├── useAuthMutations.ts │ │ │ └── Auth.tsx │ │ ├── profile │ │ │ ├── useProfile.ts │ │ │ └── Profile.tsx │ │ ├── explorer │ │ │ ├── useGetAllProducts.ts │ │ │ └── Explorer.tsx │ │ ├── home │ │ │ ├── products │ │ │ │ ├── useProducts.ts │ │ │ │ └── Products.tsx │ │ │ ├── categories │ │ │ │ ├── useGetAllCategories.ts │ │ │ │ └── Categories.tsx │ │ │ ├── Home.tsx │ │ │ ├── Header.tsx │ │ │ └── banner │ │ │ │ └── Banner.tsx │ │ ├── thanks │ │ │ └── Thanks.tsx │ │ ├── favorites │ │ │ └── Favorites.tsx │ │ ├── category │ │ │ ├── Category.tsx │ │ │ └── useCategory.ts │ │ └── cart │ │ │ ├── cart-item │ │ │ ├── CartActions.tsx │ │ │ └── CartItem.tsx │ │ │ ├── Cart.tsx │ │ │ └── useCheckout.ts │ ├── ui │ │ ├── button │ │ │ ├── button.interface.ts │ │ │ └── Button.tsx │ │ ├── catalog │ │ │ ├── catalog.interface.ts │ │ │ ├── product-item │ │ │ │ ├── ProductInfo.tsx │ │ │ │ └── ProductItem.tsx │ │ │ └── Catalog.tsx │ │ ├── Loader.tsx │ │ ├── field │ │ │ ├── field.interface.ts │ │ │ ├── DismissKeyboard.tsx │ │ │ └── Field.tsx │ │ ├── Heading.tsx │ │ └── Toast.tsx │ └── layout │ │ ├── bottom-menu │ │ ├── menu.interface.ts │ │ ├── menu.data.ts │ │ ├── MenuItem.tsx │ │ └── BottomMenu.tsx │ │ └── Layout.tsx ├── types │ ├── category.interface.ts │ ├── cart.interface.ts │ ├── icon.interface.ts │ ├── user.interface.ts │ ├── order.interface.ts │ ├── product.interface.ts │ └── auth.interface.ts ├── store │ ├── root-actions.ts │ ├── cart │ │ ├── cart.types.ts │ │ └── cart.slice.ts │ └── store.ts ├── utils │ ├── getMediaSource.ts │ └── convertPrice.ts ├── hooks │ ├── useAuth.ts │ ├── useTypedSelector.ts │ ├── useTypedNavigation.ts │ ├── useTypedRoute.ts │ ├── useCart.ts │ ├── useActions.ts │ └── useDebounce.ts ├── services │ ├── api │ │ ├── error.api.ts │ │ ├── request.api.ts │ │ ├── helper.auth.ts │ │ └── interceptors.api.ts │ ├── category.service.ts │ ├── user.service.ts │ ├── order.service.ts │ ├── product.service.ts │ └── auth │ │ ├── auth.service.ts │ │ └── auth.helper.ts ├── providers │ ├── auth-provder.interface.ts │ ├── AuthProvider.tsx │ └── useCheckAuth.ts ├── config │ └── api.config.ts └── navigation │ ├── navigation.types.ts │ ├── PrivateNavigator.tsx │ ├── routes.ts │ └── Navigation.tsx ├── tailwind.config.js ├── tsconfig.json ├── metro.config.js ├── babel.config.js ├── .gitignore ├── .prettierrc ├── app.json ├── package.json └── App.tsx /nativewind-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.example.env: -------------------------------------------------------------------------------- 1 | SERVER_URL=http://192.168.88.20:4200 2 | STRIPE_KEY= 3 | -------------------------------------------------------------------------------- /app/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KaplunMaxym/Delivery-app/HEAD/app/assets/icon.png -------------------------------------------------------------------------------- /app/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KaplunMaxym/Delivery-app/HEAD/app/assets/splash.png -------------------------------------------------------------------------------- /app/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KaplunMaxym/Delivery-app/HEAD/app/assets/favicon.png -------------------------------------------------------------------------------- /app/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KaplunMaxym/Delivery-app/HEAD/app/assets/adaptive-icon.png -------------------------------------------------------------------------------- /app/assets/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KaplunMaxym/Delivery-app/HEAD/app/assets/images/banner.png -------------------------------------------------------------------------------- /app/components/screens/search/search.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ISearchFormData { 2 | searchTerm: string 3 | } 4 | -------------------------------------------------------------------------------- /app/types/category.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ICategory { 2 | id: string 3 | name: string 4 | slug: string 5 | image: string 6 | } 7 | -------------------------------------------------------------------------------- /app/store/root-actions.ts: -------------------------------------------------------------------------------- 1 | import { cartSlice } from './cart/cart.slice' 2 | 3 | export const rootActions = { 4 | ...cartSlice.actions 5 | } 6 | -------------------------------------------------------------------------------- /app/utils/getMediaSource.ts: -------------------------------------------------------------------------------- 1 | import { SERVER_URL } from '@/config/api.config' 2 | 3 | export const getMediaSource = (path: string) => ({ 4 | uri: SERVER_URL + path 5 | }) 6 | -------------------------------------------------------------------------------- /app/components/ui/button/button.interface.ts: -------------------------------------------------------------------------------- 1 | import { PressableProps } from 'react-native' 2 | 3 | export interface IButton extends PressableProps { 4 | className?: string 5 | } 6 | -------------------------------------------------------------------------------- /app/components/screens/product/product-page.interface.ts: -------------------------------------------------------------------------------- 1 | import { IProduct } from '@/types/product.interface' 2 | 3 | export interface IProductComponent { 4 | product: IProduct 5 | } 6 | -------------------------------------------------------------------------------- /app/hooks/useAuth.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | 3 | import { AuthContext } from '@/providers/AuthProvider' 4 | 5 | export const useAuth = () => useContext(AuthContext) 6 | -------------------------------------------------------------------------------- /app/utils/convertPrice.ts: -------------------------------------------------------------------------------- 1 | export const convertPrice = (price: number) => { 2 | return price.toLocaleString('en-US', { 3 | style: 'currency', 4 | currency: 'USD' 5 | }) 6 | } 7 | -------------------------------------------------------------------------------- /app/components/ui/catalog/catalog.interface.ts: -------------------------------------------------------------------------------- 1 | import { IProduct } from '@/types/product.interface' 2 | 3 | export interface ICatalog { 4 | title?: string 5 | products: IProduct[] 6 | } 7 | -------------------------------------------------------------------------------- /app/types/cart.interface.ts: -------------------------------------------------------------------------------- 1 | import { IProduct } from './product.interface' 2 | 3 | export interface ICartItem { 4 | id: string 5 | product: IProduct 6 | quantity: number 7 | price: number 8 | } 9 | -------------------------------------------------------------------------------- /app/components/screens/auth/email.regex.ts: -------------------------------------------------------------------------------- 1 | export const validEmail = 2 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ 3 | -------------------------------------------------------------------------------- /app/hooks/useTypedSelector.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useSelector } from 'react-redux' 2 | 3 | import { TypeRootState } from '../store/store' 4 | 5 | export const useTypedSelector: TypedUseSelectorHook = useSelector 6 | -------------------------------------------------------------------------------- /app/types/icon.interface.ts: -------------------------------------------------------------------------------- 1 | import { Feather, MaterialIcons } from '@expo/vector-icons' 2 | 3 | export type TypeFeatherIconNames = keyof typeof Feather.glyphMap 4 | export type TypeMaterialIconNames = keyof typeof MaterialIcons.glyphMap 5 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./App.{js,jsx,ts,tsx}", "./app/**/*.{js,jsx,ts,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | } 9 | 10 | -------------------------------------------------------------------------------- /app/components/ui/Loader.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { ActivityIndicator } from 'react-native' 3 | 4 | const Loader: FC = () => { 5 | return 6 | } 7 | 8 | export default Loader 9 | -------------------------------------------------------------------------------- /app/types/user.interface.ts: -------------------------------------------------------------------------------- 1 | import { IProduct } from './product.interface' 2 | 3 | export interface IUser { 4 | id: string 5 | email: string 6 | password: string 7 | name: string 8 | avatarPath: string 9 | favorites: IProduct[] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "strict": true, 6 | "baseUrl": ".", 7 | "paths": { 8 | "@/*": ["./app/*"] 9 | } 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /app/types/order.interface.ts: -------------------------------------------------------------------------------- 1 | import { ICartItem } from './cart.interface' 2 | import { IUser } from './user.interface' 3 | 4 | export interface IOrder { 5 | id: string 6 | createdAt: string 7 | items: ICartItem[] 8 | user: IUser 9 | total: number 10 | } 11 | -------------------------------------------------------------------------------- /app/services/api/error.api.ts: -------------------------------------------------------------------------------- 1 | export const errorCatch = (error: any): string => { 2 | const message = error?.response?.data?.message 3 | 4 | return message 5 | ? typeof error.response.data.message === 'object' 6 | ? message[0] 7 | : message 8 | : error.message 9 | } 10 | -------------------------------------------------------------------------------- /app/hooks/useTypedNavigation.ts: -------------------------------------------------------------------------------- 1 | import { NavigationProp, useNavigation } from '@react-navigation/native' 2 | 3 | import { TypeRootStackParamList } from '@/navigation/navigation.types' 4 | 5 | export const useTypedNavigation = () => 6 | useNavigation>() 7 | -------------------------------------------------------------------------------- /app/hooks/useTypedRoute.ts: -------------------------------------------------------------------------------- 1 | import { RouteProp, useRoute } from '@react-navigation/native' 2 | 3 | import { TypeRootStackParamList } from '@/navigation/navigation.types' 4 | 5 | export const useTypedRoute = () => 6 | useRoute>() 7 | -------------------------------------------------------------------------------- /app/types/product.interface.ts: -------------------------------------------------------------------------------- 1 | import { ICategory } from './category.interface' 2 | 3 | export interface IProduct { 4 | id: string 5 | name: string 6 | slug: string 7 | description: string 8 | price: number 9 | image: string 10 | createdAt: string 11 | category: ICategory 12 | } 13 | -------------------------------------------------------------------------------- /app/providers/auth-provder.interface.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction } from 'react' 2 | 3 | import { IUser } from '@/types/user.interface' 4 | 5 | export type TypeUserState = IUser | null 6 | 7 | export interface IContext { 8 | user: TypeUserState 9 | setUser: Dispatch> 10 | } 11 | -------------------------------------------------------------------------------- /app/hooks/useCart.ts: -------------------------------------------------------------------------------- 1 | import { useTypedSelector } from './useTypedSelector' 2 | 3 | export const useCart = () => { 4 | const items = useTypedSelector(state => state.cart.items) 5 | 6 | const total = items.reduce( 7 | (acc, item) => acc + item.price * item.quantity, 8 | 0 9 | ) 10 | 11 | return { items, total } 12 | } 13 | -------------------------------------------------------------------------------- /app/store/cart/cart.types.ts: -------------------------------------------------------------------------------- 1 | import { ICartItem } from '@/types/cart.interface' 2 | 3 | export interface ICartInitialState { 4 | items: ICartItem[] 5 | } 6 | 7 | export interface IAddToCartPayload extends Omit {} 8 | 9 | export interface IChangeQuantityPayload extends Pick { 10 | type: 'minus' | 'plus' 11 | } 12 | -------------------------------------------------------------------------------- /app/components/screens/profile/useProfile.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query' 2 | 3 | import { UserService } from '@/services/user.service' 4 | 5 | export const useProfile = () => { 6 | const { data: profile } = useQuery({ 7 | queryKey: ['get profile'], 8 | queryFn: () => UserService.getProfile() 9 | }) 10 | 11 | return { profile } 12 | } 13 | -------------------------------------------------------------------------------- /app/hooks/useActions.ts: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from '@reduxjs/toolkit' 2 | import { useMemo } from 'react' 3 | import { useDispatch } from 'react-redux' 4 | 5 | import { rootActions } from '@/store/root-actions' 6 | 7 | export const useActions = () => { 8 | const dispatch = useDispatch() 9 | 10 | return useMemo(() => bindActionCreators(rootActions, dispatch), [dispatch]) 11 | } 12 | -------------------------------------------------------------------------------- /app/components/layout/bottom-menu/menu.interface.ts: -------------------------------------------------------------------------------- 1 | import { TypeFeatherIconNames } from '@/types/icon.interface' 2 | 3 | import { TypeRootStackParamList } from '@/navigation/navigation.types' 4 | 5 | export interface IMenuItem { 6 | iconName: TypeFeatherIconNames 7 | path: keyof TypeRootStackParamList 8 | } 9 | 10 | export type TypeNavigate = (screenName: keyof TypeRootStackParamList) => void 11 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require('expo/metro-config'); 2 | 3 | module.exports = (async () => { 4 | const defaultConfig = await getDefaultConfig(__dirname); 5 | 6 | return { 7 | ...defaultConfig, 8 | resolver: { 9 | ...defaultConfig.resolver, 10 | extraNodeModules: { 11 | ...defaultConfig.resolver.extraNodeModules, 12 | '@/': './app/', 13 | }, 14 | }, 15 | }; 16 | })(); 17 | -------------------------------------------------------------------------------- /app/components/screens/explorer/useGetAllProducts.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query' 2 | 3 | import { ProductService } from '@/services/product.service' 4 | 5 | export const useGetAllProducts = () => { 6 | const { data: products, isLoading } = useQuery({ 7 | queryKey: ['get all products'], 8 | queryFn: () => ProductService.getAll(), 9 | select: data => data 10 | }) 11 | 12 | return { products, isLoading } 13 | } 14 | -------------------------------------------------------------------------------- /app/components/screens/home/products/useProducts.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query' 2 | 3 | import { ProductService } from '@/services/product.service' 4 | 5 | export const useProducts = () => { 6 | const { data: products, isLoading } = useQuery({ 7 | queryKey: ['get products'], 8 | queryFn: () => ProductService.getAll(), 9 | select: data => data.slice(0, 4) 10 | }) 11 | 12 | return { products, isLoading } 13 | } 14 | -------------------------------------------------------------------------------- /app/components/screens/home/categories/useGetAllCategories.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query' 2 | 3 | import { CategoryService } from '@/services/category.service' 4 | 5 | export const useGetAllCategories = () => { 6 | const { data: categories, isLoading } = useQuery({ 7 | queryKey: ['get categories'], 8 | queryFn: () => CategoryService.getAll(), 9 | select: data => data 10 | }) 11 | 12 | return { categories, isLoading } 13 | } 14 | -------------------------------------------------------------------------------- /app/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | export const useDebounce = (value: T, delay: number): T => { 4 | const [debouncedValue, setDebouncedValue] = useState(value) 5 | 6 | useEffect(() => { 7 | const handler = setTimeout(() => { 8 | setDebouncedValue(value) 9 | }, delay) 10 | 11 | return () => { 12 | clearTimeout(handler) 13 | } 14 | }, [value, delay]) 15 | 16 | return debouncedValue 17 | } 18 | -------------------------------------------------------------------------------- /app/config/api.config.ts: -------------------------------------------------------------------------------- 1 | export const SERVER_URL = process.env.SERVER_URL 2 | export const API_URL = `${SERVER_URL}/api` 3 | 4 | export const getAuthUrl = (string: string) => `/auth${string}` 5 | export const getUsersUrl = (string: string) => `/users${string}` 6 | export const getProductsUrl = (string: string) => `/products${string}` 7 | export const getCategoriesUrl = (string: string) => `/categories${string}` 8 | export const getOrdersUrl = (string: string) => `/orders${string}` 9 | -------------------------------------------------------------------------------- /app/components/screens/thanks/Thanks.tsx: -------------------------------------------------------------------------------- 1 | import { FontAwesome } from '@expo/vector-icons' 2 | import { FC } from 'react' 3 | import { Text, View } from 'react-native' 4 | 5 | const Thanks: FC = () => { 6 | return ( 7 | 8 | 9 | Thank you! 10 | 11 | ) 12 | } 13 | 14 | export default Thanks 15 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: [ 6 | [ 7 | 'babel-plugin-root-import', 8 | { 9 | rootPathSuffix: 'app/', 10 | rootPathPrefix: '@/' 11 | } 12 | ], 13 | ['nativewind/babel'], 14 | ['inline-dotenv'], 15 | ] 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /app/components/layout/bottom-menu/menu.data.ts: -------------------------------------------------------------------------------- 1 | import { IMenuItem } from './menu.interface' 2 | 3 | export const menuItems: IMenuItem[] = [ 4 | { 5 | iconName: 'home', 6 | path: 'Home' 7 | }, 8 | { 9 | iconName: 'heart', 10 | path: 'Favorites' 11 | }, 12 | { 13 | iconName: 'search', 14 | path: 'Search' 15 | }, 16 | { 17 | iconName: 'shopping-bag', 18 | path: 'Explorer' 19 | }, 20 | { 21 | iconName: 'user', 22 | path: 'Profile' 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /app/components/screens/favorites/Favorites.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | 3 | import Layout from '@/components/layout/Layout' 4 | import Catalog from '@/components/ui/catalog/Catalog' 5 | 6 | import { useProfile } from '../profile/useProfile' 7 | 8 | const Favorites: FC = () => { 9 | const { profile } = useProfile() 10 | 11 | return ( 12 | 13 | 14 | 15 | ) 16 | } 17 | 18 | export default Favorites 19 | -------------------------------------------------------------------------------- /app/components/screens/home/products/Products.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | 3 | import Loader from '@/components/ui/Loader' 4 | import Catalog from '@/components/ui/catalog/Catalog' 5 | 6 | import { useProducts } from './useProducts' 7 | 8 | const Products: FC = () => { 9 | const { isLoading, products } = useProducts() 10 | 11 | return isLoading ? ( 12 | 13 | ) : ( 14 | 15 | ) 16 | } 17 | 18 | export default Products 19 | -------------------------------------------------------------------------------- /app/components/screens/home/Home.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | 3 | import Layout from '@/components/layout/Layout' 4 | 5 | import Header from './Header' 6 | import Banner from './banner/Banner' 7 | import Categories from './categories/Categories' 8 | import Products from './products/Products' 9 | 10 | const Home: FC = () => { 11 | return ( 12 | 13 |
14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | 21 | export default Home 22 | -------------------------------------------------------------------------------- /app/components/ui/field/field.interface.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Control, 3 | FieldPath, 4 | FieldValues, 5 | RegisterOptions 6 | } from 'react-hook-form' 7 | import { TextInputProps } from 'react-native' 8 | 9 | export interface IField 10 | extends Omit { 11 | control: Control 12 | name: FieldPath 13 | rules?: Omit< 14 | RegisterOptions>, 15 | 'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled' 16 | > 17 | } 18 | -------------------------------------------------------------------------------- /app/navigation/navigation.types.ts: -------------------------------------------------------------------------------- 1 | import { ComponentType } from 'react' 2 | 3 | export type TypeRootStackParamList = { 4 | Auth: undefined 5 | Home: undefined 6 | Favorites: undefined 7 | Search: undefined 8 | Explorer: undefined 9 | Profile: undefined 10 | Cart: undefined 11 | Thanks: undefined 12 | Category: { 13 | slug: string 14 | } 15 | Product: { 16 | slug: string 17 | } 18 | } 19 | 20 | export interface IRoute { 21 | name: keyof TypeRootStackParamList 22 | component: ComponentType 23 | } 24 | -------------------------------------------------------------------------------- /app/types/auth.interface.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from '@/types/user.interface' 2 | 3 | export interface IAuthFormData extends Pick {} 4 | 5 | export enum EnumSecureStore { 6 | ACCESS_TOKEN = 'accessToken', 7 | REFRESH_TOKEN = 'refreshToken' 8 | } 9 | 10 | export enum EnumAsyncStorage { 11 | USER = 'user' 12 | } 13 | 14 | export interface ITokens { 15 | accessToken: string 16 | refreshToken: string 17 | } 18 | 19 | export interface IAuthResponse extends ITokens { 20 | user: IUser 21 | } 22 | -------------------------------------------------------------------------------- /app/components/screens/product/useProduct.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query' 2 | 3 | import { useTypedRoute } from '@/hooks/useTypedRoute' 4 | 5 | import { ProductService } from '@/services/product.service' 6 | 7 | export const useProduct = () => { 8 | const { params } = useTypedRoute<'Product'>() 9 | 10 | const { isLoading, data: product } = useQuery({ 11 | queryKey: ['get product by slug', params.slug], 12 | queryFn: () => ProductService.getBySlug(params.slug) 13 | }) 14 | 15 | return { isLoading, product } 16 | } 17 | -------------------------------------------------------------------------------- /app/components/layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'clsx' 2 | import { FC, PropsWithChildren } from 'react' 3 | import { ScrollView, View } from 'react-native' 4 | 5 | interface ILayout { 6 | className?: string 7 | } 8 | 9 | const Layout: FC> = ({ children, className }) => { 10 | return ( 11 | 12 | 13 | {children} 14 | 15 | 16 | ) 17 | } 18 | 19 | export default Layout 20 | -------------------------------------------------------------------------------- /app/services/category.service.ts: -------------------------------------------------------------------------------- 1 | import { ICategory } from '@/types/category.interface' 2 | 3 | import { getCategoriesUrl } from '@/config/api.config' 4 | 5 | import { request } from './api/request.api' 6 | 7 | export const CategoryService = { 8 | async getAll() { 9 | return request({ 10 | url: getCategoriesUrl(''), 11 | method: 'GET' 12 | }) 13 | }, 14 | 15 | async getBySlug(slug: string) { 16 | return request({ 17 | url: getCategoriesUrl(`/by-slug/${slug}`), 18 | method: 'GET' 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from '@/types/user.interface' 2 | 3 | import { getUsersUrl } from '@/config/api.config' 4 | 5 | import { request } from './api/request.api' 6 | 7 | export const UserService = { 8 | async getProfile() { 9 | return request({ 10 | url: getUsersUrl('/profile'), 11 | method: 'GET' 12 | }) 13 | }, 14 | 15 | async toggleFavorite(productId: string) { 16 | return request({ 17 | url: getUsersUrl(`/profile/favorites/${productId}`), 18 | method: 'PATCH' 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # debug 23 | npm-debug.* 24 | yarn-debug.* 25 | yarn-error.* 26 | 27 | # macOS 28 | .DS_Store 29 | *.pem 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | .env 37 | /.idea 38 | /ios 39 | /android 40 | /.expo 41 | -------------------------------------------------------------------------------- /app/components/screens/search/useSearchForm.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { useForm } from 'react-hook-form' 3 | 4 | import { useDebounce } from '@/hooks/useDebounce' 5 | 6 | import { ISearchFormData } from './search.interface' 7 | 8 | export const useSearchForm = () => { 9 | const { control, watch } = useForm({ 10 | mode: 'onChange' 11 | }) 12 | 13 | const searchTerm = watch('searchTerm') 14 | const debouncedSearch = useDebounce(searchTerm, 500) 15 | 16 | return useMemo( 17 | () => ({ debouncedSearch, searchTerm, control }), 18 | [searchTerm] 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /app/components/screens/search/useSearch.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query' 2 | 3 | import { ProductService } from '@/services/product.service' 4 | 5 | import { useSearchForm } from './useSearchForm' 6 | 7 | export const useSearch = () => { 8 | const { searchTerm, debouncedSearch, control } = useSearchForm() 9 | 10 | const { data: products, isLoading } = useQuery({ 11 | queryKey: ['search products', debouncedSearch], 12 | queryFn: () => ProductService.getAll(debouncedSearch), 13 | enabled: !!debouncedSearch 14 | }) 15 | 16 | return { products, isLoading, control, searchTerm } 17 | } 18 | -------------------------------------------------------------------------------- /app/components/ui/Heading.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'clsx' 2 | import { FC, PropsWithChildren } from 'react' 3 | import { Text } from 'react-native' 4 | 5 | interface IHeading { 6 | isCenter?: boolean 7 | className?: string 8 | } 9 | 10 | const Heading: FC> = ({ 11 | children, 12 | isCenter = false, 13 | className 14 | }) => { 15 | return ( 16 | 23 | {children} 24 | 25 | ) 26 | } 27 | 28 | export default Heading 29 | -------------------------------------------------------------------------------- /app/components/screens/explorer/Explorer.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | 3 | import Layout from '@/components/layout/Layout' 4 | import Loader from '@/components/ui/Loader' 5 | import Catalog from '@/components/ui/catalog/Catalog' 6 | 7 | import { useGetAllProducts } from './useGetAllProducts' 8 | 9 | const Explorer: FC = () => { 10 | const { products, isLoading } = useGetAllProducts() 11 | 12 | return ( 13 | 14 | {isLoading ? ( 15 | 16 | ) : ( 17 | 18 | )} 19 | 20 | ) 21 | } 22 | 23 | export default Explorer 24 | -------------------------------------------------------------------------------- /app/components/ui/field/DismissKeyboard.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren } from 'react' 2 | import { 3 | Keyboard, 4 | TouchableWithoutFeedback, 5 | View, 6 | ViewProps 7 | } from 'react-native' 8 | 9 | const DismissKeyboard: FC> = ({ 10 | children, 11 | ...rest 12 | }) => { 13 | return ( 14 | 15 | 21 | {children} 22 | 23 | 24 | ) 25 | } 26 | 27 | export default DismissKeyboard 28 | -------------------------------------------------------------------------------- /app/services/api/request.api.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios' 2 | import Toast from 'react-native-toast-message' 3 | 4 | import { errorCatch } from './error.api' 5 | import instance from './interceptors.api' 6 | 7 | export const request = async (config: AxiosRequestConfig) => { 8 | const onSuccess = (response: AxiosResponse) => response.data 9 | 10 | const onError = (error: AxiosError) => { 11 | Toast.show({ 12 | type: 'error', 13 | text1: 'Request error', 14 | text2: errorCatch(error) 15 | }) 16 | 17 | return Promise.reject(error) 18 | } 19 | 20 | return instance(config).then(onSuccess).catch(onError) 21 | } 22 | -------------------------------------------------------------------------------- /app/components/ui/button/Button.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'clsx' 2 | import { FC, PropsWithChildren } from 'react' 3 | import { Pressable, Text } from 'react-native' 4 | 5 | import { IButton } from './button.interface' 6 | 7 | const Button: FC> = ({ 8 | className, 9 | children, 10 | ...rest 11 | }) => { 12 | return ( 13 | 20 | 21 | {children} 22 | 23 | 24 | ) 25 | } 26 | 27 | export default Button 28 | -------------------------------------------------------------------------------- /app/components/screens/category/Category.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Text } from 'react-native' 3 | 4 | import Layout from '@/components/layout/Layout' 5 | import Loader from '@/components/ui/Loader' 6 | import Catalog from '@/components/ui/catalog/Catalog' 7 | 8 | import { useCategory } from './useCategory' 9 | 10 | const Category: FC = () => { 11 | const { category, products, isLoading } = useCategory() 12 | 13 | if (isLoading) return 14 | 15 | return ( 16 | 17 | {category ? ( 18 | 19 | ) : ( 20 | Category not found 21 | )} 22 | 23 | ) 24 | } 25 | 26 | export default Category 27 | -------------------------------------------------------------------------------- /app/components/screens/product/product-info/ProductInfo.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Text, View } from 'react-native' 3 | 4 | import { convertPrice } from '@/utils/convertPrice' 5 | 6 | import { IProductComponent } from '../product-page.interface' 7 | 8 | const ProductInfo: FC = ({ product }) => { 9 | return ( 10 | 11 | {product.name} 12 | 13 | {product.description} 14 | 15 | 16 | {convertPrice(product.price)} 17 | 18 | 19 | ) 20 | } 21 | 22 | export default ProductInfo 23 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "useTabs": true, 4 | "semi": false, 5 | "tabWidth": 4, 6 | "jsxSingleQuote": true, 7 | "singleQuote": true, 8 | "arrowParens": "avoid", 9 | "importOrder": [ 10 | "", 11 | "^@/components/(.*)$", 12 | "^@/hooks/(.*)$", 13 | "^@/types/(.*)$", 14 | "^@/services/(.*)$", 15 | "^@/assets/(.*)$", 16 | "^@/utils/(.*)$", 17 | "^@/config/(.*)$", 18 | "^@/providers/(.*)$", 19 | "^@/navigation/(.*)$", 20 | "^@/api/(.*)$", 21 | "^@/store/(.*)$", 22 | "^../(.*)$", 23 | "^./(.*)$" 24 | ], 25 | "importOrderSeparation": true, 26 | "importOrderSortSpecifiers": true, 27 | "plugins": ["@trivago/prettier-plugin-sort-imports"] 28 | } 29 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Delivery", 4 | "slug": "client", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./app/assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./app/assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#4fae5a" 13 | }, 14 | "ios": { 15 | "supportsTablet": true, 16 | "bundleIdentifier": "com.kaplunmaxym.client" 17 | }, 18 | "android": { 19 | "adaptiveIcon": { 20 | "foregroundImage": "./app/assets/adaptive-icon.png", 21 | "backgroundColor": "#4fae5a" 22 | }, 23 | "package": "com.kaplunmaxym.client" 24 | }, 25 | "web": { 26 | "favicon": "./app/assets/favicon.png" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/components/ui/Toast.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import RnToast, { BaseToast } from 'react-native-toast-message' 3 | 4 | const options = (primaryColor: string) => ({ 5 | style: { backgroundColor: '#080808', borderLeftColor: primaryColor }, 6 | text1Style: { 7 | color: '#fff', 8 | fontSize: 16 9 | }, 10 | text2Style: { 11 | fontSize: 14 12 | } 13 | }) 14 | 15 | const Toast: FC = () => { 16 | return ( 17 | ( 21 | 22 | ), 23 | info: props => , 24 | error: props => 25 | }} 26 | /> 27 | ) 28 | } 29 | 30 | export default Toast 31 | -------------------------------------------------------------------------------- /app/components/ui/catalog/product-item/ProductInfo.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Text, View } from 'react-native' 3 | 4 | import { IProduct } from '@/types/product.interface' 5 | 6 | import { convertPrice } from '@/utils/convertPrice' 7 | 8 | interface IProductInfo { 9 | product: IProduct 10 | } 11 | 12 | const ProductInfo: FC = ({ product }) => { 13 | return ( 14 | 15 | {product.name} 16 | {product.category.name} 17 | 18 | {convertPrice(product.price)} 19 | 20 | 21 | ) 22 | } 23 | 24 | export default ProductInfo 25 | -------------------------------------------------------------------------------- /app/components/ui/catalog/Catalog.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Text, View } from 'react-native' 3 | 4 | import Heading from '../Heading' 5 | 6 | import { ICatalog } from './catalog.interface' 7 | import ProductItem from './product-item/ProductItem' 8 | 9 | const Catalog: FC = ({ title, products }) => { 10 | return ( 11 | 12 | {title && {title}} 13 | 14 | {products?.length ? ( 15 | 16 | {products.map(product => ( 17 | 18 | ))} 19 | 20 | ) : ( 21 | Products not found 22 | )} 23 | 24 | ) 25 | } 26 | 27 | export default Catalog 28 | -------------------------------------------------------------------------------- /app/components/layout/bottom-menu/MenuItem.tsx: -------------------------------------------------------------------------------- 1 | import { Feather } from '@expo/vector-icons' 2 | import { FC } from 'react' 3 | import { Pressable } from 'react-native' 4 | 5 | import { IMenuItem, TypeNavigate } from './menu.interface' 6 | 7 | interface IMenuItemProps { 8 | item: IMenuItem 9 | nav: TypeNavigate 10 | currentRoute?: string 11 | } 12 | 13 | const MenuItem: FC = ({ currentRoute, item, nav }) => { 14 | const isActive = currentRoute === item.path 15 | 16 | return ( 17 | nav(item.path)} 20 | > 21 | 26 | 27 | ) 28 | } 29 | 30 | export default MenuItem 31 | -------------------------------------------------------------------------------- /app/components/screens/home/Header.tsx: -------------------------------------------------------------------------------- 1 | import { Ionicons } from '@expo/vector-icons' 2 | import { FC } from 'react' 3 | import { Pressable, Text, View } from 'react-native' 4 | 5 | import { useTypedNavigation } from '@/hooks/useTypedNavigation' 6 | 7 | import { useProfile } from '../profile/useProfile' 8 | 9 | const Header: FC = () => { 10 | const { navigate } = useTypedNavigation() 11 | 12 | const { profile } = useProfile() 13 | 14 | return ( 15 | 16 | 17 | Hello, {profile?.name}! 18 | 19 | 20 | navigate('Cart')}> 21 | 22 | 23 | 24 | ) 25 | } 26 | 27 | export default Header 28 | -------------------------------------------------------------------------------- /app/services/order.service.ts: -------------------------------------------------------------------------------- 1 | import { IOrder } from '@/types/order.interface' 2 | 3 | import { getOrdersUrl } from '@/config/api.config' 4 | 5 | import { request } from './api/request.api' 6 | 7 | type TypeData = { 8 | items: { 9 | quantity: number 10 | price: number 11 | productId: string 12 | }[] 13 | } 14 | 15 | export const OrderService = { 16 | async getAll() { 17 | return request({ 18 | url: getOrdersUrl(''), 19 | method: 'GET' 20 | }) 21 | }, 22 | 23 | async getByUserId() { 24 | return request({ 25 | url: getOrdersUrl('/by-user'), 26 | method: 'GET' 27 | }) 28 | }, 29 | 30 | async place(data: TypeData) { 31 | return request<{ clientSecret: string }>({ 32 | url: getOrdersUrl(''), 33 | method: 'POST', 34 | data 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/components/screens/product/ProductHeader.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { View } from 'react-native' 3 | 4 | import { useTypedNavigation } from '@/hooks/useTypedNavigation' 5 | 6 | import ProductButton from './ProductButton' 7 | import FavoriteButton from './favorite-button/FavoriteButton' 8 | import { IProductComponent } from './product-page.interface' 9 | 10 | const ProductHeader: FC = ({ product }) => { 11 | const { goBack } = useTypedNavigation() 12 | 13 | return ( 14 | 15 | 16 | 22 | 23 | 24 | 25 | ) 26 | } 27 | 28 | export default ProductHeader 29 | -------------------------------------------------------------------------------- /app/services/api/helper.auth.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { getItemAsync } from 'expo-secure-store' 3 | 4 | import { EnumSecureStore, IAuthResponse } from '@/types/auth.interface' 5 | 6 | import { saveToStorage } from '@/services/auth/auth.helper' 7 | 8 | import { API_URL, getAuthUrl } from '@/config/api.config' 9 | 10 | export const getNewTokens = async () => { 11 | try { 12 | const refreshToken = await getItemAsync(EnumSecureStore.REFRESH_TOKEN) 13 | const response = await axios.post( 14 | API_URL + getAuthUrl('/login/access-token'), 15 | { refreshToken }, 16 | { 17 | headers: { 18 | 'Content-Type': 'application/json' 19 | } 20 | } 21 | ) 22 | 23 | if (response.data.accessToken) await saveToStorage(response.data) 24 | 25 | return response 26 | } catch (e) {} 27 | } 28 | -------------------------------------------------------------------------------- /app/services/product.service.ts: -------------------------------------------------------------------------------- 1 | import { IProduct } from '@/types/product.interface' 2 | 3 | import { getProductsUrl } from '@/config/api.config' 4 | 5 | import { request } from './api/request.api' 6 | 7 | export const ProductService = { 8 | async getAll(searchTerm?: string) { 9 | return request({ 10 | url: getProductsUrl(''), 11 | method: 'GET', 12 | params: searchTerm 13 | ? { 14 | searchTerm 15 | } 16 | : {} 17 | }) 18 | }, 19 | 20 | async getBySlug(slug: string) { 21 | return request({ 22 | url: getProductsUrl(`/by-slug/${slug}`), 23 | method: 'GET' 24 | }) 25 | }, 26 | 27 | async getByCategory(categorySlug: string) { 28 | return request({ 29 | url: getProductsUrl(`/by-category/${categorySlug}`), 30 | method: 'GET', 31 | // data: { categorySlug }r 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/components/screens/home/banner/Banner.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Image, Pressable, Text, View } from 'react-native' 3 | 4 | import { useTypedNavigation } from '@/hooks/useTypedNavigation' 5 | 6 | const Banner: FC = () => { 7 | const { navigate } = useTypedNavigation() 8 | 9 | return ( 10 | 11 | 12 | 13 | The best choice for you - and your family 14 | 15 | 16 | navigate('Explorer')} 18 | className='bg-black py-2 rounded-full w-28 mt-4' 19 | > 20 | 21 | Order now 22 | 23 | 24 | 25 | 26 | ) 27 | } 28 | 29 | export default Banner 30 | -------------------------------------------------------------------------------- /app/components/layout/bottom-menu/BottomMenu.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { View } from 'react-native' 3 | import { useSafeAreaInsets } from 'react-native-safe-area-context' 4 | 5 | import MenuItem from './MenuItem' 6 | import { menuItems } from './menu.data' 7 | import { TypeNavigate } from './menu.interface' 8 | 9 | interface IBottomMenu { 10 | nav: TypeNavigate 11 | currentRoute?: string 12 | } 13 | 14 | const BottomMenu: FC = props => { 15 | const { bottom } = useSafeAreaInsets() 16 | 17 | return ( 18 | 24 | {menuItems.map(item => ( 25 | 26 | ))} 27 | 28 | ) 29 | } 30 | 31 | export default BottomMenu 32 | -------------------------------------------------------------------------------- /app/services/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage' 2 | 3 | import { EnumAsyncStorage, IAuthResponse } from '@/types/auth.interface' 4 | 5 | import { deleteTokensStorage, saveToStorage } from '@/services/auth/auth.helper' 6 | 7 | import { getAuthUrl } from '@/config/api.config' 8 | 9 | import { request } from '../api/request.api' 10 | 11 | export const AuthService = { 12 | async main(variant: 'reg' | 'login', email: string, password: string) { 13 | const response = await request({ 14 | url: getAuthUrl(`/${variant === 'reg' ? 'register' : 'login'}`), 15 | method: 'POST', 16 | data: { email, password } 17 | }) 18 | 19 | if (response.accessToken) await saveToStorage(response) 20 | 21 | return response 22 | }, 23 | 24 | async logout() { 25 | await deleteTokensStorage() 26 | await AsyncStorage.removeItem(EnumAsyncStorage.USER) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/navigation/PrivateNavigator.tsx: -------------------------------------------------------------------------------- 1 | import { createNativeStackNavigator } from '@react-navigation/native-stack' 2 | import { FC } from 'react' 3 | 4 | import Auth from '@/components/screens/auth/Auth' 5 | 6 | import { useAuth } from '@/hooks/useAuth' 7 | 8 | import { TypeRootStackParamList } from './navigation.types' 9 | import { routes } from './routes' 10 | 11 | const Stack = createNativeStackNavigator() 12 | 13 | const PrivateNavigator: FC = () => { 14 | const { user } = useAuth() 15 | 16 | return ( 17 | 25 | {user ? ( 26 | routes.map(route => ( 27 | 28 | )) 29 | ) : ( 30 | 31 | )} 32 | 33 | ) 34 | } 35 | 36 | export default PrivateNavigator 37 | -------------------------------------------------------------------------------- /app/components/screens/category/useCategory.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query' 2 | 3 | import { useTypedRoute } from '@/hooks/useTypedRoute' 4 | 5 | import { CategoryService } from '@/services/category.service' 6 | import { ProductService } from '@/services/product.service' 7 | 8 | export const useCategory = () => { 9 | const { params } = useTypedRoute<'Category'>() 10 | 11 | const { isLoading: isCategoryLoading, data: category } = useQuery({ 12 | queryKey: ['get category by slug', params.slug], 13 | queryFn: () => CategoryService.getBySlug(params.slug) 14 | }) 15 | 16 | const categoryId = category?.id || '' 17 | 18 | const { isLoading: isProductLoading, data: products } = useQuery({ 19 | queryKey: ['get products by category', params.slug], 20 | queryFn: () => ProductService.getByCategory(params.slug), 21 | enabled: !!categoryId 22 | }) 23 | 24 | return { 25 | category, 26 | products, 27 | isLoading: isCategoryLoading || isProductLoading 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/components/screens/product/ProductButton.tsx: -------------------------------------------------------------------------------- 1 | import { Feather } from '@expo/vector-icons' 2 | import cn from 'clsx' 3 | import { FC, PropsWithChildren } from 'react' 4 | import { Pressable, PressableProps, View } from 'react-native' 5 | 6 | import { TypeFeatherIconNames } from '@/types/icon.interface' 7 | 8 | interface IProductButton extends PressableProps { 9 | icon?: TypeFeatherIconNames 10 | iconSize?: number 11 | color?: string 12 | className?: string 13 | } 14 | 15 | const ProductButton: FC> = ({ 16 | children, 17 | icon, 18 | iconSize, 19 | color, 20 | className, 21 | ...rest 22 | }) => { 23 | return ( 24 | 25 | 31 | {children ? ( 32 | children 33 | ) : ( 34 | 35 | )} 36 | 37 | 38 | ) 39 | } 40 | 41 | export default ProductButton 42 | -------------------------------------------------------------------------------- /app/components/screens/profile/Profile.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Image, View } from 'react-native' 3 | 4 | import Layout from '@/components/layout/Layout' 5 | import Heading from '@/components/ui/Heading' 6 | import Button from '@/components/ui/button/Button' 7 | 8 | import { useAuth } from '@/hooks/useAuth' 9 | 10 | import { AuthService } from '@/services/auth/auth.service' 11 | 12 | import { useProfile } from './useProfile' 13 | 14 | const Profile: FC = () => { 15 | const { setUser } = useAuth() 16 | 17 | const { profile } = useProfile() 18 | 19 | return ( 20 | 21 | Profile 22 | 23 | 24 | 28 | 29 | 30 | 36 | 37 | ) 38 | } 39 | 40 | export default Profile 41 | -------------------------------------------------------------------------------- /app/components/screens/product/product-info/AddToCartButton.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | 3 | import Button from '@/components/ui/button/Button' 4 | 5 | import { useActions } from '@/hooks/useActions' 6 | import { useCart } from '@/hooks/useCart' 7 | 8 | import { IProduct } from '@/types/product.interface' 9 | 10 | interface IAddToCartButton { 11 | product: IProduct 12 | } 13 | 14 | const AddToCartButton: FC = ({ product }) => { 15 | const { addToCart, removeFromCart } = useActions() 16 | const { items } = useCart() 17 | 18 | const currentElement = items.find( 19 | cartItem => cartItem.product.id === product.id 20 | ) 21 | 22 | return ( 23 | 37 | ) 38 | } 39 | 40 | export default AddToCartButton 41 | -------------------------------------------------------------------------------- /app/components/ui/catalog/product-item/ProductItem.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Image, Pressable, View } from 'react-native' 3 | 4 | import { useTypedNavigation } from '@/hooks/useTypedNavigation' 5 | 6 | import { IProduct } from '@/types/product.interface' 7 | 8 | import { getMediaSource } from '@/utils/getMediaSource' 9 | 10 | import ProductInfo from './ProductInfo' 11 | 12 | interface IProductItem { 13 | product: IProduct 14 | } 15 | 16 | const ProductItem: FC = ({ product }) => { 17 | const { navigate } = useTypedNavigation() 18 | 19 | return ( 20 | 21 | navigate('Product', { slug: product.slug })} 23 | className='bg-gray-100 rounded-xl relative overflow-hidden p-5 flex items-center justify-center' 24 | > 25 | 30 | 31 | 32 | 33 | ) 34 | } 35 | 36 | export default ProductItem 37 | -------------------------------------------------------------------------------- /app/components/screens/product/Product.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Image, View } from 'react-native' 3 | 4 | import Layout from '@/components/layout/Layout' 5 | import Loader from '@/components/ui/Loader' 6 | 7 | import { getMediaSource } from '@/utils/getMediaSource' 8 | 9 | import ProductHeader from './ProductHeader' 10 | import AddToCartButton from './product-info/AddToCartButton' 11 | import ProductInfo from './product-info/ProductInfo' 12 | import { useProduct } from './useProduct' 13 | 14 | const Product: FC = () => { 15 | const { isLoading, product } = useProduct() 16 | 17 | if (isLoading) return 18 | if (!product) return null 19 | 20 | return ( 21 | 22 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | ) 34 | } 35 | 36 | export default Product 37 | -------------------------------------------------------------------------------- /app/components/screens/search/Search.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { View } from 'react-native' 3 | 4 | import Layout from '@/components/layout/Layout' 5 | import Heading from '@/components/ui/Heading' 6 | import Loader from '@/components/ui/Loader' 7 | import Catalog from '@/components/ui/catalog/Catalog' 8 | import Field from '@/components/ui/field/Field' 9 | 10 | import { ISearchFormData } from './search.interface' 11 | import { useSearch } from './useSearch' 12 | 13 | const Search: FC = () => { 14 | const { searchTerm, isLoading, control, products } = useSearch() 15 | 16 | return ( 17 | 18 | Search 19 | 20 | 21 | 22 | placeholder='Type something...' 23 | control={control} 24 | name='searchTerm' 25 | keyboardType='web-search' 26 | /> 27 | 28 | {!!searchTerm ? ( 29 | 30 | {isLoading ? ( 31 | 32 | ) : ( 33 | 34 | )} 35 | 36 | ) : null} 37 | 38 | ) 39 | } 40 | 41 | export default Search 42 | -------------------------------------------------------------------------------- /app/components/screens/cart/cart-item/CartActions.tsx: -------------------------------------------------------------------------------- 1 | import { AntDesign } from '@expo/vector-icons' 2 | import { FC } from 'react' 3 | import { Pressable, Text, View } from 'react-native' 4 | 5 | import { useActions } from '@/hooks/useActions' 6 | import { useCart } from '@/hooks/useCart' 7 | 8 | import { ICartItem } from '@/types/cart.interface' 9 | 10 | interface ICartActions { 11 | item: ICartItem 12 | } 13 | 14 | const CartActions: FC = ({ item }) => { 15 | const { changeQuantity } = useActions() 16 | 17 | const { items } = useCart() 18 | 19 | const quantity = items.find(cartItem => cartItem.id === item.id)?.quantity 20 | 21 | return ( 22 | 23 | changeQuantity({ id: item.id, type: 'minus' })} 25 | disabled={quantity === 1} 26 | > 27 | 28 | 29 | {quantity} 30 | changeQuantity({ id: item.id, type: 'plus' })} 32 | > 33 | 34 | 35 | 36 | ) 37 | } 38 | 39 | export default CartActions 40 | -------------------------------------------------------------------------------- /app/store/store.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage' 2 | import { combineReducers, configureStore } from '@reduxjs/toolkit' 3 | import { 4 | FLUSH, 5 | PAUSE, 6 | PERSIST, 7 | PURGE, 8 | PersistConfig, 9 | REGISTER, 10 | REHYDRATE, 11 | persistStore 12 | } from 'redux-persist' 13 | import persistReducer from 'redux-persist/es/persistReducer' 14 | 15 | import { cartSlice } from './cart/cart.slice' 16 | 17 | const persistConfig: PersistConfig = { 18 | key: 'root', 19 | storage: AsyncStorage, 20 | whitelist: ['cart'] 21 | } 22 | 23 | const rootReducer = combineReducers({ 24 | cart: cartSlice.reducer 25 | }) 26 | 27 | const persistedReducer = persistReducer( 28 | persistConfig, 29 | rootReducer 30 | ) 31 | 32 | export const store = configureStore({ 33 | reducer: persistedReducer, 34 | middleware: getDefaultMiddleware => 35 | getDefaultMiddleware({ 36 | serializableCheck: { 37 | ignoredActions: [ 38 | FLUSH, 39 | REHYDRATE, 40 | PAUSE, 41 | PERSIST, 42 | PURGE, 43 | REGISTER 44 | ] 45 | } 46 | }) 47 | }) 48 | 49 | export const persistor = persistStore(store) 50 | 51 | export type TypeRootState = ReturnType 52 | -------------------------------------------------------------------------------- /app/components/screens/auth/AuthFields.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Control } from 'react-hook-form' 3 | 4 | import Field from '@/components/ui/field/Field' 5 | 6 | import { IAuthFormData } from '@/types/auth.interface' 7 | 8 | import { validEmail } from './email.regex' 9 | 10 | interface IAuthFields { 11 | control: Control 12 | isPassRequired?: boolean 13 | } 14 | 15 | const AuthFields: FC = ({ control, isPassRequired }) => { 16 | return ( 17 | <> 18 | 19 | placeholder='Enter email' 20 | control={control} 21 | name='email' 22 | rules={{ 23 | required: 'Email is required!', 24 | pattern: { 25 | value: validEmail, 26 | message: 'Please enter a valid email address' 27 | } 28 | }} 29 | keyboardType='email-address' 30 | /> 31 | 32 | placeholder='Enter password' 33 | control={control} 34 | name='password' 35 | secureTextEntry 36 | rules={{ 37 | required: 'Password is required!', 38 | minLength: { 39 | value: 6, 40 | message: 'Password should be minimum 6 characters long' 41 | } 42 | }} 43 | /> 44 | 45 | ) 46 | } 47 | 48 | export default AuthFields 49 | -------------------------------------------------------------------------------- /app/navigation/routes.ts: -------------------------------------------------------------------------------- 1 | import Cart from '@/components/screens/cart/Cart' 2 | import Category from '@/components/screens/category/Category' 3 | import Explorer from '@/components/screens/explorer/Explorer' 4 | import Favorites from '@/components/screens/favorites/Favorites' 5 | import Home from '@/components/screens/home/Home' 6 | import Product from '@/components/screens/product/Product' 7 | import Profile from '@/components/screens/profile/Profile' 8 | import Search from '@/components/screens/search/Search' 9 | import Thanks from '@/components/screens/thanks/Thanks' 10 | 11 | import { IRoute } from './navigation.types' 12 | 13 | export const routes: IRoute[] = [ 14 | { 15 | name: 'Home', 16 | component: Home 17 | }, 18 | { 19 | name: 'Favorites', 20 | component: Favorites 21 | }, 22 | { 23 | name: 'Search', 24 | component: Search 25 | }, 26 | { 27 | name: 'Explorer', 28 | component: Explorer 29 | }, 30 | { 31 | name: 'Profile', 32 | component: Profile 33 | }, 34 | { 35 | name: 'Cart', 36 | component: Cart 37 | }, 38 | { 39 | name: 'Category', 40 | component: Category 41 | }, 42 | { 43 | name: 'Product', 44 | component: Product 45 | }, 46 | { 47 | name: 'Thanks', 48 | component: Thanks 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /app/store/cart/cart.slice.ts: -------------------------------------------------------------------------------- 1 | import { PayloadAction, createSlice } from '@reduxjs/toolkit' 2 | 3 | import { 4 | IAddToCartPayload, 5 | ICartInitialState, 6 | IChangeQuantityPayload 7 | } from './cart.types' 8 | 9 | const initialState: ICartInitialState = { 10 | items: [] 11 | } 12 | 13 | export const cartSlice = createSlice({ 14 | name: 'cart', 15 | initialState, 16 | reducers: { 17 | addToCart: (state, action: PayloadAction) => { 18 | const isExist = state.items.some( 19 | item => item.product.id === action.payload.product.id 20 | ) 21 | 22 | if (!isExist) 23 | state.items.push({ 24 | ...action.payload, 25 | id: state.items.length.toString() 26 | }) 27 | }, 28 | removeFromCart: (state, action: PayloadAction<{ id: string }>) => { 29 | state.items = state.items.filter( 30 | item => item.id !== action.payload.id 31 | ) 32 | }, 33 | changeQuantity: ( 34 | state, 35 | action: PayloadAction 36 | ) => { 37 | const { id, type } = action.payload 38 | const item = state.items.find(item => item.id === id) 39 | if (item) type === 'plus' ? item.quantity++ : item.quantity-- 40 | }, 41 | reset: state => { 42 | state.items = [] 43 | } 44 | } 45 | }) 46 | -------------------------------------------------------------------------------- /app/components/ui/field/Field.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'clsx' 2 | import { Controller } from 'react-hook-form' 3 | import { Text, TextInput, View } from 'react-native' 4 | 5 | import { IField } from './field.interface' 6 | 7 | const Field = >({ 8 | control, 9 | rules, 10 | name, 11 | className, 12 | ...rest 13 | }: IField): JSX.Element => { 14 | return ( 15 | ( 23 | <> 24 | 30 | 39 | 40 | {error && ( 41 | {error.message} 42 | )} 43 | 44 | )} 45 | /> 46 | ) 47 | } 48 | 49 | export default Field 50 | -------------------------------------------------------------------------------- /app/components/screens/cart/Cart.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Text, View } from 'react-native' 3 | 4 | import Layout from '@/components/layout/Layout' 5 | import Heading from '@/components/ui/Heading' 6 | import Button from '@/components/ui/button/Button' 7 | 8 | import { useCart } from '@/hooks/useCart' 9 | 10 | import { convertPrice } from '@/utils/convertPrice' 11 | 12 | import CartItem from './cart-item/CartItem' 13 | import { useCheckout } from './useCheckout' 14 | 15 | const Cart: FC = () => { 16 | const { items, total } = useCart() 17 | const { onCheckout } = useCheckout() 18 | 19 | return ( 20 | <> 21 | 22 | Cart 23 | 24 | {items.length ? ( 25 | items.map(item => { 26 | console.log(item.id); 27 | return () 28 | }) 29 | ) : ( 30 | Product not found 31 | )} 32 | 33 | 34 | {items.length ? ( 35 | 36 | 37 | Total: {convertPrice(total)} 38 | 39 | 40 | 41 | ) : null} 42 | 43 | ) 44 | } 45 | 46 | export default Cart 47 | -------------------------------------------------------------------------------- /app/components/screens/auth/useAuthMutations.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@tanstack/react-query' 2 | import { useMemo } from 'react' 3 | import { UseFormReset } from 'react-hook-form' 4 | 5 | import { useAuth } from '@/hooks/useAuth' 6 | 7 | import { IAuthFormData } from '@/types/auth.interface' 8 | 9 | import { AuthService } from '@/services/auth/auth.service' 10 | 11 | export const useAuthMutations = (reset: UseFormReset) => { 12 | const { setUser } = useAuth() 13 | 14 | const { mutate: loginSync, isPending: isLoginLoading } = useMutation({ 15 | mutationKey: ['login'], 16 | mutationFn: ({ email, password }: IAuthFormData) => 17 | AuthService.main('login', email, password), 18 | onSuccess(data) { 19 | reset() 20 | setUser(data.user) 21 | } 22 | }) 23 | 24 | const { mutate: registerSync, isPending: isRegisterLoading } = useMutation({ 25 | mutationKey: ['register'], 26 | mutationFn: ({ email, password }: IAuthFormData) => 27 | AuthService.main('reg', email, password), 28 | 29 | onSuccess(data) { 30 | reset() 31 | setUser(data.user) 32 | } 33 | }) 34 | 35 | return useMemo( 36 | () => ({ 37 | loginSync, 38 | registerSync, 39 | isLoading: isLoginLoading || isRegisterLoading 40 | }), 41 | [isLoginLoading, isRegisterLoading] 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /app/components/screens/cart/cart-item/CartItem.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Image, Pressable, Text, View } from 'react-native' 3 | 4 | import { useTypedNavigation } from '@/hooks/useTypedNavigation' 5 | 6 | import { ICartItem } from '@/types/cart.interface' 7 | 8 | import { convertPrice } from '@/utils/convertPrice' 9 | import { getMediaSource } from '@/utils/getMediaSource' 10 | 11 | import CartIActions from './CartActions' 12 | 13 | interface ICartItemProps { 14 | item: ICartItem 15 | } 16 | 17 | const CartItem: FC = ({ item }) => { 18 | const { navigate } = useTypedNavigation() 19 | 20 | return ( 21 | 22 | navigate('Product', { slug: item.product.slug })} 24 | className='bg-gray-100 rounded-xl overflow-hidden py-3 px-3 items-center w-28' 25 | > 26 | 31 | 32 | 33 | 34 | 35 | {item.product.name} 36 | 37 | {convertPrice(item.price)} 38 | 39 | 40 | 41 | ) 42 | } 43 | 44 | export default CartItem 45 | -------------------------------------------------------------------------------- /app/navigation/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | NavigationContainer, 3 | useNavigationContainerRef 4 | } from '@react-navigation/native' 5 | import { FC, useEffect, useState } from 'react' 6 | 7 | import BottomMenu from '@/components/layout/bottom-menu/BottomMenu' 8 | 9 | import { useAuth } from '@/hooks/useAuth' 10 | 11 | import { useCheckAuth } from '@/providers/useCheckAuth' 12 | 13 | import PrivateNavigator from './PrivateNavigator' 14 | 15 | const Navigation: FC = () => { 16 | const { user } = useAuth() 17 | 18 | const [currentRoute, setCurrentRoute] = useState( 19 | undefined 20 | ) 21 | 22 | const navRef = useNavigationContainerRef() 23 | 24 | useEffect(() => { 25 | setCurrentRoute(navRef.getCurrentRoute()?.name) 26 | 27 | const listener = navRef.addListener('state', () => 28 | setCurrentRoute(navRef.getCurrentRoute()?.name) 29 | ) 30 | 31 | return () => { 32 | navRef.removeListener('state', listener) 33 | } 34 | }, []) 35 | 36 | useCheckAuth(currentRoute) 37 | 38 | return ( 39 | <> 40 | 41 | 42 | 43 | {user && currentRoute && ( 44 | 45 | )} 46 | 47 | ) 48 | } 49 | 50 | export default Navigation 51 | -------------------------------------------------------------------------------- /app/providers/AuthProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as SplashScreen from 'expo-splash-screen' 2 | import { 3 | FC, 4 | PropsWithChildren, 5 | createContext, 6 | useEffect, 7 | useState 8 | } from 'react' 9 | 10 | import { getAccessToken, getUserFromStorage } from '@/services/auth/auth.helper' 11 | 12 | import { IContext, TypeUserState } from './auth-provder.interface' 13 | 14 | export const AuthContext = createContext({} as IContext) 15 | 16 | let ignore = SplashScreen.preventAutoHideAsync() 17 | 18 | const AuthProvider: FC> = ({ children }) => { 19 | const [user, setUser] = useState(null) 20 | 21 | useEffect(() => { 22 | let isMounted = true 23 | 24 | const checkAccessToken = async () => { 25 | try { 26 | const accessToken = await getAccessToken() 27 | 28 | if (accessToken) { 29 | const user = await getUserFromStorage() 30 | if (isMounted) setUser(user) 31 | } 32 | } catch { 33 | } finally { 34 | await SplashScreen.hideAsync() 35 | } 36 | } 37 | 38 | let ignore = checkAccessToken() 39 | 40 | return () => { 41 | isMounted = false 42 | } 43 | }, []) 44 | 45 | return ( 46 | 47 | {children} 48 | 49 | ) 50 | } 51 | 52 | export default AuthProvider 53 | -------------------------------------------------------------------------------- /app/providers/useCheckAuth.ts: -------------------------------------------------------------------------------- 1 | import { getItemAsync } from 'expo-secure-store' 2 | import { useEffect } from 'react' 3 | import { useAuth } from '@/hooks/useAuth' 4 | import { EnumSecureStore } from '@/types/auth.interface' 5 | import { errorCatch } from '@/services/api/error.api' 6 | import { getNewTokens } from '@/services/api/helper.auth' 7 | import { getAccessToken } from '@/services/auth/auth.helper' 8 | import { AuthService } from '@/services/auth/auth.service' 9 | 10 | 11 | export const useCheckAuth = (routeName?: string) => { 12 | const { user, setUser } = useAuth() 13 | 14 | useEffect(() => { 15 | const checkAccessToken = async () => { 16 | const accessToken = await getAccessToken() 17 | if (accessToken) { 18 | try { 19 | await getNewTokens() 20 | } catch (e) { 21 | if (errorCatch(e) === 'jwt expired') { 22 | await AuthService.logout() 23 | setUser(null) 24 | } 25 | } 26 | } 27 | } 28 | 29 | let ignore = checkAccessToken() 30 | }, []) 31 | 32 | useEffect(() => { 33 | const checkRefreshToken = async () => { 34 | const refreshToken = await getItemAsync( 35 | EnumSecureStore.REFRESH_TOKEN 36 | ) 37 | if (!refreshToken && user) { 38 | await AuthService.logout() 39 | setUser(null) 40 | } 41 | } 42 | 43 | let ignore = checkRefreshToken() 44 | }, [routeName]) 45 | } 46 | -------------------------------------------------------------------------------- /app/services/auth/auth.helper.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage' 2 | import { deleteItemAsync, getItemAsync, setItemAsync } from 'expo-secure-store' 3 | 4 | import { 5 | EnumAsyncStorage, 6 | EnumSecureStore, 7 | IAuthResponse, 8 | ITokens 9 | } from '@/types/auth.interface' 10 | 11 | export const getAccessToken = async () => { 12 | const accessToken = await getItemAsync(EnumSecureStore.ACCESS_TOKEN) 13 | return accessToken || null 14 | } 15 | 16 | export const saveTokensStorage = async (data: ITokens) => { 17 | await setItemAsync(EnumSecureStore.ACCESS_TOKEN, data.accessToken) 18 | await setItemAsync(EnumSecureStore.REFRESH_TOKEN, data.refreshToken) 19 | } 20 | 21 | export const deleteTokensStorage = async () => { 22 | await deleteItemAsync(EnumSecureStore.ACCESS_TOKEN) 23 | await deleteItemAsync(EnumSecureStore.REFRESH_TOKEN) 24 | } 25 | 26 | export const getUserFromStorage = async () => { 27 | try { 28 | return JSON.parse( 29 | (await AsyncStorage.getItem(EnumAsyncStorage.USER)) || '{}' 30 | ) 31 | } catch (e) { 32 | return null 33 | } 34 | } 35 | 36 | export const saveToStorage = async (data: IAuthResponse) => { 37 | await saveTokensStorage(data) 38 | try { 39 | await AsyncStorage.setItem( 40 | EnumAsyncStorage.USER, 41 | JSON.stringify(data.user) 42 | ) 43 | } catch (e) {} 44 | } 45 | -------------------------------------------------------------------------------- /app/services/api/interceptors.api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | import { 4 | deleteTokensStorage, 5 | getAccessToken 6 | } from '@/services/auth/auth.helper' 7 | 8 | import { API_URL } from '@/config/api.config' 9 | 10 | import { errorCatch } from './error.api' 11 | import { getNewTokens } from './helper.auth' 12 | 13 | const instance = axios.create({ 14 | baseURL: API_URL, 15 | headers: { 16 | 'Content-Type': 'application/json' 17 | } 18 | }) 19 | 20 | instance.interceptors.request.use(async config => { 21 | const accessToken = await getAccessToken() 22 | 23 | if (config.headers && accessToken) 24 | config.headers.Authorization = `Bearer ${accessToken}` 25 | 26 | return config 27 | }) 28 | 29 | instance.interceptors.response.use( 30 | config => config, 31 | async error => { 32 | const originalRequest = error.config 33 | 34 | if ( 35 | (error.response.status === 401 || 36 | errorCatch(error) === 'jwt expired' || 37 | errorCatch(error) === 'jwt must be provided') && 38 | error.config && 39 | !error.config._isRetry 40 | ) { 41 | originalRequest._isRetry = true 42 | try { 43 | await getNewTokens() 44 | return instance.request(originalRequest) 45 | } catch (error) { 46 | if (errorCatch(error) === 'jwt expired') 47 | await deleteTokensStorage() 48 | } 49 | } 50 | 51 | throw error 52 | } 53 | ) 54 | export default instance 55 | -------------------------------------------------------------------------------- /app/components/screens/product/favorite-button/FavoriteButton.tsx: -------------------------------------------------------------------------------- 1 | import { MaterialCommunityIcons } from '@expo/vector-icons' 2 | import { useMutation, useQueryClient } from '@tanstack/react-query' 3 | import { FC } from 'react' 4 | 5 | import { UserService } from '@/services/user.service' 6 | 7 | import { useProfile } from '../../profile/useProfile' 8 | import ProductButton from '../ProductButton' 9 | 10 | interface IFavoriteButton { 11 | productId: string 12 | } 13 | 14 | const FavoriteButton: FC = ({ productId }) => { 15 | const { profile } = useProfile() 16 | 17 | const queryClient = useQueryClient() 18 | 19 | const { mutate } = useMutation({ 20 | mutationKey: ['toggle favorite'], 21 | mutationFn: () => UserService.toggleFavorite(productId), 22 | 23 | onSuccess() { 24 | queryClient.invalidateQueries({ queryKey: ['get profile'] }) 25 | } 26 | }) 27 | 28 | if (!profile) return null 29 | 30 | const isExists = profile.favorites.some( 31 | favorite => favorite.id === productId 32 | ) 33 | 34 | return ( 35 | mutate()}> 36 | {isExists ? ( 37 | 42 | ) : ( 43 | 48 | )} 49 | 50 | ) 51 | } 52 | 53 | export default FavoriteButton 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "start": "expo start --dev-client", 6 | "android": "expo run:android", 7 | "ios": "expo run:ios", 8 | "web": "expo start --web" 9 | }, 10 | "dependencies": { 11 | "@expo/vector-icons": "^14.0.2", 12 | "@react-native-async-storage/async-storage": "^1.23.1", 13 | "@react-native/metro-config": "^0.74.85", 14 | "@react-navigation/native": "^6.1.17", 15 | "@react-navigation/native-stack": "^6.10.0", 16 | "@reduxjs/toolkit": "^2.2.6", 17 | "@stripe/stripe-react-native": "^0.38.1", 18 | "@tanstack/react-query": "^5.50.1", 19 | "@trivago/prettier-plugin-sort-imports": "^4.3.0", 20 | "axios": "^1.7.2", 21 | "babel-plugin-inline-dotenv": "^1.7.0", 22 | "babel-plugin-root-import": "^6.6.0", 23 | "clsx": "^2.1.1", 24 | "expo": "~51.0.18", 25 | "expo-secure-store": "^13.0.2", 26 | "expo-splash-screen": "^0.27.5", 27 | "expo-status-bar": "~1.12.1", 28 | "nativewind": "^2.0.11", 29 | "prettier": "^3.3.2", 30 | "react": "18.2.0", 31 | "react-hook-form": "^7.52.1", 32 | "react-native": "0.74.3", 33 | "react-native-safe-area-context": "4.10.1", 34 | "react-native-screens": "3.31.1", 35 | "react-native-toast-message": "^2.2.0", 36 | "react-redux": "^9.1.2", 37 | "redux-persist": "^6.0.0" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "^7.20.0", 41 | "@types/node": "^20.14.9", 42 | "@types/react": "~18.2.45", 43 | "tailwindcss": "3.3.2", 44 | "typescript": "^5.1.3" 45 | }, 46 | "private": true 47 | } 48 | -------------------------------------------------------------------------------- /app/components/screens/home/categories/Categories.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Image, Pressable, ScrollView, Text, View } from 'react-native' 3 | 4 | import Heading from '@/components/ui/Heading' 5 | import Loader from '@/components/ui/Loader' 6 | 7 | import { useTypedNavigation } from '@/hooks/useTypedNavigation' 8 | 9 | import { getMediaSource } from '@/utils/getMediaSource' 10 | 11 | import { useGetAllCategories } from './useGetAllCategories' 12 | 13 | const Categories: FC = () => { 14 | const { categories, isLoading } = useGetAllCategories() 15 | 16 | const { navigate } = useTypedNavigation() 17 | 18 | return isLoading ? ( 19 | 20 | ) : ( 21 | 22 | 23 | Categories 24 | 25 | 30 | {categories?.map(category => ( 31 | 33 | navigate('Category', { slug: category.slug }) 34 | } 35 | key={category.id} 36 | className='rounded-xl bg-gray-100 p-2 w-[100]' 37 | > 38 | 45 | 46 | {category.name} 47 | 48 | 49 | ))} 50 | 51 | 52 | ) 53 | } 54 | 55 | export default Categories 56 | -------------------------------------------------------------------------------- /app/components/screens/cart/useCheckout.ts: -------------------------------------------------------------------------------- 1 | import { useStripe } from '@stripe/stripe-react-native' 2 | import { useMutation } from '@tanstack/react-query' 3 | 4 | import { useActions } from '@/hooks/useActions' 5 | import { useAuth } from '@/hooks/useAuth' 6 | import { useCart } from '@/hooks/useCart' 7 | import { useTypedNavigation } from '@/hooks/useTypedNavigation' 8 | 9 | import { OrderService } from '@/services/order.service' 10 | 11 | export const useCheckout = () => { 12 | const { items, total } = useCart() 13 | const { user } = useAuth() 14 | const { reset } = useActions() 15 | const { navigate } = useTypedNavigation() 16 | 17 | const { initPaymentSheet, presentPaymentSheet } = useStripe() 18 | 19 | const { mutateAsync: placeOrder } = useMutation({ 20 | mutationKey: ['place order'], 21 | mutationFn: () => 22 | OrderService.place({ 23 | items: items.map(item => ({ 24 | price: item.price, 25 | quantity: item.quantity, 26 | productId: item.product.id 27 | })) 28 | }) 29 | }) 30 | 31 | const onCheckout = async () => { 32 | try { 33 | const { clientSecret } = await placeOrder() 34 | 35 | const { error } = await initPaymentSheet({ 36 | merchantDisplayName: 'Your Merchant Name', 37 | paymentIntentClientSecret: clientSecret 38 | }) 39 | 40 | if (error) { 41 | console.error('Error initializing payment sheet:', error) 42 | return 43 | } 44 | 45 | const { error: paymentError } = await presentPaymentSheet() 46 | if (paymentError) { 47 | console.error('Error presenting payment sheet:', paymentError) 48 | return 49 | } 50 | 51 | reset() 52 | navigate('Thanks') 53 | } catch (error) { 54 | console.error('Checkout error:', error) 55 | } 56 | } 57 | 58 | return { onCheckout } 59 | } 60 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import { StripeProvider } from '@stripe/stripe-react-native' 2 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 3 | import { StatusBar } from 'expo-status-bar' 4 | import { SafeAreaProvider } from 'react-native-safe-area-context' 5 | import { Provider } from 'react-redux' 6 | import { PersistGate } from 'redux-persist/integration/react' 7 | 8 | import Toast from '@/components/ui/Toast' 9 | 10 | import AuthProvider from '@/providers/AuthProvider' 11 | 12 | import Navigation from '@/navigation/Navigation' 13 | 14 | import { persistor, store } from '@/store/store' 15 | import AsyncStorage from '@react-native-async-storage/async-storage' 16 | import {log} from "expo/build/devtools/logger"; 17 | 18 | const queryClient = new QueryClient({ 19 | defaultOptions: { 20 | queries: { 21 | refetchOnWindowFocus: false 22 | } 23 | } 24 | }) 25 | 26 | // queryClient.clear(); 27 | // const clearStorage = async () => { 28 | // try { 29 | // await AsyncStorage.clear(); 30 | // console.log('Storage successfully cleared!'); 31 | // } catch (e) { 32 | // console.log('Failed to clear the async storage.'); 33 | // } 34 | // }; 35 | // 36 | // // Викликайте цю функцію в потрібному місці вашого додатку 37 | // clearStorage(); 38 | 39 | export default function App() { 40 | return ( 41 | 42 | 43 | 44 | 45 | 46 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /app/components/screens/auth/Auth.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState } from 'react' 2 | import { SubmitHandler, useForm } from 'react-hook-form' 3 | import { Pressable, Text, View } from 'react-native' 4 | 5 | import Loader from '@/components/ui/Loader' 6 | import Button from '@/components/ui/button/Button' 7 | 8 | import { IAuthFormData } from '@/types/auth.interface' 9 | 10 | import AuthFields from './AuthFields' 11 | import { useAuthMutations } from './useAuthMutations' 12 | 13 | const Auth: FC = () => { 14 | const [isReg, setIsReg] = useState(false) 15 | 16 | const { handleSubmit, reset, control } = useForm({ 17 | mode: 'onChange' 18 | }) 19 | 20 | const { isLoading, registerSync, loginSync } = useAuthMutations(reset) 21 | 22 | const onSubmit: SubmitHandler = data => { 23 | if (isReg) registerSync(data) 24 | else loginSync(data) 25 | } 26 | 27 | return ( 28 | 29 | 30 | 31 | {isReg ? 'Sign Up' : 'Login'} 32 | 33 | {isLoading ? ( 34 | 35 | ) : ( 36 | <> 37 | 38 | 39 | 42 | 43 | setIsReg(!isReg)}> 44 | 45 | {isReg 46 | ? 'Already have an account? ' 47 | : "Don't have an account? "} 48 | 49 | {isReg ? 'Login' : 'Sign up'} 50 | 51 | 52 | 53 | 54 | )} 55 | 56 | 57 | ) 58 | } 59 | 60 | export default Auth 61 | --------------------------------------------------------------------------------