Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
thalia
ThaliApp
Commits
194500ef
Verified
Commit
194500ef
authored
Aug 12, 2018
by
Sébastiaan Versteeg
Browse files
Add status to session with animated activity indicator on login screen
parent
b1ade63b
Changes
7
Hide whitespace changes
Inline
Side-by-side
__tests__/sagas/session.spec.js
View file @
194500ef
...
@@ -47,15 +47,6 @@ describe('session saga', () => {
...
@@ -47,15 +47,6 @@ describe('session saga', () => {
const
error
=
new
Error
(
'
error
'
);
const
error
=
new
Error
(
'
error
'
);
describe
(
'
logging in
'
,
()
=>
{
describe
(
'
logging in
'
,
()
=>
{
it
(
'
should show a snackbar on start
'
,
()
=>
expectSaga
(
sessionSaga
)
.
dispatch
(
sessionActions
.
signIn
(
'
username
'
,
'
password
'
))
.
silentRun
()
.
then
(()
=>
{
expect
(
Snackbar
.
show
).
toBeCalledWith
(
{
title
:
'
Logging in
'
,
duration
:
Snackbar
.
LENGTH_INDEFINITE
},
);
}));
it
(
'
should put the result data when the request succeeds
'
,
()
=>
expectSaga
(
sessionSaga
)
it
(
'
should put the result data when the request succeeds
'
,
()
=>
expectSaga
(
sessionSaga
)
.
provide
([
.
provide
([
[
matchers
.
call
.
like
({
fn
:
apiRequest
,
args
:
[
'
token-auth
'
]
}),
{
token
:
'
abc123
'
}],
[
matchers
.
call
.
like
({
fn
:
apiRequest
,
args
:
[
'
token-auth
'
]
}),
{
token
:
'
abc123
'
}],
...
@@ -74,7 +65,6 @@ describe('session saga', () => {
...
@@ -74,7 +65,6 @@ describe('session saga', () => {
.
dispatch
(
sessionActions
.
signIn
(
'
username
'
,
'
password
'
))
.
dispatch
(
sessionActions
.
signIn
(
'
username
'
,
'
password
'
))
.
silentRun
()
.
silentRun
()
.
then
(()
=>
{
.
then
(()
=>
{
expect
(
Snackbar
.
dismiss
).
toBeCalled
();
expect
(
Snackbar
.
show
).
toBeCalledWith
(
expect
(
Snackbar
.
show
).
toBeCalledWith
(
{
title
:
'
Login successful
'
},
{
title
:
'
Login successful
'
},
);
);
...
@@ -94,6 +84,14 @@ describe('session saga', () => {
...
@@ -94,6 +84,14 @@ describe('session saga', () => {
]);
]);
}));
}));
it
(
'
should put token invalid when the request fails
'
,
()
=>
expectSaga
(
sessionSaga
)
.
provide
([
[
matchers
.
call
.
fn
(
apiRequest
),
throwError
(
error
)],
])
.
put
(
sessionActions
.
tokenInvalid
())
.
dispatch
(
sessionActions
.
signIn
(
'
username
'
,
'
password
'
))
.
silentRun
());
it
(
'
should show a snackbar when the request fails
'
,
()
=>
expectSaga
(
sessionSaga
)
it
(
'
should show a snackbar when the request fails
'
,
()
=>
expectSaga
(
sessionSaga
)
.
provide
([
.
provide
([
[
matchers
.
call
.
fn
(
apiRequest
),
throwError
(
error
)],
[
matchers
.
call
.
fn
(
apiRequest
),
throwError
(
error
)],
...
@@ -101,7 +99,6 @@ describe('session saga', () => {
...
@@ -101,7 +99,6 @@ describe('session saga', () => {
.
dispatch
(
sessionActions
.
signIn
(
'
username
'
,
'
password
'
))
.
dispatch
(
sessionActions
.
signIn
(
'
username
'
,
'
password
'
))
.
silentRun
()
.
silentRun
()
.
then
(()
=>
{
.
then
(()
=>
{
expect
(
Snackbar
.
dismiss
).
toBeCalled
();
expect
(
Snackbar
.
show
).
toBeCalledWith
(
expect
(
Snackbar
.
show
).
toBeCalledWith
(
{
title
:
'
Login failed
'
},
{
title
:
'
Login failed
'
},
);
);
...
...
__tests__/ui/components/navigator/Sidebar.spec.js
View file @
194500ef
...
@@ -24,7 +24,7 @@ describe('Sidebar component', () => {
...
@@ -24,7 +24,7 @@ describe('Sidebar component', () => {
it
(
'
renders correctly
'
,
()
=>
{
it
(
'
renders correctly
'
,
()
=>
{
const
tree
=
renderer
const
tree
=
renderer
.
create
(
<
Sidebar
store
=
{
store
}
navigation
=
{
mockNavigation
}
/>
)
.
create
(
<
Sidebar
store
=
{
store
}
navigation
=
{
mockNavigation
}
activeItemKey
=
"
unknown
"
/>
)
.
toJSON
();
.
toJSON
();
expect
(
tree
).
toMatchSnapshot
();
expect
(
tree
).
toMatchSnapshot
();
});
});
...
...
app/app.js
View file @
194500ef
import
React
,
{
Component
}
from
'
react
'
;
import
React
,
{
Component
}
from
'
react
'
;
import
{
Linking
,
Platform
}
from
'
react-native
'
;
import
{
Linking
,
Platform
,
NativeModules
}
from
'
react-native
'
;
import
{
applyMiddleware
,
createStore
}
from
'
redux
'
;
import
{
applyMiddleware
,
createStore
}
from
'
redux
'
;
import
{
Provider
}
from
'
react-redux
'
;
import
{
Provider
}
from
'
react-redux
'
;
import
{
I18nextProvider
}
from
'
react-i18next
'
;
import
{
I18nextProvider
}
from
'
react-i18next
'
;
...
@@ -18,6 +18,13 @@ import * as deepLinkingActions from './actions/deepLinking';
...
@@ -18,6 +18,13 @@ import * as deepLinkingActions from './actions/deepLinking';
import
{
register
}
from
'
./actions/pushNotifications
'
;
import
{
register
}
from
'
./actions/pushNotifications
'
;
import
NavigationService
from
'
./navigation
'
;
import
NavigationService
from
'
./navigation
'
;
const
{
UIManager
}
=
NativeModules
;
/* istanbul ignore next */
// eslint-disable-next-line no-unused-expressions
UIManager
.
setLayoutAnimationEnabledExperimental
&&
UIManager
.
setLayoutAnimationEnabledExperimental
(
true
);
const
sagaMiddleware
=
createSagaMiddleware
();
const
sagaMiddleware
=
createSagaMiddleware
();
const
store
=
createStore
(
reducers
,
applyMiddleware
(
sagaMiddleware
));
const
store
=
createStore
(
reducers
,
applyMiddleware
(
sagaMiddleware
));
sagaMiddleware
.
run
(
sagas
);
sagaMiddleware
.
run
(
sagas
);
...
...
app/reducers/session.js
View file @
194500ef
...
@@ -2,7 +2,12 @@ import { defaultProfileImage } from '../utils/url';
...
@@ -2,7 +2,12 @@ import { defaultProfileImage } from '../utils/url';
import
*
as
sessionActions
from
'
../actions/session
'
;
import
*
as
sessionActions
from
'
../actions/session
'
;
export
const
STATUS_SIGNED_OUT
=
'
SIGNED_OUT
'
;
export
const
STATUS_SIGNED_IN
=
'
SIGNED_IN
'
;
export
const
STATUS_SIGNING_IN
=
'
SIGNING_IN
'
;
const
initialState
=
{
const
initialState
=
{
status
:
STATUS_SIGNED_OUT
,
token
:
''
,
token
:
''
,
username
:
''
,
username
:
''
,
displayName
:
''
,
displayName
:
''
,
...
@@ -11,9 +16,15 @@ const initialState = {
...
@@ -11,9 +16,15 @@ const initialState = {
export
default
function
session
(
state
=
initialState
,
action
=
{})
{
export
default
function
session
(
state
=
initialState
,
action
=
{})
{
switch
(
action
.
type
)
{
switch
(
action
.
type
)
{
case
sessionActions
.
SIGN_IN
:
return
{
...
state
,
status
:
STATUS_SIGNING_IN
,
};
case
sessionActions
.
SIGNED_IN
:
case
sessionActions
.
SIGNED_IN
:
return
{
return
{
...
state
,
...
state
,
status
:
STATUS_SIGNED_IN
,
username
:
action
.
payload
.
username
,
username
:
action
.
payload
.
username
,
token
:
action
.
payload
.
token
,
token
:
action
.
payload
.
token
,
};
};
...
...
app/sagas/session.js
View file @
194500ef
import
{
import
{
call
,
put
,
takeEvery
,
select
,
call
,
put
,
takeEvery
,
select
,
}
from
'
redux-saga/effects
'
;
}
from
'
redux-saga/effects
'
;
import
{
delay
}
from
'
redux-saga
'
;
import
{
AsyncStorage
}
from
'
react-native
'
;
import
{
AsyncStorage
}
from
'
react-native
'
;
import
Snackbar
from
'
react-native-snackbar
'
;
import
Snackbar
from
'
react-native-snackbar
'
;
import
{
Sentry
}
from
'
react-native-sentry
'
;
import
{
Sentry
}
from
'
react-native-sentry
'
;
...
@@ -52,8 +53,6 @@ function* init() {
...
@@ -52,8 +53,6 @@ function* init() {
function
*
signIn
(
action
)
{
function
*
signIn
(
action
)
{
const
{
user
,
pass
}
=
action
.
payload
;
const
{
user
,
pass
}
=
action
.
payload
;
Snackbar
.
show
({
title
:
'
Logging in
'
,
duration
:
Snackbar
.
LENGTH_INDEFINITE
});
const
data
=
{
const
data
=
{
method
:
'
POST
'
,
method
:
'
POST
'
,
headers
:
{
headers
:
{
...
@@ -65,6 +64,9 @@ function* signIn(action) {
...
@@ -65,6 +64,9 @@ function* signIn(action) {
password
:
pass
,
password
:
pass
,
}),
}),
};
};
const
currentTimestamp
=
Date
.
now
();
try
{
try
{
const
response
=
yield
call
(
apiRequest
,
'
token-auth
'
,
data
);
const
response
=
yield
call
(
apiRequest
,
'
token-auth
'
,
data
);
const
{
token
}
=
response
;
const
{
token
}
=
response
;
...
@@ -76,11 +78,16 @@ function* signIn(action) {
...
@@ -76,11 +78,16 @@ function* signIn(action) {
yield
put
(
sessionActions
.
signedIn
(
user
,
token
));
yield
put
(
sessionActions
.
signedIn
(
user
,
token
));
yield
put
(
sessionActions
.
fetchUserInfo
());
yield
put
(
sessionActions
.
fetchUserInfo
());
yield
put
(
pushNotificationsActions
.
register
());
yield
put
(
pushNotificationsActions
.
register
());
Snackbar
.
dismiss
();
Snackbar
.
show
({
title
:
'
Login successful
'
});
Snackbar
.
show
({
title
:
'
Login successful
'
});
}
catch
(
e
)
{
}
catch
(
e
)
{
// Delay failure to make sure animation is finished
const
now
=
Date
.
now
();
if
(
now
-
currentTimestamp
<
150
)
{
yield
call
(
delay
,
now
-
currentTimestamp
);
}
yield
put
(
sessionActions
.
tokenInvalid
());
Sentry
.
captureException
(
e
);
Sentry
.
captureException
(
e
);
Snackbar
.
dismiss
();
Snackbar
.
show
({
title
:
'
Login failed
'
});
Snackbar
.
show
({
title
:
'
Login failed
'
});
}
}
}
}
...
...
app/ui/screens/user/Login.js
View file @
194500ef
import
React
,
{
Component
}
from
'
react
'
;
import
React
,
{
Component
}
from
'
react
'
;
import
PropTypes
from
'
prop-types
'
;
import
PropTypes
from
'
prop-types
'
;
import
{
import
{
ActivityIndicator
,
Image
,
Image
,
Keyboard
,
Keyboard
,
KeyboardAvoidingView
,
KeyboardAvoidingView
,
...
@@ -8,6 +9,7 @@ import {
...
@@ -8,6 +9,7 @@ import {
Text
,
Text
,
TextInput
,
TextInput
,
View
,
View
,
LayoutAnimation
,
}
from
'
react-native
'
;
}
from
'
react-native
'
;
import
{
connect
}
from
'
react-redux
'
;
import
{
connect
}
from
'
react-redux
'
;
import
{
translate
}
from
'
react-i18next
'
;
import
{
translate
}
from
'
react-i18next
'
;
...
@@ -18,9 +20,17 @@ import DismissKeyboardView from '../../components/dismissKeyboardView/DismissKey
...
@@ -18,9 +20,17 @@ import DismissKeyboardView from '../../components/dismissKeyboardView/DismissKey
import
Button
from
'
../../components/button/Button
'
;
import
Button
from
'
../../components/button/Button
'
;
import
styles
from
'
./style/Login
'
;
import
styles
from
'
./style/Login
'
;
import
Colors
from
'
../../style/Colors
'
;
import
Colors
from
'
../../style/Colors
'
;
import
{
STATUS_SIGNING_IN
}
from
'
../../../reducers/session
'
;
const
image
=
require
(
'
../../../assets/img/logo.png
'
);
const
image
=
require
(
'
../../../assets/img/logo.png
'
);
const
configureNextAnimation
=
()
=>
{
/* istanbul ignore next */
LayoutAnimation
.
configureNext
(
LayoutAnimation
.
create
(
150
,
LayoutAnimation
.
Types
.
linear
,
LayoutAnimation
.
Properties
.
opacity
));
};
class
Login
extends
Component
{
class
Login
extends
Component
{
constructor
(
props
)
{
constructor
(
props
)
{
super
(
props
);
super
(
props
);
...
@@ -30,8 +40,61 @@ class Login extends Component {
...
@@ -30,8 +40,61 @@ class Login extends Component {
};
};
}
}
componentDidMount
()
{
configureNextAnimation
();
}
render
()
{
render
()
{
const
{
login
,
t
}
=
this
.
props
;
configureNextAnimation
();
const
{
login
,
t
,
status
}
=
this
.
props
;
let
content
=
(
<
View
>
<
View
>
<
TextInput
style
=
{
styles
.
input
}
placeholder
=
{
t
(
'
Username
'
)}
autoCapitalize
=
"
none
"
underlineColorAndroid
=
{
Colors
.
textColour
}
onChangeText
=
{
username
=>
this
.
setState
({
username
})}
/
>
<
TextInput
style
=
{
styles
.
input
}
placeholder
=
{
t
(
'
Password
'
)}
underlineColorAndroid
=
{
Colors
.
textColour
}
autoCapitalize
=
"
none
"
secureTextEntry
onChangeText
=
{
password
=>
this
.
setState
({
password
})}
onSubmitEditing
=
{()
=>
{
login
(
this
.
state
.
username
,
this
.
state
.
password
);
}}
/
>
<
/View
>
<
Button
title
=
{
t
(
'
LOGIN
'
)}
onPress
=
{()
=>
login
(
this
.
state
.
username
,
this
.
state
.
password
)}
color
=
{
Colors
.
darkGrey
}
style
=
{
styles
.
loginButton
}
textStyle
=
{
styles
.
loginButtonText
}
underlayColor
=
{
Colors
.
white
}
/
>
<
Text
style
=
{
styles
.
forgotpass
}
onPress
=
{()
=>
Linking
.
openURL
(
`
${
url
}
/password_reset/`
)}
>
{
t
(
'
Forgot password?
'
)}
<
/Text
>
<
/View
>
);
if
(
status
===
STATUS_SIGNING_IN
)
{
content
=
(
<
ActivityIndicator
style
=
{
styles
.
activityIndicator
}
color
=
{
Colors
.
white
}
size
=
"
large
"
animating
/>
);
}
return
(
return
(
<
KeyboardAvoidingView
<
KeyboardAvoidingView
style
=
{
styles
.
rootWrapper
}
style
=
{
styles
.
rootWrapper
}
...
@@ -42,37 +105,7 @@ class Login extends Component {
...
@@ -42,37 +105,7 @@ class Login extends Component {
contentStyle
=
{
styles
.
wrapper
}
contentStyle
=
{
styles
.
wrapper
}
>
>
<
Image
style
=
{
styles
.
logo
}
source
=
{
image
}
/
>
<
Image
style
=
{
styles
.
logo
}
source
=
{
image
}
/
>
<
View
>
{
content
}
<
TextInput
style
=
{
styles
.
input
}
placeholder
=
{
t
(
'
Username
'
)}
autoCapitalize
=
"
none
"
underlineColorAndroid
=
{
Colors
.
textColour
}
onChangeText
=
{
username
=>
this
.
setState
({
username
})}
/
>
<
TextInput
style
=
{
styles
.
input
}
placeholder
=
{
t
(
'
Password
'
)}
underlineColorAndroid
=
{
Colors
.
textColour
}
autoCapitalize
=
"
none
"
secureTextEntry
onChangeText
=
{
password
=>
this
.
setState
({
password
})}
onSubmitEditing
=
{()
=>
{
login
(
this
.
state
.
username
,
this
.
state
.
password
);
}}
/
>
<
/View
>
<
Button
title
=
{
t
(
'
LOGIN
'
)}
onPress
=
{()
=>
login
(
this
.
state
.
username
,
this
.
state
.
password
)}
color
=
{
Colors
.
darkGrey
}
style
=
{
styles
.
loginButton
}
textStyle
=
{
styles
.
loginButtonText
}
underlayColor
=
{
Colors
.
white
}
/
>
<
Text
style
=
{
styles
.
forgotpass
}
onPress
=
{()
=>
Linking
.
openURL
(
`
${
url
}
/password_reset/`
)}
>
{
t
(
'
Forgot password?
'
)}
<
/Text
>
<
/DismissKeyboardView
>
<
/DismissKeyboardView
>
<
/KeyboardAvoidingView
>
<
/KeyboardAvoidingView
>
);
);
...
@@ -82,6 +115,7 @@ class Login extends Component {
...
@@ -82,6 +115,7 @@ class Login extends Component {
Login
.
propTypes
=
{
Login
.
propTypes
=
{
login
:
PropTypes
.
func
.
isRequired
,
login
:
PropTypes
.
func
.
isRequired
,
t
:
PropTypes
.
func
.
isRequired
,
t
:
PropTypes
.
func
.
isRequired
,
status
:
PropTypes
.
string
.
isRequired
,
};
};
const
mapStateToProps
=
state
=>
state
.
session
;
const
mapStateToProps
=
state
=>
state
.
session
;
...
...
app/ui/screens/user/style/Login.js
View file @
194500ef
...
@@ -61,6 +61,9 @@ const styles = StyleSheet.create({
...
@@ -61,6 +61,9 @@ const styles = StyleSheet.create({
flex
:
1
,
flex
:
1
,
justifyContent
:
'
center
'
,
justifyContent
:
'
center
'
,
},
},
activityIndicator
:
{
marginTop
:
24
,
},
});
});
export
default
styles
;
export
default
styles
;
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment