Inital commit.

This commit is contained in:
Tim Brandin 2016-03-28 22:34:50 +02:00
commit 504b70904c
17 changed files with 1097 additions and 0 deletions

9
.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
*.swp
*~
*.iml
.*.haste_cache.*
.DS_Store
.idea
npm-debug.log
node_modules
dist

3
CHANGELOG.md Normal file
View file

@ -0,0 +1,3 @@
# ChangeLog
### v1.0.0

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Studio Interact Inc. <info@studiointeract.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

101
README.md Normal file
View file

@ -0,0 +1,101 @@
# React Accounts UI for Meteor 1.3
Current version 1.0.0
## Features
1. **Easy to use**, mixing the ideas of useraccounts configuration and accounts-ui that everyone already knows.
2. **Server Side Rendering** are supported, trough FlowRouter (SSR).
3. **Components** are everywhere, and extensible by replacing them on Accounts.ui.
4. **Basic routing** included.
5. **Unstyled** is the default, no CSS included.
### Styled version
* Check back here later for styled version i.e. bootstrap, semantic-ui.
We're hoping package developers write extensions for these by importing like this:
```javascript
import { Accounts } from 'meteor/studiointeract:react-accounts-ui';
/**
* Form.propTypes = {
* fields: React.PropTypes.object.isRequired,
* buttons: React.PropTypes.object.isRequired,
* error: React.PropTypes.string,
* ready: React.PropTypes.bool
* };
*/
class Form extends Accounts.ui.Form {
render() {
const { fields, buttons, error, message, ready = true} = this.props;
return (
<form className={ready ? "ready" : null} onSubmit={ evt => evt.preventDefault() } className="accounts-ui">
<Accounts.ui.Fields fields={ fields } />
<Accounts.ui.Buttons buttons={ buttons } />
<Accounts.ui.FormMessage message={ message } />
</form>
);
}
}
// Overwrite provided form.
Accounts.ui.Form = Form;
exports default Accounts;
```
### Available components
* Accounts.ui.LoginForm
* Accounts.ui.Form
* Accounts.ui.Fields
* Accounts.ui.Field
* Accounts.ui.Buttons
* Accounts.ui.Button
* Accounts.ui.FormMessage
## Installation
### Meteor 1.3
`meteor add studiointeract:react-accounts-ui`
> Configuration instructions coming shortly! In the meantime check the example below, and for full details about configuration options check in `./lib/accounts_ui.js`
### Meteor 1.2
> Not supported.
## Example setup using FlowRouter (Meteor 1.3)
```javascript
import { FlowRouter } from 'meteor/kadira:flow-router-ssr';
import { Accounts } from 'meteor/studiointeract:react-accounts';
import React from 'react';
Accounts.ui.config({
passwordSignupFields: 'USERNAME_AND_OPTIONAL_EMAIL',
onSubmitHook(error, state) {
console.log('onSubmitHook')
},
preSignUpHook(password, info) {
console.log('preSignUpHook')
},
postSignUpHook(userId, info) {
console.log('postSignUpHook')
},
onSignedInHook: () => redirect('/general'),
onSignedOutHook: () => redirect('/')
});
FlowRouter.route("/login", {
action(params) {
mount(MainLayout, {
content: <Accounts.ui.LoginForm />
});
}
});
```

178
imports/accounts_ui.js Normal file
View file

