├── .watchmanconfig ├── .node-version ├── .ruby-version ├── src ├── index.ts ├── utils │ ├── get-price-text.ts │ ├── cut-string.ts │ ├── get-basket-total-price.ts │ ├── testing.tsx │ └── layout.ts ├── navigation │ ├── route-names.ts │ ├── types.ts │ └── product-stack.tsx ├── api │ ├── axios.instance.ts │ └── product │ │ ├── index.ts │ │ ├── key-factory.ts │ │ ├── types.ts │ │ ├── get-all-products.ts │ │ └── get-product-by-id.ts ├── screens │ ├── index.ts │ ├── error.tsx │ ├── __tests__ │ │ ├── error.test.tsx │ │ ├── basket.test.tsx │ │ ├── product-list.test.tsx │ │ └── product-detail.test.tsx │ ├── product-list.tsx │ ├── basket.tsx │ └── product-detail.tsx ├── styles │ └── common-styles.ts ├── components │ ├── spacing.tsx │ ├── screen-loading.tsx │ ├── close-icon.tsx │ ├── delete-icon.tsx │ ├── error-boundary.tsx │ ├── basket-icon.tsx │ ├── quantity-toggler.tsx │ ├── product-list-card.tsx │ └── basket-card.tsx ├── hooks │ └── useRefreshByUser.ts ├── store │ ├── helpers.ts │ ├── product.test.ts │ ├── helpers.test.ts │ └── product.ts └── app.tsx ├── .bundle └── config ├── app.json ├── docs ├── e2e-iOS.mp4 ├── coverage-ss.png ├── e2e-android.mp4 ├── basket-screen-overview.gif ├── product-detail-overview.gif └── product-list-screen-overview.gif ├── ios ├── Fonts │ ├── Entypo.ttf │ ├── Feather.ttf │ ├── Fontisto.ttf │ ├── Ionicons.ttf │ ├── Octicons.ttf │ ├── Zocial.ttf │ ├── AntDesign.ttf │ ├── EvilIcons.ttf │ ├── Foundation.ttf │ ├── FontAwesome.ttf │ ├── MaterialIcons.ttf │ ├── SimpleLineIcons.ttf │ ├── FontAwesome5_Solid.ttf │ ├── FontAwesome5_Brands.ttf │ ├── FontAwesome5_Regular.ttf │ └── MaterialCommunityIcons.ttf ├── ReactNativeZustandRQ │ ├── Images.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── AppDelegate.h │ ├── main.m │ ├── AppDelegate.mm │ ├── Info.plist │ └── LaunchScreen.storyboard ├── ReactNativeZustandRQ.xcworkspace │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── contents.xcworkspacedata ├── .xcode.env ├── ReactNativeZustandRQTests │ ├── Info.plist │ └── ReactNativeZustandRQTests.m ├── Podfile ├── ReactNativeZustandRQ.xcodeproj │ └── xcshareddata │ │ └── xcschemes │ │ └── ReactNativeZustandRQ.xcscheme └── Podfile.lock ├── android ├── app │ ├── debug.keystore │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── values │ │ │ │ │ ├── strings.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── xml │ │ │ │ │ └── network_security_config.xml │ │ │ │ └── drawable │ │ │ │ │ └── rn_edit_text_material.xml │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── reactnativezustandrq │ │ │ │ ├── MainActivity.java │ │ │ │ └── MainApplication.java │ │ ├── debug │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── reactnativezustandrq │ │ │ │ └── ReactNativeFlipper.java │ │ ├── release │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── reactnativezustandrq │ │ │ │ └── ReactNativeFlipper.java │ │ └── androidTest │ │ │ └── java │ │ │ └── com │ │ │ └── reactnativezustandrq │ │ │ └── DetoxTest.java │ ├── proguard-rules.pro │ └── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── settings.gradle ├── build.gradle ├── gradle.properties ├── gradlew.bat └── gradlew ├── .eslintrc.js ├── tsconfig.spec.json ├── babel.config.js ├── .prettierrc.js ├── react-native.config.js ├── Gemfile ├── tsconfig.eslint.json ├── index.js ├── tsconfig.json ├── metro.config.js ├── e2e ├── jest.config.js └── starter.test.js ├── .github └── workflows │ └── ci.yml ├── jest.setup.ts ├── __mocks__ ├── zustand.js └── msw │ ├── handlers.ts │ └── mock-data.ts ├── jest.config.js ├── .gitignore ├── package.json ├── .detoxrc.js └── Gemfile.lock /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.6 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {default as App} from './app'; 2 | -------------------------------------------------------------------------------- /.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /src/utils/get-price-text.ts: -------------------------------------------------------------------------------- 1 | export const getPriceText = (price: number) => `$ ${price}`; 2 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ReactNativeZustandRQ", 3 | "displayName": "ReactNativeZustandRQ" 4 | } -------------------------------------------------------------------------------- /docs/e2e-iOS.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/docs/e2e-iOS.mp4 -------------------------------------------------------------------------------- /docs/coverage-ss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/docs/coverage-ss.png -------------------------------------------------------------------------------- /docs/e2e-android.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/docs/e2e-android.mp4 -------------------------------------------------------------------------------- /ios/Fonts/Entypo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/ios/Fonts/Entypo.ttf -------------------------------------------------------------------------------- /ios/Fonts/Feather.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/ios/Fonts/Feather.ttf -------------------------------------------------------------------------------- /ios/Fonts/Fontisto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/ios/Fonts/Fontisto.ttf -------------------------------------------------------------------------------- /ios/Fonts/Ionicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/ios/Fonts/Ionicons.ttf -------------------------------------------------------------------------------- /ios/Fonts/Octicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/ios/Fonts/Octicons.ttf -------------------------------------------------------------------------------- /ios/Fonts/Zocial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/ios/Fonts/Zocial.ttf -------------------------------------------------------------------------------- /ios/Fonts/AntDesign.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/ios/Fonts/AntDesign.ttf -------------------------------------------------------------------------------- /ios/Fonts/EvilIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/ios/Fonts/EvilIcons.ttf -------------------------------------------------------------------------------- /ios/Fonts/Foundation.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/ios/Fonts/Foundation.ttf -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/android/app/debug.keystore -------------------------------------------------------------------------------- /ios/Fonts/FontAwesome.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/ios/Fonts/FontAwesome.ttf -------------------------------------------------------------------------------- /ios/Fonts/MaterialIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/ios/Fonts/MaterialIcons.ttf -------------------------------------------------------------------------------- /ios/Fonts/SimpleLineIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/ios/Fonts/SimpleLineIcons.ttf -------------------------------------------------------------------------------- /docs/basket-screen-overview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/docs/basket-screen-overview.gif -------------------------------------------------------------------------------- /docs/product-detail-overview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/docs/product-detail-overview.gif -------------------------------------------------------------------------------- /ios/Fonts/FontAwesome5_Solid.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/ios/Fonts/FontAwesome5_Solid.ttf -------------------------------------------------------------------------------- /ios/Fonts/FontAwesome5_Brands.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/ios/Fonts/FontAwesome5_Brands.ttf -------------------------------------------------------------------------------- /ios/Fonts/FontAwesome5_Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/ios/Fonts/FontAwesome5_Regular.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ReactNativeZustandRQ 3 | 4 | -------------------------------------------------------------------------------- /docs/product-list-screen-overview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/docs/product-list-screen-overview.gif -------------------------------------------------------------------------------- /ios/Fonts/MaterialCommunityIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/ios/Fonts/MaterialCommunityIcons.ttf -------------------------------------------------------------------------------- /src/utils/cut-string.ts: -------------------------------------------------------------------------------- 1 | export const cutString = (title: string) => 2 | title.length >= 25 ? title.substring(0, 25) + '...' : title; 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@react-native-community', 4 | ignorePatterns: ['**/e2e/*.js'], 5 | }; 6 | -------------------------------------------------------------------------------- /ios/ReactNativeZustandRQ/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | // tsconfig.spec.json 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "jsx": "react" 6 | } 7 | } -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | presets: ['module:metro-react-native-babel-preset'], 4 | }; 5 | -------------------------------------------------------------------------------- /ios/ReactNativeZustandRQ/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : RCTAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/navigation/route-names.ts: -------------------------------------------------------------------------------- 1 | export enum RouteNames { 2 | productList = 'product-list', 3 | productDetail = 'product-detail', 4 | basket = 'basket', 5 | } 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'avoid', 3 | bracketSameLine: true, 4 | bracketSpacing: false, 5 | singleQuote: true, 6 | trailingComma: 'all', 7 | }; 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarikfp/react-native-testing/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src/api/axios.instance.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const BASE_URL = 'https://fakestoreapi.com'; 4 | 5 | export const api = axios.create({ 6 | baseURL: BASE_URL, 7 | }); 8 | -------------------------------------------------------------------------------- /src/api/product/index.ts: -------------------------------------------------------------------------------- 1 | export {useGetAllProducts} from './get-all-products'; 2 | export {useGetProductById} from './get-product-by-id'; 3 | export type {Product, Rating} from './types'; 4 | -------------------------------------------------------------------------------- /src/api/product/key-factory.ts: -------------------------------------------------------------------------------- 1 | export const productKeyFactory = { 2 | products: ['all-products'], 3 | productById: (id: number) => [...productKeyFactory.products, id], 4 | } as const; 5 | -------------------------------------------------------------------------------- /react-native.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dependencies: { 3 | 'react-native-vector-icons': { 4 | platforms: { 5 | ios: null, 6 | }, 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 4 | ruby File.read(File.join(__dir__, '.ruby-version')).strip 5 | 6 | gem 'cocoapods', '~> 1.11', '>= 1.11.3' 7 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "src/**/*.ts", 5 | "src/**/*.tsx", 6 | "__mocks__/**/*.ts", 7 | "test/**/*.ts", 8 | "test/**/*.tsx", 9 | "src/**/*.json" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/screens/index.ts: -------------------------------------------------------------------------------- 1 | export {default as BasketScreen} from './basket'; 2 | export {default as ErrorScreen} from './error'; 3 | export {default as ProductDetailScreen} from './product-detail'; 4 | export {default as ProductListScreen} from './product-list'; 5 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import {AppRegistry} from 'react-native'; 6 | import 'react-native-gesture-handler'; 7 | import {name as appName} from './app.json'; 8 | import {App} from './src'; 9 | 10 | AppRegistry.registerComponent(appName, () => App); 11 | -------------------------------------------------------------------------------- /ios/ReactNativeZustandRQ/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | @autoreleasepool { 8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/styles/common-styles.ts: -------------------------------------------------------------------------------- 1 | export const COMMON_STYLES = { 2 | flex: { 3 | flex: 1, 4 | }, 5 | flexCenter: { 6 | flex: 1, 7 | justifyContent: 'center', 8 | alignItems: 'center', 9 | }, 10 | screenPadding: 16, 11 | screenBgColor: '#E2E2E2', 12 | } as const; 13 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'ReactNativeZustandRQ' 2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 3 | include ':app' 4 | includeBuild('../node_modules/react-native-gradle-plugin') 5 | -------------------------------------------------------------------------------- /src/api/product/types.ts: -------------------------------------------------------------------------------- 1 | export interface Product { 2 | id: number; 3 | title: string; 4 | price: number; 5 | description: string; 6 | category: string; 7 | image: string; 8 | rating: Rating; 9 | } 10 | 11 | export interface Rating { 12 | rate: number; 13 | count: number; 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/get-basket-total-price.ts: -------------------------------------------------------------------------------- 1 | import {ProductInBasket} from '../store/product'; 2 | 3 | export const getBasketTotalPrice = ( 4 | favoritedProducts: Array, 5 | ) => { 6 | return favoritedProducts 7 | .reduce((acc, curr) => acc + curr.product.price * curr.quantity, 0) 8 | .toFixed(2); 9 | }; 10 | -------------------------------------------------------------------------------- /ios/ReactNativeZustandRQ.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/ReactNativeZustandRQ.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10.0.2.2 5 | localhost 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/react-native/tsconfig.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "node", 6 | "detox", 7 | "@types/jest", 8 | ], 9 | }, 10 | "exclude": [ 11 | "node_modules", 12 | "babel.config.js", 13 | "metro.config.js", 14 | "jest.config.js", 15 | "e2e/**/*.js", 16 | ], 17 | } 18 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: true, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/spacing.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {View} from 'react-native'; 3 | 4 | type Props = { 5 | height?: number; 6 | width?: number; 7 | backgroundColor?: string; 8 | }; 9 | 10 | const Spacing: React.FC = ({ 11 | height, 12 | width, 13 | backgroundColor = '#ffff', 14 | }) => { 15 | return ; 16 | }; 17 | 18 | export default Spacing; 19 | -------------------------------------------------------------------------------- /e2e/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@jest/types').Config.InitialOptions} */ 2 | module.exports = { 3 | rootDir: '..', 4 | testMatch: ['/e2e/**/*.test.js'], 5 | testTimeout: 120000, 6 | maxWorkers: 1, 7 | globalSetup: 'detox/runners/jest/globalSetup', 8 | globalTeardown: 'detox/runners/jest/globalTeardown', 9 | reporters: ['detox/runners/jest/reporter'], 10 | testEnvironment: 'detox/runners/jest/testEnvironment', 11 | verbose: true, 12 | }; 13 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /src/components/screen-loading.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {ActivityIndicator, View} from 'react-native'; 3 | import {COMMON_STYLES} from '../styles/common-styles'; 4 | 5 | const ScreenLoading: React.FC = () => { 6 | return ( 7 | 8 | 13 | 14 | ); 15 | }; 16 | 17 | export default ScreenLoading; 18 | -------------------------------------------------------------------------------- /src/hooks/useRefreshByUser.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function useRefreshByUser(refetch: any) { 4 | const [isRefetchingByUser, setIsRefetchingByUser] = React.useState(false); 5 | 6 | async function refetchByUser() { 7 | setIsRefetchingByUser(true); 8 | 9 | try { 10 | await refetch(); 11 | } finally { 12 | setIsRefetchingByUser(false); 13 | } 14 | } 15 | 16 | return { 17 | isRefetchingByUser, 18 | refetchByUser, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/testing.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {QueryClient, QueryClientProvider} from 'react-query'; 3 | 4 | export const createReactQueryWrapper = ({ 5 | children, 6 | }: { 7 | children: React.ReactNode; 8 | }) => { 9 | const queryClient = new QueryClient({ 10 | defaultOptions: { 11 | queries: { 12 | retry: false, 13 | }, 14 | }, 15 | }); 16 | return ( 17 | {children} 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /src/components/close-icon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {Pressable} from 'react-native'; 3 | import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; 4 | 5 | type Props = { 6 | onPress: () => void; 7 | color: string; 8 | }; 9 | 10 | const CloseIcon: React.FC = ({onPress, color}) => { 11 | return ( 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default CloseIcon; 19 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/delete-icon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {Pressable} from 'react-native'; 3 | import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; 4 | 5 | type Props = { 6 | onPress: () => void; 7 | }; 8 | 9 | const DeleteIcon: React.FC = ({onPress}) => { 10 | return ( 11 | 12 | 18 | 19 | ); 20 | }; 21 | 22 | export default DeleteIcon; 23 | -------------------------------------------------------------------------------- /src/navigation/types.ts: -------------------------------------------------------------------------------- 1 | import {NativeStackScreenProps} from '@react-navigation/native-stack'; 2 | import {RouteNames} from './route-names'; 3 | 4 | export type ProductStackParamList = { 5 | [RouteNames.productList]: undefined; 6 | [RouteNames.basket]: undefined; 7 | [RouteNames.productDetail]: {id: number}; 8 | }; 9 | 10 | export type ProductListScreenProps = NativeStackScreenProps< 11 | ProductStackParamList, 12 | RouteNames.productList 13 | >; 14 | 15 | export type ProductDetailScreenProps = NativeStackScreenProps< 16 | ProductStackParamList, 17 | RouteNames.productDetail 18 | >; 19 | 20 | export type BasketScreen = NativeStackScreenProps< 21 | ProductStackParamList, 22 | RouteNames.basket 23 | >; 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | - release 8 | push: 9 | branches: 10 | - main 11 | - release 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Node.js 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: 14 23 | - name: Install dependencies 24 | run: yarn install 25 | - name: Run tests 26 | run: yarn test --updateSnapshot --detectOpenHandles 27 | - name: Run yarn lint 28 | run: | 29 | yarn lint 30 | - name: Run yarn tsc 31 | run: | 32 | yarn build 33 | -------------------------------------------------------------------------------- /src/api/product/get-all-products.ts: -------------------------------------------------------------------------------- 1 | import {AxiosError} from 'axios'; 2 | import {useQuery, UseQueryOptions} from 'react-query'; 3 | import {api} from '../axios.instance'; 4 | import {productKeyFactory} from './key-factory'; 5 | import {Product} from './types'; 6 | 7 | export const getAllProducts = async () => { 8 | return (await api.get>('/products')).data; 9 | }; 10 | 11 | export const useGetAllProducts = ( 12 | options?: UseQueryOptions< 13 | Array, 14 | AxiosError, 15 | Array, 16 | readonly [string] 17 | >, 18 | ) => { 19 | return useQuery({ 20 | queryKey: [...productKeyFactory.products], 21 | queryFn: getAllProducts, 22 | ...options, 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import 'react-native-gesture-handler/jestSetup'; 2 | import {mswServer} from './__mocks__/msw/handlers'; 3 | 4 | jest.useFakeTimers(); 5 | 6 | // https://mswjs.io/docs/getting-started/integrate/node#setup 7 | 8 | // Establish API mocking before all tests. 9 | beforeAll(() => mswServer.listen()); 10 | // Reset any request handlers that we may add during the tests, 11 | // so they don't affect other tests. 12 | afterEach(() => mswServer.resetHandlers()); 13 | // Clean up after the tests are finished. 14 | afterAll(() => mswServer.close()); 15 | 16 | // Silence the warning: Animated: `useNativeDriver` is not supported because the native animated module is missing 17 | jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper'); 18 | -------------------------------------------------------------------------------- /src/api/product/get-product-by-id.ts: -------------------------------------------------------------------------------- 1 | import {AxiosError} from 'axios'; 2 | import {useQuery, UseQueryOptions} from 'react-query'; 3 | import {api} from '../axios.instance'; 4 | import {productKeyFactory} from './key-factory'; 5 | import {Product} from './types'; 6 | 7 | export const getProductById = async (productId: number) => { 8 | return (await api.get(`/products/${productId}`)).data; 9 | }; 10 | 11 | export const useGetProductById = ( 12 | productId: number, 13 | options?: UseQueryOptions< 14 | Product, 15 | AxiosError, 16 | Product, 17 | readonly (string | number)[] 18 | >, 19 | ) => { 20 | return useQuery({ 21 | queryFn: () => getProductById(productId), 22 | queryKey: [...productKeyFactory.productById(productId)], 23 | ...options, 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /android/app/src/release/java/com/reactnativezustandrq/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | *

