@@ -2,6 +2,7 @@ import React, { Component, Fragment } from 'react';
2
2
import classNames from 'classnames' ;
3
3
import cryptoRandomString from '../utils/crypto-random-string' ;
4
4
import { get } from 'lodash' ;
5
+ import { isDev } from '../../desktop/env' ;
5
6
import MailIcon from '../icons/mail' ;
6
7
import SimplenoteLogo from '../icons/simplenote' ;
7
8
import Spinner from '../components/spinner' ;
@@ -19,18 +20,26 @@ type OwnProps = {
19
20
hasTooManyRequests : boolean ;
20
21
hasUnverifiedAccount : boolean ;
21
22
login : ( username : string , password : string ) => any ;
23
+ loginRequested : boolean ;
24
+ isCompletingLogin : boolean ;
25
+ hasCodeError : boolean ;
26
+ requestLogin : ( username : string ) => any ;
27
+ completeLogin : ( username : string , code : string ) => any ;
22
28
requestSignup : ( username : string ) => any ;
23
- tokenLogin : ( username : string , token : string ) => any ;
24
29
resetErrors : ( ) => any ;
30
+ tokenLogin : ( username : string , token : string ) => any ;
25
31
} ;
26
32
27
33
type Props = OwnProps ;
28
34
29
35
export class Auth extends Component < Props > {
30
36
state = {
37
+ authState : '' ,
31
38
isCreatingAccount : false ,
32
39
passwordErrorMessage : null ,
33
40
onLine : window . navigator . onLine ,
41
+ usePassword : isDev , // Magic link login doesn't work in dev mode
42
+ emailForPasswordForm : null ,
34
43
} ;
35
44
36
45
componentDidMount ( ) {
@@ -43,6 +52,15 @@ export class Auth extends Component<Props> {
43
52
window . removeEventListener ( 'offline' , this . setConnectivity , false ) ;
44
53
}
45
54
55
+ componentDidUpdate ( ) {
56
+ // Add the email to the username/password form if it was set
57
+ if ( this . state . usePassword && this . state . emailForPasswordForm ) {
58
+ this . usernameInput . value = this . state . emailForPasswordForm ;
59
+ this . setState ( { emailForPasswordForm : null } ) ;
60
+ this . passwordInput . focus ( ) ;
61
+ }
62
+ }
63
+
46
64
setConnectivity = ( ) => this . setState ( { onLine : window . navigator . onLine } ) ;
47
65
48
66
render ( ) {
@@ -51,14 +69,21 @@ export class Auth extends Component<Props> {
51
69
return null ;
52
70
}
53
71
54
- const { isCreatingAccount, passwordErrorMessage } = this . state ;
72
+ const { isCreatingAccount, passwordErrorMessage, usePassword } = this . state ;
55
73
const submitClasses = classNames ( 'button' , 'button-primary' , {
56
74
pending : this . props . authPending ,
57
75
} ) ;
58
76
59
77
const signUpText = 'Sign up' ;
60
78
const logInText = 'Log in' ;
61
- const buttonLabel = isCreatingAccount ? signUpText : logInText ;
79
+ const headerLabel = isCreatingAccount ? signUpText : logInText ;
80
+ const buttonLabel = isCreatingAccount
81
+ ? signUpText
82
+ : ! isCreatingAccount && ! usePassword
83
+ ? 'Log in with email'
84
+ : logInText ;
85
+ const wpccLabel =
86
+ ( isCreatingAccount ? signUpText : logInText ) + ' with WordPress.com' ;
62
87
const helpLinkLabel = isCreatingAccount ? logInText : signUpText ;
63
88
const helpMessage = isCreatingAccount
64
89
? 'Already have an account?'
@@ -81,7 +106,7 @@ export class Auth extends Component<Props> {
81
106
.
82
107
</ >
83
108
) : (
84
- 'Could not log in with the provided email address and password .'
109
+ 'Could not log in with the provided credentials .'
85
110
) ;
86
111
87
112
const mainClasses = classNames ( 'login' , {
@@ -92,14 +117,14 @@ export class Auth extends Component<Props> {
92
117
return (
93
118
< div className = { mainClasses } >
94
119
{ isElectron && isMac && < div className = "login__draggable-area" /> }
95
- < div className = "accountRequested " >
120
+ < div className = "account-requested " >
96
121
< MailIcon />
97
- < p className = "accountRequested__message " >
122
+ < p className = "account-requested__message " >
98
123
We've sent an email to{ ' ' }
99
124
< strong > { this . props . emailSentTo } </ strong > . Please check your inbox
100
125
and follow the instructions.
101
126
</ p >
102
- < p className = "accountRequested__footer " >
127
+ < p className = "account-requested__footer " >
103
128
Didn't get an email? You may already have an account
104
129
associated with this email address. Contact{ ' ' }
105
130
< a
@@ -124,12 +149,74 @@ export class Auth extends Component<Props> {
124
149
) ;
125
150
}
126
151
152
+ if (
153
+ this . props . loginRequested ||
154
+ this . props . isCompletingLogin ||
155
+ this . props . hasCodeError
156
+ ) {
157
+ return (
158
+ < div className = { mainClasses } >
159
+ { isElectron && isMac && < div className = "login__draggable-area" /> }
160
+ < div className = "account-requested" >
161
+ < form className = "login__form" onSubmit = { this . onSubmitCode } >
162
+ < MailIcon />
163
+ < p className = "account-requested__message" >
164
+ We've sent a code to{ ' ' }
165
+ < strong > { this . props . emailSentTo } </ strong > . The code will be
166
+ valid for a few minutes.
167
+ </ p >
168
+ { ( passwordErrorMessage || this . props . hasCodeError ) && (
169
+ < p className = "login__auth-message is-error" >
170
+ { passwordErrorMessage
171
+ ? passwordErrorMessage
172
+ : 'Could not log in. Check the code and try again.' }
173
+ </ p >
174
+ ) }
175
+ < input
176
+ type = "text"
177
+ className = "account-requested__code"
178
+ placeholder = "Code"
179
+ maxLength = { 6 }
180
+ autoFocus
181
+ ref = { ( ref ) => ( this . codeInput = ref ) }
182
+ > </ input >
183
+ < button className = "button button-primary" type = "submit" >
184
+ { this . props . isCompletingLogin ? (
185
+ < Spinner isWhite = { true } size = { 20 } thickness = { 5 } />
186
+ ) : (
187
+ 'Log in'
188
+ ) }
189
+ </ button >
190
+ < Fragment >
191
+ < div className = "or-section" >
192
+ < span className = "or" > Or</ span >
193
+ < span className = "or-line" > </ span >
194
+ </ div >
195
+ < button
196
+ className = "button button-secondary account-requested__password-button"
197
+ onClick = { this . togglePassword }
198
+ >
199
+ Enter password
200
+ </ button >
201
+ </ Fragment >
202
+ < button
203
+ onClick = { this . clearRequestedAccount }
204
+ className = "button-borderless"
205
+ >
206
+ Go Back
207
+ </ button >
208
+ </ form >
209
+ </ div >
210
+ </ div >
211
+ ) ;
212
+ }
213
+
127
214
return (
128
215
< div className = { mainClasses } >
129
216
{ isElectron && isMac && < div className = "login__draggable-area" /> }
130
217
< SimplenoteLogo />
131
218
< form className = "login__form" onSubmit = { this . onSubmit } >
132
- < h1 > { buttonLabel } </ h1 >
219
+ < h1 > { headerLabel } </ h1 >
133
220
{ ! this . state . onLine && (
134
221
< p className = "login__auth-message is-error" > Offline</ p >
135
222
) }
@@ -204,7 +291,7 @@ export class Auth extends Component<Props> {
204
291
className = "login__auth-message is-error"
205
292
data-error-name = "too-many-requests"
206
293
>
207
- Too many log in attempts. Try again later.
294
+ Too many login attempts. Try again later.
208
295
</ p >
209
296
) }
210
297
{ ( this . props . hasInvalidCredentials || this . props . hasLoginError ) && (
@@ -236,7 +323,7 @@ export class Auth extends Component<Props> {
236
323
required
237
324
autoFocus
238
325
/>
239
- { ! isCreatingAccount && (
326
+ { ! isCreatingAccount && usePassword && (
240
327
< >
241
328
< label className = "login__field" htmlFor = "login__field-password" >
242
329
Password
@@ -270,7 +357,20 @@ export class Auth extends Component<Props> {
270
357
) }
271
358
</ button >
272
359
273
- { ! isCreatingAccount && (
360
+ { usePassword && (
361
+ < Fragment >
362
+ < a
363
+ className = "login__forgot"
364
+ href = "#"
365
+ rel = "noopener noreferrer"
366
+ onClick = { this . togglePassword }
367
+ >
368
+ Log in with email
369
+ </ a >
370
+ </ Fragment >
371
+ ) }
372
+
373
+ { ! isCreatingAccount && usePassword && (
274
374
< a
275
375
className = "login__forgot"
276
376
href = "https://app.simplenote.com/forgot/"
@@ -286,7 +386,7 @@ export class Auth extends Component<Props> {
286
386
< span className = "or" > Or</ span >
287
387
< span className = "or-line" > </ span >
288
388
< button className = "wpcc-button" onClick = { this . onWPLogin } >
289
- { buttonLabel } with WordPress.com
389
+ { wpccLabel }
290
390
</ button >
291
391
</ Fragment >
292
392
) }
@@ -364,7 +464,7 @@ export class Auth extends Component<Props> {
364
464
this . props . resetErrors ( ) ;
365
465
} ;
366
466
367
- onSubmit = ( event ) => {
467
+ onSubmit = ( event : React . FormEvent ) => {
368
468
event . preventDefault ( ) ;
369
469
370
470
// clear any existing error messages on submit
@@ -383,7 +483,7 @@ export class Auth extends Component<Props> {
383
483
}
384
484
} else if (
385
485
this . usernameInput . validity . valueMissing ||
386
- this . passwordInput . validity . valueMissing
486
+ ( this . state . usePassword && this . passwordInput . validity . valueMissing )
387
487
) {
388
488
this . setState ( {
389
489
passwordErrorMessage : 'Please fill out email and password.' ,
@@ -406,28 +506,54 @@ export class Auth extends Component<Props> {
406
506
return ;
407
507
}
408
508
409
- const password = get ( this . passwordInput , 'value' ) ;
509
+ if ( this . state . usePassword ) {
510
+ const password = get ( this . passwordInput , 'value' ) ;
511
+
512
+ // login has slightly more relaxed password rules
513
+ if ( ! this . passwordInput . validity . valid ) {
514
+ this . setState ( {
515
+ passwordErrorMessage : 'Passwords must contain at least 4 characters.' ,
516
+ } ) ;
517
+ return ;
518
+ }
519
+ this . setState ( { passwordErrorMessage : null } ) ;
520
+ this . props . login ( username , password ) ;
521
+ return ;
522
+ }
523
+
524
+ // default: magic link login
525
+ this . props . requestLogin ( username ) ;
526
+ } ;
527
+
528
+ onSubmitCode = ( event : React . FormEvent ) => {
529
+ event . preventDefault ( ) ;
530
+ this . setState ( { authState : 'login-requested' } ) ;
531
+ this . setState ( {
532
+ passwordErrorMessage : '' ,
533
+ } ) ;
410
534
411
- // login has slightly more relaxed password rules
412
- if ( ! this . passwordInput . validity . valid ) {
535
+ const code = get ( this . codeInput , 'value' ) ;
536
+ const alphanumericRegex = / ^ [ a - z A - Z 0 - 9 ] { 6 } $ / ;
537
+ if ( ! alphanumericRegex . test ( code ) ) {
413
538
this . setState ( {
414
- passwordErrorMessage : 'Passwords must contain at least 4 characters.' ,
539
+ passwordErrorMessage : 'Code must be 6 characters.' ,
415
540
} ) ;
416
541
return ;
417
542
}
418
543
419
- this . setState ( { passwordErrorMessage : null } ) ;
420
- this . props . login ( username , password ) ;
544
+ const email = this . props . emailSentTo ;
545
+
546
+ this . props . completeLogin ( email , code ) ;
421
547
} ;
422
548
423
549
onWPLogin = ( ) => {
424
550
const redirectUrl = encodeURIComponent ( config . wpcc_redirect_url ) ;
425
- this . authState = `app-${ cryptoRandomString ( 20 ) } ` ;
426
- const authUrl = `https://public-api.wordpress.com/oauth2/authorize?client_id=${ config . wpcc_client_id } &redirect_uri=${ redirectUrl } &response_type=code&scope=global&state=${ this . authState } ` ;
551
+ this . setState ( { authState : `app-${ cryptoRandomString ( 20 ) } ` } ) ;
552
+ const authUrl = `https://public-api.wordpress.com/oauth2/authorize?client_id=${ config . wpcc_client_id } &redirect_uri=${ redirectUrl } &response_type=code&scope=global&state=${ this . state . authState } ` ;
427
553
428
554
window . electron . send ( 'wpLogin' , authUrl ) ;
429
555
430
- window . electron . receive ( 'wpLogin' , ( url ) => {
556
+ window . electron . receive ( 'wpLogin' , ( url : string ) => {
431
557
const { searchParams } = new URL ( url ) ;
432
558
433
559
const errorCode = searchParams . get ( 'error' )
@@ -454,34 +580,43 @@ export class Auth extends Component<Props> {
454
580
return this . authError ( 'An error was encountered while signing in.' ) ;
455
581
}
456
582
457
- if ( authState !== this . authState ) {
583
+ if ( authState !== this . state . authState ) {
458
584
return ;
459
585
}
460
586
this . props . tokenLogin ( userEmail , simperiumToken ) ;
461
587
} ) ;
462
588
} ;
463
589
464
- authError = ( errorMessage ) => {
590
+ authError = ( errorMessage : string ) => {
465
591
this . setState ( {
466
592
passwordErrorMessage : errorMessage ,
467
593
} ) ;
468
594
} ;
469
595
470
- onForgot = ( event ) => {
596
+ onForgot = ( event : React . MouseEvent ) => {
471
597
event . preventDefault ( ) ;
472
598
window . open (
473
- event . currentTarget . href ,
474
- null ,
599
+ ( event . currentTarget as HTMLAnchorElement ) . href ,
600
+ undefined ,
475
601
'width=640,innerWidth=640,height=480,innerHeight=480,useContentSize=true,chrome=yes,centerscreen=yes'
476
602
) ;
477
603
} ;
478
604
479
- toggleSignUp = ( event ) => {
605
+ toggleSignUp = ( event : React . MouseEvent ) => {
480
606
event . preventDefault ( ) ;
481
607
this . props . resetErrors ( ) ;
482
608
this . setState ( {
483
609
passwordErrorMessage : '' ,
484
610
} ) ;
485
611
this . setState ( { isCreatingAccount : ! this . state . isCreatingAccount } ) ;
486
612
} ;
613
+ togglePassword = ( event : React . MouseEvent ) => {
614
+ event . preventDefault ( ) ;
615
+ this . props . resetErrors ( ) ;
616
+ this . setState ( {
617
+ passwordErrorMessage : '' ,
618
+ emailForPasswordForm : this . props . emailSentTo ,
619
+ usePassword : ! this . state . usePassword ,
620
+ } ) ;
621
+ } ;
487
622
}
0 commit comments