@ -0,0 +1,178 @@
import {Accounts} from 'meteor/accounts-base';
import { redirect } from './helpers.js';
/**
* @summary Accounts UI
* @namespace
* @memberOf Accounts
*/
Accounts.ui = {};
Accounts.ui._options = {
requestPermissions: {},
requestOfflineToken: {},
forceApprovalPrompt: {},
loginPath: '/login',
onSubmitHook: [],
preSignUpHook: [],
postSignUpHook: [],
loginHook: () => redirect(`${Accounts.ui._options.loginPath}`),
signUpHook: () => redirect(`${Accounts.ui._options.loginPath}`),
resetPasswordHook: () => redirect(`${Accounts.ui._options.loginPath}`),
changePasswordHook: () => redirect(`${Accounts.ui._options.loginPath}`),
onEnrollAccountHook: () => redirect(`${Accounts.ui._options.loginPath}`),
onResetPasswordHook: () => redirect(`${Accounts.ui._options.loginPath}`),
onVerifyEmailHook: () => redirect(`${Accounts.ui._options.loginPath}`),
onSignedInHook: () => redirect('/'),
onSignedOutHook: () => redirect('/')
};
/**
* @summary Configure the behavior of [`{{> loginButtons}}`](#accountsui).
* @locus Client
* @param {Object} options
* @param {Object} options.requestPermissions Which [permissions](#requestpermissions) to request from the user for each external service.
* @param {Object} options.requestOfflineToken To ask the user for permission to act on their behalf when offline, map the relevant external service to `true`. Currently only supported with Google. See [Meteor.loginWithExternalService](#meteor_loginwithexternalservice) for more details.
* @param {Object} options.forceApprovalPrompt If true, forces the user to approve the app's permissions, even if previously approved. Currently only supported with Google.
* @param {String} options.passwordSignupFields Which fields to display in the user creation form. One of '`USERNAME_AND_EMAIL`', '`USERNAME_AND_OPTIONAL_EMAIL`', '`USERNAME_ONLY`', or '`EMAIL_ONLY`' (default).
*/
Accounts.ui.config = function(options) {
// validate options keys
const VALID_KEYS = [
'passwordSignupFields',
'requestPermissions',
'requestOfflineToken',
'forceApprovalPrompt',
'loginPath',
'onSubmitHook',
'preSignUpHook',
'postSignUpHook',
'loginHook',
'signUpHook',
'resetPasswordHook',
'changePasswordHook',
'onEnrollAccountHook',
'onResetPasswordHook',
'onVerifyEmailHook',
'onSignedInHook',
'onSignedOutHook'
];
_.each(_.keys(options), function (key) {
if (!_.contains(VALID_KEYS, key))
throw new Error("Accounts.ui.config: Invalid key: " + key);
});
// deal with `passwordSignupFields`
if (options.passwordSignupFields) {
if (_.contains([
"USERNAME_AND_EMAIL",
"USERNAME_AND_OPTIONAL_EMAIL",
"USERNAME_ONLY",
"EMAIL_ONLY"
], options.passwordSignupFields)) {
if (Accounts.ui._options.passwordSignupFields)
throw new Error("Accounts.ui.config: Can't set `passwordSignupFields` more than once");
else
Accounts.ui._options.passwordSignupFields = options.passwordSignupFields;
}
else {
throw new Error("Accounts.ui.config: Invalid option for `passwordSignupFields`: " + options.passwordSignupFields);
}
}
// deal with `requestPermissions`
if (options.requestPermissions) {
_.each(options.requestPermissions, function (scope, service) {
if (Accounts.ui._options.requestPermissions[service]) {
throw new Error("Accounts.ui.config: Can't set `requestPermissions` more than once for " + service);
}
else if (!(scope instanceof Array)) {
throw new Error("Accounts.ui.config: Value for `requestPermissions` must be an array");
}
else {
Accounts.ui._options.requestPermissions[service] = scope;
}
});
}
// deal with `requestOfflineToken`
if (options.requestOfflineToken) {
_.each(options.requestOfflineToken, function (value, service) {
if (service !== 'google')
throw new Error("Accounts.ui.config: `requestOfflineToken` only supported for Google login at the moment.");
if (Accounts.ui._options.requestOfflineToken[service]) {
throw new Error("Accounts.ui.config: Can't set `requestOfflineToken` more than once for " + service);
}
else {
Accounts.ui._options.requestOfflineToken[service] = value;
}
});
}
// deal with `forceApprovalPrompt`
if (options.forceApprovalPrompt) {
_.each(options.forceApprovalPrompt, function (value, service) {
if (service !== 'google')
throw new Error("Accounts.ui.config: `forceApprovalPrompt` only supported for Google login at the moment.");
if (Accounts.ui._options.forceApprovalPrompt[service]) {
throw new Error("Accounts.ui.config: Can't set `forceApprovalPrompt` more than once for " + service);
}
else {
Accounts.ui._options.forceApprovalPrompt[service] = value;
}
});
}
// deal with the hooks.
for (let hook of ['onSubmitHook', 'preSignUpHook', 'postSignUpHook']) {
if (options[hook]) {
if (typeof options[hook] != 'function') {
throw new Error(`Accounts.ui.config: "${hook}" not a function`);
}
else {
Accounts.ui._options[hook].push(options[hook]);
}
}
}
// deal with `forceApprovalPrompt`.
if (options.loginPath) {
if (typeof options.loginPath != 'string') {
Accounts.ui._options.loginPath = options.loginPath;
}
else {
throw new Error(`Accounts.ui.config: "loginPath" not an absolute or relative path`);
}
}
// deal with redirect hooks.
for (let hook of [
'loginHook',
'signUpHook',
'resetPasswordHook',
'changePasswordHook',
'onEnrollAccountHook',
'onResetPasswordHook',
'onVerifyEmailHook',
'onSignedInHook',
'onSignedOutHook']) {
if (options[hook]) {
if (typeof options[hook] == 'function') {
Accounts.ui._options[hook] = options[hook];
}
else if (typeof options[hook] == 'string') {
Accounts.ui._options[hook] = () => {
location.href = options[hook];
};
}
else {
throw new Error(`Accounts.ui.config: "${hook}" not a function or an absolute or relative path`);
}
}
}
};
export default Accounts;

