diff --git a/README.md b/README.md index 662b5dcb..7ce57198 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,10 @@ We no longer recommend using an express server with Rails. It's simply not neces 2. Open a browser tab to http://localhost:4000 for the Hot Module Replacement Example just using an express server (no Rails involved). This is good for fast prototyping of React components. However, this setup is not as useful now that we have hot reloading working for Rails! 3. Try Hot Reloading steps below! +## React Native + +See React Native mobile app that works for both iOS and Android under the `mobile` folder. + ## Contributors [The Shaka Code team!](http://www.shakacode.com/about/), led by [Justin Gordon](https://github.com/justin808/), along with with many others. See [contributors.md](docs/contributors.md) 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..a49e511f --- /dev/null +++ b/mobile/ReactNativeTutorial/Readme.md @@ -0,0 +1,77 @@ +## 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 +``` + +### Detailed docs + +Can be found in `docs` folder. See [Introduction](docs/Introduction.md) to start. 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..b4a5ed4e --- /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.1" + + 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 })} /> +