Merge branch 'devel'

This commit is contained in:
Tim Brandin 2016-04-01 07:50:04 +02:00
commit aaf11254cf
15 changed files with 344 additions and 77 deletions

View file

@ -50,7 +50,7 @@ session@1.1.3
softwarerero:accounts-t9n@0.0.20 softwarerero:accounts-t9n@0.0.20
spacebars@1.0.9 spacebars@1.0.9
spacebars-compiler@1.0.9 spacebars-compiler@1.0.9
studiointeract:react-accounts-ui@1.0.20 studiointeract:react-accounts-ui@1.0.21-rc.1
tracker@1.0.11 tracker@1.0.11
ui@1.0.9 ui@1.0.9
underscore@1.0.6 underscore@1.0.6

View file

@ -1,5 +1,11 @@
# ChangeLog # ChangeLog
### v1.0.21
* Buttons for oauth services
* Option for "NO_PASSWORD" changed to "EMAIL_ONLY_NO_PASSWORD"
* Added new options to accounts-password "USERNAME_AND_EMAIL_NO_PASSWORD".
### v1.0.20 ### v1.0.20
* Clear the password when logging in or out. * Clear the password when logging in or out.

View file

@ -1,6 +1,6 @@
# React Accounts UI # React Accounts UI
Current version 1.0.20 Current version 1.0.21
## Features ## Features
@ -48,7 +48,7 @@ Configure the behavior of `<Accounts.ui.LoginForm />`
If true, forces the user to approve the app's permissions, even if previously approved. Currently only supported with Google. If true, forces the user to approve the app's permissions, even if previously approved. Currently only supported with Google.
* **passwordSignupFields**&nbsp;&nbsp;&nbsp; String * **passwordSignupFields**&nbsp;&nbsp;&nbsp; String
Which fields to display in the user creation form. One of `'USERNAME_AND_EMAIL'`, `'USERNAME_AND_OPTIONAL_EMAIL'`, `'USERNAME_ONLY'`, or `'EMAIL_ONLY'`, **`'NO_PASSWORD'`** (**default**). Which fields to display in the user creation form. One of `'USERNAME_AND_EMAIL'`, `'USERNAME_AND_OPTIONAL_EMAIL'`, `'USERNAME_ONLY'`, `'EMAIL_ONLY'`, `'USERNAME_AND_EMAIL_NO_PASSWORD'`, **`'EMAIL_ONLY_NO_PASSWORD'`** (**default**).
* **loginPath**&nbsp;&nbsp;&nbsp; String * **loginPath**&nbsp;&nbsp;&nbsp; String
Change the default path a user should be redirected to after a clicking a link in a mail provided to them from the accounts system, it could be a mail set to them when they have reset their password, verifying their email if the setting for `sendVerificationEmail` is turned on ([read more on accounts configuration ](http://docs.meteor.com/#/full/accounts_config)). Change the default path a user should be redirected to after a clicking a link in a mail provided to them from the accounts system, it could be a mail set to them when they have reset their password, verifying their email if the setting for `sendVerificationEmail` is turned on ([read more on accounts configuration ](http://docs.meteor.com/#/full/accounts_config)).
@ -260,6 +260,8 @@ export { Accounts as default };
* Accounts.ui.Buttons * Accounts.ui.Buttons
* Accounts.ui.Button * Accounts.ui.Button
* Accounts.ui.FormMessage * Accounts.ui.FormMessage
* Accounts.ui.PasswordOrService
* Accounts.ui.SocialButtons
## Extra fields ## Extra fields
@ -297,6 +299,17 @@ class NewLogin extends Accounts.ui.LoginForm {
} }
``` ```
And on the server you can store the extra fields like this:
```javascript
import { Accounts } from 'meteor/accounts-base';
Accounts.onCreateUser(function (options, user) {
user.profile = options.profile || {};
user.roles = {};
return user;
});
```
## Credits ## Credits

View file

@ -9,18 +9,19 @@ import { redirect } from './helpers.js';
Accounts.ui = {}; Accounts.ui = {};
Accounts.ui._options = { Accounts.ui._options = {
requestPermissions: {}, requestPermissions: [],
requestOfflineToken: {}, requestOfflineToken: {},
forceApprovalPrompt: {}, forceApprovalPrompt: {},
passwordSignupFields: 'NO_PASSWORD', passwordSignupFields: 'NO_PASSWORD',
loginPath: '/login', loginPath: '/',
signUpPath: null,
resetPasswordPath: null,
profilePath: null,
changePasswordPath: null,
homeRoutePath: '/', homeRoutePath: '/',
onSubmitHook: () => {}, onSubmitHook: () => {},
preSignUpHook: () => new Promise(resolve => resolve()), onPreSignUpHook: () => new Promise(resolve => resolve()),
postSignUpHook: () => {}, onPostSignUpHook: () => {},
signUpHook: () => redirect(`${Accounts.ui._options.loginPath}`),
resetPasswordHook: () => redirect(`${Accounts.ui._options.loginPath}`),
changePasswordHook: () => redirect(`${Accounts.ui._options.loginPath}`),
onEnrollAccountHook: () => redirect(`${Accounts.ui._options.loginPath}`), onEnrollAccountHook: () => redirect(`${Accounts.ui._options.loginPath}`),
onResetPasswordHook: () => redirect(`${Accounts.ui._options.loginPath}`), onResetPasswordHook: () => redirect(`${Accounts.ui._options.loginPath}`),
onVerifyEmailHook: () => redirect(`${Accounts.ui._options.homeRoutePath}`), onVerifyEmailHook: () => redirect(`${Accounts.ui._options.homeRoutePath}`),
@ -45,10 +46,14 @@ Accounts.ui.config = function(options) {
'requestOfflineToken', 'requestOfflineToken',
'forbidClientAccountCreation', 'forbidClientAccountCreation',
'loginPath', 'loginPath',
'signUpPath',
'resetPasswordPath',
'profilePath',
'changePasswordPath',
'homeRoutePath',
'onSubmitHook', 'onSubmitHook',
'preSignUpHook', 'onPreSignUpHook',
'postSignUpHook', 'onPostSignUpHook',
'resetPasswordHook',
'onEnrollAccountHook', 'onEnrollAccountHook',
'onResetPasswordHook', 'onResetPasswordHook',
'onVerifyEmailHook', 'onVerifyEmailHook',
@ -68,7 +73,8 @@ Accounts.ui.config = function(options) {
"USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL",
"USERNAME_ONLY", "USERNAME_ONLY",
"EMAIL_ONLY", "EMAIL_ONLY",
"NO_PASSWORD" "EMAIL_ONLY_NO_PASSWORD",
"USERNAME_AND_EMAIL_NO_PASSWORD"
], options.passwordSignupFields)) { ], options.passwordSignupFields)) {
Accounts.ui._options.passwordSignupFields = options.passwordSignupFields; Accounts.ui._options.passwordSignupFields = options.passwordSignupFields;
} }