66
imports/helpers.js Normal file
View file

@ -0,0 +1,66 @@
export function t9n(str) {
return str;
};
export function getLoginServices() {
// First look for OAuth services.
const services = Package['accounts-oauth'] ? Accounts.oauth.serviceNames() : [];
// Be equally kind to all login services. This also preserves
// backwards-compatibility.
services.sort();
return _.map(services, function(name){
return {name: name};
});
};
// Export getLoginServices using old style globals for accounts-base which
// requires it.
this.getLoginServices = getLoginServices;
export function loginResultCallback(redirect) {
if (Meteor.isClient){
if (typeof redirect === 'string'){
window.location.href = redirect;
}
if (typeof redirect === 'function'){
redirect();
}
}
};
export function passwordSignupFields() {
return Accounts.ui._options.passwordSignupFields || "EMAIL_ONLY";
};
export function validatePassword(password){
if (password.length >= 6) {
return true;
} else {
return false;
}
};
// Helper to dynamically inject react components.
Global = this;
export function Inject(component, ...args) {
return Global[component] ? React.createElement.apply(React.createElement, [Global[component], ...args]) : '';
}
export function redirect(path) {
if (Meteor.isClient) {
if (window.history) {
window.history.replaceState( {} , 'redirect', path );
}
else {
window.location.href = href;
}
}
}
// capitalize = function(str){
// str = str == null ? '' : String(str);
//
// return str.charAt(0).toUpperCase() + str.slice(1);
// };

96
imports/login_session.js Normal file
View file

