diff --git a/.eslintrc.js b/.eslintrc.js index 04e262f..1f34c48 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,30 +1,45 @@ - const fs = require('fs'); const path = require('path'); + const prettierOptions = JSON.parse( fs.readFileSync(path.resolve(__dirname, '.prettierrc'), 'utf8'), ); - -// const app = JSON.parse(path.resolve(__dirname, 'app')); - module.exports = { root: true, parser: 'babel-eslint', - extends: ['airbnb', 'prettier', 'prettier/react'], - plugins: ['prettier', 'redux-saga', 'react-native', 'react', 'react-hooks', 'jsx-a11y'], + extends: [ + 'airbnb', + 'prettier', + 'prettier/react', + 'plugin:prettier/recommended', + 'plugin:sonarjs/recommended', + 'plugin:security/recommended-legacy', + 'plugin:fp/recommended' + ], + plugins: [ + 'github', + 'immutable', + 'sonarjs', + 'redux-saga', + 'react-native', + 'react', + 'react-hooks', + 'jsx-a11y', + 'fp' + ], env: { jest: true, browser: true, node: true, - es6: true, + es6: true }, parserOptions: { ecmaVersion: 6, sourceType: 'module', ecmaFeatures: { - jsx: true, - }, + jsx: true + } }, rules: { 'prettier/prettier': ['error', prettierOptions], @@ -53,8 +68,8 @@ module.exports = { // NOTE: If this error triggers, either disable it or add // your custom components, labels and attributes via these options // See https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/label-has-associated-control.md - controlComponents: ['Input'], - }, + controlComponents: ['Input'] + } ], 'jsx-a11y/label-has-for': 0, 'jsx-a11y/mouse-events-have-key-events': 2, @@ -82,29 +97,76 @@ module.exports = { 'redux-saga/no-yield-in-race': 2, 'redux-saga/yield-effects': 2, 'require-yield': 0, - 'react/no-array-index-key': 0 + 'react/no-array-index-key': 0, + 'react/jsx-curly-newline': 0, + 'react/jsx-one-expression-per-line': 0, + 'react/jsx-wrap-multilines': 0, + 'react/no-unused-prop-types': 0, + 'max-lines-per-function': ['error', 250], + 'no-else-return': 'error', + 'max-params': ['error', 3], + 'require-jsdoc': [ + 'error', + { + require: { + FunctionDeclaration: true, + MethodDefinition: false, + ClassDeclaration: false, + ArrowFunctionExpression: false, + FunctionExpression: false + } + } + ], + 'no-shadow': 'error', + complexity: ['error', 10], + 'no-empty': 'error', + 'import/order': [ + 'error', + { + groups: [ + ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'] + ] + } + ], + 'immutable/no-let': 2, + 'immutable/no-this': 2, + 'max-lines': ['error', 350], + 'react-native/no-unused-styles': 2, + 'react-native/split-platform-components': 2, + 'react-native/no-inline-styles': 2, + 'react-native/no-color-literals': 2, + 'react-native/no-raw-text': 2, + 'react-native/no-single-element-style-arrays': 2, + 'fp/no-mutation': [ + 'error', + { + exceptions: [{ property: 'propTypes' }, { property: 'defaultProps' }] + } + ], + 'fp/no-nil': 0, + 'fp/no-unused-expression': 0 }, - "settings": { - "import/resolver": { - "node": { - "app": "./app", - "context": "app", - "resolve": { - "alias": { - "@assets": "./app/assets", - "@components": "./app/components", - "@containers": "./app/containers", - "@config": "./app/config", - "@navigators": "./app/navigators", - "@services": "./app/services", - "@themes": "./app/themes", - "@utils": "./app/utils" + settings: { + 'import/resolver': { + node: { + app: './app', + context: 'app', + resolve: { + alias: { + '@assets': './app/assets', + '@components': './app/components', + '@containers': './app/containers', + '@config': './app/config', + '@navigators': './app/navigators', + '@services': './app/services', + '@themes': './app/themes', + '@utils': './app/utils' }, - "paths": ["app"], - "modules": ["app", 'node_modules'], - "extensions": ['.js', '.jsx', '.json', '.coffee', '.cjsx'], + paths: ['app'], + modules: ['app', 'node_modules'], + extensions: ['.js', '.jsx', '.json', '.coffee', '.cjsx'] } } } } -} +}; \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0653bf0..f5cf8f0 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,5 @@ buck-out/ .expo/ web-build/ dist/ +ios +android \ No newline at end of file diff --git a/App.js b/App.js index 426ce63..fc85faa 100644 --- a/App.js +++ b/App.js @@ -12,9 +12,7 @@ if (!window.Intl) { }) .then(() => Promise.all([import('intl/locale-data/jsonp/en.js')])) .then(() => AppRegistry.registerComponent(appName, () => App)) - .catch(err => { - throw err; - }); + .catch(alert); } else { AppRegistry.registerComponent(appName, () => App); } diff --git a/android/app/my-upload-key.keystore b/android/app/my-upload-key.keystore deleted file mode 100644 index ce68c72..0000000 Binary files a/android/app/my-upload-key.keystore and /dev/null differ diff --git a/app.json b/app.json index 414589c..cff2191 100644 --- a/app.json +++ b/app.json @@ -19,6 +19,9 @@ }, "android": { "package": "com.wednesdaysolutions.rntws" + }, + "ios": { + "bundleIdentifier": "com.wednesdaysolutions.rntws" } } } diff --git a/app/components/atoms/LanguageProvider/actions.js b/app/components/atoms/LanguageProvider/actions.js index 487dfc4..4959510 100644 --- a/app/components/atoms/LanguageProvider/actions.js +++ b/app/components/atoms/LanguageProvider/actions.js @@ -1,11 +1,10 @@ -/* - * - * LanguageProvider actions - * - */ - import { CHANGE_LOCALE } from './constants'; +/** + * Changes the locale/language of the application. + * @param {string} languageLocale - The new locale/language to set. + * @returns {object} An action object with type 'CHANGE_LOCALE' and the new locale/language. + */ export function changeLocale(languageLocale) { return { type: CHANGE_LOCALE, diff --git a/app/components/atoms/LanguageProvider/index.js b/app/components/atoms/LanguageProvider/index.js index 421aa5d..7517ef5 100644 --- a/app/components/atoms/LanguageProvider/index.js +++ b/app/components/atoms/LanguageProvider/index.js @@ -14,6 +14,14 @@ import { IntlProvider } from 'react-intl'; import { makeSelectLocale } from './selectors'; +/** + * Provides internationalization (i18n) support by wrapping components with an IntlProvider. + * @param {object} props - The props object containing component properties. + * @param {string} props.locale - The locale/language code for internationalization. + * @param {object} props.messages - An object containing locale-specific message translations. + * @param {React.ReactNode} props.children - The child elements/components to be wrapped and rendered. + * @returns {React.ReactNode} A JSX element wrapping the provided child components with IntlProvider. + */ export function LanguageProvider(props) { return ( ({ locale })); +/** + * Generates and returns an object containing action dispatch functions. + * @param {function} dispatch - The Redux store's dispatch function. + * @returns {object} An object containing action dispatch functions wrapped for use in components. + */ function mapDispatchToProps(dispatch) { return { dispatch diff --git a/app/components/atoms/LanguageProvider/reducer.js b/app/components/atoms/LanguageProvider/reducer.js index 7388b34..ec846d7 100644 --- a/app/components/atoms/LanguageProvider/reducer.js +++ b/app/components/atoms/LanguageProvider/reducer.js @@ -22,12 +22,11 @@ export const initialState = fromJS({ /* eslint-disable default-case, no-param-reassign */ export const languageProviderReducer = (state = initialState, action) => produce(state, (/* draft */) => { - switch (action.type) { - case languageProviderTypes.CHANGE_LOCALE: - return state.set('locale', action.locale); - default: - return state; - } + const actionType = { + [languageProviderTypes.CHANGE_LOCALE]: () => + state.set('locale', action.locale) + }; + return action.type in actionType ? actionType[action.type]() : state; }); export default languageProviderReducer; diff --git a/app/components/atoms/LanguageProvider/tests/action.test.js b/app/components/atoms/LanguageProvider/tests/action.test.js new file mode 100644 index 0000000..a0bd652 --- /dev/null +++ b/app/components/atoms/LanguageProvider/tests/action.test.js @@ -0,0 +1,15 @@ +import { changeLocale } from '../actions'; +import { CHANGE_LOCALE } from '../constants'; + +// Test suite for the changeLocale action creator +describe('changeLocale action creator', () => { + it('should create an action to change the locale', () => { + const languageLocale = 'en'; + const expectedAction = { + type: CHANGE_LOCALE, + locale: languageLocale + }; + const action = changeLocale(languageLocale); + expect(action).toEqual(expectedAction); + }); +}); diff --git a/app/components/atoms/LanguageProvider/tests/index.test.js b/app/components/atoms/LanguageProvider/tests/index.test.js index 5a943ae..03aba31 100644 --- a/app/components/atoms/LanguageProvider/tests/index.test.js +++ b/app/components/atoms/LanguageProvider/tests/index.test.js @@ -1,5 +1,4 @@ import React from 'react'; -import 'react-native'; import { render } from '@testing-library/react-native'; import { Provider } from 'react-redux'; import T from '@atoms/T'; @@ -7,10 +6,14 @@ import createStore from 'app/rootReducer'; import { translationMessages } from 'app/i18n'; import { renderWithIntl } from '@utils/testUtils'; import ConnectedLanguageProvider, { LanguageProvider } from '../index'; - +import { Text } from 'react-native'; describe(' container tests', () => { it('should render its children', () => { - const children =