View file

@ -7,14 +7,30 @@ import { Accounts } from 'meteor/accounts-base';
// Method called by a user to request a password reset email. This is // Method called by a user to request a password reset email. This is
// the start of the reset process. // the start of the reset process.
Meteor.methods({loginWithoutPassword: function (options) { Meteor.methods({loginWithoutPassword: function ({ email, username = null }) {
check(options, {email: String}); if (username !== null) {
check(username, String);
var user = Meteor.users.findOne({"emails.address": options.email}); var user = Meteor.users.findOne({ $or: [{
if (!user) "username": username, "emails.address": { $exists: 1 }
throw new Meteor.Error(403, "User not found"); }, {
"emails.address": email
}]
});
if (!user)
throw new Meteor.Error(403, "User not found");
Accounts.sendLoginEmail(user._id, options.email); email = user.emails[0].address;
}
else {
check(email, String);
var user = Meteor.users.findOne({ "emails.address": email });
if (!user)
throw new Meteor.Error(403, "User not found");
}
Accounts.sendLoginEmail(user._id, email);
}}); }});
/** /**
@ -84,17 +100,20 @@ Accounts.sendLoginEmail = function (userId, address) {
Email.send(options); Email.send(options);
}; };
Accounts.emailTemplates.loginNoPassword = { // Check for installed accounts-password dependency.
subject: function(user) { if (Accounts.emailTemplates) {
return "Login on " + Accounts.emailTemplates.siteName; Accounts.emailTemplates.loginNoPassword = {
}, subject: function(user) {
text: function(user, url) { return "Login on " + Accounts.emailTemplates.siteName;
var greeting = (user.profile && user.profile.name) ? },
("Hello " + user.profile.name + ",") : "Hello,"; text: function(user, url) {
return `${greeting} var greeting = (user.profile && user.profile.name) ?
("Hello " + user.profile.name + ",") : "Hello,";
return `${greeting}
To login, simply click the link below. To login, simply click the link below.
${url} ${url}
Thanks. Thanks.
`; `;
} }
}; };
}

View file

@ -22,6 +22,11 @@ export function getLoginServices() {
// requires it. // requires it.
this.getLoginServices = getLoginServices; this.getLoginServices = getLoginServices;
export function hasPasswordService() {
// First look for OAuth services.
return !!Package['accounts-password'];
};
export function loginResultCallback(redirect, error) { export function loginResultCallback(redirect, error) {
if (Meteor.isClient) { if (Meteor.isClient) {
if (typeof redirect === 'string'){ if (typeof redirect === 'string'){
@ -35,7 +40,7 @@ export function loginResultCallback(redirect, error) {
}; };
export function passwordSignupFields() { export function passwordSignupFields() {
return Accounts.ui._options.passwordSignupFields || "EMAIL_ONLY"; return Accounts.ui._options.passwordSignupFields || "EMAIL_ONLY_NO_PASSWORD";
}; };
export function validatePassword(password){ export function validatePassword(password){
@ -63,3 +68,9 @@ export function redirect(redirect) {
} }
} }
} }
export function capitalize(string) {
return string.replace(/\-/, ' ').split(' ').map(word => {
return word.charAt(0).toUpperCase() + word.slice(1);
}).join(' ');
}

View file

@ -4,5 +4,13 @@ T9n.map('en', {
'Enter newPassword': 'Enter new password', 'Enter newPassword': 'Enter new password',
'Enter email': 'Enter email', 'Enter email': 'Enter email',
'Enter username': 'Enter username', 'Enter username': 'Enter username',
'Enter username or email': 'Enter username or email' 'Enter username or email': 'Enter username or email',
error: {
accounts: {
"Invalid email or username": "Invalid email or username",
"Internal server error": "Internal server error",
"undefined": "Service not configured"
}
},
'or use': 'Or use'
}); });

View file

@ -4,5 +4,11 @@ T9n.map('sv', {
'Enter newPassword': 'Mata in nytt lösenord', 'Enter newPassword': 'Mata in nytt lösenord',
'Enter email': 'Mata in e-post', 'Enter email': 'Mata in e-post',
'Enter username': 'Mata in användarnamn', 'Enter username': 'Mata in användarnamn',
'Enter username or email': 'Mata in användarnamn eller e-post' 'Enter username or email': 'Mata in användarnamn eller e-post',
error: {
accounts: {
"Invalid email or username": "Ogiltig e-postadress eller användarnamn"
}
},
'or use': 'Eller använd'
}); });

View file

@ -2,13 +2,23 @@ import React from 'react';
import { Accounts } from 'meteor/accounts-base'; import { Accounts } from 'meteor/accounts-base';
export class Button extends React.Component { export class Button extends React.Component {
handleClick(evt) {
let { onClick, href } = this.props;
if (!href) {
evt.preventDefault();
onClick(evt);
}
}
render () { render () {
const { label, type, disabled = false, onClick, className } = this.props; const { label, href = null, type, disabled = false, className } = this.props;
return type == 'link' ? ( return type == 'link' ? (
<a className={ className } onClick={ onClick }>{ label }</a> <a href={ href } className={ className } onClick={ this.handleClick.bind(this) }>{ label }</a>
) : ( ) : (
<button className={ className } type={type} disabled={ disabled } <button className={ className }
onClick={ onClick }>{ label }</button> type={type} 
disabled={ disabled }
onClick={ this.handleClick.bind(this) }>{ label }</button>
); );
} }
} }

View file

@ -3,21 +3,35 @@ import {Accounts} from 'meteor/accounts-base';
import './Fields.jsx'; import './Fields.jsx';
import './Buttons.jsx'; import './Buttons.jsx';
import './FormMessage.jsx'; import './FormMessage.jsx';
import './PasswordOrService.jsx';
import './SocialButtons.jsx';
export class Form extends React.Component { export class Form extends React.Component {
render() { render() {
const { fields, buttons, error, message, ready = true, className } = this.props; const {
hasPasswordService,
oauthServices,
fields,
buttons,
error,
message,
ready = true,
className
} = this.props;
return ( return (
<form className={[className, ready ? "ready" : null].join(' ')} <form className={[className, ready ? "ready" : null].join(' ')}
onSubmit={ evt => evt.preventDefault() } className="accounts-ui"> onSubmit={ evt => evt.preventDefault() } className="accounts-ui">
<Accounts.ui.Fields fields={ fields } /> <Accounts.ui.Fields fields={ fields } />
<Accounts.ui.Buttons buttons={ buttons } /> <Accounts.ui.Buttons buttons={ buttons } />
<Accounts.ui.FormMessage message={ message } /> <Accounts.ui.PasswordOrService oauthServices={ oauthServices } />
<Accounts.ui.SocialButtons oauthServices={ oauthServices } />
<Accounts.ui.FormMessage {...message} />
</form> </form>
); );
} }
} }
Form.propTypes = { Form.propTypes = {
oauthServices: React.PropTypes.object,
fields: React.PropTypes.object.isRequired, fields: React.PropTypes.object.isRequired,
buttons: React.PropTypes.object.isRequired, buttons: React.PropTypes.object.isRequired,
error: React.PropTypes.string, error: React.PropTypes.string,

View file

@ -10,17 +10,27 @@ import {
passwordSignupFields, passwordSignupFields,
validatePassword, validatePassword,
loginResultCallback, loginResultCallback,
getLoginServices getLoginServices,
hasPasswordService,
capitalize
} from '../../helpers.js'; } from '../../helpers.js';
export class LoginForm extends Tracker.Component { export class LoginForm extends Tracker.Component {
constructor(props) { constructor(props) {
super(props); super(props);
let {
formState = STATES.SIGN_IN,
loginPath,
signUpPath,
resetPasswordPath,
profilePath,
changePasswordPath
} = this.props;
// Set inital state. // Set inital state.
this.state = { this.state = {
message: null, message: null,
waiting: true, waiting: true,
formState: Meteor.user() ? STATES.SIGN_OUT : STATES.SIGN_IN formState: Meteor.user() ? STATES.SIGN_OUT : formState
}; };
// Listen reactively. // Listen reactively.
@ -52,21 +62,21 @@ export class LoginForm extends Tracker.Component {
} }
} }
validateUsername( username ){ validateUsername( username ) {
if ( username.length >= 3 ) { if ( username ) {
return true; return true;
} }
else { else {
this.showMessage(T9n.get("error.usernameTooShort"), 'warning'); this.showMessage(T9n.get("error.usernameRequired"), 'warning');
if (formState == STATES.SIGN_UP) { if (this.state.formState == STATES.SIGN_UP) {
Accounts.ui._options.onSubmitHook("error.usernameTooShort", formState); Accounts.ui._options.onSubmitHook("error.accounts.usernameRequired", this.state.formState);
} }
return false; return false;
} }
} }
validateEmail( email ){ validateEmail( email ) {
if (passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL" && email === '') if (passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL" && email === '')
return true; return true;
@ -83,7 +93,7 @@ export class LoginForm extends Tracker.Component {
} }
} }
getUsernameOrEmailField(){ getUsernameOrEmailField() {
return { return {
id: 'usernameOrEmail', id: 'usernameOrEmail',
hint: T9n.get('Enter username or email'), hint: T9n.get('Enter username or email'),
@ -94,7 +104,7 @@ export class LoginForm extends Tracker.Component {
}; };
} }
getUsernameField(){ getUsernameField() {
return { return {
id: 'username', id: 'username',
hint: T9n.get('Enter username'), hint: T9n.get('Enter username'),
@ -105,7 +115,7 @@ export class LoginForm extends Tracker.Component {
}; };
} }
getEmailField(){ getEmailField() {
return { return {
id: 'email', id: 'email',
hint: T9n.get('Enter email'), hint: T9n.get('Enter email'),
@ -117,7 +127,7 @@ export class LoginForm extends Tracker.Component {
}; };
} }
getPasswordField(){ getPasswordField() {
return { return {
id: 'password', id: 'password',
hint: T9n.get('Enter password'), hint: T9n.get('Enter password'),
@ -129,7 +139,7 @@ export class LoginForm extends Tracker.Component {
}; };
} }
getNewPasswordField(){ getNewPasswordField() {
return { return {
id: 'newPassword', id: 'newPassword',
hint: T9n.get('Enter newPassword'), hint: T9n.get('Enter newPassword'),
@ -155,9 +165,12 @@ export class LoginForm extends Tracker.Component {
const loginFields = []; const loginFields = [];
const { formState } = this.state; const { formState } = this.state;
if (formState == STATES.SIGN_IN) { if (hasPasswordService() && formState == STATES.SIGN_IN) {
if (_.contains(["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL"], if (_.contains([
passwordSignupFields())) { "USERNAME_AND_EMAIL",
"USERNAME_AND_OPTIONAL_EMAIL",
"USERNAME_AND_EMAIL_NO_PASSWORD"
], passwordSignupFields())) {
loginFields.push(this.getUsernameOrEmailField()); loginFields.push(this.getUsernameOrEmailField());
} }
@ -165,30 +178,48 @@ export class LoginForm extends Tracker.Component {
loginFields.push(this.getUsernameField()); loginFields.push(this.getUsernameField());
} }
if (_.contains(["EMAIL_ONLY", "NO_PASSWORD"], passwordSignupFields())) { if (_.contains([
"EMAIL_ONLY",
"EMAIL_ONLY_NO_PASSWORD"
], passwordSignupFields())) {
loginFields.push(this.getEmailField()); loginFields.push(this.getEmailField());
} }
if (passwordSignupFields() !== "NO_PASSWORD") { if (!_.contains([
"EMAIL_ONLY_NO_PASSWORD",
"USERNAME_AND_EMAIL_NO_PASSWORD"
], passwordSignupFields())) {
loginFields.push(this.getPasswordField()); loginFields.push(this.getPasswordField());
} }
} }
if (formState == STATES.SIGN_UP) { if (hasPasswordService() && formState == STATES.SIGN_UP) {
if (_.contains(["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"], if (_.contains([
passwordSignupFields())) { "USERNAME_AND_EMAIL",
"USERNAME_AND_OPTIONAL_EMAIL",
"USERNAME_ONLY",
"USERNAME_AND_EMAIL_NO_PASSWORD"
], passwordSignupFields())) {
loginFields.push(this.getUsernameField()); loginFields.push(this.getUsernameField());
} }
if (_.contains(["USERNAME_AND_EMAIL", "EMAIL_ONLY", "NO_PASSWORD"], passwordSignupFields())) { if (_.contains([
loginFields.push(Object.assign(this.getEmailField())); "USERNAME_AND_EMAIL",
"EMAIL_ONLY",
"EMAIL_ONLY_NO_PASSWORD",
"USERNAME_AND_EMAIL_NO_PASSWORD"
], passwordSignupFields())) {
loginFields.push(this.getEmailField());
} }
if (passwordSignupFields() !== "USERNAME_AND_OPTIONAL_EMAIL") { if (_.contains(["USERNAME_AND_OPTIONAL_EMAIL"], passwordSignupFields())) {
loginFields.push(Object.assign(this.getEmailField(), {required: false})); loginFields.push(Object.assign(this.getEmailField(), {required: false}));
} }
if (passwordSignupFields() !== "NO_PASSWORD") { if (!_.contains([
"EMAIL_ONLY_NO_PASSWORD",
"USERNAME_AND_EMAIL_NO_PASSWORD"
], passwordSignupFields())) {
loginFields.push(this.getPasswordField()); loginFields.push(this.getPasswordField());
} }
} }
@ -226,6 +257,7 @@ export class LoginForm extends Tracker.Component {
id: 'switchToSignUp', id: 'switchToSignUp',
label: T9n.get('signUp'), label: T9n.get('signUp'),
type: 'link', type: 'link',
href: this.props.signUpPath,
onClick: this.switchToSignUp.bind(this) onClick: this.switchToSignUp.bind(this)
}); });
} }
@ -235,6 +267,7 @@ export class LoginForm extends Tracker.Component {
id: 'switchToSignIn', id: 'switchToSignIn',
label: T9n.get('signIn'), label: T9n.get('signIn'),
type: 'link', type: 'link',
href: this.props.loginPath,
onClick: this.switchToSignIn.bind(this) onClick: this.switchToSignIn.bind(this)
}); });
} }
@ -244,6 +277,7 @@ export class LoginForm extends Tracker.Component {
id: 'switchToPasswordReset', id: 'switchToPasswordReset',
label: T9n.get('forgotPassword'), label: T9n.get('forgotPassword'),
type: 'link', type: 'link',
href: this.props.passwordResetPath,
onClick: this.switchToPasswordReset.bind(this) onClick: this.switchToPasswordReset.bind(this)
}); });
} }
@ -253,6 +287,7 @@ export class LoginForm extends Tracker.Component {
id: 'switchToChangePassword', id: 'switchToChangePassword',
label: T9n.get('changePassword'), label: T9n.get('changePassword'),
type: 'link', type: 'link',
href: this.props.changePasswordPath,
onClick: this.switchToChangePassword.bind(this) onClick: this.switchToChangePassword.bind(this)
}); });
} }
@ -261,9 +296,10 @@ export class LoginForm extends Tracker.Component {
loginButtons.push({ loginButtons.push({
id: 'signUp', id: 'signUp',
label: T9n.get('signUp'), label: T9n.get('signUp'),
type: 'submit', type: hasPasswordService() ? 'submit' : 'link',
className: 'active',
disabled: waiting, disabled: waiting,
onClick: this.signUp.bind(this) onClick: hasPasswordService() ? this.signUp.bind(this) : null
}); });
} }
@ -271,9 +307,10 @@ export class LoginForm extends Tracker.Component {
loginButtons.push({ loginButtons.push({
id: 'signIn', id: 'signIn',
label: T9n.get('signIn'), label: T9n.get('signIn'),
type: 'submit', type: hasPasswordService() ? 'submit' : 'link',
className: 'active',
disabled: waiting, disabled: waiting,
onClick: this.signIn.bind(this) onClick: hasPasswordService() ? this.signIn.bind(this) : null
}); });
} }
@ -300,12 +337,15 @@ export class LoginForm extends Tracker.Component {
id: 'switchToSignOut', id: 'switchToSignOut',
label: T9n.get('cancel'), label: T9n.get('cancel'),
type: 'link', type: 'link',
href: this.props.profilePath,
onClick: this.switchToSignOut.bind(this) onClick: this.switchToSignOut.bind(this)
}); });
} }
// Sort the button array so that the submit button always comes first. // Sort the button array so that the submit button always comes first.
loginButtons.sort((a, b) => { loginButtons.sort((a, b) => {
return a.label.localeCompare(b.label);
}).sort((a, b) => {
return (b.type == 'submit') - (a.type == 'submit'); return (b.type == 'submit') - (a.type == 'submit');
}); });
@ -378,12 +418,12 @@ export class LoginForm extends Tracker.Component {
loginSelector = { username: username }; loginSelector = { username: username };
} }
} }
else if (email !== null) { else if (email !== null && usernameOrEmail == null) {
if (!this.validateEmail(email)) { if (!this.validateEmail(email)) {
return; return;
} }
else { else {
if (passwordSignupFields() === "NO_PASSWORD") { if (_.contains([ "EMAIL_ONLY_NO_PASSWORD" ], passwordSignupFields())) {
this.loginWithoutPassword(); this.loginWithoutPassword();
return; return;
} }
@ -397,6 +437,10 @@ export class LoginForm extends Tracker.Component {
return; return;
} }
else { else {
if (_.contains([ "USERNAME_AND_EMAIL_NO_PASSWORD" ], passwordSignupFields())) {
this.loginWithoutPassword();
return;
}
loginSelector = usernameOrEmail; loginSelector = usernameOrEmail;
} }
} }
@ -417,6 +461,45 @@ export class LoginForm extends Tracker.Component {
}); });
} }
oauthButtons() {
const { formState, waiting } = this.state;
let oauthButtons = [];
if (formState == STATES.SIGN_IN || formState == STATES.SIGN_UP ) {
if(Accounts.oauth) {
Accounts.oauth.serviceNames().map((service) => {
oauthButtons.push({
id: service,
label: capitalize(service),
disabled: waiting,
type: 'submit',
onClick: this.oauthSignIn.bind(this, service)
});
});
}
}
return _.indexBy(oauthButtons, 'id');
}
oauthSignIn(service) {
const { formState, waiting, user } = this.state;
//Thanks Josh Owens for this one.
function capitalService() {
return service.charAt(0).toUpperCase() + service.slice(1);
}
login = Meteor["loginWith" + capitalService()];
login({requestPermissions: Accounts.ui._options.requestPermissions}, (error) => {
if (error) {
this.showMessage(T9n.get(`error.accounts.${error.reason}`) || T9n.get("Unknown error"));
} else {
this.setState({ formState: STATES.SIGN_OUT, message: '' });
loginResultCallback(() => {
Meteor.setTimeout(() => Accounts.ui._options.onSignedInHook(), 100);
});
}
});
}
signUp(options = {}) { signUp(options = {}) {
const { const {
username = null, username = null,
@ -434,6 +517,14 @@ export class LoginForm extends Tracker.Component {
options.username = username; options.username = username;
} }
} }
else {
if (_.contains([
"USERNAME_AND_EMAIL",
"USERNAME_AND_EMAIL_NO_PASSWORD"
], passwordSignupFields()) && !this.validateUsername(username) ) {
return;
}
}
if (email !== null) { if (email !== null) {
if ( !this.validateEmail(email) ){ if ( !this.validateEmail(email) ){
@ -444,7 +535,10 @@ export class LoginForm extends Tracker.Component {
} }
} }
if (passwordSignupFields() === "NO_PASSWORD") { if (_.contains([
"EMAIL_ONLY_NO_PASSWORD",
"USERNAME_AND_EMAIL_NO_PASSWORD"
], passwordSignupFields())) {
// Generate a random password. // Generate a random password.
options.password = Meteor.uuid(); options.password = Meteor.uuid();
} }
@ -496,6 +590,7 @@ export class LoginForm extends Tracker.Component {
loginWithoutPassword(){ loginWithoutPassword(){
const { const {
email = '', email = '',
usernameOrEmail = '',
waiting waiting
} = this.state; } = this.state;
@ -517,8 +612,27 @@ export class LoginForm extends Tracker.Component {
this.setState({ waiting: false }); this.setState({ waiting: false });
}); });
} }
else if (this.validateUsername(usernameOrEmail)) {
this.setState({ waiting: true });
Accounts.loginWithoutPassword({ email: usernameOrEmail, username: usernameOrEmail }, (error) => {
if (error) {
this.showMessage(T9n.get(`error.accounts.${error.reason}`) || T9n.get("Unknown error"), 'error');
}
else {
this.showMessage(T9n.get("info.emailSent"), 'success', 5000);
}
this.setState({ waiting: false });
});
}
else { else {
this.showMessage(T9n.get("error.accounts.Invalid email"), 'warning'); if (_.contains([ "USERNAME_AND_EMAIL_NO_PASSWORD" ], passwordSignupFields())) {
this.showMessage(T9n.get("error.accounts.Invalid email or username"), 'warning');
}
else {
this.showMessage(T9n.get("error.accounts.Invalid email"), 'warning');
}
} }
} }
@ -615,8 +729,12 @@ export class LoginForm extends Tracker.Component {
} }
render() { render() {
return <Accounts.ui.Form fields={this.fields()} buttons={this.buttons()} {...this.state} />; this.oauthButtons();
return <Accounts.ui.Form oauthServices={this.oauthButtons()}
fields={this.fields()} 
buttons={this.buttons()}
{...this.state} />;
} }
} };
Accounts.ui.LoginForm = LoginForm; Accounts.ui.LoginForm = LoginForm;

View file

@ -0,0 +1,37 @@
import React from 'react';
import { Accounts } from 'meteor/accounts-base';
import { T9n } from 'meteor/softwarerero:accounts-t9n';
import { hasPasswordService } from '../../helpers.js';
export class PasswordOrService extends React.Component {
constructor(props) {
super(props);
this.state = {
hasPasswordService: hasPasswordService()
};
}
render () {
let {
oauthServices = {},
className,
style = {}
} = this.props;
let { hasPasswordService } = this.state;
let labels = Object.keys(oauthServices).map(service => oauthServices[service].label);
if (labels.length > 2) {
labels = [];
}
if (hasPasswordService && Object.keys(oauthServices).length > 0) {
return (
<div style={ style } className={ className }>
{ `${T9n.get('or use')} ${ labels.join(' / ') }` }
</div>
);
}
return null;
}
}
Accounts.ui.PasswordOrService = PasswordOrService;

View file

@ -0,0 +1,19 @@
import React from 'react';
import './Button.jsx';
import {Accounts} from 'meteor/accounts-base';
export class SocialButtons extends React.Component {
render() {
let { oauthServices = {}, className = "social_buttons" } = this.props;
return(
<div className={ className }>
{Object.keys(oauthServices).map((id, i) => {
return <Accounts.ui.Button {...oauthServices[id]} key={i} />;
})}
</div>
);
}
}
Accounts.ui.SocialButtons = SocialButtons;

View file

@ -1,6 +1,6 @@
Package.describe({ Package.describe({
name: 'studiointeract:react-accounts-ui', name: 'std:react-accounts-ui',
version: '1.0.20', version: '1.1.0',
summary: 'Accounts UI for React in Meteor 1.3', summary: 'Accounts UI for React in Meteor 1.3',
git: 'https://github.com/studiointeract/react-accounts-ui', git: 'https://github.com/studiointeract/react-accounts-ui',
documentation: 'README.md' documentation: 'README.md'

View file

@ -1,5 +1,5 @@
{ {
"name": "react-accounts", "name": "react-accounts-ui",
"description": "Accounts UI for React Component in Meteor", "description": "Accounts UI for React Component in Meteor",
"repository": { "repository": {
"type": "git", "type": "git",