@ -0,0 +1,96 @@
import {Accounts} from 'meteor/accounts-base';
import {
loginResultCallback,
getLoginServices
} from './helpers.js';
const VALID_KEYS = [
'dropdownVisible',
// XXX consider replacing these with one key that has an enum for values.
'inSignupFlow',
'inForgotPasswordFlow',
'inChangePasswordFlow',
'inMessageOnlyFlow',
'errorMessage',
'infoMessage',
// dialogs with messages (info and error)
'resetPasswordToken',
'enrollAccountToken',
'justVerifiedEmail',
'justResetPassword',
'configureLoginServiceDialogVisible',
'configureLoginServiceDialogServiceName',
'configureLoginServiceDialogSaveDisabled',
'configureOnDesktopVisible'
];
const validateKey = function (key) {
if (!_.contains(VALID_KEYS, key))
throw new Error("Invalid key in loginButtonsSession: " + key);
};
const KEY_PREFIX = "Meteor.loginButtons.";
// XXX This should probably be package scope rather than exported
// (there was even a comment to that effect here from before we had
// namespacing) but accounts-ui-viewer uses it, so leave it as is for
// now
Accounts._loginButtonsSession = {
set: function(key, value) {
validateKey(key);
if (_.contains(['errorMessage', 'infoMessage'], key))
throw new Error("Don't set errorMessage or infoMessage directly. Instead, use errorMessage() or infoMessage().");
this._set(key, value);
},
_set: function(key, value) {
Session.set(KEY_PREFIX + key, value);
},
get: function(key) {
validateKey(key);
return Session.get(KEY_PREFIX + key);
}
};
if (Meteor.isClient){
// In the login redirect flow, we'll have the result of the login
// attempt at page load time when we're redirected back to the
// application. Register a callback to update the UI (i.e. to close
// the dialog on a successful login or display the error on a failed
// login).
//
Accounts.onPageLoadLogin(function (attemptInfo) {
// Ignore if we have a left over login attempt for a service that is no longer registered.
if (_.contains(_.pluck(getLoginServices(), "name"), attemptInfo.type))
loginResultCallback(attemptInfo.type, attemptInfo.error);
});
let doneCallback;
Accounts.onResetPasswordLink(function (token, done) {
Accounts._loginButtonsSession.set("resetPasswordToken", token);
doneCallback = done;
});
Accounts.onEnrollmentLink(function (token, done) {
Accounts._loginButtonsSession.set("enrollAccountToken", token);
doneCallback = done;
});
Accounts.onEmailVerificationLink(function (token, done) {
Accounts.verifyEmail(token, function (error) {
if (! error) {
Accounts._loginButtonsSession.set('justVerifiedEmail', true);
}
done();
// XXX show something if there was an error.
});
});
}

View file

@ -0,0 +1,19 @@
import React from 'react';
import { Accounts } from 'meteor/accounts-base';
export class Button extends React.Component {
render () {
const { label, type, disabled = false, onClick } = this.props;
return type == 'link' ? (
<a onClick={ onClick }>{ label }</a>
) : (
<button type={type} disabled={ disabled }
onClick={ onClick }>{ label }</button>
);
}
}
Button.propTypes = {
onClick: React.PropTypes.func
};
Accounts.ui.Button = Button;

View file

@ -0,0 +1,13 @@
import React from 'react';
import './Button.jsx';
import { Accounts } from 'meteor/accounts-base';
export const Buttons = ({ buttons = {} }) => (
<div className="buttons">
{Object.keys(buttons).map((id, i) =>
<Accounts.ui.Button {...buttons[id]} key={i} />
)}
</div>
);
Accounts.ui.Buttons = Buttons;

View file

@ -0,0 +1,29 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Accounts } from 'meteor/accounts-base';
export class Field extends React.Component {
componentDidMount() {
// Trigger an onChange on inital load, to support browser prefilled values.
const { onChange } = this.props;
let node = ReactDOM.findDOMNode(this);
let value = node.getElementsByTagName('input')[0].value;
// Match the data format of a typical onChange event.
onChange({ target: { value: value } });
}
render() {
const { id, hint, label, type = 'text', onChange } = this.props;
return (
<div className="field">
<label htmlFor={ id }>{ label }</label>
<input id={ id } type={ type } onChange={ onChange } placeholder={ hint } />
</div>
);
}
}
Field.propTypes = {
onChange: React.PropTypes.func
};
Accounts.ui.Field = Field;

View file

@ -0,0 +1,13 @@
import React from 'react';
import { Accounts } from 'meteor/accounts-base';
import './Field.jsx';
export const Fields = ({ fields = {} }) => (
<div className="fields">
{Object.keys(fields).map((id, i) =>
<Accounts.ui.Field {...fields[id]} key={i} />
)}
</div>
);
Accounts.ui.Fields = Fields;

View file