Test

; + const children = ( +

+ Test +

+ ); const container = renderWithIntl( {children} @@ -19,16 +22,10 @@ describe(' container tests', () => { expect(container.firstChild).not.toBeNull(); }); }); - +const setupReduxStore = () => ({ reduxStore: createStore().store }); describe(' container tests', () => { - let reduxStore; - - beforeAll(() => { - const { store } = createStore(); - reduxStore = store; - }); - it('should render the default language messages', () => { + const { reduxStore } = setupReduxStore(); const { queryByText } = render( diff --git a/app/components/atoms/LanguageProvider/tests/reducer.test.js b/app/components/atoms/LanguageProvider/tests/reducer.test.js index cf185c1..baea145 100644 --- a/app/components/atoms/LanguageProvider/tests/reducer.test.js +++ b/app/components/atoms/LanguageProvider/tests/reducer.test.js @@ -3,26 +3,23 @@ import { languageProviderTypes, languageProviderReducer } from '../reducer'; - +const setupMockedState = () => ({ mockedState: initialState }); /* eslint-disable default-case, no-param-reassign */ describe('Tests for LanguageProvider actions', () => { - let mockedState; - beforeEach(() => { - mockedState = initialState; - }); - it('returns the initial state', () => { + const { mockedState } = setupMockedState(); expect(languageProviderReducer(undefined, {})).toEqual(mockedState); }); it('changes the locale', () => { + const { mockedState } = setupMockedState(); const locale = 'de'; - mockedState = mockedState.set('locale', locale); + const UpdateMockedState = mockedState.set('locale', locale); expect( languageProviderReducer(undefined, { type: languageProviderTypes.CHANGE_LOCALE, locale }) - ).toEqual(mockedState); + ).toEqual(UpdateMockedState); }); }); diff --git a/app/components/molecules/CharacterWithQuote/index.js b/app/components/molecules/CharacterWithQuote/index.js index 3ccb5a7..e214833 100644 --- a/app/components/molecules/CharacterWithQuote/index.js +++ b/app/components/molecules/CharacterWithQuote/index.js @@ -1,6 +1,6 @@ import React from 'react'; import styled from 'styled-components/native'; -import { get } from 'lodash'; +import get from 'lodash/get'; import PropTypes from 'prop-types'; import { fonts } from '@themes'; import T from '@atoms/T'; @@ -17,13 +17,22 @@ const CharacterImage = styled.Image` margin: 0 auto; `; +/** + * A component that displays a character's information along with their quote. + * @param {object} props - The props object containing component properties. + * @param {object} props.user - The user object representing the character's details. + * @param {string} props.user.character - The character's name. + * @param {string} props.user.image - The URL of the character's image. + * @param {string} props.user.quote - The character's quote. + * @returns {React.ReactNode} JSX elements displaying the character's information and quote. + */ function CharacterWithQuote({ user }) { return ( <> diff --git a/app/components/molecules/If/index.js b/app/components/molecules/If/index.js index eb4c77b..0d2cab4 100644 --- a/app/components/molecules/If/index.js +++ b/app/components/molecules/If/index.js @@ -4,21 +4,24 @@ * */ // eslint-disable-next-line -import React from 'react' -import Proptypes from 'prop-types'; +import PropTypes from 'prop-types'; + const If = props => (props.condition ? props.children : props.otherwise); -If.propsTypes = { - condition: Proptypes.bool, - otherwise: Proptypes.oneOfType([ - Proptypes.arrayOf(Proptypes.node), - Proptypes.node + +If.propTypes = { + condition: PropTypes.bool, + otherwise: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node ]), - children: Proptypes.oneOfType([ - Proptypes.arrayOf(Proptypes.node), - Proptypes.node + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node ]) }; + If.defaultProps = { otherwise: null }; + export default If; diff --git a/app/components/molecules/If/tests/index.test.js b/app/components/molecules/If/tests/index.test.js index 52e0090..632a0d8 100644 --- a/app/components/molecules/If/tests/index.test.js +++ b/app/components/molecules/If/tests/index.test.js @@ -32,9 +32,9 @@ describe('', () => { ); expect(getByText(conditionTrueText)).toBeTruthy(); - props.condition = false; + const falseProps = { ...props, condition: false }; const { getByText: textQueryOnReRender } = render( - + ); diff --git a/app/components/molecules/LogoWithInstructions/index.js b/app/components/molecules/LogoWithInstructions/index.js index 89bf929..cead76e 100644 --- a/app/components/molecules/LogoWithInstructions/index.js +++ b/app/components/molecules/LogoWithInstructions/index.js @@ -29,6 +29,12 @@ const Instructions = styled(T)` font-style: italic; `; +/** + * A component that displays a logo along with instructions. + * @param {object} props - The props object containing component properties. + * @param {string} props.instructions - The instructions text to display. + * @returns {React.ReactNode} JSX elements displaying the logo and instructions. + */ function LogoWithInstructions({ instructions }) { return ( diff --git a/app/components/organisms/SimpsonsLoveWednesday/index.js b/app/components/organisms/SimpsonsLoveWednesday/index.js index ffb6c5a..a8decb7 100644 --- a/app/components/organisms/SimpsonsLoveWednesday/index.js +++ b/app/components/organisms/SimpsonsLoveWednesday/index.js @@ -6,7 +6,7 @@ import If from '@molecules/If'; import CharacterWithQuote from '@molecules/CharacterWithQuote'; import LogoWithInstructions from '@molecules/LogoWithInstructions'; -const Error = styled.Text` +const Err = styled.Text` ${fonts.style.standard()}; text-align: center; margin-bottom: 5px; @@ -19,13 +19,21 @@ const SeparatedView = styled.View` } `; +/** + * A component that displays Simpsons-themed content related to Wednesday, including instructions, character details, and error messages. + * @param {object} props - The props object containing component properties. + * @param {string} props.instructions - The instructions text to display. + * @param {object} props.user - The user object representing character details and quote. + * @param {string} props.userErrorMessage - The error message to display if user data retrieval fails. + * @returns {React.ReactNode} JSX elements displaying Simpsons-themed content based on provided props. + */ function SimpsonsLoveWednesday({ instructions, user, userErrorMessage }) { return ( <> {userErrorMessage}} + otherwise={{userErrorMessage}} > diff --git a/app/components/organisms/SimpsonsLoveWednesday/tests/index.test.js b/app/components/organisms/SimpsonsLoveWednesday/tests/index.test.js index 9978c9c..8e1156c 100644 --- a/app/components/organisms/SimpsonsLoveWednesday/tests/index.test.js +++ b/app/components/organisms/SimpsonsLoveWednesday/tests/index.test.js @@ -10,7 +10,7 @@ import React from 'react'; import { renderWithIntl } from '@utils/testUtils'; import { rerender } from '@testing-library/react-native'; import SimpsonsLoveWednesday from '../index'; - +import set from 'lodash/set'; describe('', () => { it('Should render and match the snapshot', () => { const baseElement = renderWithIntl(); @@ -29,7 +29,7 @@ describe('', () => { }; const { getByText } = renderWithIntl(); expect(getByText(props.userErrorMessage)).toBeTruthy(); - props.userErrorMessage = null; + set(props, 'userErrorMessage', null); const { getByText: textQueryOnReRender } = renderWithIntl( , rerender diff --git a/app/i18n.js b/app/i18n.js index 87c780f..6e375c1 100644 --- a/app/i18n.js +++ b/app/i18n.js @@ -7,12 +7,12 @@ * script `extract-intl`, and must use CommonJS module syntax * You CANNOT use import/export in this file. */ +import get from 'lodash/get'; const addLocaleData = require('react-intl').addLocaleData; //eslint-disable-line const enLocaleData = require('react-intl/locale-data/en'); const enTranslationMessages = require('./translations/en.json'); - addLocaleData(enLocaleData); export const DEFAULT_LOCALE = 'en'; @@ -28,11 +28,15 @@ export const formatTranslationMessages = (locale, messages) => { ? formatTranslationMessages(DEFAULT_LOCALE, enTranslationMessages) : {}; const flattenFormattedMessages = (formattedMessages, key) => { - const formattedMessage = - !messages[key] && locale !== DEFAULT_LOCALE - ? defaultFormattedMessages[key] - : messages[key]; - return Object.assign(formattedMessages, { [key]: formattedMessage }); + const formattedMessageOptions = { + true: get(defaultFormattedMessages, key), + false: get(messages, key) + }; + const formattedCondition = !get(messages, key) && locale !== DEFAULT_LOCALE; + return { + ...formattedMessages, + [key]: get(formattedMessageOptions, formattedCondition) + }; }; return Object.keys(messages).reduce(flattenFormattedMessages, {}); }; diff --git a/app/navigators/AppNavigator.js b/app/navigators/AppNavigator.js index d3baa1c..b03dff6 100644 --- a/app/navigators/AppNavigator.js +++ b/app/navigators/AppNavigator.js @@ -3,7 +3,7 @@ import { createStackNavigator } from '@react-navigation/stack'; import SplashScreen from '@scenes/SplashScreen/'; import ExampleScreen from '@scenes/ExampleScreen'; import { NavigationContainer } from '@react-navigation/native'; -import NavigationService from '../services/NavigationService'; +import { setTopLevelNavigator } from '../services/NavigationService'; const Stack = createStackNavigator(); /** * The root screen contains the application's navigation. @@ -12,7 +12,7 @@ const Stack = createStackNavigator(); */ export default function AppNavigator() { return ( - + diff --git a/app/rootSaga.js b/app/rootSaga.js index cf9fcec..ae7fd88 100644 --- a/app/rootSaga.js +++ b/app/rootSaga.js @@ -2,6 +2,11 @@ import { fork } from 'redux-saga/effects'; import exampleSaga from '@scenes/ExampleScreen/saga'; import startupSaga from '@scenes/RootScreen/saga'; +/** + * Root saga generator function that orchestrates other sagas. + * This function sets up and manages the execution of multiple sagas using fork effects. + * @returns {IterableIterator} An iterator for managing the execution of sagas. + */ export default function* root() { yield fork(exampleSaga); yield fork(startupSaga); diff --git a/app/scenes/ExampleScreen/index.js b/app/scenes/ExampleScreen/index.js index 1d7df58..3b273c1 100644 --- a/app/scenes/ExampleScreen/index.js +++ b/app/scenes/ExampleScreen/index.js @@ -1,4 +1,3 @@ -import React from 'react'; import { Button, Platform, View, ActivityIndicator } from 'react-native'; import { connect } from 'react-redux'; import { compose } from 'redux'; @@ -6,6 +5,7 @@ import { PropTypes } from 'prop-types'; import styled from 'styled-components/native'; import { createStructuredSelector } from 'reselect'; import { injectIntl } from 'react-intl'; +import React, { useEffect } from 'react'; import AppContainer from '@atoms/Container'; import SimpsonsLoveWednesday from '@organisms/SimpsonsLoveWednesday'; @@ -44,36 +44,32 @@ const instructions = Platform.select({ 'Double tap R on your keyboard to reload,\nShake or press menu button for dev menu.' }); -class ExampleScreen extends React.Component { - componentDidMount() { - this.requestFetchUser()(); - } - - requestFetchUser = () => () => { - this.props.fetchUser(); +const ExampleScreen = props => { + const requestFetchUser = () => { + props.fetchUser(); }; - - render() { - return ( - - {this.props.userIsLoading ? ( - - ) : ( - - - -