This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package com.reactnativezustandrq; 8 | 9 | import android.content.Context; 10 | import com.facebook.react.ReactInstanceManager; 11 | 12 | /** 13 | * Class responsible of loading Flipper inside your React Native application. This is the release 14 | * flavor of it so it's empty as we don't want to load Flipper. 15 | */ 16 | public class ReactNativeFlipper { 17 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 18 | // Do nothing as we don't want to initialize Flipper on Release. 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /__mocks__/zustand.js: -------------------------------------------------------------------------------- 1 | // https://docs.pmnd.rs/zustand/guides/testing#resetting-state-between-tests-in-react-dom 2 | const {create: actualCreate} = jest.requireActual('zustand'); // if using jest 3 | import {act} from '@testing-library/react-native'; 4 | 5 | // a variable to hold reset functions for all stores declared in the app 6 | const storeResetFns = new Set(); 7 | 8 | // when creating a store, we get its initial state, create a reset function and add it in the set 9 | export const create = createState => { 10 | const store = actualCreate(createState); 11 | const initialState = store.getState(); 12 | storeResetFns.add(() => store.setState(initialState, true)); 13 | return store; 14 | }; 15 | 16 | // Reset all stores after each test run 17 | beforeEach(() => { 18 | act(() => storeResetFns.forEach(resetFn => resetFn())); 19 | }); 20 | -------------------------------------------------------------------------------- /ios/ReactNativeZustandRQTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/screens/error.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {Button, StyleSheet, Text, View} from 'react-native'; 3 | 4 | type Props = { 5 | resetError: () => void; 6 | }; 7 | 8 | const ErrorScreen: React.FC = ({resetError}) => { 9 | return ( 10 | 11 | 12 | An error occurred... 13 | 14 |