├── .eslintrc ├── .flowconfig ├── .gitignore ├── LICENSE ├── README.md ├── circle.yml ├── lerna.json ├── package.json ├── packages ├── material-ui │ ├── .babelrc │ ├── .npmignore │ ├── .storybook │ │ ├── addons.js │ │ ├── config.js │ │ └── webpack.config.js │ ├── package.json │ ├── src │ │ ├── Accounts.js │ │ ├── Container.js │ │ ├── Content.js │ │ ├── FormError.js │ │ ├── components │ │ │ ├── avatar.js │ │ │ ├── email.js │ │ │ ├── header.js │ │ │ ├── password.js │ │ │ └── submitButton.js │ │ ├── font.css │ │ ├── forgotPasswordComponents.js │ │ ├── index.js │ │ ├── index.story.js │ │ ├── loginComponents.js │ │ ├── resetPasswordComponents.js │ │ └── signupComponents.js │ ├── wallaby.js │ ├── webpack.config.js │ └── yarn.lock ├── react-native │ ├── .npmigonre │ ├── package.json │ ├── src │ │ ├── immutable-form │ │ │ ├── actions.js │ │ │ ├── reducer.js │ │ │ ├── selectors.js │ │ │ └── state.js │ │ ├── index.js │ │ ├── login │ │ │ ├── actions.js │ │ │ ├── components │ │ │ │ ├── password-field.container.js │ │ │ │ ├── password-field.js │ │ │ │ ├── user-field.container.js │ │ │ │ └── user-field.js │ │ │ ├── index.js │ │ │ ├── login-form.container.js │ │ │ ├── login-form.js │ │ │ ├── reducer.js │ │ │ ├── selectors.js │ │ │ └── styles.js │ │ ├── reducer.js │ │ └── redux │ │ │ └── util.js │ └── yarn.lock └── react │ ├── .babelrc │ ├── .npmignore │ ├── .storybook │ ├── config.js │ └── webpack.config.js │ ├── package.json │ ├── src │ ├── Accounts.js │ ├── ForgotPassword.js │ ├── FormTypes.js │ ├── Login.js │ ├── ResetPassword.js │ ├── Signup.js │ ├── accountRoutes.js │ ├── authenticate.js │ ├── index.js │ ├── index.spec.js │ └── withCurrentUser.js │ ├── stories │ └── index.js │ ├── wallaby.js │ ├── webpack.config.js │ └── yarn.lock └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "mocha": true, 5 | "node": true, 6 | "browser": true 7 | }, 8 | "extends": "airbnb", 9 | "parser": "babel-eslint", 10 | "plugins": [ 11 | "react" 12 | ], 13 | "rules": { 14 | "brace-style": 0, 15 | "linebreak-style": 0, 16 | "new-cap": 0, 17 | "newline-per-chained-call": 1, 18 | "no-mixed-operators": 0, 19 | "radix": 1, 20 | "no-use-before-define": 0, 21 | "import/no-extraneous-dependencies": 0, 22 | "import/prefer-default-export": 0, 23 | "jsx-a11y/no-static-element-interactions": 0, 24 | "react/forbid-prop-types": 0, 25 | "react/jsx-filename-extension": [ 26 | 1, 27 | { 28 | "extensions": [ 29 | ".js", 30 | ".jsx" 31 | ] 32 | } 33 | ], 34 | "react/jsx-no-bind": [ 35 | "error" 36 | ], 37 | "strict": 0 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [include] 2 | 3 | [ignore] 4 | 5 | [libs] 6 | 7 | [options] 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | coverage/ 4 | npm-debug.log 5 | .idea 6 | lerna-debug.log 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tim Mikeladze 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @accounts/react 2 | 3 | ## Note 4 | 5 | This package is under active development. 6 | 7 | ## Install 8 | 9 | ``` 10 | yarn add @accounts/react 11 | yarn add @accounts/client 12 | yarn add @accounts/rest-client 13 | yarn add @accounts/react-material-ui 14 | ``` 15 | 16 | ## Usage 17 | 18 | This is a simple example with react-router. 19 | 20 | ```javascript 21 | import AccountsClient from '@accounts/client'; 22 | import restClient from '@accounts/rest-client'; 23 | import { accountRoutes, withUser, Authenticated } from '@accounts/react'; 24 | 25 | // If you want the material-ui view 26 | import '@accounts/react-material-ui'; 27 | 28 | // Setup client config and try to resume session to know if user is logged 29 | (async () => { 30 | AccountsClient.config({ 31 | server: 'http://localhost:3010', 32 | history: browserHistory, 33 | title: 'my-app-title', 34 | loginPath: '/login', 35 | signUpPath: '/signup', 36 | homePath: '/home', 37 | reduxLogger: createLogger(), 38 | }, restClient); 39 | 40 | await AccountsClient.resumeSession(); 41 | })(); 42 | 43 | // The withUser hoc pass a user prop to the component 44 | const Home = withUser(({ user }) => 45 |
46 | Signed in user info 47 |
48 | {Object.keys(user).map(key =>
{key} : {user[key]}
)} 49 |
, 50 | ); 51 | 52 | // Use the Authenticated component in the router will check if a user is logged and redirect to /login if not 53 | render(( 54 | 55 | 56 | 57 | 58 | 59 | 60 | {accountRoutes()} 61 | 62 | 63 | ), document.getElementById('root')); 64 | ``` 65 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6.2.1 4 | 5 | test: 6 | post: 7 | - npm run coverage 8 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.0.0-beta.38", 3 | "packages": [ 4 | "packages/*" 5 | ], 6 | "version": "0.0.13" 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "lerna exec -- npm start", 4 | "link": "lerna exec -- npm link @accounts/client; lerna exec -- npm link @accounts/common; lerna exec -- npm link; lerna exec -- npm link @accounts/react", 5 | "compile": "lerna run compile", 6 | "flow:check": "flow check", 7 | "lint": "eslint packages/*/src", 8 | "prebootstrap": "npm install", 9 | "postinstall": "lerna bootstrap", 10 | "pretest": "npm run compile", 11 | "test": "npm run testonly", 12 | "posttest": "npm run lint", 13 | "testonly": "lerna run testonly", 14 | "coverage": "lerna run coverage", 15 | "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/js-accounts/react" 20 | }, 21 | "license": "MIT", 22 | "devDependencies": { 23 | "babel-eslint": "^7.1.1", 24 | "eslint": "^3.10.2", 25 | "eslint-config-airbnb": "^13.0.0", 26 | "eslint-config-airbnb-base": "^10.0.1", 27 | "eslint-plugin-import": "^2.2.0", 28 | "eslint-plugin-jsx-a11y": "^2.2.3", 29 | "eslint-plugin-react": "^6.7.1", 30 | "flow-bin": "^0.38.0", 31 | "lerna": "2.0.0-beta.38" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/material-ui/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react" 5 | ], 6 | "plugins": [ 7 | "syntax-async-functions", 8 | "transform-regenerator", 9 | "transform-object-rest-spread", 10 | "transform-async-to-generator", 11 | "transform-class-properties" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/material-ui/.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | coverage/ 3 | .storybook/ 4 | wallaby.js 5 | webpack.config.js 6 | .babelrc 7 | .eslintrc 8 | .flowconfig 9 | -------------------------------------------------------------------------------- /packages/material-ui/.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import 'storybook-addon-material-ui'; 2 | -------------------------------------------------------------------------------- /packages/material-ui/.storybook/config.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import { configure, addDecorator, setAddon } from '@kadira/storybook'; 3 | import { muiTheme } from 'storybook-addon-material-ui'; 4 | import infoAddon from '@kadira/react-storybook-addon-info'; 5 | 6 | addDecorator(muiTheme()); 7 | setAddon(infoAddon); 8 | 9 | const req = require.context('../src', true, /.story.js$/); 10 | 11 | const loadStories = () => req.keys().forEach(filename => req(filename)); 12 | 13 | configure(loadStories, module); 14 | -------------------------------------------------------------------------------- /packages/material-ui/.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | // you can use this file to add your custom webpack plugins, loaders and anything you like. 2 | // This is just the basic way to add addional webpack configurations. 3 | // For more information refer the docs: https://goo.gl/qPbSyX 4 | 5 | // IMPORTANT 6 | // When you add this file, we won't add the default configurations which is similar 7 | // to "React Create App". This only has babel loader to load JavaScript. 8 | const path = require('path'); 9 | 10 | module.exports = { 11 | plugins: [ 12 | // your custom plugins 13 | ], 14 | module: { 15 | loaders: [ 16 | { 17 | test: /\.css/, 18 | loader: 'style!css', 19 | }, 20 | { 21 | test: /\.(jpe?g|png|gif|svg)$/i, 22 | loader: 'url?limit=10000!img?progressive=true', 23 | }, 24 | ], 25 | }, 26 | resolveLoader: { 27 | modules: [ 28 | path.join(__dirname, 'node_modules'), 29 | ], 30 | }, 31 | resolve: { 32 | alias: { 33 | react: path.resolve('./node_modules/react'), 34 | }, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /packages/material-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@accounts/react-material-ui", 3 | "version": "0.0.13", 4 | "description": "React frontend build with material-ui for js-accounts", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "start": "babel --watch ./src --out-dir ./lib", 8 | "compile": "babel ./src --out-dir ./lib", 9 | "flow:check": "flow check", 10 | "prepublish": "npm run compile", 11 | "pretest": "npm run lint", 12 | "test": "npm run testonly", 13 | "testonly": "jest", 14 | "coverage": "npm run testonly -- --coverage", 15 | "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage", 16 | "storybook": "start-storybook -p 6006", 17 | "build-storybook": "build-storybook" 18 | }, 19 | "publishConfig": { 20 | "access": "public" 21 | }, 22 | "jest": { 23 | "testEnvironment": "node", 24 | "testRegex": "(/.*.(test|spec)).(js|jsx)$" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/js-accounts/react-material-ui.git" 29 | }, 30 | "keywords": [ 31 | "react", 32 | "auth", 33 | "authentication", 34 | "accounts", 35 | "users", 36 | "material" 37 | ], 38 | "author": "Tim Mikeladze", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/js-accounts/react-material-ui/issues" 42 | }, 43 | "homepage": "https://github.com/js-accounts/react-material-ui#readme", 44 | "devDependencies": { 45 | "@accounts/client": "^0.0.15", 46 | "@accounts/common": "^0.0.15", 47 | "@kadira/react-storybook-addon-info": "^3.3.0", 48 | "@kadira/storybook": "^2.30.1", 49 | "babel-cli": "^6.18.0", 50 | "babel-core": "^6.18.2", 51 | "babel-loader": "^6.2.8", 52 | "babel-plugin-syntax-async-functions": "^6.13.0", 53 | "babel-plugin-transform-async-to-generator": "^6.16.0", 54 | "babel-plugin-transform-class-properties": "^6.19.0", 55 | "babel-plugin-transform-object-rest-spread": "^6.19.0", 56 | "babel-plugin-transform-regenerator": "^6.16.1", 57 | "babel-polyfill": "^6.20.0", 58 | "babel-preset-es2015": "^6.18.0", 59 | "babel-preset-es2015-node4": "^2.1.0", 60 | "babel-preset-react": "^6.16.0", 61 | "chai": "^3.5.0", 62 | "chai-as-promised": "^6.0.0", 63 | "coveralls": "^2.11.15", 64 | "css-loader": "^0.26.0", 65 | "img-loader": "^1.3.1", 66 | "istanbul": "^1.1.0-alpha.1", 67 | "jest": "^18.1.0", 68 | "material-ui": "^0.17.3", 69 | "mocha": "^3.1.2", 70 | "react": "^15.4.0", 71 | "react-dom": "^15.4.0", 72 | "sinon": "^1.17.6", 73 | "sinon-chai": "^2.8.0", 74 | "storybook-addon-material-ui": "^0.7.3", 75 | "webpack": "^1.13.3", 76 | "webpack-node-externals": "^1.5.4" 77 | }, 78 | "peerDependencies": { 79 | "@accounts/client": "^0.0.15", 80 | "@accounts/common": "^0.0.15", 81 | "material-ui": "^0.16.4" 82 | }, 83 | "dependencies": { 84 | "@accounts/react": "^0.0.13", 85 | "flexbox-react": "^4.1.0", 86 | "prop-types": "^15.5.8", 87 | "react-dom": "^15.4.0", 88 | "react-redux": "^4.4.6", 89 | "recompose": "^0.23.1" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/material-ui/src/Accounts.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { Accounts as AccountsBase } from '@accounts/react'; 4 | import merge from 'lodash/merge'; 5 | import _loginComponents from './loginComponents'; 6 | import _signupComponents from './signupComponents'; 7 | import _forgotPasswordComponents from './forgotPasswordComponents'; 8 | import _resetPasswordComponents from './resetPasswordComponents'; 9 | 10 | const Accounts = ({ loginComponents, signupComponents, forgotPasswordComponents, 11 | resetPasswordComponents, ...otherProps }) => 12 | ; 19 | 20 | Accounts.propTypes = { 21 | loginComponents: PropTypes.object, 22 | signupComponents: PropTypes.object, 23 | forgotPasswordComponents: PropTypes.object, 24 | resetPasswordComponents: PropTypes.object, 25 | }; 26 | 27 | Accounts.defaultProps = { 28 | loginComponents: {}, 29 | signupComponents: {}, 30 | forgotPasswordComponents: {}, 31 | resetPasswordComponents: {}, 32 | }; 33 | 34 | export default Accounts; 35 | -------------------------------------------------------------------------------- /packages/material-ui/src/Container.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import Flexbox from 'flexbox-react'; 4 | 5 | const Container = ({ children }) => 6 | 7 | {children} 8 | ; 9 | 10 | Container.propTypes = { 11 | children: PropTypes.node, 12 | }; 13 | 14 | export default Container; 15 | -------------------------------------------------------------------------------- /packages/material-ui/src/Content.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import Flexbox from 'flexbox-react'; 4 | import Paper from 'material-ui/Paper'; 5 | 6 | const style = { 7 | minWidth: 350, 8 | padding: 40, 9 | backgroundColor: '#f7f7f7', 10 | marginBottom: 20, 11 | }; 12 | 13 | const Content = ({ children, noPaper }) => 14 | (noPaper ? 15 |
16 | 17 | {children} 18 | 19 |
20 | : 21 | 24 | 25 | {children} 26 | 27 | ); 28 | 29 | Content.propTypes = { 30 | children: PropTypes.node, 31 | noPaper: PropTypes.bool, 32 | }; 33 | 34 | Content.defaltProps = { 35 | noPaper: false, 36 | }; 37 | 38 | export default Content; 39 | -------------------------------------------------------------------------------- /packages/material-ui/src/FormError.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { red500 } from 'material-ui/styles/colors'; 4 | 5 | const FormError = ({ errorText }) => 6 | (errorText.length > 0 7 | ? 8 |
14 | {errorText} 15 |
16 | : null); 17 | 18 | FormError.propTypes = { 19 | errorText: PropTypes.node, 20 | }; 21 | 22 | export default FormError; 23 | -------------------------------------------------------------------------------- /packages/material-ui/src/components/avatar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MuiAvatar from 'material-ui/Avatar'; 3 | import SocialPerson from 'material-ui/svg-icons/social/person'; 4 | 5 | const Avatar = () => 6 | } style={{ 8 | marginBottom: 10, 9 | }} 10 | />; 11 | 12 | export default Avatar; 13 | -------------------------------------------------------------------------------- /packages/material-ui/src/components/email.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import TextField from 'material-ui/TextField'; 4 | 5 | const EmailField = ({ label, ...otherProps }) => 6 | ; 13 | 14 | EmailField.propTypes = { 15 | label: PropTypes.string.isRequired, 16 | }; 17 | 18 | export default EmailField; 19 | -------------------------------------------------------------------------------- /packages/material-ui/src/components/header.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import getContext from 'recompose/getContext'; 4 | 5 | const Header = getContext({ 6 | accounts: PropTypes.object, 7 | })(({ accounts }) => 8 |
16 | {accounts.options().title} 17 |
, 18 | ); 19 | 20 | export default Header; 21 | -------------------------------------------------------------------------------- /packages/material-ui/src/components/password.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import TextField from 'material-ui/TextField'; 4 | 5 | const PasswordField = ({ label, ...otherProps }) => 6 | ; 14 | 15 | PasswordField.propTypes = { 16 | label: PropTypes.string.isRequired, 17 | }; 18 | 19 | export default PasswordField; 20 | -------------------------------------------------------------------------------- /packages/material-ui/src/components/submitButton.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import RaisedButton from 'material-ui/RaisedButton'; 4 | 5 | const SubmitButton = ({ label, ...otherProps }) => 6 | ; 16 | 17 | SubmitButton.propTypes = { 18 | label: PropTypes.string.isRequired, 19 | }; 20 | 21 | export default SubmitButton; 22 | -------------------------------------------------------------------------------- /packages/material-ui/src/font.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500'); 2 | -------------------------------------------------------------------------------- /packages/material-ui/src/forgotPasswordComponents.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | /* eslint-disable no-shadow */ 3 | import React from 'react'; 4 | import Flexbox from 'flexbox-react'; 5 | import getContext from 'recompose/getContext'; 6 | import Container from './Container'; 7 | import Content from './Content'; 8 | import FormError from './FormError'; 9 | import Header from './components/header'; 10 | import Avatar from './components/avatar'; 11 | import EmailField from './components/email'; 12 | import SubmitButton from './components/submitButton'; 13 | 14 | const ForgotPasswordFields = ({ children }) => 15 | 16 | {children} 17 | ; 18 | 19 | ForgotPasswordFields.propTypes = { 20 | children: PropTypes.node, 21 | }; 22 | 23 | const LoginButton = getContext({ 24 | setFormType: PropTypes.func, 25 | })(({ setFormType, ...otherProps }) => 26 | 27 | setFormType('login')} 35 | {...otherProps} 36 | > 37 | Log in 38 | 39 | ); 40 | 41 | const Footer = ({ ...otherProps }) => 42 | ; 43 | 44 | Footer.propTypes = { 45 | SignupButton: PropTypes.node, 46 | }; 47 | 48 | export default { 49 | Container, 50 | Content, 51 | Avatar, 52 | ForgotPasswordFields, 53 | ForgotPasswordEmailField: EmailField, 54 | ForgotPasswordButton: SubmitButton, 55 | Header, 56 | Footer, 57 | FormError, 58 | }; 59 | -------------------------------------------------------------------------------- /packages/material-ui/src/index.js: -------------------------------------------------------------------------------- 1 | import Accounts from './Accounts'; 2 | import loginComponents from './loginComponents'; 3 | import signupComponents from './signupComponents'; 4 | import forgotPasswordComponents from './forgotPasswordComponents'; 5 | import Container from './Container'; 6 | import Content from './Content'; 7 | import FormError from './FormError'; 8 | 9 | export default Accounts; 10 | 11 | export { 12 | Container, 13 | Content, 14 | FormError, 15 | Accounts, 16 | loginComponents, 17 | signupComponents, 18 | forgotPasswordComponents, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/material-ui/src/index.story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@kadira/storybook'; 3 | import AccountsClient from '@accounts/client'; 4 | import { PasswordSignupFields } from '@accounts/common'; 5 | import { Accounts } from './index'; 6 | import './font.css'; 7 | 8 | AccountsClient.config({ 9 | passwordSignupFields: PasswordSignupFields.USERNAME_AND_EMAIL, 10 | title: 'Site Title', 11 | }, {}); 12 | 13 | const style = { 14 | marginTop: 75, 15 | }; 16 | 17 | storiesOf('Accounts', module) 18 | .add('login', () => ( 19 |
20 | 21 |
22 | )) 23 | .add('signup', () => ( 24 |
25 | 26 |
27 | )) 28 | .add('forgot-password', () => ( 29 |
30 | 31 |
32 | )) 33 | .add('reset-password', () => ( 34 |
35 | 36 |
37 | ), 38 | ); 39 | -------------------------------------------------------------------------------- /packages/material-ui/src/loginComponents.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | /* eslint-disable no-shadow */ 3 | import React from 'react'; 4 | import Flexbox from 'flexbox-react'; 5 | import getContext from 'recompose/getContext'; 6 | import Container from './Container'; 7 | import Content from './Content'; 8 | import FormError from './FormError'; 9 | import Header from './components/header'; 10 | import Avatar from './components/avatar'; 11 | import EmailField from './components/email'; 12 | import PasswordField from './components/password'; 13 | import SubmitButton from './components/submitButton'; 14 | 15 | const LoginFields = ({ children }) => 16 | 17 | {children} 18 | ; 19 | 20 | LoginFields.propTypes = { 21 | children: PropTypes.node, 22 | }; 23 | 24 | const RecoverButton = getContext({ 25 | setFormType: PropTypes.func, 26 | })(({ setFormType, ...otherProps }) => 27 | 28 | setFormType('forgot-password')} 36 | {...otherProps} 37 | > 38 | Forgot password 39 | 40 | ); 41 | 42 | const SignupButton = getContext({ 43 | setFormType: PropTypes.func, 44 | })(({ setFormType, ...otherProps }) => 45 | 46 | setFormType('signup')} 54 | {...otherProps} 55 | > 56 | Create account 57 | 58 | ); 59 | 60 | const Footer = ({ ...otherProps }) => 61 | ; 62 | 63 | Footer.propTypes = { 64 | SignupButton: PropTypes.node, 65 | }; 66 | 67 | export default { 68 | Container, 69 | Content, 70 | Avatar, 71 | LoginFields, 72 | LoginUserField: EmailField, 73 | LoginPasswordField: PasswordField, 74 | RecoverButton, 75 | LoginButton: SubmitButton, 76 | SignupButton, 77 | Header, 78 | Footer, 79 | FormError, 80 | }; 81 | -------------------------------------------------------------------------------- /packages/material-ui/src/resetPasswordComponents.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | /* eslint-disable no-shadow */ 3 | import React from 'react'; 4 | import Flexbox from 'flexbox-react'; 5 | import getContext from 'recompose/getContext'; 6 | import Container from './Container'; 7 | import Content from './Content'; 8 | import FormError from './FormError'; 9 | import Header from './components/header'; 10 | import Avatar from './components/avatar'; 11 | import PasswordField from './components/password'; 12 | import SubmitButton from './components/submitButton'; 13 | 14 | const ResetPasswordFields = ({ children }) => 15 | 16 | {children} 17 | ; 18 | 19 | ResetPasswordFields.propTypes = { 20 | children: PropTypes.node, 21 | }; 22 | 23 | const LoginButton = getContext({ 24 | setFormType: PropTypes.func, 25 | })(({ setFormType, ...otherProps }) => 26 | 27 | setFormType('login')} 35 | {...otherProps} 36 | > 37 | Log in 38 | 39 | ); 40 | 41 | const Footer = ({ ...otherProps }) => 42 | ; 43 | 44 | Footer.propTypes = { 45 | SignupButton: PropTypes.node, 46 | }; 47 | 48 | export default { 49 | Container, 50 | Content, 51 | Avatar, 52 | ResetPasswordFields, 53 | ResetPasswordPasswordField: PasswordField, 54 | ResetPasswordPasswordConfirmField: PasswordField, 55 | ResetPasswordButton: SubmitButton, 56 | Header, 57 | Footer, 58 | FormError, 59 | }; 60 | -------------------------------------------------------------------------------- /packages/material-ui/src/signupComponents.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | /* eslint-disable no-shadow */ 3 | import React from 'react'; 4 | import Flexbox from 'flexbox-react'; 5 | import getContext from 'recompose/getContext'; 6 | import Container from './Container'; 7 | import Content from './Content'; 8 | import FormError from './FormError'; 9 | import Header from './components/header'; 10 | import Avatar from './components/avatar'; 11 | import EmailField from './components/email'; 12 | import PasswordField from './components/password'; 13 | import SubmitButton from './components/submitButton'; 14 | 15 | const SignupFields = ({ children }) => 16 | 17 | {children} 18 | ; 19 | 20 | SignupFields.propTypes = { 21 | children: PropTypes.node, 22 | }; 23 | 24 | const LoginButton = getContext({ 25 | setFormType: PropTypes.func, 26 | })(({ setFormType, ...otherProps }) => 27 | 28 | setFormType('login')} 36 | {...otherProps} 37 | > 38 | Already have an account? 39 | 40 | ); 41 | 42 | const Footer = ({ 43 | ...otherProps 44 | }) => 45 | ; 46 | 47 | export default { 48 | Container, 49 | Content, 50 | Avatar, 51 | SignupFields, 52 | SignupEmailOptionalField: EmailField, 53 | SignupEmailField: EmailField, 54 | SignupUsernameField: EmailField, 55 | SignupPasswordField: PasswordField, 56 | SignupPasswordConfirmField: PasswordField, 57 | SignupButton: SubmitButton, 58 | LoginButton, 59 | Header, 60 | Footer, 61 | FormError, 62 | }; 63 | -------------------------------------------------------------------------------- /packages/material-ui/wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = function(wallaby) { 2 | return { 3 | files: [ 4 | 'src/**/*.js', 5 | '!src/**/*.spec.js' 6 | ], 7 | 8 | tests: [ 9 | 'src/**/*.spec.js' 10 | ], 11 | 12 | compilers: { 13 | 'src/**/*.js': wallaby.compilers.babel() 14 | }, 15 | 16 | env: { 17 | type: 'node' 18 | } 19 | }; 20 | }; -------------------------------------------------------------------------------- /packages/material-ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const nodeExternals = require('webpack-node-externals'); 3 | 4 | module.exports = { 5 | entry: './src/index.js', 6 | target: 'node', 7 | externals: [nodeExternals()], 8 | output: { 9 | path: path.join(__dirname, '/lib'), 10 | filename: 'index.js', 11 | library: '@accounts/react-material-ui', 12 | libraryTarget: 'umd', 13 | umdNamedDefine: true, 14 | }, 15 | modulesDirectories: [ 16 | 'src', 17 | 'node_modules', 18 | ], 19 | module: { 20 | loaders: [ 21 | { 22 | test: /\.js$/, 23 | loader: 'babel', 24 | exclude: /node_modules/, 25 | }, 26 | ], 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /packages/react-native/.npmigonre: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /packages/react-native/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@accounts/react-native", 3 | "version": "0.0.12", 4 | "description": "react-native components for js-accounts", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/js-accounts/react.git" 12 | }, 13 | "author": "David Yahalomi", 14 | "license": "MIT", 15 | "dependencies": { 16 | "immutable": "^3.8.1", 17 | "lodash.isempty": "^4.4.0", 18 | "lodash.memoize": "^4.1.2", 19 | "prop-types": "^15.5.8", 20 | "reselect": "^2.5.4" 21 | }, 22 | "devDependencies": { 23 | "@accounts/client": "^0.0.5", 24 | "@accounts/common": "^0.0.5", 25 | "flow-bin": "^0.38.0", 26 | "react": "^15.4.2", 27 | "react-native": "^0.41.1", 28 | "react-redux": "^5.0.2", 29 | "redux": "^3.6.0", 30 | "redux-thunk": "^2.2.0" 31 | }, 32 | "peerDependencies": { 33 | "@accounts/client": "^0.0.5", 34 | "@accounts/common": "^0.0.5", 35 | "react": "^15.4.2", 36 | "react-native": "^0.41.1", 37 | "react-redux": "^5.0.2", 38 | "redux": "^3.6.0", 39 | "redux-thunk": "^2.2.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/react-native/src/immutable-form/actions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import memoize from 'lodash.memoize'; 3 | import type { Dispatch } from 'redux'; 4 | 5 | const DOM = 'FORMS/'; 6 | export const FORM_SUBMITTED = `${DOM}FORM_SUBMITTED`; 7 | export const FORM_APPROVED = `${DOM}FORM_APPROVED`; 8 | export const FORM_ERROR = `${DOM}FORM_ERROR`; 9 | export const FORM_CLEAR = `${DOM}FORM_CLEAR`; 10 | export const FIELD_UPDATED = `${DOM}FIELD_UPDATED`; 11 | export const FIELD_CLEAR = `${DOM}FIELD_CLEAR`; 12 | export const FIELD_ERROR = `${DOM}FIELD_RESET`; 13 | 14 | export const submittedForm = (formName: string) => () => ({ 15 | type: FORM_SUBMITTED, 16 | formName, 17 | }); 18 | 19 | export const approvedForm = (formName: string) => (result: Object) => ({ 20 | type: FORM_APPROVED, 21 | formName, 22 | result, 23 | }); 24 | 25 | export const errorForm = (formName: string) => (error: string | Error) => ({ 26 | type: FORM_ERROR, 27 | formName, 28 | error, 29 | }); 30 | 31 | const errorMessage = err => ((typeof err !== 'string') ? err.message : err); 32 | 33 | export const submitForm = (formName: string) => (submit: (Object) => Promise) => () => 34 | (dispatch: Dispatch, getState: Function) => { 35 | const boundSubmittedForm = submittedForm(formName); 36 | dispatch(boundSubmittedForm()); 37 | 38 | const state = getState(); 39 | 40 | submit(state).then( 41 | (res) => { 42 | dispatch(approvedForm(formName)(res)); 43 | dispatch(clearForm(formName)()); 44 | }, 45 | err => dispatch(errorForm(formName)(errorMessage(err))), 46 | ); 47 | }; 48 | 49 | export const clearForm = (formName: string) => () => ({ 50 | type: FORM_CLEAR, 51 | formName, 52 | }); 53 | 54 | export const updateField = (formName: string) => (fieldName: string) => (event: Object) => ({ 55 | type: FIELD_UPDATED, 56 | formName, 57 | fieldName, 58 | value: event && event.nativeEvent && event.nativeEvent.text, 59 | }); 60 | 61 | export const clearField = (formName: string) => (fieldName: string) => () => ({ 62 | type: FIELD_CLEAR, 63 | formName, 64 | fieldName, 65 | }); 66 | 67 | export const errorField = (formName: string) => (fieldName: string) => (error: string) => ({ 68 | type: FIELD_ERROR, 69 | formName, 70 | fieldName, 71 | error, 72 | }); 73 | 74 | export const createFormActions = memoize((formName: string) => ({ 75 | submitForm: submitForm(formName), 76 | approvedForm: approvedForm(formName), 77 | errorForm: errorForm(formName), 78 | clearForm: clearForm(formName), 79 | updateField: updateField(formName), 80 | clearField: clearField(formName), 81 | errorField: errorField(formName), 82 | })); 83 | -------------------------------------------------------------------------------- /packages/react-native/src/immutable-form/reducer.js: -------------------------------------------------------------------------------- 1 | import { createReducer, filterActions } from '../redux/util'; 2 | 3 | import { 4 | FIELD_UPDATED, 5 | FIELD_ERROR, 6 | FIELD_CLEAR, 7 | FORM_SUBMITTED, 8 | FORM_APPROVED, 9 | FORM_ERROR, 10 | FORM_CLEAR, 11 | } from './actions'; 12 | 13 | const fieldUpdatedHandler = (state, action) => { 14 | const validate = state.getIn([action.fieldName, 'validate']); 15 | 16 | return state.setIn([action.fieldName, 'value'], action.value) 17 | .setIn([action.fieldName, 'error'], validate(action.value)); 18 | }; 19 | 20 | const formReducer = initialState => createReducer(initialState, { 21 | // Per field reducers 22 | [FIELD_UPDATED]: fieldUpdatedHandler, 23 | [FIELD_CLEAR]: (state, action) => state.remove(action.fieldName), 24 | [FIELD_ERROR]: (state, action) => state.setIn([action.fieldName, 'error'], action.error), 25 | 26 | // Per form reducer 27 | [FORM_SUBMITTED]: state => state.set('submitting', true), 28 | [FORM_APPROVED]: state => state.set('submitting', false), 29 | [FORM_ERROR]: (state, action) => state.set('error', action.error), 30 | [FORM_CLEAR]: () => initialState, 31 | }); 32 | 33 | const formNameFilter = formName => action => action.formName === formName; 34 | export const createFormReducer = (formName, initialState) => 35 | filterActions(formNameFilter(formName), formReducer(initialState), initialState); 36 | -------------------------------------------------------------------------------- /packages/react-native/src/immutable-form/selectors.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Field } from './state'; 3 | 4 | export const selectFieldProps = (field: Field) => ({ 5 | value: field.value, 6 | error: (field.value && field.error) ? field.error : null, 7 | }); 8 | -------------------------------------------------------------------------------- /packages/react-native/src/immutable-form/state.js: -------------------------------------------------------------------------------- 1 | import { Record } from 'immutable'; 2 | 3 | export const Field = Record({ 4 | value: null, 5 | error: null, 6 | validate: () => null, 7 | }); 8 | -------------------------------------------------------------------------------- /packages/react-native/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export * from './login'; 3 | export * from './reducer'; 4 | -------------------------------------------------------------------------------- /packages/react-native/src/login/actions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import AccountsClient from '@accounts/client'; 4 | import { createFormActions } from '../immutable-form/actions'; 5 | import { getStoreKey } from '../reducer'; 6 | import { selectUserField, selectPasswordField } from './selectors'; 7 | 8 | const actions = createFormActions('login'); 9 | 10 | const accountsLogin = (state: Object) => AccountsClient.loginWithPassword( 11 | selectUserField(state[getStoreKey()]).value, 12 | selectPasswordField(state[getStoreKey()]).value, 13 | ); 14 | 15 | export const submitLoginForm = actions.submitForm(accountsLogin); 16 | export const clearLoginForm = actions.clearForm; 17 | export const userFieldUpdate = actions.updateField('user'); 18 | export const passwordFieldUpdate = actions.updateField('password'); 19 | export const userFieldClear = actions.clearField('user'); 20 | export const passwordFieldClear = actions.clearField('password'); 21 | -------------------------------------------------------------------------------- /packages/react-native/src/login/components/password-field.container.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux'; 4 | import type { Dispatch } from 'redux'; 5 | import { PasswordField } from './password-field'; 6 | import { passwordFieldUpdate } from '../actions'; 7 | import { selectPropsPasswordField } from '../selectors'; 8 | import { getStoreKey } from '../../reducer'; 9 | 10 | function mapDispatch(dispatch: Dispatch) { 11 | return { 12 | onChange: event => dispatch(passwordFieldUpdate(event)), 13 | }; 14 | } 15 | 16 | const mapState = (state: Object) => selectPropsPasswordField(state[getStoreKey()]); 17 | 18 | const PasswordFieldContainer = 19 | connect(mapState, mapDispatch, null, { withRef: true })(PasswordField); 20 | 21 | export default PasswordFieldContainer; 22 | -------------------------------------------------------------------------------- /packages/react-native/src/login/components/password-field.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import { View, TextInput, Text } from 'react-native'; 4 | import { styles } from '../styles'; 5 | 6 | export class PasswordField extends Component { 7 | static propTypes = { 8 | ...TextInput.propTypes, 9 | shouldRenderErrorLabel: PropTypes.bool, 10 | renderErrorLabel: PropTypes.func, 11 | error: PropTypes.string, 12 | containerStyle: View.propTypes.style, 13 | }; 14 | 15 | static defaultProps = { 16 | secureTextEntry: true, 17 | multiline: false, 18 | autoFocus: false, 19 | blurOnSubmit: true, 20 | autoCorrect: false, 21 | placeholder: 'Password', 22 | autoCapitalize: 'none', 23 | shouldRenderErrorLabel: true, 24 | containerStyle: null, 25 | }; 26 | 27 | clear() { 28 | if (this.textInput) { 29 | this.textInput.clear(); 30 | } 31 | } 32 | 33 | isFocused() { 34 | return this.textInput && this.textInput.isFocused(); 35 | } 36 | 37 | focus() { 38 | if (this.textInput && this.textInput.focus) { 39 | this.textInput.focus(); 40 | } 41 | } 42 | 43 | renderError() { 44 | const { 45 | error, 46 | renderErrorLabel, 47 | shouldRenderErrorLabel, 48 | } = this.props; 49 | if (!shouldRenderErrorLabel || !error) return null; 50 | 51 | if (renderErrorLabel) return renderErrorLabel(error); 52 | 53 | return ( 54 | {error} 55 | ); 56 | } 57 | 58 | render() { 59 | const { style, containerStyle, ...restProps } = this.props; 60 | return ( 61 | 62 | this.textInput = ref} 67 | /> 68 | 69 | {this.renderError()} 70 | 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/react-native/src/login/components/user-field.container.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux'; 4 | import type { Dispatch } from 'redux'; 5 | import { UserField } from './user-field'; 6 | import { userFieldUpdate } from '../actions'; 7 | import { selectPropsUserField } from '../selectors'; 8 | import { getStoreKey } from '../../reducer'; 9 | 10 | function mapDispatch(dispatch: Dispatch) { 11 | return { 12 | onChange: event => dispatch(userFieldUpdate(event)), 13 | }; 14 | } 15 | 16 | const mapState = (state: Object) => selectPropsUserField(state[getStoreKey()]); 17 | 18 | const UserFieldContainer = connect(mapState, mapDispatch, null, { withRef: true })(UserField); 19 | 20 | export default UserFieldContainer; 21 | -------------------------------------------------------------------------------- /packages/react-native/src/login/components/user-field.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import { View, TextInput, Text } from 'react-native'; 4 | import { styles } from '../styles'; 5 | 6 | export class UserField extends Component { 7 | static propTypes = { 8 | ...TextInput.propTypes, 9 | shouldRenderErrorLabel: PropTypes.bool, 10 | renderErrorLabel: PropTypes.func, 11 | error: PropTypes.string, 12 | containerStyle: View.propTypes.style, 13 | }; 14 | 15 | static defaultProps = { 16 | multiline: false, 17 | autoFocus: true, 18 | blurOnSubmit: true, 19 | autoCorrect: false, 20 | placeholder: 'Username / Email', 21 | autoCapitalize: 'none', 22 | shouldRenderErrorLabel: true, 23 | containerStyle: null, 24 | }; 25 | 26 | clear() { 27 | if (this.textInput) { 28 | this.textInput.clear(); 29 | } 30 | } 31 | 32 | isFocused() { 33 | return this.textInput && this.textInput.isFocused(); 34 | } 35 | 36 | focus() { 37 | if (this.textInput && this.textInput.focus) { 38 | this.textInput.focus(); 39 | } 40 | } 41 | 42 | renderError() { 43 | const { 44 | error, 45 | renderErrorLabel, 46 | shouldRenderErrorLabel, 47 | } = this.props; 48 | if (!shouldRenderErrorLabel || !error) return null; 49 | 50 | if (renderErrorLabel) return renderErrorLabel(error); 51 | 52 | return ( 53 | {error} 54 | ); 55 | } 56 | 57 | render() { 58 | const { style, containerStyle, ...restProps } = this.props; 59 | return ( 60 | 61 | this.textInput = ref} 66 | /> 67 | 68 | {this.renderError()} 69 | 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/react-native/src/login/index.js: -------------------------------------------------------------------------------- 1 | export { default as ConnectedLoginForm } from './login-form.container'; 2 | export { default as ConnectedUserField } from './components/user-field.container'; 3 | export { default as ConnectedPasswordField } from './components/password-field.container'; 4 | export * from './login-form'; 5 | 6 | export { default as LoginFormReducer } from './reducer'; 7 | export * from './selectors'; 8 | export * from './actions'; 9 | export * from './components/user-field'; 10 | export * from './components/password-field'; 11 | -------------------------------------------------------------------------------- /packages/react-native/src/login/login-form.container.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { connect } from 'react-redux'; 4 | import type { Dispatch } from 'redux'; 5 | import { getStoreKey } from '../reducer'; 6 | import { LoginForm } from './login-form'; 7 | import { selectPropsLoginForm } from './selectors'; 8 | import { submitLoginForm } from './actions'; 9 | 10 | const mapDispatch = (dispatch: Dispatch) => ({ 11 | onSubmit: () => dispatch(submitLoginForm()), 12 | }); 13 | 14 | const mapState = (state: Object) => selectPropsLoginForm(state[getStoreKey()]); 15 | 16 | const LoginFormContainer = connect(mapState, mapDispatch)(LoginForm); 17 | 18 | export default LoginFormContainer; 19 | -------------------------------------------------------------------------------- /packages/react-native/src/login/login-form.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import { View, Text, TextInput, Button, ActivityIndicator, StyleSheet } from 'react-native'; 4 | 5 | import UserField from './components/user-field.container'; 6 | import PasswordField from './components/password-field.container'; 7 | 8 | export class LoginForm extends Component { 9 | static propTypes = { 10 | style: View.propTypes.style, 11 | fieldsStyle: TextInput.propTypes.style, 12 | onSubmit: PropTypes.func.isRequired, 13 | submitting: PropTypes.bool, 14 | disableSubmit: PropTypes.bool, 15 | error: PropTypes.string, 16 | renderUserField: PropTypes.func, 17 | renderPasswordField: PropTypes.func, 18 | renderSubmitButton: PropTypes.func, 19 | renderErrorLabel: PropTypes.func, 20 | }; 21 | 22 | static defaultProps = { 23 | submitting: false, 24 | error: null, 25 | renderSubmitButton: null, 26 | renderErrorLabel: null, 27 | renderUserField: null, 28 | renderPasswordField: null, 29 | disableSubmit: true, 30 | }; 31 | 32 | renderButton() { 33 | const { renderSubmitButton, onSubmit, submitting, disableSubmit: disabled } = this.props; 34 | 35 | if (renderSubmitButton) return renderSubmitButton({ onSubmit, disabled }); 36 | 37 | return ( 38 | 48 | ); 49 | } 50 | 51 | renderError() { 52 | const { error, renderErrorLabel } = this.props; 53 | if (!error) return null; 54 | 55 | if (renderErrorLabel) return renderErrorLabel(error); 56 | 57 | return ( 58 | {error} 59 | ); 60 | } 61 | 62 | renderUserField() { 63 | const { fieldsStyle, renderUserField } = this.props; 64 | 65 | if (renderUserField) return renderUserField(); 66 | 67 | return ( 68 | 69 | ); 70 | } 71 | 72 | renderPasswordField() { 73 | const { fieldsStyle, renderPasswordField } = this.props; 74 | 75 | if (renderPasswordField) return renderPasswordField(); 76 | 77 | return ( 78 | 79 | ); 80 | } 81 | 82 | render() { 83 | const { style } = this.props; 84 | return ( 85 | 86 | {this.renderUserField()} 87 | {this.renderPasswordField()} 88 | {this.renderButton()} 89 | {this.renderError()} 90 | 91 | ); 92 | } 93 | } 94 | 95 | const styles = StyleSheet.create({ 96 | container: { 97 | flex: 1, 98 | alignItems: 'center', 99 | }, 100 | }); 101 | -------------------------------------------------------------------------------- /packages/react-native/src/login/reducer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Record } from 'immutable'; 4 | import { validators } from '@accounts/common'; 5 | import { createFormReducer } from '../immutable-form/reducer'; 6 | import { Field } from '../immutable-form/state'; 7 | 8 | const validateUserField = value => 9 | (!validators.validateEmail(value) && !validators.validateUsername(value) ? 10 | 'Invalid username or email!' : null); 11 | 12 | const validatePassword = value => (!validators.validatePassword(value) ? 'Invalid password' : null); 13 | 14 | // State definition 15 | 16 | const LoginState = Record({ 17 | user: new Field({ validate: validateUserField }), 18 | password: new Field({ validate: validatePassword }), 19 | submitting: false, 20 | error: null, 21 | }); 22 | 23 | // Reducer 24 | 25 | export default createFormReducer('login', new LoginState()); 26 | -------------------------------------------------------------------------------- /packages/react-native/src/login/selectors.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { createSelector } from 'reselect'; 4 | import isEmpty from 'lodash.isempty'; 5 | import { selectFieldProps } from '../immutable-form/selectors'; 6 | 7 | export const selectLoginState = (state: Object) => state.login; 8 | 9 | export const selectUserField = createSelector( 10 | selectLoginState, 11 | loginState => loginState.user, 12 | ); 13 | 14 | export const selectPasswordField = createSelector( 15 | selectLoginState, 16 | loginState => loginState.password, 17 | ); 18 | 19 | export const selectPropsUserField = createSelector( 20 | selectUserField, 21 | selectFieldProps, 22 | ); 23 | 24 | export const selectPropsPasswordField = createSelector( 25 | selectPasswordField, 26 | selectFieldProps, 27 | ); 28 | 29 | export const selectPropsLoginForm = createSelector( 30 | selectLoginState, 31 | selectUserField, 32 | selectPasswordField, 33 | (loginState, user, password) => ({ 34 | submitting: loginState.submitting, 35 | disableSubmit: isEmpty(loginState.user.value) || isEmpty(loginState.password.value), 36 | error: loginState.error || user.error || password.error, 37 | }), 38 | ); 39 | -------------------------------------------------------------------------------- /packages/react-native/src/login/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | simpleFlexContainer: { 5 | flex: 1, 6 | alignItems: 'center', 7 | }, 8 | leftAlignedContainer: { 9 | alignItems: 'flex-start', 10 | }, 11 | simpleText: { 12 | flexGrow: 1, 13 | color: 'black', 14 | textAlign: 'left', 15 | }, 16 | simpleInput: { 17 | flexGrow: 1, 18 | textAlignVertical: 'center', 19 | borderBottomColor: 'black', 20 | borderBottomWidth: 1, 21 | paddingBottom: 5, 22 | paddingHorizontal: 5, 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /packages/react-native/src/reducer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { combineReducers } from 'redux'; 4 | import login from './login/reducer'; 5 | 6 | let k = 'accounts-ui'; 7 | export const getStoreKey = () => k; 8 | // eslint-disable-next-line no-return-assign 9 | export const setStoreKey = (storeKey: string) => k = storeKey; 10 | 11 | export const reducer = combineReducers({ 12 | login, 13 | }); 14 | -------------------------------------------------------------------------------- /packages/react-native/src/redux/util.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Reducer } from 'redux'; 3 | 4 | type Predicate = (action: Object) => boolean; 5 | 6 | export const getState = (initialState: Object) => (state: Object = initialState) => state; 7 | 8 | /** 9 | * 10 | * @param predicate 11 | * @param reducer 12 | * @param initialState 13 | * @returns {function(Object, Object): Object} 14 | */ 15 | export function filterActions( 16 | predicate: Predicate, 17 | reducer: Reducer, 18 | initialState: Object, 19 | ) { 20 | return (state: Object, action: Object) => 21 | (predicate(action) ? reducer(state, action) : getState(initialState)(state)); 22 | } 23 | 24 | /** 25 | * 26 | * @param initialState 27 | * @param handlers 28 | * @returns {reducer} 29 | */ 30 | export function createReducer(initialState: Object, handlers: Object) { 31 | return function reducer(state: Object = initialState, action: Object) { 32 | if (Object.prototype.hasOwnProperty.call(handlers, action.type)) { 33 | return handlers[action.type](state, action); 34 | } 35 | return state; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /packages/react/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react" 5 | ], 6 | "plugins": [ 7 | "syntax-async-functions", 8 | "transform-regenerator", 9 | "transform-object-rest-spread", 10 | "transform-async-to-generator", 11 | "transform-class-properties" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/react/.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | coverage/ 3 | wallaby.js 4 | webpack.config.js 5 | .babelrc 6 | .eslintrc 7 | .flowconfig 8 | -------------------------------------------------------------------------------- /packages/react/.storybook/config.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import { configure } from '@kadira/storybook'; 3 | 4 | function loadStories() { 5 | require('../stories'); 6 | } 7 | 8 | configure(loadStories, module); 9 | -------------------------------------------------------------------------------- /packages/react/.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | // you can use this file to add your custom webpack plugins, loaders and anything you like. 2 | // This is just the basic way to add addional webpack configurations. 3 | // For more information refer the docs: https://getstorybook.io/docs/configurations/custom-webpack-config 4 | 5 | // IMPORTANT 6 | // When you add this file, we won't add the default configurations which is similar 7 | // to "React Create App". This only has babel loader to load JavaScript. 8 | 9 | module.exports = { 10 | plugins: [ 11 | // your custom plugins 12 | ], 13 | module: { 14 | loaders: [ 15 | // add your custom loaders. 16 | ], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@accounts/react", 3 | "version": "0.0.13", 4 | "description": "React frontend for js-accounts", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "start": "babel --watch ./src --out-dir ./lib", 8 | "compile": "babel ./src --out-dir ./lib", 9 | "flow:check": "flow check", 10 | "prepublish": "npm run compile", 11 | "pretest": "npm run lint", 12 | "test": "npm run testonly", 13 | "testonly": "jest", 14 | "coverage": "npm run testonly -- --coverage", 15 | "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage", 16 | "storybook": "start-storybook -p 6006", 17 | "build-storybook": "build-storybook" 18 | }, 19 | "publishConfig": { 20 | "access": "public" 21 | }, 22 | "jest": { 23 | "testEnvironment": "node", 24 | "testRegex": "(/.*.(test|spec)).(js|jsx)$" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/js-accounts/accounts/tree/master/packages/react" 29 | }, 30 | "keywords": [ 31 | "react", 32 | "auth", 33 | "authentication", 34 | "accounts", 35 | "users", 36 | "oauth" 37 | ], 38 | "author": "Tim Mikeladze", 39 | "license": "ISC", 40 | "bugs": { 41 | "url": "https://github.com/js-accounts/react/issues" 42 | }, 43 | "homepage": "https://github.com/js-accounts/react#readme", 44 | "devDependencies": { 45 | "@accounts/client": "^0.0.15", 46 | "@accounts/common": "^0.0.15", 47 | "@kadira/storybook": "^2.21.0", 48 | "babel-cli": "^6.16.0", 49 | "babel-core": "^6.17.0", 50 | "babel-loader": "^6.2.5", 51 | "babel-plugin-react": "^1.0.0", 52 | "babel-plugin-syntax-async-functions": "^6.13.0", 53 | "babel-plugin-transform-async-to-generator": "^6.16.0", 54 | "babel-plugin-transform-class-properties": "^6.18.0", 55 | "babel-plugin-transform-object-rest-spread": "^6.16.0", 56 | "babel-plugin-transform-regenerator": "^6.16.1", 57 | "babel-preset-es2015": "^6.16.0", 58 | "babel-preset-es2015-node4": "^2.1.0", 59 | "babel-preset-react": "^6.16.0", 60 | "chai": "^3.5.0", 61 | "chai-as-promised": "^6.0.0", 62 | "coveralls": "^2.11.14", 63 | "istanbul": "^1.1.0-alpha.1", 64 | "jest": "^18.1.0", 65 | "mocha": "^3.1.0", 66 | "react": "^15.3.2", 67 | "react-dom": "^15.3.2", 68 | "sinon": "^1.17.6", 69 | "sinon-chai": "^2.8.0", 70 | "webpack": "^1.13.2", 71 | "webpack-node-externals": "^1.5.4" 72 | }, 73 | "peerDependencies": { 74 | "@accounts/client": "^0.0.15", 75 | "@accounts/common": "^0.0.15" 76 | }, 77 | "dependencies": { 78 | "prop-types": "^15.5.8", 79 | "react": "^15.3.2", 80 | "react-form": "^1.2.5", 81 | "react-redux": "^4.4.5", 82 | "react-router": "^3.0.0", 83 | "recompose": "^0.20.2", 84 | "redux": "^3.6.0" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/react/src/Accounts.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import FormTypes from './FormTypes'; 4 | import Login from './Login'; 5 | import Signup from './Signup'; 6 | import ForgotPassword from './ForgotPassword'; 7 | import ResetPassword from './ResetPassword'; 8 | 9 | class Accounts extends Component { 10 | static propTypes = { 11 | accounts: PropTypes.object, 12 | formType: PropTypes.string, 13 | loginComponents: PropTypes.object, 14 | signupComponents: PropTypes.object, 15 | forgotPasswordComponents: PropTypes.object, 16 | resetPasswordComponents: PropTypes.object, 17 | Login: PropTypes.func, 18 | Signup: PropTypes.func, 19 | ForgotPassword: PropTypes.func, 20 | ResetPassword: PropTypes.func, 21 | } 22 | static defaultProps = { 23 | formType: FormTypes.LOGIN, 24 | Login, 25 | Signup, 26 | ForgotPassword, 27 | ResetPassword, 28 | } 29 | static childContextTypes = { 30 | accounts: PropTypes.object, 31 | setFormType: PropTypes.func, 32 | }; 33 | constructor(props, context) { 34 | super(props, context); 35 | this.state = { 36 | formType: this.props.formType, 37 | }; 38 | } 39 | getChildContext() { 40 | return { 41 | accounts: this.props.accounts, 42 | setFormType: this.setFormType, 43 | }; 44 | } 45 | setFormType = (formType) => { 46 | this.setState({ 47 | formType, 48 | }); 49 | } 50 | render() { 51 | const { 52 | accounts, 53 | loginComponents, 54 | signupComponents, 55 | forgotPasswordComponents, 56 | resetPasswordComponents, 57 | Login, // eslint-disable-line no-shadow 58 | Signup, // eslint-disable-line no-shadow 59 | ForgotPassword, // eslint-disable-line no-shadow 60 | ResetPassword, // eslint-disable-line no-shadow 61 | ...otherProps 62 | } = this.props; 63 | switch (this.state.formType) { 64 | case FormTypes.LOGIN: 65 | return ( 66 | ); 72 | case FormTypes.SIGNUP: 73 | return ( 74 | ); 80 | case FormTypes.FORGOT_PASSWORD: 81 | return ( 82 | ); 88 | case FormTypes.RESET_PASSWORD: 89 | return ( 90 | ); 96 | default: 97 | break; 98 | } 99 | return null; 100 | } 101 | } 102 | 103 | export default Accounts; 104 | -------------------------------------------------------------------------------- /packages/react/src/ForgotPassword.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import { validators } from '@accounts/common'; 4 | import isString from 'lodash/isString'; 5 | import { Form, FormInput } from 'react-form'; 6 | 7 | class ForgotPassword extends Component { 8 | static propTypes = { 9 | accounts: PropTypes.object, 10 | Container: PropTypes.func, 11 | Content: PropTypes.func, 12 | Avatar: PropTypes.func, 13 | ForgotPasswordFields: PropTypes.func, 14 | ForgotPasswordEmailField: PropTypes.func, 15 | ForgotPasswordButton: PropTypes.func, 16 | Header: PropTypes.func, 17 | Footer: PropTypes.func, 18 | FormError: PropTypes.func, 19 | } 20 | 21 | constructor(props, context) { 22 | super(props, context); 23 | this.state = { 24 | formError: '', 25 | }; 26 | } 27 | 28 | onSubmit = async ({ 29 | email, 30 | }) => { 31 | const { accounts } = this.props; 32 | this.setState({ 33 | formError: '', 34 | }); 35 | try { 36 | await accounts.requestPasswordReset(email); 37 | } catch (err) { 38 | this.setState({ 39 | formError: err.message, 40 | }); 41 | } 42 | } 43 | 44 | validate = ({ 45 | email, 46 | }) => { 47 | const toReturn = { 48 | email: (() => { 49 | if (!email) { 50 | return 'Email is required'; 51 | } 52 | if (!validators.isEmail(email)) { 53 | return 'Email is not valid'; 54 | } 55 | return undefined; 56 | })(), 57 | }; 58 | return toReturn; 59 | } 60 | 61 | renderForgotPasswordFields = (form) => { 62 | const { 63 | ForgotPasswordFields, 64 | ForgotPasswordEmailField, 65 | } = this.props; 66 | return ( 67 | 68 | 69 | {() => 70 | form.setValue('email', e.target.value)} 75 | errorText={form.getTouched('email') && isString(form.getError('email')) ? form.getError('email') : ''} 76 | /> 77 | } 78 | 79 | 80 | ); 81 | } 82 | 83 | render() { 84 | const { 85 | Container, 86 | Content, 87 | Header, 88 | Footer, 89 | Avatar, 90 | ForgotPasswordButton, 91 | FormError, 92 | ...otherProps 93 | } = this.props; 94 | return ( 95 | 96 |
97 | 98 | 99 |
103 | {form => 104 |
105 | {this.renderForgotPasswordFields(form)} 106 | 110 |
111 | } 112 |
113 | 114 |
115 |