diff --git a/mobile/ReactNativeTutorial/.babelrc b/mobile/ReactNativeTutorial/.babelrc new file mode 100644 index 00000000..a9ce1369 --- /dev/null +++ b/mobile/ReactNativeTutorial/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["react-native"] +} diff --git a/mobile/ReactNativeTutorial/.buckconfig b/mobile/ReactNativeTutorial/.buckconfig new file mode 100644 index 00000000..934256cb --- /dev/null +++ b/mobile/ReactNativeTutorial/.buckconfig @@ -0,0 +1,6 @@ + +[android] + target = Google Inc.:Google APIs:23 + +[maven_repositories] + central = https://repo1.maven.org/maven2 diff --git a/mobile/ReactNativeTutorial/.eslintignore b/mobile/ReactNativeTutorial/.eslintignore new file mode 100644 index 00000000..f3ad7d7e --- /dev/null +++ b/mobile/ReactNativeTutorial/.eslintignore @@ -0,0 +1,2 @@ +ios +android diff --git a/mobile/ReactNativeTutorial/.eslintrc.yml b/mobile/ReactNativeTutorial/.eslintrc.yml new file mode 100644 index 00000000..b83d98d0 --- /dev/null +++ b/mobile/ReactNativeTutorial/.eslintrc.yml @@ -0,0 +1,45 @@ +--- + extends: + - "eslint-config-shakacode" + - "plugin:lodash-fp/recommended" + - "plugin:flowtype/recommended" + plugins: + - "react-native" + - "flowtype" + - "babel" + - "lodash-fp" + + env: + node: true + settings: + import/resolver: + node: + extensions: + - ".js" + - ".android.js" + - ".ios.js" + moduleDirectory: + - "." + - "node_modules" + flowtype: + onlyFilesWithFlowAnnotation: true + globals: + __DEV__: true + fetch: true + rules: + new-cap: 0 + react/sort-comp: 2 + no-console: 2 + + babel/no-await-in-loop: 1 + + generator-star-spacing: 0 + + 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/jsx-filename-extension: + - 1 + - extensions: [".js", ".jsx"] diff --git a/mobile/ReactNativeTutorial/.flowconfig b/mobile/ReactNativeTutorial/.flowconfig new file mode 100644 index 00000000..65d9d9ea --- /dev/null +++ b/mobile/ReactNativeTutorial/.flowconfig @@ -0,0 +1,67 @@ +[ignore] + +# We fork some components by platform. +.*/*[.]android.js + +# Ignore templates with `@flow` in header +.*/local-cli/generator.* + +# Ignore malformed json +.*/node_modules/y18n/test/.*\.json + +# Ignore the website subdir +/website/.* + +# Ignore BUCK generated dirs +/\.buckd/ + +# Ignore unexpected extra @providesModule +.*/node_modules/commoner/test/source/widget/share.js + +# Ignore duplicate module providers +# For RN Apps installed via npm, "Libraries" folder is inside node_modules/react-native but in the source repo it is in the root +.*/Libraries/react-native/React.js +.*/Libraries/react-native/ReactNative.js +.*/node_modules/jest-runtime/build/__tests__/.* +.*/node_modules/react/node_modules/.* +.*/node_modules/react-native-experimental-navigation/.* +.*/node_modules/react-native/Libraries/Components/StaticContainer.js + +# Json lint doesn't pass flow +.*/node_modules/jsonlint/.* + +[include] + +[libs] +node_modules/react-native/Libraries/react-native/react-native-interface.js +node_modules/react-native/flow +flow/ + +[options] +module.system=haste + +esproposal.class_static_fields=enable +esproposal.class_instance_fields=enable + +experimental.strict_type_args=true + +munge_underscores=true + +module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub' +module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' + +suppress_type=$FlowIssue +suppress_type=$FlowFixMe +suppress_type=$FixMe + +suppress_type=$FlowIgnore +suppress_comment=\\(.\\|\n\\)*\\$FlowIgnore + +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(3[0-2]\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(3[0-2]\\|1[0-9]\\|[1-2][0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ +suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy + +unsafe.enable_getters_and_setters=true + +[version] +^0.33.0 diff --git a/mobile/ReactNativeTutorial/.gitignore b/mobile/ReactNativeTutorial/.gitignore new file mode 100644 index 00000000..eb1535e4 --- /dev/null +++ b/mobile/ReactNativeTutorial/.gitignore @@ -0,0 +1,41 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace + +# Android/IJ +# +*.iml +.idea +.gradle +local.properties + +# node.js +# +node_modules/ +npm-debug.log + +# BUCK +buck-out/ +\.buckd/ +android/app/libs +android/keystores/debug.keystore diff --git a/mobile/ReactNativeTutorial/.jest.js b/mobile/ReactNativeTutorial/.jest.js new file mode 100644 index 00000000..64eab325 --- /dev/null +++ b/mobile/ReactNativeTutorial/.jest.js @@ -0,0 +1,19 @@ +// Mocking the global.fetch included in React Native +global.fetch = jest.fn(); + +// Helper to mock a success response (only once) +fetch.mockResponseSuccess = (body) => { + fetch.mockImplementationOnce ( + () => Promise.resolve({ + json: () => Promise.resolve(body), + text: () => Promise.resolve(JSON.stringify(body)), + }) + ); +}; + +// Helper to mock a failure response (only once) +fetch.mockResponseFailure = (error) => { + fetch.mockImplementationOnce( + () => Promise.reject(error) + ); +}; diff --git a/mobile/ReactNativeTutorial/.watchmanconfig b/mobile/ReactNativeTutorial/.watchmanconfig new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/mobile/ReactNativeTutorial/.watchmanconfig @@ -0,0 +1 @@ +{} diff --git a/mobile/ReactNativeTutorial/Readme.md b/mobile/ReactNativeTutorial/Readme.md new file mode 100644 index 00000000..a1cace1f --- /dev/null +++ b/mobile/ReactNativeTutorial/Readme.md @@ -0,0 +1,73 @@ +## React Native Tutorial +This is a simple mobile app for posting comments in React Native. +This tutorial shows how to connect to the the https://www.reactrails.com API for a sample microblog. + +### Setup +1. Install the latest version of Xcode from AppStore or https://developer.apple.com/download/ (Apple ID required) +2. Install the latest version of Android Studio from https://developer.android.com/studio/index.html +3. Install nvm (Node Version Manager) + + ``` + curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.32.1/install.sh | bash + ``` + +4. Install NodeJS stable + + ``` + nvm install node + ``` + +5. Install React Native and recommended packages + + ``` + npm install -g react-native-cli + brew install watchman + brew install flow + ``` + +6. Install npm dependencies + + ``` + npm i + ``` + +### Backend API + +* Currently connecting by default to https://www.reactrails.com/. Be aware of that! +* The url can be changed app/api/index.js. Keep in mind, that Android emulator is +a separate Virtual Machine with its own localhost binding. To make the api available under emulator, +you have to use ip address of your computer, that could be seen by running `ifconfig` in the shell + +### Running IOS +``` +react-native run-ios +``` + +### Running Android +1. Check that installed build tools match gradle config of android project: + - In gradle config (app > android > build.gradle), search `buildToolsVersion` + - Run `android sdk` from bash and find installed build tools version there +2. Run emulator from Android studio or `emulator @` from bash (you can find installed version by running `emulator -list-avds` from bash) +3. From project folder run +``` +react-native run-android +``` + +### Testing +Testing framework uses mocha + enzyme, to run tests type +``` +npm test +``` + +### Linters +This projects uses Eslint with React and React Native rules. To run linters type +``` +npm run lint +``` + + +### Flow +This projects uses Eslint with React and React Native rules. To run linters type +``` +npm run flow +``` diff --git a/mobile/ReactNativeTutorial/__mocks__/mock.js b/mobile/ReactNativeTutorial/__mocks__/mock.js new file mode 100644 index 00000000..fa7c739c --- /dev/null +++ b/mobile/ReactNativeTutorial/__mocks__/mock.js @@ -0,0 +1,8 @@ +import React from 'react'; +import _ from 'lodash/fp'; + +export default (props) => React.createElement( + 'Mock', + _.omit('store', props), + props.children, +); diff --git a/mobile/ReactNativeTutorial/__mocks__/mockCall.js b/mobile/ReactNativeTutorial/__mocks__/mockCall.js new file mode 100644 index 00000000..19ff7667 --- /dev/null +++ b/mobile/ReactNativeTutorial/__mocks__/mockCall.js @@ -0,0 +1,39 @@ +class MockCall { + constructor() { + this.queue = []; + } + + setMocks(mocks) { + this.queue = mocks; + } + + getNextMock() { + if (!this.queue || !this.queue.length) return undefined; + return this.queue.shift(); + } + + reset() { + this.queue = []; + } +} + +const mockCall = new MockCall(); + +// Mocks calls inside a function under test. This function takes several args and +// stubs the return from call in order of occurence. If no mock were specified +// it returns underfined +export const mockCalls = (...args) => mockCall.setMocks(args); + +// Clears all mocks for calls +export const resetMockCalls = () => mockCall.reset(); + +// The mock call dispatches a fake action to the store with type: 'CALL' and +// function name and args as parameters. +const call = ({ dispatch }) => (f, ...args) => { + dispatch({ type: 'CALL', name: f.name, args }); + const mock = mockCall.getNextMock(); + const result = typeof mock === 'function' ? mock() : mock; + return f.then ? Promise.resolve(result) : result; +}; + +export default call; diff --git a/mobile/ReactNativeTutorial/__mocks__/mockThunkMiddleware.js b/mobile/ReactNativeTutorial/__mocks__/mockThunkMiddleware.js new file mode 100644 index 00000000..cff35cd5 --- /dev/null +++ b/mobile/ReactNativeTutorial/__mocks__/mockThunkMiddleware.js @@ -0,0 +1,11 @@ +import call from './mockCall'; + +export const thunkMiddlewareCreator = (callEffect) => + ({ dispatch, getState }) => next => action => { + if (typeof action === 'function') { + return action(dispatch, getState, callEffect({ dispatch })); + } + return next(action); + }; + +export default thunkMiddlewareCreator(call); diff --git a/mobile/ReactNativeTutorial/__mocks__/redux-mock-store.js b/mobile/ReactNativeTutorial/__mocks__/redux-mock-store.js new file mode 100644 index 00000000..43b9ae9c --- /dev/null +++ b/mobile/ReactNativeTutorial/__mocks__/redux-mock-store.js @@ -0,0 +1,9 @@ +import configureMockStore from 'redux-mock-store'; +import { initialState as reduxInitialState } from 'ReactNativeTutorial/app/reducers'; + +import mockThunkMiddleware from './mockThunkMiddleware'; + +export const createStoreFromState = configureMockStore([mockThunkMiddleware]); +export const initialState = reduxInitialState; + +export default () => createStoreFromState(initialState); diff --git a/mobile/ReactNativeTutorial/__tests__/bundles/comments/components/Add.js b/mobile/ReactNativeTutorial/__tests__/bundles/comments/components/Add.js new file mode 100644 index 00000000..b1728869 --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/bundles/comments/components/Add.js @@ -0,0 +1,24 @@ +import React from 'react'; +import Add from 'ReactNativeTutorial/app/bundles/comments/components/Add/Add'; + +import renderer from 'react-test-renderer'; + +const actions = { + fetch: jest.fn(), + updateForm: jest.fn(), + createComment: jest.fn(), +}; + +describe('Add', () => { + it('renders correctly', () => { + const props = { + author: 'Alexey', + text: 'Some random comment', + actions, + }; + const tree = renderer.create( + + ); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/mobile/ReactNativeTutorial/__tests__/bundles/comments/components/Index.js b/mobile/ReactNativeTutorial/__tests__/bundles/comments/components/Index.js new file mode 100644 index 00000000..46e35690 --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/bundles/comments/components/Index.js @@ -0,0 +1,43 @@ +import React from 'react'; +import Index from 'ReactNativeTutorial/app/bundles/comments/components/Index/Index'; + +import renderer from 'react-test-renderer'; + +const actions = { + fetch: jest.fn(), + updateForm: jest.fn(), + createComment: jest.fn(), +}; + +describe('Index', () => { + it('renders correctly when loaded', () => { + const props = { + comments: [ + { id: 1, author: 'Alexey', text: 'Just some random comment' }, + { id: 2, author: 'Justin', text: 'Another random comment' }, + ], + meta: { + loading: false, + }, + actions, + }; + const tree = renderer.create( + + ); + expect(tree).toMatchSnapshot(); + }); + + it('renders correctly when loading', () => { + const props = { + meta: { + loading: true, + }, + actions, + }; + const tree = renderer.create( + + ); + expect(tree).toMatchSnapshot(); + }); +}); + diff --git a/mobile/ReactNativeTutorial/__tests__/bundles/comments/components/__snapshots__/Add.js.snap b/mobile/ReactNativeTutorial/__tests__/bundles/comments/components/__snapshots__/Add.js.snap new file mode 100644 index 00000000..272389c6 --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/bundles/comments/components/__snapshots__/Add.js.snap @@ -0,0 +1,325 @@ +exports[`Add renders correctly 1`] = ` + + + + AUTHOR NAME + + + + + + + + COMMENT + + + + + + + + +  + + + Send + + + + +`; diff --git a/mobile/ReactNativeTutorial/__tests__/bundles/comments/components/__snapshots__/Index.js.snap b/mobile/ReactNativeTutorial/__tests__/bundles/comments/components/__snapshots__/Index.js.snap new file mode 100644 index 00000000..452ee1f6 --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/bundles/comments/components/__snapshots__/Index.js.snap @@ -0,0 +1,300 @@ +exports[`Index renders correctly when loaded 1`] = ` + + + } + removeClippedSubviews={true} + renderRow={[Function]} + scrollEventThrottle={50} + scrollRenderAheadDistance={1000} + stickyHeaderIndices={Array []}> + + + Alexey + + + Just some random comment + + + + + Justin + + + Another random comment + + + + + + +  + + + + +`; + +exports[`Index renders correctly when loading 1`] = ` + + + } + removeClippedSubviews={true} + renderRow={[Function]} + scrollEventThrottle={50} + scrollRenderAheadDistance={1000} + stickyHeaderIndices={Array []} /> + + + +  + + + + +`; diff --git a/mobile/ReactNativeTutorial/__tests__/bundles/comments/hocs/__snapshots__/withAddProps.js.snap b/mobile/ReactNativeTutorial/__tests__/bundles/comments/hocs/__snapshots__/withAddProps.js.snap new file mode 100644 index 00000000..35dc0901 --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/bundles/comments/hocs/__snapshots__/withAddProps.js.snap @@ -0,0 +1,13 @@ +exports[`withAddProps adds AddProps to a component 1`] = ` + +`; diff --git a/mobile/ReactNativeTutorial/__tests__/bundles/comments/hocs/__snapshots__/withIndexProps.js.snap b/mobile/ReactNativeTutorial/__tests__/bundles/comments/hocs/__snapshots__/withIndexProps.js.snap new file mode 100644 index 00000000..8e4eaf0a --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/bundles/comments/hocs/__snapshots__/withIndexProps.js.snap @@ -0,0 +1,34 @@ +exports[`withIndexProps adds props to Index component 1`] = ` + +`; diff --git a/mobile/ReactNativeTutorial/__tests__/bundles/comments/hocs/withAddProps.js b/mobile/ReactNativeTutorial/__tests__/bundles/comments/hocs/withAddProps.js new file mode 100644 index 00000000..61d7e85f --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/bundles/comments/hocs/withAddProps.js @@ -0,0 +1,20 @@ +import React from 'react'; +import withAddProps from 'ReactNativeTutorial/app/bundles/comments/hocs/withAddProps'; +import Mock from 'mock'; +import renderer from 'react-test-renderer'; +import {createStoreFromState, initialState} from 'redux-mock-store'; + +describe('withAddProps', () => { + it('adds AddProps to a component', () => { + const state = initialState.mergeDeep({ + commentForm: { + author: 'Alexey', + text: 'Random comment', + }, + }); + const store = createStoreFromState(state); + const Component = withAddProps(Mock); + const tree = renderer.create(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/mobile/ReactNativeTutorial/__tests__/bundles/comments/hocs/withIndexProps.js b/mobile/ReactNativeTutorial/__tests__/bundles/comments/hocs/withIndexProps.js new file mode 100644 index 00000000..21279a93 --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/bundles/comments/hocs/withIndexProps.js @@ -0,0 +1,21 @@ +import React from 'react'; +import withIndexProps from 'ReactNativeTutorial/app/bundles/comments/hocs/withIndexProps'; +import Mock from 'mock'; +import renderer from 'react-test-renderer'; +import { createStoreFromState, initialState } from 'redux-mock-store'; + +describe('withIndexProps', () => { + it('adds props to Index component', () => { + const state = initialState.mergeDeep({ + commentsStore: { + 2: { id: 2, author: 'Justin', text: 'Another random comment' }, + 3: { id: 3, author: 'John', text: 'Yet another random comment' }, + 1: { id: 1, author: 'Alexey', text: 'Just some random comment' }, + }, + }); + const store = createStoreFromState(state); + const Component = withIndexProps(Mock); + const tree = renderer.create(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/mobile/ReactNativeTutorial/__tests__/bundles/comments/thunks/__snapshots__/createComment.js.snap b/mobile/ReactNativeTutorial/__tests__/bundles/comments/thunks/__snapshots__/createComment.js.snap new file mode 100644 index 00000000..d2090914 --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/bundles/comments/thunks/__snapshots__/createComment.js.snap @@ -0,0 +1,47 @@ +exports[`createComment creates a comment in the store and sends request to api 1`] = ` +Array [ + Object { + "entities": Object { + "temp:0": Object { + "author": "Alexey", + "id": "temp:0", + "text": "Random comment", + }, + }, + "type": "COMMENTS_STORE:CREATE", + }, + Object { + "args": Array [], + "name": "bound pop", + "type": "CALL", + }, + Object { + "args": Array [ + Object { + "author": "Alexey", + "id": "temp:0", + "text": "Random comment", + }, + ], + "name": "postComment", + "type": "CALL", + }, + Object { + "id": "temp:0", + "type": "COMMENTS_STORE:REMOVE", + }, + Object { + "entities": Object { + "1": Object { + "author": "Alexey", + "id": 1, + "text": "Random comment", + }, + }, + "type": "COMMENTS_STORE:CREATE", + }, + Object { + "type": "COMMENT_FORM:RESET", + }, +] +`; diff --git a/mobile/ReactNativeTutorial/__tests__/bundles/comments/thunks/__snapshots__/fetch.js.snap b/mobile/ReactNativeTutorial/__tests__/bundles/comments/thunks/__snapshots__/fetch.js.snap new file mode 100644 index 00000000..e85eb6f4 --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/bundles/comments/thunks/__snapshots__/fetch.js.snap @@ -0,0 +1,63 @@ +exports[`fetch failing fetch dispatches an action with error 1`] = ` +Array [ + Object { + "loading": true, + "type": "COMMENTS_STORE:SET_LOADING", + }, + Object { + "args": Array [], + "name": "fetchComments", + "type": "CALL", + }, + Object { + "args": Array [ + "Error", + "Could not connect to server", + Array [ + Object { + "text": "OK", + }, + ], + ], + "name": "alert", + "type": "CALL", + }, + Object { + "loading": false, + "type": "COMMENTS_STORE:SET_LOADING", + }, +] +`; + +exports[`fetch successful fetch saves comments into the store 1`] = ` +Array [ + Object { + "loading": true, + "type": "COMMENTS_STORE:SET_LOADING", + }, + Object { + "args": Array [], + "name": "fetchComments", + "type": "CALL", + }, + Object { + "loading": false, + "type": "COMMENTS_STORE:SET_LOADING", + }, + Object { + "entities": Object { + "1": Object { + "author": "Alexey", + "id": 1, + "text": "Just some random comment", + }, + "2": Object { + "author": "Justin", + "id": 2, + "text": "Another random comment", + }, + }, + "type": "COMMENTS_STORE:CREATE", + }, +] +`; diff --git a/mobile/ReactNativeTutorial/__tests__/bundles/comments/thunks/createComment.js b/mobile/ReactNativeTutorial/__tests__/bundles/comments/thunks/createComment.js new file mode 100644 index 00000000..c672a79a --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/bundles/comments/thunks/createComment.js @@ -0,0 +1,24 @@ +import { fromJS } from 'immutable'; +import { createStoreFromState } from 'redux-mock-store'; +import { mockCalls } from 'mockCall'; +import * as actions from 'ReactNativeTutorial/app/bundles/comments/thunks'; + +describe('createComment', () => { + it('creates a comment in the store and sends request to api', async() => { + const data = { + commentsStore: {}, + commentForm: { author: 'Alexey', text: 'Random comment' }, + }; + const store = createStoreFromState(fromJS(data)); + const response = { + entities: { + comments: { + 1: { id: 1, author: 'Alexey', text: 'Random comment' }, + }, + }, + }; + mockCalls(null, response); + await store.dispatch(actions.createComment()); + expect(store.getActions()).toMatchSnapshot(); + }); +}); diff --git a/mobile/ReactNativeTutorial/__tests__/bundles/comments/thunks/fetch.js b/mobile/ReactNativeTutorial/__tests__/bundles/comments/thunks/fetch.js new file mode 100644 index 00000000..dfeb981e --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/bundles/comments/thunks/fetch.js @@ -0,0 +1,39 @@ +import createDefaultStore from 'redux-mock-store'; +import { mockCalls, resetMockCalls } from 'mockCall'; +import * as actions from 'ReactNativeTutorial/app/bundles/comments/thunks'; + +describe('fetch', () => { + let store; + + beforeEach(() => { + store = createDefaultStore(); + }); + + afterEach(() => { + resetMockCalls(); + }); + + describe('successful fetch', () => { + it('saves comments into the store', async () => { + const response = { + entities: { + comments: { + 1: { id: 1, author: 'Alexey', text: 'Just some random comment' }, + 2: { id: 2, author: 'Justin', text: 'Another random comment' }, + }, + }, + }; + mockCalls(response); + await store.dispatch(actions.fetch()); + expect(store.getActions()).toMatchSnapshot(); + }); + }); + + describe('failing fetch', () => { + it('dispatches an action with error', async () => { + mockCalls(() => { throw new Error('Invalid Json'); }); + await store.dispatch(actions.fetch()); + expect(store.getActions()).toMatchSnapshot(); + }); + }); +}); diff --git a/mobile/ReactNativeTutorial/__tests__/bundles/common/hocs/withInitialAction.js b/mobile/ReactNativeTutorial/__tests__/bundles/common/hocs/withInitialAction.js new file mode 100644 index 00000000..e2a5e8e4 --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/bundles/common/hocs/withInitialAction.js @@ -0,0 +1,14 @@ +import React from 'react'; +import _ from 'lodash/fp'; +import withInitialAction from 'ReactNativeTutorial/app/bundles/common/hocs/withInitialAction'; +import Mock from 'mock'; +import renderer from 'react-test-renderer'; + +describe('withIndexProps', () => { + it('adds props to Index component', () => { + const Component = withInitialAction(_.get('action'))(Mock); + const action = jest.fn(); + renderer.create(); + expect(action).toBeCalled(); + }); +}); diff --git a/mobile/ReactNativeTutorial/__tests__/reducers/__snapshots__/commentFormReducer.js.snap b/mobile/ReactNativeTutorial/__tests__/reducers/__snapshots__/commentFormReducer.js.snap new file mode 100644 index 00000000..82815de0 --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/reducers/__snapshots__/commentFormReducer.js.snap @@ -0,0 +1,13 @@ +exports[`commentFormReducer resetCommentForm clears all values incl. meta 1`] = ` +Object { + "meta": Object {}, +} +`; + +exports[`commentFormReducer updateCommentForm updates comment form with payload 1`] = ` +Object { + "author": "Alexey", + "meta": Object {}, + "text": "Random text", +} +`; diff --git a/mobile/ReactNativeTutorial/__tests__/reducers/__snapshots__/commentsStoreReducer.js.snap b/mobile/ReactNativeTutorial/__tests__/reducers/__snapshots__/commentsStoreReducer.js.snap new file mode 100644 index 00000000..18c5bfd0 --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/reducers/__snapshots__/commentsStoreReducer.js.snap @@ -0,0 +1,46 @@ +exports[`commentsStoreReducer createComments adds comments to the store 1`] = ` +Object { + "1": Object { + "author": "Alexey", + "id": 1, + "text": "Random comment", + }, + "2": Object { + "author": "Justin", + "id": 2, + "text": "Another random comment", + }, + "meta": Object { + "loading": false, + }, +} +`; + +exports[`commentsStoreReducer removeComment removes comment by id 1`] = ` +Object { + "2": Object { + "author": "Justin", + "id": 2, + "text": "Another random comment", + }, + "meta": Object { + "loading": false, + }, +} +`; + +exports[`commentsStoreReducer setLoadingComments sets the loading meta information 1`] = ` +Object { + "meta": Object { + "loading": true, + }, +} +`; + +exports[`commentsStoreReducer setLoadingComments sets the loading meta information 2`] = ` +Object { + "meta": Object { + "loading": false, + }, +} +`; diff --git a/mobile/ReactNativeTutorial/__tests__/reducers/commentFormReducer.js b/mobile/ReactNativeTutorial/__tests__/reducers/commentFormReducer.js new file mode 100644 index 00000000..ccd5cc40 --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/reducers/commentFormReducer.js @@ -0,0 +1,27 @@ +import commentFormReducer, { actions, initialState } from 'ReactNativeTutorial/app/reducers/commentFormReducer'; + +describe('commentFormReducer', () => { + describe('updateCommentForm', () => { + it('updates comment form with payload', () => { + const payload = { author: 'Alexey', text: 'Random text' }; + expect( + commentFormReducer(initialState, actions.updateCommentForm(payload)) + ).toMatchSnapshot(); + }); + }); + + describe('resetCommentForm', () => { + it('clears all values incl. meta', () => { + const state = initialState.mergeDeep({ + meta: { + errors: { + author: 'Empty name', + }, + }, + author: '', + text: 'Random text', + }); + expect(commentFormReducer(state, actions.resetCommentForm())).toMatchSnapshot(); + }); + }); +}); diff --git a/mobile/ReactNativeTutorial/__tests__/reducers/commentsStoreReducer.js b/mobile/ReactNativeTutorial/__tests__/reducers/commentsStoreReducer.js new file mode 100644 index 00000000..96378a0b --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/reducers/commentsStoreReducer.js @@ -0,0 +1,37 @@ +import { fromJS } from 'immutable'; +import commentsStoreReducer, { actions, initialState } from 'ReactNativeTutorial/app/reducers/commentsStoreReducer'; + +const sampleData = { + 1: { id: 1, author: 'Alexey', text: 'Random comment' }, + 2: { id: 2, author: 'Justin', text: 'Another random comment' }, +}; + +describe('commentsStoreReducer', () => { + describe('createComments', () => { + it('adds comments to the store', () => { + expect( + commentsStoreReducer( + initialState, + actions.createComments(sampleData)) + ).toMatchSnapshot(); + }); + }); + + describe('removeComment', () => { + it('removes comment by id', () => { + expect( + commentsStoreReducer(initialState.merge(sampleData), actions.removeComment('1')) + ).toMatchSnapshot(); + }); + }); + + describe('setLoadingComments', () => { + it('sets the loading meta information', () => { + const trueState = commentsStoreReducer(initialState, actions.setLoadingComments(true)); + const falseState = commentsStoreReducer(trueState, actions.setLoadingComments(false)); + expect(trueState).toMatchSnapshot(); + expect(falseState).toMatchSnapshot(); + }); + }); +}); + diff --git a/mobile/ReactNativeTutorial/__tests__/selectors/__snapshots__/commentFormSelector.js.snap b/mobile/ReactNativeTutorial/__tests__/selectors/__snapshots__/commentFormSelector.js.snap new file mode 100644 index 00000000..a4a72c85 --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/selectors/__snapshots__/commentFormSelector.js.snap @@ -0,0 +1,6 @@ +exports[`commentFormSelector selects the comment form store 1`] = ` +Object { + "author": "Alexey", + "text": "Random comment", +} +`; diff --git a/mobile/ReactNativeTutorial/__tests__/selectors/__snapshots__/commentsPropsSelector.js.snap b/mobile/ReactNativeTutorial/__tests__/selectors/__snapshots__/commentsPropsSelector.js.snap new file mode 100644 index 00000000..b5564902 --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/selectors/__snapshots__/commentsPropsSelector.js.snap @@ -0,0 +1,24 @@ +exports[`commentsPropsSelector maps commentsStore to props 1`] = ` +Object { + "comments": Array [ + Object { + "author": "John", + "id": 3, + "text": "Yet another random comment", + }, + Object { + "author": "Justin", + "id": 2, + "text": "Another random comment", + }, + Object { + "author": "Alexey", + "id": 1, + "text": "Just some random comment", + }, + ], + "meta": Object { + "loading": false, + }, +} +`; diff --git a/mobile/ReactNativeTutorial/__tests__/selectors/__snapshots__/commentsStoreSelector.js.snap b/mobile/ReactNativeTutorial/__tests__/selectors/__snapshots__/commentsStoreSelector.js.snap new file mode 100644 index 00000000..0bf909e2 --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/selectors/__snapshots__/commentsStoreSelector.js.snap @@ -0,0 +1,14 @@ +exports[`commentsStoreSelector selects the comments store 1`] = ` +Object { + "1": Object { + "author": "Alexey", + "id": 1, + "text": "Random comment", + }, + "2": Object { + "author": "Justin", + "id": 2, + "text": "Another random comment", + }, +} +`; diff --git a/mobile/ReactNativeTutorial/__tests__/selectors/commentFormSelector.js b/mobile/ReactNativeTutorial/__tests__/selectors/commentFormSelector.js new file mode 100644 index 00000000..fa7e65d6 --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/selectors/commentFormSelector.js @@ -0,0 +1,14 @@ +import { fromJS } from 'immutable'; +import commentFormSelector from 'ReactNativeTutorial/app/selectors/commentFormSelector'; + +describe('commentFormSelector', () => { + it('selects the comment form store', () => { + const state = fromJS({ + commentForm: { + author: 'Alexey', + text: 'Random comment', + }, + }); + expect(commentFormSelector(state)).toMatchSnapshot(); + }); +}); diff --git a/mobile/ReactNativeTutorial/__tests__/selectors/commentsPropsSelector.js b/mobile/ReactNativeTutorial/__tests__/selectors/commentsPropsSelector.js new file mode 100644 index 00000000..b0f834c0 --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/selectors/commentsPropsSelector.js @@ -0,0 +1,16 @@ +import { fromJS } from 'immutable'; +import commentsPropsSelector from 'ReactNativeTutorial/app/selectors/commentsPropsSelector'; + +describe('commentsPropsSelector', () => { + it('maps commentsStore to props', () => { + const state = fromJS({ + commentsStore: { + 2: { id: 2, author: 'Justin', text: 'Another random comment' }, + 3: { id: 3, author: 'John', text: 'Yet another random comment' }, + 1: { id: 1, author: 'Alexey', text: 'Just some random comment' }, + meta: { loading: false }, + }, + }); + expect(commentsPropsSelector(state)).toMatchSnapshot(); + }); +}); diff --git a/mobile/ReactNativeTutorial/__tests__/selectors/commentsStoreSelector.js b/mobile/ReactNativeTutorial/__tests__/selectors/commentsStoreSelector.js new file mode 100644 index 00000000..7e42950e --- /dev/null +++ b/mobile/ReactNativeTutorial/__tests__/selectors/commentsStoreSelector.js @@ -0,0 +1,14 @@ +import { fromJS } from 'immutable'; +import commentsStoreSelector from 'ReactNativeTutorial/app/selectors/commentsStoreSelector'; + +describe('commentsStoreSelector', () => { + it('selects the comments store', () => { + const state = fromJS({ + commentsStore: { + 1: { id: 1, author: 'Alexey', text: 'Random comment' }, + 2: { id: 2, author: 'Justin', text: 'Another random comment' }, + }, + }); + expect(commentsStoreSelector(state)).toMatchSnapshot(); + }); +}); diff --git a/mobile/ReactNativeTutorial/android/app/BUCK b/mobile/ReactNativeTutorial/android/app/BUCK new file mode 100644 index 00000000..1d99227a --- /dev/null +++ b/mobile/ReactNativeTutorial/android/app/BUCK @@ -0,0 +1,66 @@ +import re + +# To learn about Buck see [Docs](https://buckbuild.com/). +# To run your application with Buck: +# - install Buck +# - `npm start` - to start the packager +# - `cd android` +# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` +# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck +# - `buck install -r android/app` - compile, install and run application +# + +lib_deps = [] +for jarfile in glob(['libs/*.jar']): + name = 'jars__' + re.sub(r'^.*/([^/]+)\.jar$', r'\1', jarfile) + lib_deps.append(':' + name) + prebuilt_jar( + name = name, + binary_jar = jarfile, + ) + +for aarfile in glob(['libs/*.aar']): + name = 'aars__' + re.sub(r'^.*/([^/]+)\.aar$', r'\1', aarfile) + lib_deps.append(':' + name) + android_prebuilt_aar( + name = name, + aar = aarfile, + ) + +android_library( + name = 'all-libs', + exported_deps = lib_deps +) + +android_library( + name = 'app-code', + srcs = glob([ + 'src/main/java/**/*.java', + ]), + deps = [ + ':all-libs', + ':build_config', + ':res', + ], +) + +android_build_config( + name = 'build_config', + package = 'com.reactnativetutorial', +) + +android_resource( + name = 'res', + res = 'src/main/res', + package = 'com.reactnativetutorial', +) + +android_binary( + name = 'app', + package_type = 'debug', + manifest = 'src/main/AndroidManifest.xml', + keystore = '//android/keystores:debug', + deps = [ + ':app-code', + ], +) diff --git a/mobile/ReactNativeTutorial/android/app/build.gradle b/mobile/ReactNativeTutorial/android/app/build.gradle new file mode 100644 index 00000000..096969eb --- /dev/null +++ b/mobile/ReactNativeTutorial/android/app/build.gradle @@ -0,0 +1,149 @@ +apply plugin: "com.android.application" + +import com.android.build.OutputFile + +/** + * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets + * and bundleReleaseJsAndAssets). + * These basically call `react-native bundle` with the correct arguments during the Android build + * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the + * bundle directly from the development server. Below you can see all the possible configurations + * and their defaults. If you decide to add a configuration block, make sure to add it before the + * `apply from: "../../node_modules/react-native/react.gradle"` line. + * + * project.ext.react = [ + * // the name of the generated asset file containing your JS bundle + * bundleAssetName: "index.android.bundle", + * + * // the entry file for bundle generation + * entryFile: "index.android.js", + * + * // whether to bundle JS and assets in debug mode + * bundleInDebug: false, + * + * // whether to bundle JS and assets in release mode + * bundleInRelease: true, + * + * // whether to bundle JS and assets in another build variant (if configured). + * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants + * // The configuration property can be in the following formats + * // 'bundleIn${productFlavor}${buildType}' + * // 'bundleIn${buildType}' + * // bundleInFreeDebug: true, + * // bundleInPaidRelease: true, + * // bundleInBeta: true, + * + * // the root of your project, i.e. where "package.json" lives + * root: "../../", + * + * // where to put the JS bundle asset in debug mode + * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", + * + * // where to put the JS bundle asset in release mode + * jsBundleDirRelease: "$buildDir/intermediates/assets/release", + * + * // where to put drawable resources / React Native assets, e.g. the ones you use via + * // require('./image.png')), in debug mode + * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", + * + * // where to put drawable resources / React Native assets, e.g. the ones you use via + * // require('./image.png')), in release mode + * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", + * + * // by default the gradle tasks are skipped if none of the JS files or assets change; this means + * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to + * // date; if you have any other folders that you want to ignore for performance reasons (gradle + * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ + * // for example, you might want to remove it from here. + * inputExcludes: ["android/**", "ios/**"], + * + * // override which node gets called and with what additional arguments + * nodeExecutableAndArgs: ["node"] + * + * // supply additional arguments to the packager + * extraPackagerArgs: [] + * ] + */ + +apply from: "../../node_modules/react-native/react.gradle" + +/** + * Set this to true to create two separate APKs instead of one: + * - An APK that only works on ARM devices + * - An APK that only works on x86 devices + * The advantage is the size of the APK is reduced by about 4MB. + * Upload all the APKs to the Play Store and people will download + * the correct one based on the CPU architecture of their device. + */ +def enableSeparateBuildPerCPUArchitecture = false + +/** + * Run Proguard to shrink the Java bytecode in release builds. + */ +def enableProguardInReleaseBuilds = false + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.3" + + defaultConfig { + applicationId "com.reactnativetutorial" + minSdkVersion 16 + targetSdkVersion 22 + versionCode 1 + versionName "1.0" + ndk { + abiFilters "armeabi-v7a", "x86" + } + } + signingConfigs { + release { + storeFile file(MYAPP_RELEASE_STORE_FILE) + storePassword MYAPP_RELEASE_STORE_PASSWORD + keyAlias MYAPP_RELEASE_KEY_ALIAS + keyPassword MYAPP_RELEASE_KEY_PASSWORD + } + } + splits { + abi { + reset() + enable enableSeparateBuildPerCPUArchitecture + universalApk false // If true, also generate a universal APK + include "armeabi-v7a", "x86" + } + } + buildTypes { + release { + minifyEnabled enableProguardInReleaseBuilds + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + signingConfig signingConfigs.release + } + } + // applicationVariants are e.g. debug, release + applicationVariants.all { variant -> + variant.outputs.each { output -> + // For each separate APK per architecture, set a unique version code as described here: + // http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits + def versionCodes = ["armeabi-v7a":1, "x86":2] + def abi = output.getFilter(OutputFile.ABI) + if (abi != null) { // null for the universal-debug, universal-release variants + output.versionCodeOverride = + versionCodes.get(abi) * 1048576 + defaultConfig.versionCode + } + } + } +} + +dependencies { + compile project(':react-native-vector-icons') + compile fileTree(dir: "libs", include: ["*.jar"]) + compile "com.android.support:appcompat-v7:23.0.1" + compile "com.facebook.react:react-native:+" // From node_modules +} + +// Run this once to be able to run the application with BUCK +// puts all compile dependencies into folder libs for BUCK to use +task copyDownloadableDepsToLibs(type: Copy) { + from configurations.compile + into 'libs' +} diff --git a/mobile/ReactNativeTutorial/android/app/proguard-rules.pro b/mobile/ReactNativeTutorial/android/app/proguard-rules.pro new file mode 100644 index 00000000..48361a90 --- /dev/null +++ b/mobile/ReactNativeTutorial/android/app/proguard-rules.pro @@ -0,0 +1,66 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Disabling obfuscation is useful if you collect stack traces from production crashes +# (unless you are using a system that supports de-obfuscate the stack traces). +-dontobfuscate + +# React Native + +# Keep our interfaces so they can be used by other ProGuard rules. +# See http://sourceforge.net/p/proguard/bugs/466/ +-keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip +-keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters +-keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip + +# Do not strip any method/class that is annotated with @DoNotStrip +-keep @com.facebook.proguard.annotations.DoNotStrip class * +-keep @com.facebook.common.internal.DoNotStrip class * +-keepclassmembers class * { + @com.facebook.proguard.annotations.DoNotStrip *; + @com.facebook.common.internal.DoNotStrip *; +} + +-keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * { + void set*(***); + *** get*(); +} + +-keep class * extends com.facebook.react.bridge.JavaScriptModule { *; } +-keep class * extends com.facebook.react.bridge.NativeModule { *; } +-keepclassmembers,includedescriptorclasses class * { native ; } +-keepclassmembers class * { @com.facebook.react.uimanager.UIProp ; } +-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp ; } +-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup ; } + +-dontwarn com.facebook.react.** + +# okhttp + +-keepattributes Signature +-keepattributes *Annotation* +-keep class okhttp3.** { *; } +-keep interface okhttp3.** { *; } +-dontwarn okhttp3.** + +# okio + +-keep class sun.misc.Unsafe { *; } +-dontwarn java.nio.file.* +-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement +-dontwarn okio.** diff --git a/mobile/ReactNativeTutorial/android/app/rncomments.keystore b/mobile/ReactNativeTutorial/android/app/rncomments.keystore new file mode 100644 index 00000000..2c8a582b Binary files /dev/null and b/mobile/ReactNativeTutorial/android/app/rncomments.keystore differ diff --git a/mobile/ReactNativeTutorial/android/app/src/main/AndroidManifest.xml b/mobile/ReactNativeTutorial/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bf83657 --- /dev/null +++ b/mobile/ReactNativeTutorial/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + diff --git a/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/Entypo.ttf b/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/Entypo.ttf new file mode 100644 index 00000000..1c8f5e91 Binary files /dev/null and b/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/Entypo.ttf differ diff --git a/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/EvilIcons.ttf b/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/EvilIcons.ttf new file mode 100644 index 00000000..b270f985 Binary files /dev/null and b/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/EvilIcons.ttf differ diff --git a/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/FontAwesome.ttf b/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/FontAwesome.ttf new file mode 100644 index 00000000..f221e50a Binary files /dev/null and b/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/FontAwesome.ttf differ diff --git a/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/Foundation.ttf b/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/Foundation.ttf new file mode 100644 index 00000000..6cce217d Binary files /dev/null and b/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/Foundation.ttf differ diff --git a/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/Ionicons.ttf b/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/Ionicons.ttf new file mode 100644 index 00000000..307ad889 Binary files /dev/null and b/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/Ionicons.ttf differ diff --git a/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/MaterialIcons.ttf b/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/MaterialIcons.ttf new file mode 100644 index 00000000..7015564a Binary files /dev/null and b/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/MaterialIcons.ttf differ diff --git a/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/Octicons.ttf b/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/Octicons.ttf new file mode 100644 index 00000000..0a62bb9e Binary files /dev/null and b/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/Octicons.ttf differ diff --git a/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/SimpleLineIcons.ttf b/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/SimpleLineIcons.ttf new file mode 100644 index 00000000..6ecb6868 Binary files /dev/null and b/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/SimpleLineIcons.ttf differ diff --git a/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/Zocial.ttf b/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/Zocial.ttf new file mode 100644 index 00000000..e4ae46c6 Binary files /dev/null and b/mobile/ReactNativeTutorial/android/app/src/main/assets/fonts/Zocial.ttf differ diff --git a/mobile/ReactNativeTutorial/android/app/src/main/java/com/reactnativetutorial/MainActivity.java b/mobile/ReactNativeTutorial/android/app/src/main/java/com/reactnativetutorial/MainActivity.java new file mode 100644 index 00000000..84c13414 --- /dev/null +++ b/mobile/ReactNativeTutorial/android/app/src/main/java/com/reactnativetutorial/MainActivity.java @@ -0,0 +1,15 @@ +package com.reactnativetutorial; + +import com.facebook.react.ReactActivity; + +public class MainActivity extends ReactActivity { + + /** + * Returns the name of the main component registered from JavaScript. + * This is used to schedule rendering of the component. + */ + @Override + protected String getMainComponentName() { + return "ReactNativeTutorial"; + } +} diff --git a/mobile/ReactNativeTutorial/android/app/src/main/java/com/reactnativetutorial/MainApplication.java b/mobile/ReactNativeTutorial/android/app/src/main/java/com/reactnativetutorial/MainApplication.java new file mode 100644 index 00000000..58adc112 --- /dev/null +++ b/mobile/ReactNativeTutorial/android/app/src/main/java/com/reactnativetutorial/MainApplication.java @@ -0,0 +1,37 @@ +package com.reactnativetutorial; + +import android.app.Application; +import android.util.Log; + +import com.facebook.react.ReactApplication; +import com.oblador.vectoricons.VectorIconsPackage; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactNativeHost; +import com.facebook.react.ReactPackage; +import com.facebook.react.shell.MainReactPackage; + +import java.util.Arrays; +import java.util.List; + +public class MainApplication extends Application implements ReactApplication { + + private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { + @Override + protected boolean getUseDeveloperSupport() { + return BuildConfig.DEBUG; + } + + @Override + protected List getPackages() { + return Arrays.asList( + new MainReactPackage(), + new VectorIconsPackage() + ); + } + }; + + @Override + public ReactNativeHost getReactNativeHost() { + return mReactNativeHost; + } +} diff --git a/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..cde69bcc Binary files /dev/null and b/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-hdpi/shaka_logo.png b/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-hdpi/shaka_logo.png new file mode 100644 index 00000000..f818fcdd Binary files /dev/null and b/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-hdpi/shaka_logo.png differ diff --git a/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..c133a0cb Binary files /dev/null and b/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-mdpi/shaka_logo.png b/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-mdpi/shaka_logo.png new file mode 100644 index 00000000..32e94c7c Binary files /dev/null and b/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-mdpi/shaka_logo.png differ diff --git a/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..bfa42f0e Binary files /dev/null and b/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-xhdpi/shaka_logo.png b/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-xhdpi/shaka_logo.png new file mode 100644 index 00000000..77e246ee Binary files /dev/null and b/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-xhdpi/shaka_logo.png differ diff --git a/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..324e72cd Binary files /dev/null and b/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-xxhdpi/shaka_logo.png b/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-xxhdpi/shaka_logo.png new file mode 100644 index 00000000..3ba1b08b Binary files /dev/null and b/mobile/ReactNativeTutorial/android/app/src/main/res/mipmap-xxhdpi/shaka_logo.png differ diff --git a/mobile/ReactNativeTutorial/android/app/src/main/res/values/strings.xml b/mobile/ReactNativeTutorial/android/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..c1663488 --- /dev/null +++ b/mobile/ReactNativeTutorial/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + RN Comments + diff --git a/mobile/ReactNativeTutorial/android/app/src/main/res/values/styles.xml b/mobile/ReactNativeTutorial/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..319eb0ca --- /dev/null +++ b/mobile/ReactNativeTutorial/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/mobile/ReactNativeTutorial/android/build.gradle b/mobile/ReactNativeTutorial/android/build.gradle new file mode 100644 index 00000000..fcba4c58 --- /dev/null +++ b/mobile/ReactNativeTutorial/android/build.gradle @@ -0,0 +1,24 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.3.1' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + mavenLocal() + jcenter() + maven { + // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm + url "$rootDir/../node_modules/react-native/android" + } + } +} diff --git a/mobile/ReactNativeTutorial/android/gradle.properties b/mobile/ReactNativeTutorial/android/gradle.properties new file mode 100644 index 00000000..fc2e70bd --- /dev/null +++ b/mobile/ReactNativeTutorial/android/gradle.properties @@ -0,0 +1,24 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +android.useDeprecatedNdk=true +MYAPP_RELEASE_STORE_FILE=rncomments.keystore +MYAPP_RELEASE_KEY_ALIAS=rncomments +MYAPP_RELEASE_STORE_PASSWORD=rncomments +MYAPP_RELEASE_KEY_PASSWORD=rncomments diff --git a/mobile/ReactNativeTutorial/android/gradle/wrapper/gradle-wrapper.jar b/mobile/ReactNativeTutorial/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..b5166dad Binary files /dev/null and b/mobile/ReactNativeTutorial/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/mobile/ReactNativeTutorial/android/gradle/wrapper/gradle-wrapper.properties b/mobile/ReactNativeTutorial/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..b9fbfaba --- /dev/null +++ b/mobile/ReactNativeTutorial/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip diff --git a/mobile/ReactNativeTutorial/android/gradlew b/mobile/ReactNativeTutorial/android/gradlew new file mode 100755 index 00000000..91a7e269 --- /dev/null +++ b/mobile/ReactNativeTutorial/android/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/mobile/ReactNativeTutorial/android/gradlew.bat b/mobile/ReactNativeTutorial/android/gradlew.bat new file mode 100644 index 00000000..8a0b282a --- /dev/null +++ b/mobile/ReactNativeTutorial/android/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mobile/ReactNativeTutorial/android/keystores/BUCK b/mobile/ReactNativeTutorial/android/keystores/BUCK new file mode 100644 index 00000000..15da20e6 --- /dev/null +++ b/mobile/ReactNativeTutorial/android/keystores/BUCK @@ -0,0 +1,8 @@ +keystore( + name = 'debug', + store = 'debug.keystore', + properties = 'debug.keystore.properties', + visibility = [ + 'PUBLIC', + ], +) diff --git a/mobile/ReactNativeTutorial/android/keystores/debug.keystore.properties b/mobile/ReactNativeTutorial/android/keystores/debug.keystore.properties new file mode 100644 index 00000000..121bfb49 --- /dev/null +++ b/mobile/ReactNativeTutorial/android/keystores/debug.keystore.properties @@ -0,0 +1,4 @@ +key.store=debug.keystore +key.alias=androiddebugkey +key.store.password=android +key.alias.password=android diff --git a/mobile/ReactNativeTutorial/android/settings.gradle b/mobile/ReactNativeTutorial/android/settings.gradle new file mode 100644 index 00000000..89ef4769 --- /dev/null +++ b/mobile/ReactNativeTutorial/android/settings.gradle @@ -0,0 +1,5 @@ +rootProject.name = 'ReactNativeTutorial' + +include ':app' +include ':react-native-vector-icons' +project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android') diff --git a/mobile/ReactNativeTutorial/app/App.js b/mobile/ReactNativeTutorial/app/App.js new file mode 100644 index 00000000..088632c2 --- /dev/null +++ b/mobile/ReactNativeTutorial/app/App.js @@ -0,0 +1,13 @@ +// @flow +import React from 'react'; +import { Provider } from 'react-redux'; +import store from './setup/store'; +import Router from './setup/Router/Router'; + +const App = () => ( + + + +); + +export default App; diff --git a/mobile/ReactNativeTutorial/app/api/index.js b/mobile/ReactNativeTutorial/app/api/index.js new file mode 100644 index 00000000..085e2478 --- /dev/null +++ b/mobile/ReactNativeTutorial/app/api/index.js @@ -0,0 +1,38 @@ +import _ from 'lodash/fp'; +import { normalize } from 'normalizr'; +import { commentSchema, commentsSchema } from './schemas'; + +// You can use localhost for development, but only on iOS. Android emulator is considered +// a standalone machine with it's own localhost. Workaround is to specify the actual +// IP address of your PC. +// const API_URL = __DEV__ ? 'http://localhost:3000/' : 'http://www.reactrails.com/'; +const API_URL = 'https://www.reactrails.com/'; + +const apiRequest = async (url, method, payload) => { + let options = { + method, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Auth': 'tutorial_secret', + }, + }; + if (payload) options = { ...options, body: JSON.stringify(payload) }; + const response = await fetch(`${API_URL}${url}`, options); + return await response.json(); +}; + +const getRequest = async (url) => apiRequest(url, 'GET'); +const postRequest = async (url, payload) => apiRequest(url, 'POST', payload); + +export const fetchComments = async () => { + const response = await getRequest('comments.json'); + const camelizedResponse = _.mapKeys(_.camelCase, response); + return normalize(camelizedResponse, { comments: commentsSchema }); +}; + +export const postComment = async (payload) => { + const response = await postRequest('comments.json', { comment: payload }); + const camelizedResponse = _.mapKeys(_.camelCase, response); + return normalize(camelizedResponse, commentSchema); +}; diff --git a/mobile/ReactNativeTutorial/app/api/schemas.js b/mobile/ReactNativeTutorial/app/api/schemas.js new file mode 100644 index 00000000..fede2a02 --- /dev/null +++ b/mobile/ReactNativeTutorial/app/api/schemas.js @@ -0,0 +1,4 @@ +import { Schema, arrayOf } from 'normalizr'; + +export const commentSchema = new Schema('comments'); +export const commentsSchema = arrayOf(commentSchema); diff --git a/mobile/ReactNativeTutorial/app/bundles/comments/components/Add/Add.js b/mobile/ReactNativeTutorial/app/bundles/comments/components/Add/Add.js new file mode 100644 index 00000000..1bff24e6 --- /dev/null +++ b/mobile/ReactNativeTutorial/app/bundles/comments/components/Add/Add.js @@ -0,0 +1,28 @@ +// @flow +import React from 'react'; +import { View } from 'react-native'; +import { FormLabel, FormInput, Button } from 'react-native-elements'; + +import type { AddPropsType } from '../../hocs/withAddProps'; + +import styles from './AddStyle'; + +type PropsType = AddPropsType; + +const Add = (props: PropsType) => ( + + Author name + props.actions.updateForm({ author: text })} /> + Comment + props.actions.updateForm({ text })} /> +