@ -0,0 +1,26 @@
import React from 'react';
import {Accounts} from 'meteor/accounts-base';
import './Fields.jsx';
import './Buttons.jsx';
import './FormMessage.jsx';
export class Form extends React.Component {
render() {
const { fields, buttons, error, message, ready = true} = this.props;
return (
<form className={ready ? "ready" : null} onSubmit={ evt => evt.preventDefault() } className="accounts-ui">
<Accounts.ui.Fields fields={ fields } />
<Accounts.ui.Buttons buttons={ buttons } />
<Accounts.ui.FormMessage message={ message } />
</form>
);
}
}
Form.propTypes = {
fields: React.PropTypes.object.isRequired,
buttons: React.PropTypes.object.isRequired,
error: React.PropTypes.string,
ready: React.PropTypes.bool
};
Accounts.ui.Form = Form;

View file

@ -0,0 +1,8 @@
import React from 'react';
import { Accounts } from 'meteor/accounts-base';
export const FormMessage = ({ message }) => message ? (
<div className="message">{ message }</div>
) : null;
Accounts.ui.FormMessage = FormMessage;

View file

@ -0,0 +1,458 @@
import React from 'react';
import Tracker from 'tracker-component';
import { Accounts } from 'meteor/accounts-base';
import './Form.jsx';
import {
passwordSignupFields,
validatePassword,
loginResultCallback,
getLoginServices,
t9n
} from '../../helpers.js';
const STATES = {
SIGN_IN: Symbol('SIGN_IN'),
SIGN_UP: Symbol('SIGN_UP'),
PASSWORD_CHANGE: Symbol('PASSWORD_CHANGE'),
PASSWORD_RESET: Symbol('PASSWORD_RESET')
};
// Expose available states.
Accounts.ui.STATES = STATES;
export class LoginForm extends Tracker.Component {
constructor(props) {
super(props);
// Set inital state.
this.state = {
message: '',
waiting: false,
formState: Meteor.user() ? STATES.PASSWORD_CHANGE : STATES.SIGN_IN
};
// Listen reactively.
this.autorun(() => {
this.setState({
user: Meteor.user()
});
});
}
validateUsername( username ){
if ( username.length >= 3 ) {
return true;
}
else {
this.showMessage( t9n("error.usernameTooShort") );
return false;
}
}
validateEmail( email ){
if (passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL" && email === '')
return true;
if (email.indexOf('@') !== -1) {
return true;
}
else {
this.showMessage(t9n("error.emailInvalid"));
return false;
}
}
getUsernameOrEmailField(){
return {
id: 'usernameOrEmail',
hint: t9n('Enter username or email'),
label: t9n('usernameOrEmail'),
onChange: this.handleChange.bind(this, 'usernameOrEmail')
};
}
getUsernameField(){
return {
id: 'username',
hint: t9n('Enter username'),
label: t9n('username'),
onChange: this.handleChange.bind(this, 'username')
};
}
getEmailField(){
return {
id: 'email',
hint: t9n('Enter email'),
label: t9n('email'),
onChange: this.handleChange.bind(this, 'email')
};
}
getPasswordField(){
return {
id: 'password',
hint: t9n('Enter password'),
label: t9n('password'),
type: 'password',
onChange: this.handleChange.bind(this, 'password')
};
}
getNewPasswordField(){
return {
id: 'newPassword',
hint: t9n('Enter newPassword'),
label: t9n('newPassword'),
type: 'password',
onChange: this.handleChange.bind(this, 'password')
};
}
handleChange(field, evt) {
let value = evt.target.value;
switch (field) {
case 'password': break;
default:
value = value.trim();
break;
}
this.setState({ [field]: value });
}
fields() {
const loginFields = [];
const { formState } = this.state;
if (formState == STATES.SIGN_IN) {
if (_.contains(["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL"],
passwordSignupFields())) {
loginFields.push(this.getUsernameOrEmailField());
}
if (passwordSignupFields() === "USERNAME_ONLY") {
loginFields.push(this.getUsernameField());
}
if (passwordSignupFields() === "EMAIL_ONLY") {
loginFields.push(this.getEmailField());
}
loginFields.push(this.getPasswordField());
}
if (formState == STATES.SIGN_UP) {
if (_.contains(["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"],
passwordSignupFields())) {
loginFields.push(this.getUsernameField());
}
if (_.contains(["USERNAME_AND_EMAIL", "EMAIL_ONLY"], passwordSignupFields())) {
loginFields.push(this.getEmailField());
}
loginFields.push(this.getPasswordField());
}
if (formState == STATES.PASSWORD_RESET) {
loginFields.push(this.getEmailField());
}
if (this.showPasswordChangeForm()) {
loginFields.push(this.getPasswordField());
loginFields.push(this.getNewPasswordField());
}
return _.indexBy(loginFields, 'id');
}
buttons() {
const { formState, waiting, user } = this.state;
let loginButtons = [];
if (user) {
loginButtons.push({
id: 'signOut',
label: t9n('signOut'),
onClick: this.signOut.bind(this)
});
}
if (this.showCreateAccountLink()) {
loginButtons.push({
id: 'switchToSignUp',
label: t9n('signUp'),
type: 'link',
onClick: this.switchToSignUp.bind(this)
});
}
if (formState == STATES.SIGN_UP || formState == STATES.PASSWORD_RESET) {
loginButtons.push({
id: 'switchToSignIn',
label: t9n('signIn'),
type: 'link',
onClick: this.switchToSignIn.bind(this)
});
}
if (this.showForgotPasswordLink()) {
loginButtons.push({
id: 'switchToPasswordReset',
label: t9n('resetYourPassword'),
type: 'link',
onClick: this.switchToPasswordReset.bind(this)
});
}
if (formState == STATES.SIGN_UP) {
loginButtons.push({
id: 'signUp',
label: t9n('signUp'),
type: 'submit',
disabled: waiting,
onClick: this.signUp.bind(this)
});
}
if (formState == STATES.SIGN_IN) {
loginButtons.push({
id: 'signIn',
label: t9n('signIn'),
type: 'submit',
disabled: waiting,
onClick: this.signIn.bind(this)
});
}
if (formState == STATES.PASSWORD_RESET) {
loginButtons.push({
id: 'emailResetLink',
label: t9n('emailResetLink'),
type: 'submit',
disabled: waiting,
onClick: this.passwordReset.bind(this)
});
}
if (this.showPasswordChangeForm()) {
loginButtons.push({
id: 'changePassword',
label: t9n('changePassword'),
disabled: waiting,
onClick: this.passwordChange.bind(this)
});
}
// Sort the button array so that the submit button always comes first.
loginButtons.sort((a, b) => {
return (b.type == 'submit') - (a.type == 'submit');
});
return _.indexBy(loginButtons, 'id');
}
showPasswordChangeForm() {
return(getLoginServices().indexOf('password') != -1
&& this.state.formState == STATES.PASSWORD_CHANGE);
}
showCreateAccountLink() {
return this.state.formState == STATES.SIGN_IN && !Accounts._options.forbidClientAccountCreation;
}
showForgotPasswordLink() {
return !this.state.user
&& this.state.formState != STATES.PASSWORD_RESET
&& _.contains(
["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "EMAIL_ONLY"],
passwordSignupFields());
}
switchToSignUp() {
this.setState({ formState: STATES.SIGN_UP, message: '' });
}
switchToSignIn() {
this.setState({ formState: STATES.SIGN_IN, message: '' });
}
switchToPasswordReset() {
this.setState({ formState: STATES.PASSWORD_RESET, message: '' });
}
signOut() {
Meteor.logout();
}
signIn() {
const {
username = null,
email = null,
usernameOrEmail = null,
password
} = this.state;
let loginSelector;
if (username !== null) {
if (!this.validateUsername(username)) {
return;
}
else {
loginSelector = { username: username };
}
}
else if (email !== null) {
if (!this.validateEmail(email)) {
return;
}
else {
loginSelector = { email };
}
}
else if (usernameOrEmail !== null) {
// XXX not sure how we should validate this. but this seems good enough (for now),
// since an email must have at least 3 characters anyways
if (!this.validateUsername(usernameOrEmail)) {
return;
}
else {
loginSelector = usernameOrEmail;
}
}
else {
throw new Error("Unexpected -- no element to use as a login user selector");
}
Meteor.loginWithPassword(loginSelector, password, (error, result) => {
if (error) {
this.showMessage(t9n(`error.accounts.${error.reason}`) || t9n("Unknown error"));
}
else {
this.setState({ formState: STATES.PASSWORD_CHANGE });
loginResultCallback(this.props.redirect);
}
});
}
signUp() {
const options = {}; // to be passed to Accounts.createUser
const {
username = null,
email = null,
usernameOrEmail = null,
password
} = this.state;
if (username !== null) {
if ( !this.validateUsername(username) ) {
return;
}
else {
options.username = username;
}
}
if (email !== null) {
if ( !this.validateEmail(email) ){
return;
}
else {
options.email = email;
}
}
if (!validatePassword(password)) {
this.showMessage(t9n("error.pwTooShort"));
return;
}
else {
options.password = password;
}
this.setState({waiting: true});
Accounts.createUser(options, (error)=>{
if (error) {
this.showMessage(t9n(`error.accounts.${error.reason}`) || t9n("Unknown error"));
}
else {
this.setState({
formState: STATES.PASSWORD_CHANGE,
message: ''
});
loginResultCallback(this.props.redirect);
}
this.setState({ waiting: false });
});
}
passwordReset(){
const {
email = '',
waiting
} = this.state;
if (waiting) {
return;
}
if (email.indexOf('@') !== -1) {
this.setState({ waiting: true });
Accounts.forgotPassword({ email: email }, (error) => {
if (error) {
this.showMessage(t9n(`error.accounts.${error.reason}`) || t9n("Unknown error"));
}
else {
this.showMessage(t9n("info.emailSent"));
}
this.setState({ waiting: false });
});
}
else {
this.showMessage(t9n("error.emailInvalid"));
}
}
passwordChange() {
const {
password,
newPassword
} = this.state;
if ( !validatePassword(newPassword) ){
this.showMessage(t9n("error.pwTooShort"));
return;
}
Accounts.changePassword(password, newPassword, (error) => {
if (error) {
this.showMessage(t9n(`error.accounts.${error.reason}`) || t9n("Unknown error"));
}
else {
this.showMessage(t9n('info.passwordChanged'));
}
});
}
showMessage(message){
message = message.trim();
if (message){
this.setState({ message: message });
}
}
render() {
return <Accounts.ui.Form fields={this.fields()} buttons={this.buttons()} {...this.state} />;
}
}
Accounts.ui.LoginForm = LoginForm;

9
main.js Normal file
View file

@ -0,0 +1,9 @@
import {Accounts} from 'meteor/accounts-base';
import './imports/accounts_ui.js';
import './imports/login_session.js';
import { redirect } from './imports/helpers.js';
import './imports/ui/components/LoginForm.jsx';
export { Accounts, redirect };
export default Accounts;

23
package.js Normal file
View file

@ -0,0 +1,23 @@
Package.describe({
name: 'studiointeract:react-accounts-ui',
version: '1.0.1',
summary: 'Accounts UI for React Component in Meteor',
git: 'https://github.com/studiointeract/react-accounts-ui',
documentation: 'README.md'
});
Package.onUse(function(api) {
api.versionsFrom('1.3-rc.1');
api.use('ecmascript');
api.use('tracker');
api.use('underscore');
api.use('accounts-base');
api.use('check');
api.imply('accounts-base');
api.use('accounts-oauth', {weak: true});
api.use('accounts-password', {weak: true});
api.mainModule('main.js');
});

25
package.json Normal file
View file

@ -0,0 +1,25 @@
{
"name": "react-accounts",
"version": "1.0.1",
"description": "Accounts UI for React Component in Meteor",
"repository": {
"type": "git",
"url": "https://github.com/studiointeract/tracker-component.git"
},
"keywords": [
"react",
"meteor",
"tracker"
],
"author": "timbrandin",
"license": "MIT",
"bugs": {
"url": "https://github.com/studiointeract/tracker-component/issues"
},
"homepage": "https://github.com/studiointeract/tracker-component",
"dependencies": {
"react": "^15.0.0-rc.2",
"react-dom": "^15.0.0-rc.2",
"tracker-component": "^1.3.3-rc.10"
}
}