diff --git a/.detoxrc.js b/.detoxrc.js new file mode 100644 index 0000000..e9e357e --- /dev/null +++ b/.detoxrc.js @@ -0,0 +1,29 @@ +module.exports = { + "configurations": { + "ios.sim.debug": { + "binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/ReactNativeSemaphoreNew.app", + "build": "xcodebuild ONLY_ACTIVE_ARCH=YES -workspace ios/ReactNativeSemaphoreNew.xcworkspace -scheme ReactNativeSemaphoreNew -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build -UseModernBuildSystem=YES", + "type": "ios.simulator", + "name": "iPhone 11" + }, + "ios.sim.release": { + "binaryPath": "ios/build/Build/Products/Release-iphonesimulator/ReactNativeSemaphoreNew.app", + "build": "xcodebuild ONLY_ACTIVE_ARCH=YES -workspace ios/ReactNativeSemaphoreNew.xcworkspace -scheme ReactNativeSemaphoreNew -configuration Release -sdk iphonesimulator -derivedDataPath ios/build -UseModernBuildSystem=YES", + "type": "ios.simulator", + "name": "iPhone 11" + }, + "android.emu.debug": { + "binaryPath": "android/app/build/outputs/apk/debug/app-debug.apk", + "build": "cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd ..", + "type": "android.emulator", + "name": "Pixel_4_API_28" + }, + "android.emu.release": { + "binaryPath": "android/app/build/outputs/apk/release/app-release.apk", + "build": "cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release && cd ..", + "type": "android.emulator", + "name": "Nexus_S_API_24" + } + }, + "test-runner": "jest" +}; \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index fac7075..3483265 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,37 @@ +const prettierOptions = require('./.prettierrc'); + module.exports = { root: true, - extends: '@react-native-community', + extends: [ + 'prettier', + '@react-native-community', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + ], + plugins: ['prettier', 'jest', 'testing-library'], + overrides: [ + { + files: ['*.ts', '*.tsx'], + rules: { + '@typescript-eslint/no-unused-vars': [2, {args: 'none'}], + }, + }, + ], + settings: { + 'import/resolver': { + 'babel-module': {}, + node: { + extensions: ['.js', '.jsx', '.json', '.native.js'], + }, + }, + }, + rules: { + 'prettier/prettier': ['error', prettierOptions], + '@typescript-eslint/no-var-requires': 0, + }, + env: { + 'jest/globals': true, + }, globals: { fetch: true, it: true, @@ -15,5 +46,6 @@ module.exports = { jasmine: true, beforeAll: true, afterAll: true, + HermesInternal: true, }, }; diff --git a/.nvmrc b/.nvmrc index db24ab9..da2d398 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -10.13.0 +14 \ No newline at end of file diff --git a/.semaphore/release-build-ios.yml b/.semaphore/release-build-ios.yml index 515ca87..2e1863c 100644 --- a/.semaphore/release-build-ios.yml +++ b/.semaphore/release-build-ios.yml @@ -22,6 +22,7 @@ blocks: - cache restore - bundle install --path vendor/bundle - cache store + - nvm install 14 # setup cocoapods - cd ios diff --git a/.semaphore/release-build.yml b/.semaphore/release-build.yml index 2f201f4..79040c8 100644 --- a/.semaphore/release-build.yml +++ b/.semaphore/release-build.yml @@ -30,6 +30,7 @@ blocks: - gem install bundler -v '2.1.4' - bundle install --path vendor/bundle - cache store + - nvm install 14 # setup cocoapods - cd ios diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 5043338..9805877 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -60,6 +60,7 @@ blocks: - brew tap wix/brew - brew install applesimutils - cache restore + - nvm install 14 - cd ios - pod install - cd .. diff --git a/App.js b/App.js deleted file mode 100644 index 35742f9..0000000 --- a/App.js +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Sample React Native App - * https://github.com/facebook/react-native - * - * @format - * @flow - */ - -import React, {useState} from 'react'; -import { - SafeAreaView, - StyleSheet, - ScrollView, - View, - Text, - StatusBar, - Switch, -} from 'react-native'; - -import { - Header, - LearnMoreLinks, - Colors, - DebugInstructions, - ReloadInstructions, -} from 'react-native/Libraries/NewAppScreen'; - -const App = () => { - const [isToggled, setIsToggled] = useState(false); - - const handleToggle = () => setIsToggled(!isToggled); - - return ( - <> - - - -
- {global.HermesInternal == null ? null : ( - - Engine: Hermes - - )} - - - - Step One - - Edit App.js to change this - screen and then come back to see your edits. - - - - See Your Changes - - - - - - Debug - - - - - - Learn More - - Read the docs to discover what to do next: - - - - - - - - ); -}; - -const styles = StyleSheet.create({ - scrollView: { - backgroundColor: Colors.lighter, - }, - engine: { - position: 'absolute', - right: 0, - }, - body: { - backgroundColor: Colors.white, - }, - sectionContainer: { - marginTop: 32, - paddingHorizontal: 24, - }, - sectionTitle: { - fontSize: 24, - fontWeight: '600', - color: Colors.black, - }, - sectionDescription: { - marginTop: 8, - fontSize: 18, - fontWeight: '400', - color: Colors.dark, - }, - highlight: { - fontWeight: '700', - }, - footer: { - color: Colors.dark, - fontSize: 12, - fontWeight: '600', - padding: 4, - paddingRight: 12, - textAlign: 'right', - }, -}); - -export default App; diff --git a/App.tsx b/App.tsx new file mode 100644 index 0000000..2a7701e --- /dev/null +++ b/App.tsx @@ -0,0 +1,24 @@ +/** + * Sample React Native App + * https://github.com/facebook/react-native + * + */ + +import React from 'react'; +import {StatusBar} from 'react-native'; +// import {enableScreens} from 'react-native-screens'; + +import Router from './app/router'; + +// enableScreens(); + +const App: React.FC = () => { + return ( + <> + + + + ); +}; + +export default App; diff --git a/__tests__/App-test.js b/__tests__/App-test.js deleted file mode 100644 index e693710..0000000 --- a/__tests__/App-test.js +++ /dev/null @@ -1,34 +0,0 @@ -import ReactNative from 'react-native'; -// Note: test renderer must be required after react-native. -import {shallow} from 'enzyme'; -import renderer from 'react-test-renderer'; - -import React from 'react'; -import App from '../App'; - -const {View, Text, Switch} = ReactNative; - -describe('jest snapshot tests', () => { - it('renders correctly', () => { - const tree = renderer.create().toJSON(); - expect(tree).toMatchSnapshot(); - }); -}); - -describe('enzyme tests', () => { - it('should render a component', () => { - const wrapper = shallow(); - expect(wrapper.find(View)).toHaveLength(5); - expect(wrapper.find(Text)).toHaveLength(9); - expect(wrapper.find(Switch)).toHaveLength(1); - }); - - it('should togggle switch to true', () => { - const wrapper = shallow(); - const switchValueBeforeToggle = wrapper.find(Switch).first().props().value; - expect(switchValueBeforeToggle).toBe(false); - wrapper.find(Switch).first().props().onChange(); - const switchValueAfterToggle = wrapper.find(Switch).first().props().value; - expect(switchValueAfterToggle).toBe(true); - }); -}); diff --git a/__tests__/App-test.tsx b/__tests__/App-test.tsx new file mode 100644 index 0000000..cf5aa4c --- /dev/null +++ b/__tests__/App-test.tsx @@ -0,0 +1,14 @@ +import renderer from 'react-test-renderer'; +// import {fireEvent, render} from '@testing-library/react-native'; + +import React from 'react'; +import App from '../App'; + +jest.useFakeTimers(); + +describe('jest snapshot tests', () => { + it('renders correctly', async () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/__tests__/__snapshots__/App-test.js.snap b/__tests__/__snapshots__/App-test.js.snap deleted file mode 100644 index 8eeaece..0000000 --- a/__tests__/__snapshots__/App-test.js.snap +++ /dev/null @@ -1,800 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`jest snapshot tests renders correctly 1`] = ` - - - - - - - Welcome to - - - React Native - - - - - - - Step One - - - Edit - - App.js - - to change this screen and then come back to see your edits. - - - - - See Your Changes - - - - Press - - Cmd + R - - in the simulator to reload your app's code. - - - - - - Debug - - - - Press - - Cmd + D - - in the simulator or - - - Shake - - your device to open the React Native debug menu. - - - - - - Learn More - - - Read the docs to discover what to do next: - - - - - - - The Basics - - - Explains a Hello World for React Native. - - - - - - Style - - - Covers how to use the prop named style which controls the visuals. - - - - - - Layout - - - React Native uses flexbox for layout, learn how it works. - - - - - - Components - - - The full list of components and APIs inside React Native. - - - - - - Navigation - - - How to handle moving between screens inside your application. - - - - - - Networking - - - How to use the Fetch API in React Native. - - - - - - Help - - - Need more help? There are many other React Native developers who may have the answer. - - - - - - Follow us on Twitter - - - Stay in touch with the community, join in on Q&As and more by following React Native on Twitter. - - - - - - - -`; diff --git a/__tests__/__snapshots__/App-test.tsx.snap b/__tests__/__snapshots__/App-test.tsx.snap new file mode 100644 index 0000000..af5f3f2 --- /dev/null +++ b/__tests__/__snapshots__/App-test.tsx.snap @@ -0,0 +1,1158 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`jest snapshot tests renders correctly 1`] = ` + + + + + + + + + + + + + Home + + + + + + + + + + + + + + + + + + + + + + Welcome to + + + React Native + + + + + + + Search Countries + + + + + + + Step One + + + Edit + + App.js + + to change this screen and then come back to see your edits. + + + + + See Your Changes + + + + Press + + Cmd + R + + in the simulator to reload your app's code. + + + + + + Debug + + + + Press + + Cmd + D + + in the simulator or + + + Shake + + your device to open the React Native debug menu. + + + + + + Learn More + + + Read the docs to discover what to do next: + + + + + + + The Basics + + + Explains a Hello World for React Native. + + + + + + Style + + + Covers how to use the prop named style which controls the visuals. + + + + + + Layout + + + React Native uses flexbox for layout, learn how it works. + + + + + + Components + + + The full list of components and APIs inside React Native. + + + + + + Navigation + + + How to handle moving between screens inside your application. + + + + + + Networking + + + How to use the Fetch API in React Native. + + + + + + Help + + + Need more help? There are many other React Native developers who may have the answer. + + + + + + Follow us on Twitter + + + Stay in touch with the community, join in on Q&As and more by following React Native on Twitter. + + + + + + + + + + + + + + + + +`; diff --git a/__tests__/setupTests.js b/__tests__/setupTests.js deleted file mode 100644 index 0e2cbd7..0000000 --- a/__tests__/setupTests.js +++ /dev/null @@ -1,26 +0,0 @@ -// setup-tests.js - -import 'react-native'; -import 'jest-enzyme'; -import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; //TODO: replace this with enzyme-adapter-react-17 when official react 17 support is added -import Enzyme from 'enzyme'; - -/** - * Set up Enzyme to mount to DOM, simulate events, - * and inspect the DOM in tests. - */ -Enzyme.configure({adapter: new Adapter()}); - -/** - * Ignore some expected warnings - * see: https://jestjs.io/docs/en/tutorial-react.html#snapshot-testing-with-mocks-enzyme-and-react-16 - * see https://github.com/Root-App/react-native-mock-render/issues/6 - */ -const originalConsoleError = console.error; -console.error = message => { - if (message.startsWith('Warning:')) { - return; - } - - originalConsoleError(message); -}; diff --git a/android/app/build.gradle b/android/app/build.gradle index 77d3be4..b8ab4e6 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -2,6 +2,11 @@ apply plugin: "com.android.application" import com.android.build.OutputFile +project.ext.vectoricons = [ + iconFontNames: ['Feather.ttf'] // Name of the font files you want to copy +] +apply from: "../../node_modules/react-native-vector-icons/fonts.gradle" + /** * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets * and bundleReleaseJsAndAssets). diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9b972c5..e28fd5d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -9,7 +9,8 @@ android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false" - android:theme="@style/AppTheme"> + android:theme="@style/AppTheme" + android:networkSecurityConfig="@xml/network_security_config"> + + + 10.0.2.2 + localhost + 127.0.0.1 + + \ No newline at end of file diff --git a/app/components/CountriesAutocomplete/ListItem.tsx b/app/components/CountriesAutocomplete/ListItem.tsx new file mode 100644 index 0000000..3c6a03f --- /dev/null +++ b/app/components/CountriesAutocomplete/ListItem.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import {Country} from 'countries-list'; + +import Text from 'theme/Text'; +import TouchFeedback from 'theme/TouchFeedback'; + +import style from './style'; + +type ListItemProps = { + data: Country; + onPress: (...args: unknown[]) => void; + testID: string; +}; + +const ListItem: React.FC = props => ( + + {props.data.emoji} + + ({props.data.phone}) {props.data.name} + + +); + +export default ListItem; diff --git a/app/components/CountriesAutocomplete/index.tsx b/app/components/CountriesAutocomplete/index.tsx new file mode 100644 index 0000000..526d886 --- /dev/null +++ b/app/components/CountriesAutocomplete/index.tsx @@ -0,0 +1,78 @@ +/** + * + * CountriesAutocomplete + * + */ + +import sortBy from 'lodash/sortBy'; + +import React from 'react'; +import {FlatList, Text, View} from 'react-native'; +import {countries, Country} from 'countries-list'; + +import Input from 'theme/Input'; +import useAutocomplete from 'theme/Autocomplete'; + +import ListItem from './ListItem'; +import style from './style'; + +interface ICountriesAutocompleteProps { + onSelect: (item: Country) => void; + testID?: string; +} + +const countriesList: Country[] = Object.keys(countries).map( + key => countries[key], +); + +const CountriesAutocomplete: React.FC = props => { + const autocomplete = useAutocomplete({ + data: countriesList, + filterKey: 'name', + }); + + return ( + <> + + + + {autocomplete.focus && autocomplete.data ? ( + name} + renderItem={({item}: {item: Country; index: number}) => ( + { + props.onSelect(item); + autocomplete.clear(); + }} + data={item} + testID={`listItem-${item.name}`} + /> + )} + ListEmptyComponent={ + autocomplete.value ? ( + No result for {autocomplete.value}. + ) : null + } + /> + ) : null} + + ); +}; + +export default CountriesAutocomplete; diff --git a/app/components/CountriesAutocomplete/style.ts b/app/components/CountriesAutocomplete/style.ts new file mode 100644 index 0000000..b975705 --- /dev/null +++ b/app/components/CountriesAutocomplete/style.ts @@ -0,0 +1,61 @@ +import {StyleSheet} from 'react-native'; +import Colors from 'theme/Colors'; +import Dimensions from 'theme/Dimensions'; + +const style = StyleSheet.create({ + selectedItemContainer: { + margin: Dimensions.space2x, + marginTop: Dimensions.space1x, + flexDirection: 'row', + }, + autocompleteContainer: { + flexDirection: 'row', + alignItems: 'center', + position: 'relative', + paddingHorizontal: Dimensions.space2x, + }, + input: { + marginHorizontal: 0, + opacity: 1, + width: '100%', + }, + inputCloseBtnHolder: { + backgroundColor: Colors.translucentBlackMinor, + height: 36, + width: 36, + borderRadius: 20, + position: 'absolute', + right: 16, + }, + inputCloseBtn: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + noResult: { + flex: 1, + textAlign: 'center', + paddingVertical: Dimensions.space1x, + }, + listContainer: { + zIndex: 1000, + width: '100%', + paddingHorizontal: Dimensions.space2x, + }, + listItemContainer: { + flexDirection: 'row', + padding: Dimensions.space1x, + paddingVertical: Dimensions.space2x, + alignItems: 'center', + width: '100%', + backgroundColor: Colors.white, + marginVertical: Dimensions.space1x, + }, + listItemName: { + fontSize: 14, + marginLeft: 8, + fontWeight: '500', + }, +}); + +export default style; diff --git a/app/components/CountriesAutocomplete/tests/index.test.tsx b/app/components/CountriesAutocomplete/tests/index.test.tsx new file mode 100644 index 0000000..7ab2de5 --- /dev/null +++ b/app/components/CountriesAutocomplete/tests/index.test.tsx @@ -0,0 +1,66 @@ +import 'react-native'; +import React from 'react'; +import {render, fireEvent} from '@testing-library/react-native'; + +import CountriesAutocomplete from '../index'; + +const COUNTRY_NAME = 'Serbia'; + +// Describing a test suite +describe('', () => { + // Describing our test + it('Displays Searched Item', async () => { + // Mocking onPress method so we can check if its called or not + const onSelect = jest.fn(); + + // Rendering Button component using RNTL. + const autocomplete = await render( + , + ); + + // Grabbing our input to perform actions on it. + const inputTestID = 'countriesAutocompleteInput'; + const textInput = autocomplete.getByTestId(inputTestID); + + /** + * RNTL gives us API to fire events on node + * Here we are firing on changeText event + */ + fireEvent(textInput, 'focus'); + fireEvent.changeText(textInput, COUNTRY_NAME); + expect(textInput.props.value).toBe(COUNTRY_NAME); + + // Grabbing our input to perform actions on it. + const listItemTestID = `listItem-${COUNTRY_NAME}`; + const firstListItem = autocomplete.getByTestId(listItemTestID); + expect(firstListItem).toBeTruthy(); + }); + + it('onSelect is called when item is pressed', async () => { + // Mocking onPress method so we can check if its called or not + const onSelect = jest.fn(); + + // Rendering Button component using react-native-test-renderer. + const {getByTestId} = await render( + , + ); + + // Grabbing our input to perform actions on it. + const inputTestID = 'countriesAutocompleteInput'; + const textInput = getByTestId(inputTestID); + + /** + * RNTL gives us API to fire events on node + * Here we are firing on focus & changeText event + */ + fireEvent(textInput, 'focus'); + fireEvent.changeText(textInput, COUNTRY_NAME); + + // Grabbing our input to perform actions on it. + const listItemTestID = `listItem-${COUNTRY_NAME}`; + const firstListItem = getByTestId(listItemTestID); + fireEvent.press(firstListItem); + + expect(onSelect).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app/platform/LocalStorage/index.ts b/app/platform/LocalStorage/index.ts new file mode 100644 index 0000000..3c52af5 --- /dev/null +++ b/app/platform/LocalStorage/index.ts @@ -0,0 +1,26 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; + +class LocalStorage { + setItem(key: string, data: unknown) { + return AsyncStorage.setItem(key, JSON.stringify(data)); + } + + async getItem(key: string) { + const data = await AsyncStorage.getItem(key); + try { + return JSON.parse(data || ''); + } catch (e) { + return data; + } + } + + mergeItem(key, data) { + AsyncStorage.mergeItem(key, JSON.stringify(data)); + } + + removeItem(key) { + AsyncStorage.removeItem(key); + } +} + +export default new LocalStorage(); diff --git a/app/router/index.tsx b/app/router/index.tsx new file mode 100644 index 0000000..fc0ff9c --- /dev/null +++ b/app/router/index.tsx @@ -0,0 +1,25 @@ +// import isEqual from 'lodash/isEqual'; + +import React from 'react'; + +import {NavigationContainer} from '@react-navigation/native'; +import {createStackNavigator} from '@react-navigation/stack'; + +import routes from './routes'; +import {HOME} from './routeNames'; + +const Stack = createStackNavigator(); + +const Router: React.FC = () => ( + + + {Object.keys(routes).map(routeKey => ( + + + + ))} + + +); + +export default Router; diff --git a/app/router/routeConfigs.ts b/app/router/routeConfigs.ts new file mode 100644 index 0000000..6f5f89c --- /dev/null +++ b/app/router/routeConfigs.ts @@ -0,0 +1,13 @@ +import {HOME, SEARCH} from './routeNames'; + +const routeConfigs = { + [SEARCH]: { + path: '/search', + }, + [HOME]: { + path: '/', + parse: {}, + }, +}; + +export default routeConfigs; diff --git a/app/router/routeNames.ts b/app/router/routeNames.ts new file mode 100644 index 0000000..0890409 --- /dev/null +++ b/app/router/routeNames.ts @@ -0,0 +1,2 @@ +export const HOME = 'Home'; +export const SEARCH = 'Search'; diff --git a/app/router/routes.ts b/app/router/routes.ts new file mode 100644 index 0000000..bed5012 --- /dev/null +++ b/app/router/routes.ts @@ -0,0 +1,18 @@ +import HomeScreen from 'screens/HomeScreen'; +import SearchScreen from 'screens/SearchScreen'; + +import routeConfigs from './routeConfigs'; +import * as routeNames from './routeNames'; + +const routes = { + [routeNames.HOME]: { + ...routeConfigs[routeNames.HOME], + screen: HomeScreen, + }, + [routeNames.SEARCH]: { + ...routeConfigs[routeNames.SEARCH], + screen: SearchScreen, + }, +}; + +export default routes; diff --git a/app/router/types.ts b/app/router/types.ts new file mode 100644 index 0000000..f541fb2 --- /dev/null +++ b/app/router/types.ts @@ -0,0 +1,4 @@ +export type RootStackParamList = { + Home?: Record; + Search?: Record; +}; diff --git a/app/screens/HomeScreen/index.tsx b/app/screens/HomeScreen/index.tsx new file mode 100644 index 0000000..7c7c901 --- /dev/null +++ b/app/screens/HomeScreen/index.tsx @@ -0,0 +1,89 @@ +/** + * + * Home Screen + * + */ + +import React, {useState} from 'react'; +import {ScrollView, Text, View, Switch} from 'react-native'; + +import { + Header, + LearnMoreLinks, + DebugInstructions, + ReloadInstructions, +} from 'react-native/Libraries/NewAppScreen'; + +import Button from 'theme/Button'; + +import {SEARCH} from 'router/routeNames'; + +import {HomeScreenProps} from './types'; +import styles from './style'; + +function HomeScreen(props: HomeScreenProps): React.ReactChild { + const [isToggled, setIsToggled] = useState(false); + + const handleToggle = () => setIsToggled(!isToggled); + return ( + +
+ {global.HermesInternal == null ? null : ( + + Engine: Hermes + + )} + + +