├── src ├── modules │ ├── app │ │ ├── utils │ │ │ └── example.ts │ │ ├── api │ │ │ ├── index.ts │ │ │ └── routes.ts │ │ ├── types │ │ │ ├── TokenType.ts │ │ │ ├── ILoginDTO.ts │ │ │ └── IAccountInfoDTO.ts │ │ ├── redux │ │ │ ├── IAppReducer.ts │ │ │ └── appSlice.ts │ │ ├── screens │ │ │ ├── ErrorBoundaryPage.tsx │ │ │ ├── Onboarding │ │ │ │ ├── style.ts │ │ │ │ └── index.tsx │ │ │ ├── Home │ │ │ │ ├── style.ts │ │ │ │ └── index.tsx │ │ │ ├── Login │ │ │ │ ├── style.ts │ │ │ │ └── index.tsx │ │ │ └── Signup │ │ │ │ └── index.tsx │ │ ├── components │ │ │ └── Example.tsx │ │ └── services │ │ │ └── appService.ts │ ├── news │ │ ├── api │ │ │ └── routes.ts │ │ ├── redux │ │ │ ├── INewsReducer.ts │ │ │ └── newsSlice.ts │ │ ├── types │ │ │ └── INewsArticle.ts │ │ ├── screens │ │ │ ├── NewsList │ │ │ │ ├── style.ts │ │ │ │ └── index.tsx │ │ │ └── NewsDetail │ │ │ │ ├── style.ts │ │ │ │ └── index.tsx │ │ └── services │ │ │ └── newsService.ts │ └── profile │ │ └── screens │ │ ├── Settings │ │ ├── style.ts │ │ └── index.tsx │ │ └── Profile │ │ ├── style.ts │ │ └── index.tsx ├── assets │ ├── images │ │ ├── icon.png │ │ ├── splash.png │ │ └── adaptive-icon.png │ ├── languages │ │ ├── english.json │ │ └── turkish.json │ └── font │ │ └── index.ts ├── components │ ├── GeneralActivityIndicator │ │ ├── type.ts │ │ ├── style.ts │ │ ├── index.tsx │ │ └── GeneralActivityIndicator.test.tsx │ ├── ErrorComponent │ │ ├── type.ts │ │ ├── style.ts │ │ ├── index.tsx │ │ └── ErrorComponent.test.tsx │ ├── NotFoundComponent │ │ ├── type.ts │ │ ├── style.ts │ │ ├── NotFoundComponent.test.tsx │ │ └── index.tsx │ ├── ToastMessage │ │ ├── IToastType.ts │ │ ├── ToastColorEnum.ts │ │ ├── style.ts │ │ └── index.tsx │ ├── index.js │ ├── BottomSheetContainer │ │ ├── style.ts │ │ ├── type.ts │ │ └── index.tsx │ └── Skeleton │ │ ├── index.tsx │ │ └── SkeletonVariants.tsx ├── styles │ ├── fonts.ts │ ├── index.ts │ ├── typography.ts │ ├── toast.ts │ ├── padding.ts │ └── theme.ts ├── localization │ ├── index.js │ ├── en.ts │ └── tr.ts ├── network │ ├── axios.d.ts │ ├── axiosInstance.ts │ └── baseQuery.ts ├── hooks │ ├── index.ts │ ├── useTheme.ts │ ├── useThemedStyles.ts │ └── useBottomSheet.ts ├── helpers │ ├── storage │ │ ├── storeEnum.ts │ │ ├── index.ts │ │ └── storage.test.ts │ ├── localization │ │ ├── localization.test.ts │ │ └── index.ts │ ├── toast │ │ ├── showToast.ts │ │ └── showToast.test.ts │ ├── global │ │ ├── index.ts │ │ └── global.test.ts │ └── router │ │ ├── router.test.ts │ │ └── index.ts ├── store │ ├── rootReducer.ts │ └── index.ts ├── utils │ ├── ScreenOptions.ts │ └── Routes.ts ├── providers │ ├── ThemeListener.tsx │ ├── ThemeProvider.tsx │ ├── Toast.tsx │ ├── ErrorBoundary.tsx │ ├── index.tsx │ ├── AppLoadingProvider.tsx │ ├── Localization.tsx │ ├── NetworkInfoContainer.tsx │ └── Notification.tsx └── routers │ ├── ProfileStack.tsx │ ├── BottomNavigation.tsx │ └── index.tsx ├── .vscode └── settings.json ├── custom.d.ts ├── .eslintrc.js ├── eas.json ├── jest.config.js ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── babel.config.js ├── jsconfig.json ├── .gitignore ├── App.tsx ├── LICENSE ├── tsconfig.json ├── app.config.ts ├── package.json └── README.md /src/modules/app/utils/example.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/modules/app/api/index.ts: -------------------------------------------------------------------------------- 1 | // TODO Api Example -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } -------------------------------------------------------------------------------- /src/modules/app/types/TokenType.ts: -------------------------------------------------------------------------------- 1 | export type TokenType = { 2 | accessToken: string; 3 | }; 4 | -------------------------------------------------------------------------------- /src/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milvasoft/expo-boilerplate/HEAD/src/assets/images/icon.png -------------------------------------------------------------------------------- /src/assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milvasoft/expo-boilerplate/HEAD/src/assets/images/splash.png -------------------------------------------------------------------------------- /src/modules/app/types/ILoginDTO.ts: -------------------------------------------------------------------------------- 1 | export interface ILoginDTO { 2 | userName: string; 3 | password: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/GeneralActivityIndicator/type.ts: -------------------------------------------------------------------------------- 1 | export interface GeneralActivityIndicatorProps { 2 | text?: string; 3 | } -------------------------------------------------------------------------------- /src/styles/fonts.ts: -------------------------------------------------------------------------------- 1 | export const BlackText ={ 2 | fontFamily: "Black", 3 | marginTop: 20, 4 | fontSize: 20, 5 | } -------------------------------------------------------------------------------- /src/assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milvasoft/expo-boilerplate/HEAD/src/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /src/styles/index.ts: -------------------------------------------------------------------------------- 1 | import padding from './padding'; 2 | import typography from './typography'; 3 | 4 | export {padding, typography}; -------------------------------------------------------------------------------- /src/assets/languages/english.json: -------------------------------------------------------------------------------- 1 | { 2 | "CFBundleDisplayName": "Milvasoft", 3 | "NSContactsUsageDescription": "Milvasoft" 4 | } -------------------------------------------------------------------------------- /src/assets/languages/turkish.json: -------------------------------------------------------------------------------- 1 | { 2 | "CFBundleDisplayName": "Milvasoft", 3 | "NSContactsUsageDescription": "Milvasoft" 4 | } -------------------------------------------------------------------------------- /src/components/ErrorComponent/type.ts: -------------------------------------------------------------------------------- 1 | export interface ErrorComponentProps{ 2 | errorMessage?: string; 3 | onRetry?: () => void; 4 | } -------------------------------------------------------------------------------- /src/components/NotFoundComponent/type.ts: -------------------------------------------------------------------------------- 1 | export interface NotFoundComponentrProps { 2 | title?: string; 3 | onPress?: () => void; 4 | } 5 | -------------------------------------------------------------------------------- /src/localization/index.js: -------------------------------------------------------------------------------- 1 | import EnResource from "./en"; 2 | import TrResource from "./tr"; 3 | 4 | export { EnResource, TrResource }; 5 | -------------------------------------------------------------------------------- /src/modules/app/api/routes.ts: -------------------------------------------------------------------------------- 1 | const controller = '/Account'; 2 | 3 | export const sigIn = `${controller}/SignIn`; 4 | export const sigOut = `${controller}/SignIn`; 5 | -------------------------------------------------------------------------------- /src/modules/app/types/IAccountInfoDTO.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IAccountInfoDTO { 3 | id: string; 4 | nameSurname: string; 5 | userName: string; 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/network/axios.d.ts: -------------------------------------------------------------------------------- 1 | import "axios"; 2 | 3 | declare module "axios" { 4 | export interface AxiosRequestConfig { 5 | withoutToast?: boolean; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/ToastMessage/IToastType.ts: -------------------------------------------------------------------------------- 1 | import { ToastColorEnum } from './ToastColorEnum'; 2 | 3 | export interface IToastType { 4 | msg: string; 5 | duration?: number; 6 | type?: ToastColorEnum; 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import useTheme from "./useTheme"; 2 | import useThemedStyles from "./useThemedStyles"; 3 | import { useBottomSheet } from "./useBottomSheet"; 4 | 5 | export { useTheme, useThemedStyles, useBottomSheet }; 6 | -------------------------------------------------------------------------------- /src/components/ToastMessage/ToastColorEnum.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | export enum ToastColorEnum { 3 | Error = '#F44336', 4 | Succes = '#4CAF50', 5 | Warning = '#FF9800', 6 | Info = '#2196F3' 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/typography.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents the typography styles for the application. 3 | */ 4 | export default { 5 | XSMALL: 10, 6 | SMALL: 13, 7 | NORMAL: 15, 8 | MEDIUM: 17, 9 | BIG: 20, 10 | HUGE: 32, 11 | }; -------------------------------------------------------------------------------- /src/styles/toast.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Defines the colors for different types of toasts. 3 | */ 4 | const ToastColor = { 5 | error: '#F44336', 6 | warning: '#FF9800', 7 | info: '#2196F3', 8 | success: '#4CAF50' 9 | }; 10 | 11 | export default ToastColor; 12 | 13 | -------------------------------------------------------------------------------- /src/helpers/storage/storeEnum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ali Burhan Keskin 3 | */ 4 | /** 5 | * Enum representing the different types of data that can be stored in the storage. 6 | */ 7 | export enum StoreEnum { 8 | Token, 9 | User, 10 | ColorMode 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/news/api/routes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ali Burhan Keskin 3 | */ 4 | 5 | export const NEWS_ROUTES = { 6 | GET_NEWS: "/news", 7 | GET_NEWS_BY_CATEGORY: (category: string) => `/news?category=${category}`, 8 | GET_NEWS_BY_ID: (id: string) => `/news/${id}`, 9 | }; 10 | -------------------------------------------------------------------------------- /src/modules/app/redux/IAppReducer.ts: -------------------------------------------------------------------------------- 1 | import { ColorSchemeName } from "react-native"; 2 | 3 | export interface IAppReducer { 4 | isSignedIn?: boolean; 5 | hasSeenOnboarding?: boolean; 6 | userColorScheme?: ColorSchemeName; 7 | user?: any; 8 | authToken?: string; 9 | expoToken?: string; 10 | } 11 | -------------------------------------------------------------------------------- /custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const content: any; 3 | export default content; 4 | } 5 | 6 | declare module '*.jpg' { 7 | const content: any; 8 | export default content; 9 | } 10 | 11 | declare module '*.png' { 12 | const content: any; 13 | export default content; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/helpers/localization/localization.test.ts: -------------------------------------------------------------------------------- 1 | import translate from './index'; 2 | 3 | describe('translate', () => { 4 | it('should return the translated string', () => { 5 | // Add your test case here 6 | // For example: 7 | const translatedString = translate('hello'); 8 | expect(translatedString).toEqual('translated hello'); 9 | }); 10 | }); -------------------------------------------------------------------------------- /src/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { ThemeContext } from '@src/providers/ThemeProvider'; 2 | import { useContext } from 'react'; 3 | 4 | /** 5 | * Custom hook that returns the current theme from the ThemeContext. 6 | * @returns The current theme object. 7 | */ 8 | function useTheme() { 9 | 10 | return useContext(ThemeContext); 11 | 12 | } 13 | 14 | export default useTheme; 15 | -------------------------------------------------------------------------------- /src/modules/news/redux/INewsReducer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ali Burhan Keskin 3 | */ 4 | import { INewsArticle, NewsCategory } from "../types/INewsArticle"; 5 | 6 | export interface INewsReducer { 7 | articles: INewsArticle[]; 8 | selectedArticle: INewsArticle | null; 9 | selectedCategory: NewsCategory | "all"; 10 | isLoading: boolean; 11 | error: string | null; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/NotFoundComponent/style.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | import padding from "@styles/padding"; 3 | 4 | export const styles = StyleSheet.create({ 5 | root: { 6 | marginTop: padding.XLARGE, 7 | justifyContent: "center", 8 | alignItems: "center", 9 | }, 10 | title: { 11 | fontWeight: "bold", 12 | fontSize: 20, 13 | color: "#f27a1a", 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ali Burhan Keskin 3 | */ 4 | import GeneralActivityIndicator from "./GeneralActivityIndicator"; 5 | import ToastMessage from "./ToastMessage"; 6 | import BottomSheetContainer, { 7 | BottomSheetContext, 8 | } from "./BottomSheetContainer"; 9 | 10 | export { 11 | GeneralActivityIndicator, 12 | ToastMessage, 13 | BottomSheetContainer, 14 | BottomSheetContext, 15 | }; 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true 5 | }, 6 | extends: ['@react-native-community', 'plugin:@typescript-eslint/recommended'], 7 | parser: '@typescript-eslint/parser', 8 | parserOptions: { 9 | ecmaFeatures: { 10 | jsx: true 11 | }, 12 | ecmaVersion: 'latest', 13 | sourceType: 'module' 14 | }, 15 | plugins: ['react', '@typescript-eslint'], 16 | rules: {} 17 | } 18 | -------------------------------------------------------------------------------- /src/styles/padding.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Defines the padding values used in the application. 3 | */ 4 | export default { 5 | /** 6 | * Tiny padding value. 7 | */ 8 | TINY: 4, 9 | /** 10 | * Small padding value. 11 | */ 12 | SMALL: 8, 13 | /** 14 | * Medium padding value. 15 | */ 16 | MEDIUM: 16, 17 | /** 18 | * Large padding value. 19 | */ 20 | LARGE: 24, 21 | /** 22 | * Huge padding value. 23 | */ 24 | XLARGE: 32, 25 | }; 26 | -------------------------------------------------------------------------------- /src/store/rootReducer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ali Burhan Keskin 3 | */ 4 | import { combineReducers } from "redux"; 5 | import AppReducer from "@modules/app/redux/appSlice"; 6 | import NewsReducer from "@modules/news/redux/newsSlice"; 7 | 8 | /** 9 | * Root reducer function that combines all the reducers. 10 | * @returns The combined reducer object. 11 | */ 12 | export default combineReducers({ 13 | AppReducer, 14 | NewsReducer, 15 | }); 16 | -------------------------------------------------------------------------------- /eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 0.48.1" 4 | }, 5 | "build": { 6 | "development": { 7 | "developmentClient": true, 8 | "distribution": "internal", 9 | "channel": "development" 10 | }, 11 | "preview": { 12 | "distribution": "internal", 13 | "channel": "preview" 14 | }, 15 | "production": { 16 | "channel": "production" 17 | } 18 | }, 19 | "submit": { 20 | "production": {} 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/helpers/localization/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ali Burhan Keskin 3 | */ 4 | import { Scope, TranslateOptions } from 'i18n-js'; 5 | import i18n from '@providers/Localization'; 6 | 7 | const translate = (key: Scope, props?: TranslateOptions):string => i18n.t(key, props); 8 | 9 | 10 | /** 11 | * Translates the given text into the current language. 12 | * 13 | * @param text - The text to be translated. 14 | * @returns The translated text. 15 | */ 16 | export default translate; 17 | -------------------------------------------------------------------------------- /src/hooks/useThemedStyles.ts: -------------------------------------------------------------------------------- 1 | import useTheme from './useTheme'; 2 | 3 | /** 4 | * Custom hook that applies themed styles to a component. 5 | * 6 | * @template T - The type of the styles function. 7 | * @param {T} styles - The styles function that accepts the theme as an argument. 8 | * @returns {ReturnType} - The result of applying the styles function to the current theme. 9 | */ 10 | const useThemedStyles = any >(styles: any) : ReturnType => styles(useTheme()); 11 | 12 | export default useThemedStyles; 13 | -------------------------------------------------------------------------------- /src/modules/app/screens/ErrorBoundaryPage.tsx: -------------------------------------------------------------------------------- 1 | import { Text, StyleSheet, View } from 'react-native' 2 | import React from 'react' 3 | 4 | const ErrorBoundaryPage = () => { 5 | return ( 6 | 7 | Hata! 8 | 9 | ) 10 | } 11 | 12 | const styles = StyleSheet.create({ 13 | container: { 14 | flex: 1, 15 | justifyContent: 'center', 16 | alignSelf: 'center' 17 | }, 18 | text:{ 19 | 20 | } 21 | }) 22 | 23 | export default ErrorBoundaryPage 24 | -------------------------------------------------------------------------------- /src/helpers/toast/showToast.ts: -------------------------------------------------------------------------------- 1 | import { toastActions } from '@src/providers/Toast'; 2 | import { ToastColorEnum } from '../../components/ToastMessage/ToastColorEnum'; 3 | 4 | 5 | /** 6 | * Displays a toast message. 7 | * 8 | * @param msg - The message to display in the toast. 9 | * @param type - The color of the toast. Optional. 10 | * @param duration - The duration of the toast in milliseconds. Optional. 11 | */ 12 | export function showToast(msg: string, type?: ToastColorEnum, duration?: number) { 13 | toastActions.open({ msg, type, duration }); 14 | } 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | preset: "jest-expo", 4 | transformIgnorePatterns: [ 5 | "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|react-redux|native-base|react-native-svg)", 6 | ], 7 | collectCoverage: true, 8 | collectCoverageFrom: [ 9 | "**/*.{js,jsx}", 10 | "!**/coverage/**", 11 | "!**/node_modules/**", 12 | "!**/babel.config.js", 13 | "!**/jest.setup.js", 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/BottomSheetContainer/style.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Bottom Sheet Container Styles 3 | */ 4 | import { StyleSheet } from "react-native"; 5 | 6 | export const styles = StyleSheet.create({ 7 | container: { 8 | flex: 1, 9 | }, 10 | contentContainer: { 11 | flex: 1, 12 | paddingHorizontal: 16, 13 | }, 14 | backdrop: { 15 | ...StyleSheet.absoluteFillObject, 16 | }, 17 | handleIndicator: { 18 | backgroundColor: "#D1D5DB", 19 | width: 40, 20 | height: 4, 21 | borderRadius: 2, 22 | }, 23 | background: { 24 | borderTopLeftRadius: 16, 25 | borderTopRightRadius: 16, 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/ToastMessage/style.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | export const toastHeight = 120; 4 | 5 | export const styles = StyleSheet.create({ 6 | root: { 7 | height: toastHeight, 8 | position: "absolute", 9 | left: 0, 10 | top: 0, 11 | right: 0, 12 | flexDirection: "row", 13 | justifyContent: "flex-start", 14 | alignItems: "center", 15 | zIndex: 99999, 16 | paddingLeft: 30, 17 | paddingRight: 30, 18 | paddingTop: 35, 19 | }, 20 | text: { 21 | fontWeight: "700", 22 | color: "white", 23 | fontSize: 16, 24 | }, 25 | }); -------------------------------------------------------------------------------- /src/modules/news/types/INewsArticle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ali Burhan Keskin 3 | */ 4 | 5 | export interface INewsArticle { 6 | id: string; 7 | title: string; 8 | description: string; 9 | content: string; 10 | author: string; 11 | publishedAt: string; 12 | imageUrl: string; 13 | category: NewsCategory; 14 | source: string; 15 | url: string; 16 | } 17 | 18 | export type NewsCategory = 19 | | "technology" 20 | | "sports" 21 | | "business" 22 | | "entertainment" 23 | | "health" 24 | | "science"; 25 | 26 | export interface INewsResponse { 27 | articles: INewsArticle[]; 28 | totalResults: number; 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/ScreenOptions.ts: -------------------------------------------------------------------------------- 1 | import { CardStyleInterpolators, StackNavigationOptions } from "@react-navigation/stack"; 2 | import { Dimensions } from "react-native"; 3 | 4 | /** 5 | * Options for configuring the screen behavior and appearance. 6 | */ 7 | export const ScreenOptions: StackNavigationOptions = { 8 | gestureEnabled: true, 9 | gestureResponseDistance: Dimensions.get('screen').width, 10 | headerShown: true, 11 | cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS, 12 | headerStyle: { backgroundColor: '#FFF', }, 13 | headerTitleStyle: { fontFamily: 'Bold', }, 14 | headerTitleAlign: 'center' 15 | }; 16 | 17 | -------------------------------------------------------------------------------- /src/modules/app/components/Example.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View, Text, StyleSheet } from "react-native"; 3 | import { useThemedStyles } from "@src/hooks"; 4 | import { ITheme } from "@styles/theme"; 5 | 6 | const Example = () => { 7 | const themedStyles = useThemedStyles(styles); 8 | 9 | return ( 10 | 11 | example 12 | 13 | ); 14 | }; 15 | 16 | const styles = (theme: ITheme) => 17 | StyleSheet.create({ 18 | root: {}, 19 | 20 | text: { 21 | color: theme.secondary, 22 | }, 23 | }); 24 | 25 | export default React.memo(Example); 26 | -------------------------------------------------------------------------------- /src/components/NotFoundComponent/NotFoundComponent.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "@testing-library/react-native"; 3 | import NotFoundComponent from "./index"; 4 | 5 | const testID = "errorcomponent"; 6 | const title = "Page Not Found"; 7 | 8 | describe("NotFoundComponent", () => { 9 | test("renders component with correct title", () => { 10 | const { getByTestId } = render(); 11 | const component = getByTestId(`${testID}-container-view`); 12 | const titleElement = getByTestId(`${testID}-title-text`); 13 | expect(component).toBeTruthy(); 14 | expect(titleElement.props.children).toBe(title); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/components/GeneralActivityIndicator/style.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | import type { ITheme } from "@styles/theme"; 3 | 4 | export const styles = (theme: ITheme) => 5 | StyleSheet.create({ 6 | activityIndicator: { 7 | position: "absolute", 8 | zIndex: 9999, 9 | width: "100%", 10 | height: "100%", 11 | flex: 1, 12 | alignItems: "center", 13 | justifyContent: "center", 14 | flexDirection: "column", 15 | backgroundColor: "rgba(0, 0, 0, 0.85)", 16 | }, 17 | 18 | activityIndicatorText: { 19 | marginTop: 2, 20 | fontWeight: "bold", 21 | fontSize: 4, 22 | color: theme.primary, 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/helpers/global/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ali Burhan Keskin 3 | */ 4 | 5 | /** 6 | * Checks if a given string is a valid JSON. 7 | * 8 | * @param str - The string to be checked. 9 | * @returns A boolean indicating whether the string is a valid JSON or not. 10 | */ 11 | export function isValidJSON(str: any) { 12 | 13 | try { 14 | 15 | JSON.parse(str); 16 | return true; 17 | 18 | } catch (e) { 19 | 20 | return false; 21 | 22 | } 23 | 24 | } 25 | 26 | /** 27 | * Checks if a value is object-like. 28 | * 29 | * @param val - The value to check. 30 | * @returns Returns `true` if the value is object-like, else `false`. 31 | */ 32 | export const isObjectLike = (val: any):any => val !== null && typeof val === 'object'; 33 | -------------------------------------------------------------------------------- /src/components/ErrorComponent/style.ts: -------------------------------------------------------------------------------- 1 | import padding from "@styles/padding"; 2 | import { StyleSheet } from "react-native"; 3 | 4 | export const styles = StyleSheet.create({ 5 | container: { 6 | flex: 1, 7 | justifyContent: "center", 8 | alignItems: "center", 9 | }, 10 | message: { 11 | fontSize: 18, 12 | marginVertical: padding.MEDIUM, 13 | textAlign: "center", 14 | }, 15 | retryButton: { 16 | backgroundColor: "#f27a1a", 17 | paddingVertical: padding.SMALL, 18 | paddingHorizontal: padding.MEDIUM, 19 | borderRadius: 5, 20 | marginTop: padding.MEDIUM, 21 | }, 22 | buttonText: { 23 | color: "white", 24 | fontSize: 16, 25 | fontWeight: "bold", 26 | textAlign: "center", 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /src/hooks/useBottomSheet.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom hook to use Bottom Sheet 3 | */ 4 | import { useContext } from "react"; 5 | import { BottomSheetContext } from "@src/components/BottomSheetContainer"; 6 | import type { BottomSheetContextValue } from "@src/components/BottomSheetContainer/type"; 7 | 8 | /** 9 | * Hook to access bottom sheet functionality 10 | * @example 11 | * ```tsx 12 | * const { show, hide, isVisible } = useBottomSheet(); 13 | * 14 | * show({ 15 | * content: , 16 | * snapPoints: ['50%', '90%'], 17 | * }); 18 | * ``` 19 | */ 20 | export const useBottomSheet = (): BottomSheetContextValue => { 21 | const context = useContext(BottomSheetContext); 22 | 23 | if (!context) { 24 | throw new Error("useBottomSheet must be used within BottomSheetProvider"); 25 | } 26 | 27 | return context; 28 | }; 29 | -------------------------------------------------------------------------------- /src/helpers/toast/showToast.test.ts: -------------------------------------------------------------------------------- 1 | import { toastActions } from '@providers/Toast'; 2 | import { showToast } from './showToast'; 3 | import { ToastColorEnum } from '@components/ToastMessage/ToastColorEnum'; 4 | 5 | jest.mock('@providers/Toast'); 6 | 7 | describe('showToast', () => { 8 | it('should call toastActions.open with the provided message', () => { 9 | const msg = 'Test message'; 10 | 11 | showToast(msg); 12 | 13 | expect(toastActions.open).toHaveBeenCalledWith({ msg }); 14 | }); 15 | 16 | it('should call toastActions.open with the provided message, type, and duration', () => { 17 | const msg = 'Test message'; 18 | const type = ToastColorEnum.Succes; 19 | const duration = 3000; 20 | 21 | showToast(msg, type, duration); 22 | 23 | expect(toastActions.open).toHaveBeenCalledWith({ msg, type, duration }); 24 | }); 25 | }); -------------------------------------------------------------------------------- /src/utils/Routes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Enum representing the available routes in the application. 3 | */ 4 | export enum Routes { 5 | Onboarding = "Onboarding", 6 | Login = "Login", 7 | Signup = "Signup", 8 | Home = "Home", 9 | Profile = "Profile", 10 | News = "News", 11 | NewsDetail = "NewsDetail", 12 | Settings = "Settings", 13 | } 14 | 15 | /** 16 | * Represents the parameter types for the root stack navigation. 17 | */ 18 | export type RootStackParams = { 19 | [Routes.Onboarding]: undefined; 20 | [Routes.Login]: undefined; 21 | [Routes.Signup]: undefined; 22 | [Routes.Home]: undefined; 23 | [Routes.NewsDetail]: undefined; 24 | [Routes.Settings]: undefined; 25 | }; 26 | 27 | /** 28 | * Represents the navigation parameters for the root stack. 29 | */ 30 | export type NavigationParams = RootStackParams; 31 | 32 | export default Routes; 33 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useSelector } from 'react-redux' 2 | import { configureStore } from '@reduxjs/toolkit' 3 | import RootReducer from './rootReducer'// Root reducer 4 | 5 | /** 6 | * The Redux store instance. 7 | */ 8 | export const Store = configureStore({ reducer: RootReducer }) 9 | 10 | /** 11 | * The type representing the state of the root store. 12 | */ 13 | export type RootState = ReturnType 14 | 15 | /** 16 | * A custom selector hook that allows you to select values from the Redux store. 17 | * 18 | * @template T - The type of the selected value. 19 | * @param selector - A selector function that takes the root state and returns the selected value. 20 | * @returns The selected value from the Redux store. 21 | */ 22 | export const useAppSelector: TypedUseSelectorHook = useSelector 23 | 24 | export default Store 25 | -------------------------------------------------------------------------------- /src/styles/theme.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents the light theme colors. 3 | */ 4 | const LightTheme = { 5 | primary: "#f27a1a", 6 | secondary: "#202124", 7 | background: "rgb(242, 242, 242)", 8 | card: "rgb(255, 255, 255)", 9 | text: "rgb(28, 28, 30)", 10 | textSecondary: "rgb(142, 142, 147)", 11 | border: "rgb(216, 216, 216)", 12 | notification: "rgb(255, 59, 48)", 13 | }; 14 | 15 | /** 16 | * Represents the type for the theme object. 17 | */ 18 | export type ITheme = typeof LightTheme; 19 | 20 | /** 21 | * Dark theme object. 22 | */ 23 | const DarkTheme: ITheme = { 24 | primary: "#f27a1a", 25 | secondary: "#efefef", 26 | background: "rgb(1, 1, 1)", 27 | card: "rgb(18, 18, 18)", 28 | text: "rgb(229, 229, 231)", 29 | textSecondary: "rgb(142, 142, 147)", 30 | border: "rgb(39, 39, 41)", 31 | notification: "rgb(255, 69, 58)", 32 | }; 33 | 34 | export { LightTheme, DarkTheme }; 35 | -------------------------------------------------------------------------------- /src/components/NotFoundComponent/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View, Text } from "react-native"; 3 | import { styles } from "./style"; 4 | import { NotFoundComponentrProps } from "./type"; 5 | 6 | const testID = "errorcomponent"; 7 | /** 8 | * Renders a component for displaying a "Not Found" message. 9 | * 10 | * @param {Object} props - The component props. 11 | * @param {string} props.title - The title to be displayed. 12 | * @param {function} props.onPress - The function to be called when the component is pressed. 13 | * @returns {JSX.Element} The rendered component. 14 | */ 15 | const NotFoundComponent = ({ title, onPress }: NotFoundComponentrProps) => ( 16 | 17 | 18 | {title} 19 | 20 | 21 | ); 22 | 23 | export default NotFoundComponent; 24 | -------------------------------------------------------------------------------- /src/providers/ThemeListener.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { Appearance } from "react-native"; 3 | import throttle from "lodash.throttle"; 4 | import { useDispatch } from "react-redux"; 5 | import { SetColorShceme } from "@modules/app/redux/appSlice"; 6 | 7 | /** 8 | * Listens for changes in the device's color scheme and dispatches an action to update the app's theme accordingly. 9 | */ 10 | export default function ThemeListener() { 11 | const dispatch = useDispatch(); 12 | 13 | useEffect(() => { 14 | const handleColorModeChange = async ( 15 | preferences: Appearance.AppearancePreferences 16 | ) => { 17 | dispatch(SetColorShceme(preferences?.colorScheme)); 18 | }; 19 | 20 | Appearance.addChangeListener( 21 | throttle(handleColorModeChange, 100, { leading: false, trailing: true }) 22 | ); 23 | 24 | return () => {}; 25 | }, [dispatch]); 26 | 27 | return <>; 28 | } 29 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ["babel-preset-expo"], 5 | plugins: [ 6 | "react-native-reanimated/plugin", 7 | [ 8 | "module-resolver", 9 | { 10 | extensions: [".js", ".jsx", ".ts", ".tsx"], 11 | root: ["./"], 12 | alias: { 13 | "@components": "./src/components", 14 | "@modules": "./src/modules", 15 | "@routers": "./src/routers", 16 | "@screens": "./src/screens", 17 | "@helpers": "./src/helpers", 18 | "@assets": "./src/assets", 19 | "@icons": "./src/assets/icons", 20 | "@providers": "./src/providers", 21 | "@utils": "./src/utils", 22 | "@src": "./src", 23 | "@styles": "./src/styles", 24 | "@store": "./src/store", 25 | }, 26 | }, 27 | ], 28 | ], 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/modules/app/services/appService.ts: -------------------------------------------------------------------------------- 1 | import Store from '@store/index'; 2 | import { removeStoreDataAsync } from '@src/helpers/storage'; 3 | import { StoreEnum } from "@helpers/storage/storeEnum"; 4 | import { LoggedOut } from '../redux/appSlice'; 5 | import { ILoginDTO } from '../types/ILoginDTO'; 6 | 7 | /** 8 | * Signs in the user. 9 | * @param loginDto - The login data transfer object. 10 | */ 11 | export async function signIn(loginDto: ILoginDTO) { 12 | console.log(loginDto); 13 | } 14 | 15 | /** 16 | * Clears the user data by removing the token from the store and dispatching a LoggedOut action. 17 | * @returns {Promise} A promise that resolves once the user data is cleared. 18 | */ 19 | export async function clearUser() { 20 | await removeStoreDataAsync(StoreEnum.Token); 21 | 22 | Store.dispatch(LoggedOut()); 23 | } 24 | 25 | /** 26 | * Signs out the user by clearing user data. 27 | */ 28 | export function signOut() { 29 | clearUser(); 30 | } 31 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-native", 4 | "target": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "skipLibCheck": true, 7 | "noEmit": true, 8 | "allowSyntheticDefaultImports": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "moduleResolution": "node", 12 | "baseUrl": "./", 13 | "paths": { 14 | "@assets/*": ["./src/assets/*"], 15 | "@components/*": ["./src/components/*"], 16 | "@helpers/*": ["./src/helpers/*"], 17 | "@hooks/*": ["./src/hooks/*"], 18 | "@modules/*": ["./src/modules/*"], 19 | "@routers/*": ["./src/routers/*"], 20 | "@providers/*": ["./src/providers/*"], 21 | "@screens/*": ["./src/screens/*"], 22 | "@store/*": ["./src/store/*"], 23 | "@styles/*": ["./src/styles/*"], 24 | "@utils/*": ["./src/utils/*"], 25 | "@src/*": ["./src/*"], 26 | }, 27 | }, 28 | "include": ["src/**/*"], 29 | "exclude": ["node_modules"], 30 | } 31 | -------------------------------------------------------------------------------- /src/components/ErrorComponent/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View, Text, TouchableOpacity } from "react-native"; 3 | import { MaterialIcons } from "@expo/vector-icons"; 4 | import useTheme from "@hooks/useTheme"; 5 | import translate from "@helpers/localization"; 6 | import type { ErrorComponentProps } from "./type"; 7 | import { styles } from "./style"; 8 | 9 | const testID = "errorcomponent"; 10 | const ErrorComponent = ({ errorMessage, onRetry }: ErrorComponentProps) => { 11 | const theme = useTheme(); 12 | return ( 13 | 14 | 15 | 16 | {errorMessage} 17 | 18 | 19 | {translate("retry")} 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default ErrorComponent; 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /src/providers/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useAppSelector } from "@src/store"; 3 | import { DarkTheme, LightTheme } from "@styles/theme"; 4 | import type { ITheme } from "@styles/theme"; 5 | 6 | export const ThemeContext = React.createContext(LightTheme); 7 | 8 | type Props = { 9 | children: React.ReactNode; 10 | }; 11 | 12 | /** 13 | * Provides the theme for the application based on the user's color scheme. 14 | * 15 | * @param {Props} props - The component props. 16 | * @param {ReactNode} props.children - The child components to be wrapped by the theme provider. 17 | * @returns {JSX.Element} The JSX element representing the theme provider. 18 | */ 19 | function ThemeProvider({ children }: Props) { 20 | const userColorScheme = useAppSelector((s) => s?.AppReducer?.userColorScheme); 21 | const selectedTheme = userColorScheme === "dark" ? DarkTheme : LightTheme; 22 | return ( 23 | 24 | {children} 25 | 26 | ); 27 | } 28 | 29 | export default ThemeProvider; 30 | -------------------------------------------------------------------------------- /src/providers/Toast.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ToastMessage } from "@src/components"; 3 | import { IToastType } from "@components/ToastMessage/IToastType"; 4 | 5 | let useToastRef: any; 6 | 7 | /** 8 | * Sets the reference to the `useToastRef` property. 9 | * @param useToastRefProp - The reference to the `useToastRef` property. 10 | */ 11 | const setUseBackDropRef = (useToastRefProp: any) => { 12 | useToastRef = useToastRefProp; 13 | }; 14 | 15 | /** 16 | * Utility function for displaying toast messages. 17 | * @returns A JSX element representing the toast message. 18 | */ 19 | function ToastUtils() { 20 | return ; 21 | } 22 | 23 | /** 24 | * Object containing actions related to toast notifications. 25 | */ 26 | export const toastActions = { 27 | /** 28 | * Opens a toast notification with the specified parameters. 29 | * @param param - The parameters for the toast notification. 30 | */ 31 | open(param: IToastType) { 32 | useToastRef?.open(param); 33 | }, 34 | }; 35 | 36 | export default ToastUtils; 37 | -------------------------------------------------------------------------------- /src/helpers/router/router.test.ts: -------------------------------------------------------------------------------- 1 | import Routes from '@utils/Routes'; 2 | import { navigationRef, pop, push } from './index'; 3 | import { StackActions } from '@react-navigation/native'; 4 | 5 | jest.mock('./navigationRef'); 6 | 7 | jest.mock('@react-navigation/native', () => ({ 8 | StackActions: { 9 | push: jest.fn(), 10 | pop: jest.fn(), 11 | }, 12 | })); 13 | 14 | describe('push', () => { 15 | it('should call navigationRef.current?.dispatch with the provided route name and params', () => { 16 | const name = Routes.Home; 17 | 18 | push(name ); 19 | 20 | expect(navigationRef.isReady).toHaveBeenCalled(); 21 | expect(navigationRef.current?.dispatch).toHaveBeenCalledWith(StackActions.push(name)); 22 | }); 23 | }); 24 | 25 | describe('pop', () => { 26 | it('should call navigationRef.current?.dispatch with the provided count', () => { 27 | const count = 1; 28 | 29 | pop(count); 30 | 31 | expect(navigationRef.isReady).toHaveBeenCalled(); 32 | expect(navigationRef.current?.dispatch).toHaveBeenCalledWith(StackActions.pop(count)); 33 | }); 34 | }); 35 | 36 | -------------------------------------------------------------------------------- /src/routers/ProfileStack.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ali Burhan Keskin 3 | */ 4 | import * as React from "react"; 5 | import { createStackNavigator } from "@react-navigation/stack"; 6 | import { useTheme } from "@src/hooks"; 7 | import translate from "@helpers/localization"; 8 | import NewsList from "@modules/news/screens/NewsList"; 9 | 10 | const Stack = createStackNavigator(); 11 | 12 | function ProfileStack() { 13 | const theme = useTheme(); 14 | 15 | return ( 16 | 29 | 36 | 37 | ); 38 | } 39 | 40 | export default React.memo(ProfileStack); 41 | -------------------------------------------------------------------------------- /src/assets/font/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-no-useless-fragment */ 2 | /** 3 | * @author Ali Burhan Keskin 4 | */ 5 | import { 6 | Montserrat_100Thin, 7 | Montserrat_200ExtraLight, 8 | Montserrat_300Light, 9 | Montserrat_400Regular, 10 | Montserrat_500Medium, 11 | Montserrat_600SemiBold, 12 | Montserrat_700Bold, 13 | Montserrat_800ExtraBold, 14 | Montserrat_900Black, 15 | } from '@expo-google-fonts/montserrat'; 16 | 17 | const Thin = Montserrat_100Thin; 18 | const ExtraLight = Montserrat_200ExtraLight; 19 | const Light = Montserrat_300Light; 20 | const Regular = Montserrat_400Regular; 21 | const Medium = Montserrat_500Medium; 22 | const SemiBold = Montserrat_600SemiBold; 23 | const Bold = Montserrat_700Bold; 24 | const ExtraBold = Montserrat_800ExtraBold; 25 | const Black = Montserrat_900Black; 26 | 27 | /** 28 | * Represents the Montserrat font object. 29 | */ 30 | const MontserratFont = { 31 | Thin, 32 | ExtraLight, 33 | Light, 34 | Regular, 35 | Medium, 36 | SemiBold, 37 | Bold, 38 | ExtraBold, 39 | Black, 40 | }; 41 | 42 | export default MontserratFont; 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | dist/ 8 | build/ 9 | *.pbxuser 10 | !default.pbxuser 11 | *.mode1v3 12 | !default.mode1v3 13 | *.mode2v3 14 | !default.mode2v3 15 | *.perspectivev3 16 | !default.perspectivev3 17 | xcuserdata 18 | *.xccheckout 19 | *.moved-aside 20 | DerivedData 21 | *.hmap 22 | *.ipa 23 | *.xcuserstate 24 | project.xcworkspace 25 | 26 | # Android/IntelliJ 27 | # 28 | build/ 29 | .idea 30 | .gradle 31 | local.properties 32 | *.iml 33 | 34 | # node.js 35 | # 36 | node_modules/ 37 | npm-debug.log 38 | yarn-error.log 39 | 40 | # BUCK 41 | buck-out/ 42 | \.buckd/ 43 | *.keystore 44 | 45 | # fastlane 46 | # 47 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 48 | # screenshots whenever they are needed. 49 | # For more information about the recommended setup visit: 50 | # https://docs.fastlane.tools/best-practices/source-control/ 51 | 52 | */fastlane/report.xml 53 | */fastlane/Preview.html 54 | */fastlane/screenshots 55 | 56 | # Bundle artifacts 57 | *.jsbundle 58 | 59 | # CocoaPods 60 | /ios/Pods/ 61 | 62 | # Expo 63 | .expo/* 64 | web-build/ -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ali Burhan Keskin 3 | */ 4 | import React, { useEffect } from "react"; 5 | import { enableScreens } from "react-native-screens"; 6 | import "react-native-gesture-handler"; 7 | import { Provider } from "react-redux"; 8 | import * as ScreenOrientation from "expo-screen-orientation"; 9 | import { Platform } from "react-native"; 10 | import Store from "./src/store"; 11 | import RootNavigation from "./src/routers"; 12 | import CustomProvider from "./src/providers"; 13 | import ErrorBoundary from "./src/providers/ErrorBoundary"; 14 | 15 | enableScreens(); 16 | 17 | function App() { 18 | useEffect(() => { 19 | if (Platform.OS !== "web") { 20 | // TODO: Orientation Congihuration 21 | ScreenOrientation.lockAsync( 22 | ScreenOrientation.OrientationLock.PORTRAIT_UP 23 | ); 24 | } 25 | }, []); 26 | 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | } 37 | 38 | export default App; 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Milvasoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/providers/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ErrorBoundaryPage from "@modules/app/screens/ErrorBoundaryPage"; 3 | 4 | type ErrorBoundaryProps = { 5 | children: any; 6 | }; 7 | 8 | /** 9 | * ErrorBoundary is a React component that catches and handles errors in its child components. 10 | * It renders an error page when an error occurs, otherwise it renders its children. 11 | */ 12 | export default class ErrorBoundary extends React.Component { 13 | state = { 14 | hasError: false, 15 | }; 16 | 17 | static getDerivedStateFromError() { 18 | return { hasError: true }; 19 | } 20 | 21 | componentDidCatch(error: any, errorInfo: any): void { 22 | /** 23 | * We can capture the error with any error tracking tool 24 | * Like: 25 | * 26 | * Sentry.captureException(error); 27 | * crashlytics().recordError(error); 28 | * Bugsnag.notify(error) 29 | * rollbar.error(error) 30 | */ 31 | console.log("error", error, errorInfo); 32 | } 33 | 34 | render(): React.ReactElement { 35 | if (this.state.hasError) { 36 | return ; 37 | } 38 | return this.props.children; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/ErrorComponent/ErrorComponent.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, fireEvent } from "@testing-library/react-native"; 3 | import { Provider } from "react-redux"; 4 | import ErrorComponent from "./index"; 5 | import configureStore from "redux-mock-store"; 6 | 7 | const mockStore = configureStore(); 8 | const States = { 9 | AppReducer: { 10 | userColorScheme: "light", 11 | }, 12 | }; 13 | const Store = mockStore(States); 14 | 15 | describe("ErrorComponent", () => { 16 | test("renders error message correctly", () => { 17 | const errorMessage = "Something went wrong"; 18 | const { getByText } = render( 19 | 20 | 21 | 22 | ); 23 | const messageElement = getByText(errorMessage); 24 | expect(messageElement).toBeTruthy(); 25 | }); 26 | 27 | test("calls onRetry function when retry button is pressed", () => { 28 | const onRetryMock = jest.fn(); 29 | const { getByText } = render(); 30 | const retryButton = getByText("Retry"); 31 | fireEvent.press(retryButton); 32 | expect(onRetryMock).toHaveBeenCalled(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/providers/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ali Burhan Keskin 3 | */ 4 | import React from "react"; 5 | import NetworkInfoContainer from "./NetworkInfoContainer"; 6 | import AppLoadingProvider from "./AppLoadingProvider"; 7 | import "./Localization"; 8 | import Toast from "./Toast"; 9 | import Notification from "./Notification"; 10 | import ThemeProvider from "./ThemeProvider"; 11 | import ThemeListener from "./ThemeListener"; 12 | import { BottomSheetContainer } from "@src/components"; 13 | 14 | type Props = { 15 | children: React.ReactNode; 16 | }; 17 | 18 | /** 19 | * Providers for `global` transactions. 20 | * The `CustomProvider` is used to `monitor` and take action at every moment of the application. 21 | */ 22 | function CustomProvider({ children }: Props) { 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {children} 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | } 41 | 42 | export default CustomProvider; 43 | -------------------------------------------------------------------------------- /src/providers/AppLoadingProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from "react"; 2 | import { View } from "react-native"; 3 | import * as SplashScreen from "expo-splash-screen"; 4 | import { useFonts } from "expo-font"; 5 | import MontserratFont from "@assets/font"; 6 | 7 | SplashScreen.preventAutoHideAsync(); 8 | 9 | type Props = { 10 | children: React.ReactNode; 11 | }; 12 | 13 | /** 14 | * Provides an app loading screen that preloads fonts and hides the splash screen 15 | * once the app is ready to render. 16 | * 17 | * @param {Props} props - The component props. 18 | * @param {ReactNode} props.children - The child components to render. 19 | * @returns {ReactElement | null} The rendered component. 20 | */ 21 | function AppLoadingProvider({ children }: Props) { 22 | const [fontsLoaded, fontError] = useFonts(MontserratFont); 23 | 24 | const onLayoutRootView = useCallback(async () => { 25 | if (fontsLoaded || fontError) { 26 | await SplashScreen.hideAsync(); 27 | } 28 | }, [fontsLoaded, fontError]); 29 | 30 | if (!fontsLoaded && !fontError) { 31 | return null; 32 | } 33 | 34 | return ( 35 | 36 | {children} 37 | 38 | ); 39 | } 40 | 41 | export default AppLoadingProvider; 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-native", 4 | "target": "esnext", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "noImplicitAny": true, 7 | "noImplicitThis": true, 8 | "strictNullChecks": true, 9 | "strict": false, 10 | "allowJs": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "skipLibCheck": true, 13 | "noEmit": true, 14 | "allowSyntheticDefaultImports": true, 15 | "resolveJsonModule": true, 16 | "esModuleInterop": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "isolatedModules": true, 20 | "baseUrl": "./", 21 | "paths": { 22 | "@assets/*": ["./src/assets/*"], 23 | "@components/*": ["./src/components/*"], 24 | "@helpers/*": ["./src/helpers/*"], 25 | "@hooks/*": ["./src/hooks/*"], 26 | "@modules/*": ["./src/modules/*"], 27 | "@routers/*": ["./src/routers/*"], 28 | "@providers/*": ["./src/providers/*"], 29 | "@screens/*": ["./src/screens/*"], 30 | "@store/*": ["./src/store/*"], 31 | "@styles/*": ["./src/styles/*"], 32 | "@utils/*": ["./src/utils/*"], 33 | "@src/*": ["./src/*"], 34 | } 35 | }, 36 | "include": ["src/**/*","custom.d.ts"], 37 | "exclude": ["node_modules"], 38 | "extends": "expo/tsconfig.base" 39 | } 40 | -------------------------------------------------------------------------------- /src/components/GeneralActivityIndicator/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Text, View, ActivityIndicator } from "react-native"; 3 | import { useTheme, useThemedStyles } from "@src/hooks"; 4 | import translate from "@helpers/localization"; 5 | import { styles } from "./style"; 6 | import type { GeneralActivityIndicatorProps } from "./type"; 7 | 8 | const testID = "generalactivityindicator"; 9 | /** 10 | * Renders a general activity indicator with an optional text. 11 | * 12 | * @param {GeneralActivityIndicatorProps} props - The component props. 13 | * @param {string} props.text - The optional text to display below the activity indicator. 14 | * @returns {JSX.Element} The rendered GeneralActivityIndicator component. 15 | */ 16 | function GeneralActivityIndicator({ text }: GeneralActivityIndicatorProps) { 17 | const theme = useTheme(); 18 | const themedStyles = useThemedStyles(styles); 19 | 20 | // TODO Modal 21 | return ( 22 | 26 | 32 | 33 | {text || translate("generalActivityIndicatorText")} 34 | 35 | 36 | ); 37 | } 38 | 39 | export default GeneralActivityIndicator; 40 | -------------------------------------------------------------------------------- /src/components/GeneralActivityIndicator/GeneralActivityIndicator.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "@testing-library/react-native"; 3 | import GeneralActivityIndicator from "./index"; 4 | 5 | const testID = "generalactivityindicator"; 6 | describe("GeneralActivityIndicator", () => { 7 | test("renders activity indicator with correct color", () => { 8 | const { getByTestId } = render(); 9 | const activityIndicator = getByTestId( 10 | `${testID}-container-activityindicator` 11 | ); 12 | expect(activityIndicator.props.color).toBe("#f27a1a"); 13 | }); 14 | 15 | test("renders activity indicator with correct size", () => { 16 | const { getByTestId } = render(); 17 | const activityIndicator = getByTestId( 18 | `${testID}-container-activityindicator` 19 | ); 20 | expect(activityIndicator.props.size).toBe("large"); 21 | }); 22 | 23 | test("renders activity indicator text correctly", () => { 24 | const text = "Loading..."; 25 | const { getByText } = render(); 26 | const textElement = getByText(text); 27 | expect(textElement).toBeTruthy(); 28 | }); 29 | 30 | test("renders default activity indicator text when no text prop is provided", () => { 31 | const { getByText } = render(); 32 | const defaultText = "Loading..."; 33 | const textElement = getByText(defaultText); 34 | expect(textElement).toBeTruthy(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/components/Skeleton/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ali Burhan Keskin 3 | */ 4 | import React, { useEffect, useRef } from "react"; 5 | import { View, Animated, StyleSheet } from "react-native"; 6 | import { useTheme } from "@src/hooks"; 7 | 8 | interface SkeletonProps { 9 | width?: number | string; 10 | height?: number | string; 11 | borderRadius?: number; 12 | style?: any; 13 | } 14 | 15 | const Skeleton: React.FC = ({ 16 | width = "100%", 17 | height = 20, 18 | borderRadius = 4, 19 | style, 20 | }) => { 21 | const theme = useTheme(); 22 | const animatedValue = useRef(new Animated.Value(0)).current; 23 | 24 | useEffect(() => { 25 | Animated.loop( 26 | Animated.sequence([ 27 | Animated.timing(animatedValue, { 28 | toValue: 1, 29 | duration: 1000, 30 | useNativeDriver: true, 31 | }), 32 | Animated.timing(animatedValue, { 33 | toValue: 0, 34 | duration: 1000, 35 | useNativeDriver: true, 36 | }), 37 | ]) 38 | ).start(); 39 | }, []); 40 | 41 | const opacity = animatedValue.interpolate({ 42 | inputRange: [0, 1], 43 | outputRange: [0.3, 0.7], 44 | }); 45 | 46 | return ( 47 | 59 | ); 60 | }; 61 | 62 | export default Skeleton; 63 | -------------------------------------------------------------------------------- /src/providers/Localization.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ali Burhan Keskin 3 | */ 4 | import moment from "moment"; 5 | import "moment/min/locales"; 6 | import * as Localization from "expo-localization"; 7 | import { I18n } from "i18n-js"; 8 | import { TrResource, EnResource } from "@src/localization/index"; 9 | 10 | const defaultLocale = "en"; 11 | const deviceLanguage = 12 | Localization.getLocales()?.[0]?.languageCode || defaultLocale; 13 | 14 | /** 15 | * The internationalization object used for localization. 16 | * 17 | * @remarks 18 | * This object is responsible for managing the localization resources and settings. 19 | * 20 | * @example 21 | * const i18n = new I18n( 22 | * { 23 | * en: EnResource, 24 | * tr: TrResource, 25 | * }, 26 | * { 27 | * locale: Localization.locale, 28 | * enableFallback: true, 29 | * defaultLocale: "en", 30 | * } 31 | * ); 32 | */ 33 | const i18n = new I18n( 34 | { 35 | en: EnResource, 36 | tr: TrResource, 37 | }, 38 | { 39 | locale: deviceLanguage, 40 | enableFallback: true, 41 | defaultLocale: defaultLocale, 42 | } 43 | ); 44 | 45 | export default i18n; 46 | 47 | moment.locale(deviceLanguage); 48 | 49 | // moment(1316116057189).fromNow(); // il y a 7 ans 50 | 51 | // moment("20111031", "YYYYMMDD").fromNow(); // 9 years ago 52 | // moment("20120620", "YYYYMMDD").fromNow(); // 9 years ago 53 | // moment().startOf('day').fromNow(); // 20 hours ago 54 | // moment().endOf('day').fromNow(); // in 4 hours 55 | // moment().startOf('hour').fromNow(); 56 | -------------------------------------------------------------------------------- /app.config.ts: -------------------------------------------------------------------------------- 1 | import { ExpoConfig, ConfigContext } from "@expo/config"; 2 | 3 | export default ({ config }: ConfigContext): ExpoConfig => ({ 4 | ...config, 5 | name: "Milvasoft Expo BoilerPalte", 6 | description: "Milvasoft Expo BoilerPalte Description", 7 | slug: "milvasoft-expo-boilerplate", 8 | scheme: "com.milvasoft.expoboilerplate", 9 | version: "1.0.0", 10 | sdkVersion: "54.0.0", 11 | orientation: "portrait", 12 | icon: "./src/assets/images/icon.png", 13 | userInterfaceStyle: "automatic", 14 | runtimeVersion: { 15 | policy: "sdkVersion", 16 | }, 17 | assetBundlePatterns: ["./src/assets/images/*"], 18 | locales: { 19 | tr: "./src/assets/languages/turkish.json", 20 | en: "./src/assets/languages/english.json", 21 | }, 22 | splash: { 23 | image: "./src/assets/images/splash.png", 24 | resizeMode: "contain", 25 | backgroundColor: "#ffffff", 26 | }, 27 | ios: { 28 | bundleIdentifier: "com.milvasoft.expoboilerplate", 29 | buildNumber: "1.0.0", 30 | infoPlist: { 31 | CFBundleAllowMixedLocalizations: true, 32 | }, 33 | }, 34 | web: { 35 | bundler: "metro", 36 | }, 37 | android: { 38 | adaptiveIcon: { 39 | foregroundImage: "./src/assets/images/adaptive-icon.png", 40 | backgroundColor: "#ffffff", 41 | }, 42 | package: "com.milvasoft.expoboilerplate", 43 | versionCode: 1, 44 | }, 45 | updates: { 46 | enabled: true, 47 | url: "https://u.expo.dev/49e4e24d-c928-4ff1-815d-f1a58ca580bd", 48 | }, 49 | extra: { 50 | eas: { 51 | projectId: "49e4e24d-c928-4ff1-815d-f1a58ca580bd", 52 | }, 53 | }, 54 | plugins: ["expo-font", "expo-localization"], 55 | }); 56 | -------------------------------------------------------------------------------- /src/modules/app/redux/appSlice.ts: -------------------------------------------------------------------------------- 1 | import { IAppReducer } from "@modules/app/redux/IAppReducer"; 2 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 3 | 4 | const initialState: IAppReducer = { 5 | hasSeenOnboarding: false, 6 | }; 7 | 8 | export const appSlice = createSlice({ 9 | name: "appReducer", 10 | initialState, 11 | reducers: { 12 | setAuthToken: (state, action: PayloadAction) => { 13 | state = { ...state, authToken: action.payload }; 14 | return state; 15 | }, 16 | SetUser: (state, action: PayloadAction) => { 17 | state = { 18 | ...state, 19 | isSignedIn: true, 20 | user: action.payload, 21 | hasSeenOnboarding: true, 22 | }; 23 | return state; 24 | }, 25 | LoggedOut: (state) => { 26 | state = { ...state, user: null, isSignedIn: false, authToken: "" }; 27 | return state; 28 | }, 29 | SetColorShceme: (state, action: PayloadAction) => { 30 | state = { ...state, userColorScheme: action.payload }; 31 | return state; 32 | }, 33 | SetExpoToken: (state, action: PayloadAction) => { 34 | state = { ...state, expoToken: action.payload }; 35 | return state; 36 | }, 37 | SetHasSeenOnboarding: (state, action: PayloadAction) => { 38 | state = { ...state, hasSeenOnboarding: action.payload }; 39 | return state; 40 | }, 41 | }, 42 | }); 43 | 44 | export const { 45 | setAuthToken, 46 | SetUser, 47 | LoggedOut, 48 | SetColorShceme, 49 | SetExpoToken, 50 | SetHasSeenOnboarding, 51 | } = appSlice.actions; 52 | 53 | export const Logout = LoggedOut; 54 | 55 | export default appSlice.reducer; 56 | -------------------------------------------------------------------------------- /src/network/axiosInstance.ts: -------------------------------------------------------------------------------- 1 | import { ToastColorEnum } from "@components/ToastMessage/ToastColorEnum"; 2 | import { showToast } from "@helpers/toast/showToast"; 3 | import { LoggedOut } from "@modules/app/redux/appSlice"; 4 | import Store from "@store/index"; 5 | import axios from "axios"; 6 | 7 | const options = { 8 | baseURL: "baseURL", 9 | headers: { 10 | "Content-Type": "application/json", 11 | Authorization: Store.getState().AppReducer.authToken, 12 | }, 13 | }; 14 | 15 | const axiosInstance = axios.create(options); 16 | 17 | axiosInstance.interceptors.request.use((config) => { 18 | console.log(config.url, " - request -", config.data); 19 | 20 | return config; 21 | }); 22 | 23 | axiosInstance.interceptors.response.use( 24 | (response) => { 25 | if (response?.data?.messages && response?.config?.withoutToast !== true) { 26 | response.data.messages?.forEach((message: any) => { 27 | showToast(message.message, message.type); 28 | }); 29 | } 30 | return response; 31 | }, 32 | (error) => { 33 | console.log( 34 | error.response.config.url, 35 | "- error response -", 36 | error.response?.data 37 | ); 38 | 39 | if (error.response?.status === 401) { 40 | showToast("Unauthorized", ToastColorEnum.Error); 41 | 42 | Store.dispatch(LoggedOut()); 43 | } else if (error.response?.data) { 44 | error.response?.data.Messages?.forEach((message: any) => { 45 | showToast(message.message, message.type); 46 | }); 47 | } else { 48 | showToast("An error occurred ", ToastColorEnum.Error); 49 | } 50 | return Promise.reject(error); 51 | } 52 | ); 53 | 54 | export default axiosInstance; 55 | -------------------------------------------------------------------------------- /src/helpers/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createNavigationContainerRef, StackActions } from '@react-navigation/native'; 2 | import Routes, { NavigationParams } from '@utils/Routes'; 3 | 4 | /** 5 | * Reference to the navigation container. 6 | */ 7 | export const navigationRef = createNavigationContainerRef(); 8 | 9 | /** 10 | * Pushes a new route onto the navigation stack. 11 | * 12 | * @param name - The name of the route to push. 13 | * @param params - Optional parameters to pass to the route. 14 | */ 15 | export function push(name: Routes, params?: NavigationParams[RouteName]) { 16 | 17 | if (navigationRef.isReady()) navigationRef.current?.dispatch(StackActions.push(name, params)); 18 | 19 | } 20 | 21 | /** 22 | * Pops the specified number of screens from the navigation stack. 23 | * 24 | * @param count - The number of screens to pop from the stack. 25 | */ 26 | export function pop(count:number) { 27 | 28 | if (navigationRef.isReady()) navigationRef.current?.dispatch(StackActions.pop(count)); 29 | 30 | } 31 | 32 | /** 33 | * Navigates to a specific route in the app. 34 | * 35 | * @param name - The name of the route to navigate to. 36 | * @param params - Optional parameters to pass to the route. 37 | */ 38 | export function navigate(name: string, params?: NavigationParams[RouteName]) { 39 | 40 | // @ts-ignore 41 | if (navigationRef.isReady()) navigationRef.navigate(name, params); 42 | 43 | } 44 | 45 | /** 46 | * Navigates back to the previous screen. 47 | */ 48 | export function goBack() { 49 | 50 | if (navigationRef.isReady()) navigationRef.current?.goBack(); 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/modules/news/redux/newsSlice.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ali Burhan Keskin 3 | */ 4 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 5 | import { INewsReducer } from "./INewsReducer"; 6 | import { INewsArticle, NewsCategory } from "../types/INewsArticle"; 7 | 8 | const initialState: INewsReducer = { 9 | articles: [], 10 | selectedArticle: null, 11 | selectedCategory: "all", 12 | isLoading: false, 13 | error: null, 14 | }; 15 | 16 | const newsSlice = createSlice({ 17 | name: "news", 18 | initialState, 19 | reducers: { 20 | setArticles: (state, action: PayloadAction) => { 21 | state.articles = action.payload; 22 | state.error = null; 23 | }, 24 | setSelectedArticle: (state, action: PayloadAction) => { 25 | state.selectedArticle = action.payload; 26 | }, 27 | setSelectedCategory: ( 28 | state, 29 | action: PayloadAction 30 | ) => { 31 | state.selectedCategory = action.payload; 32 | }, 33 | setLoading: (state, action: PayloadAction) => { 34 | state.isLoading = action.payload; 35 | }, 36 | setError: (state, action: PayloadAction) => { 37 | state.error = action.payload; 38 | state.isLoading = false; 39 | }, 40 | clearNews: (state) => { 41 | state.articles = []; 42 | state.selectedArticle = null; 43 | state.error = null; 44 | }, 45 | }, 46 | }); 47 | 48 | export const { 49 | setArticles, 50 | setSelectedArticle, 51 | setSelectedCategory, 52 | setLoading, 53 | setError, 54 | clearNews, 55 | } = newsSlice.actions; 56 | 57 | export default newsSlice.reducer; 58 | -------------------------------------------------------------------------------- /src/providers/NetworkInfoContainer.tsx: -------------------------------------------------------------------------------- 1 | import { useNetInfo } from "@react-native-community/netinfo"; 2 | import React, { useState, useEffect } from "react"; 3 | import { View, Text, Modal, Button, StyleSheet } from "react-native"; 4 | 5 | const NetworkInfoContainer = ({ children }: any) => { 6 | const { isConnected } = useNetInfo(); 7 | console.log("isConnected", isConnected); 8 | const [modalVisible, setModalVisible] = useState(false); 9 | 10 | useEffect(() => { 11 | if (isConnected === false) { 12 | setModalVisible(true); 13 | } else { 14 | setModalVisible(false); 15 | } 16 | }, [isConnected]); 17 | 18 | return ( 19 | 20 | { 25 | setModalVisible(false); 26 | }} 27 | > 28 | 29 | 30 | 31 | Check your internet connection! 32 | 33 |