Created
March 22, 2019 22:14
-
-
Save jmporchet/c29f49498c4244bc7b8cbe1e20ccc867 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { AsyncStorage } from 'react-native'; | |
class AsyncStorageService { | |
static async get(key) { | |
const data = await AsyncStorage.getItem(key); | |
return JSON.parse(data); | |
} | |
static set(key, value) { | |
return AsyncStorage.setItem(key, JSON.stringify(value)); | |
} | |
static merge(key, value) { | |
return AsyncStorage.mergeItem(key, JSON.stringify(value)); | |
} | |
static remove(key) { | |
return AsyncStorage.removeItem(key); | |
} | |
} | |
export default AsyncStorageService; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import _ from 'lodash'; | |
import React, { Component } from 'react'; | |
import { connect } from 'react-redux'; | |
import { | |
View, | |
Text, | |
StyleSheet, | |
Platform, | |
TouchableOpacity | |
} from 'react-native'; | |
import { Formik } from 'formik'; | |
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; | |
import * as Yup from 'yup'; | |
import AsyncStorageService from '../../services/AsyncStorageService'; | |
import { | |
authenticateUser, | |
clearError, | |
setIsUserRegistering | |
} from '../../actions/user'; | |
import Button from '../../components/SubmitButton'; | |
import FloatingLabelInput from '../../components/FloatingLabelInput'; | |
const isAllRequiredInfoInProfile = userInfo => { | |
// take the keys we're interested in and flatten them | |
const flattened = { | |
..._.pick(_.get(userInfo, 'profile'), ['sex', 'birthDate', 'timeZone']), | |
height: _.get(userInfo, 'height'), | |
weight: _.get(userInfo, 'weight'), | |
physicalActivity: _.get(userInfo, 'profileAspects.physicalActivity', null) | |
}; | |
// return true only if every key has a value, which means the profile was filled in correctly | |
return _.values(flattened).every(el => !_.isNil(el)); | |
}; | |
const formInitialValues = { | |
email: '', | |
password: '' | |
}; | |
const loginValidationSchema = Yup.object().shape({ | |
email: Yup.string() | |
.email('Not a valid email') | |
.required('Email is required'), | |
password: Yup.string().required('Please enter a password') | |
}); | |
export class Login extends Component { | |
componentDidMount() { | |
// componentDidMount is executed only once, at app load when using StackNavigators. | |
// Every screen will be mounted but hidden. | |
// This triggers custom code whenever the screen becomes the active one. | |
this.willFocusListener = this.props.navigation.addListener( | |
'willFocus', | |
this.onScreenWillFocus | |
); | |
} | |
componentWillUnmount() { | |
this.willFocusListener.remove(); | |
} | |
onScreenWillFocus = () => { | |
// whenever this screen is shown | |
this.props.clearError(); | |
this.formik && this.formik.resetForm(formInitialValues); | |
}; | |
handleSubmit = async (values, formik = { setSubmitting: () => {} }) => { | |
if (!values.email) { | |
return false; | |
} | |
const { authenticateUser, setIsUserRegistering, navigation } = this.props; | |
formik.setSubmitting(true); | |
const data = await AsyncStorageService.get(values.email); | |
authenticateUser(values, async () => { | |
// app created account (didn't complete mandatory questions) | |
if (data && data.hasAnsweredMandatoryQuestions === false) { | |
navigation.navigate('Personalize'); | |
} | |
// app created account (didn't complete optional questions) | |
else if (data && data.hasCompletedOnBoarding === false) { | |
navigation.navigate('OnBoarding'); | |
} | |
// web created account | |
else if (!isAllRequiredInfoInProfile(this.props.user)) { | |
await AsyncStorageService.set(values.email, { | |
comesFromTheWeb: true | |
}); | |
navigation.navigate('Personalize'); | |
} | |
// valid account, proceed to app | |
else if ( | |
!data && | |
isAllRequiredInfoInProfile(this.props.user) && | |
!!this.props.user.authorizationToken | |
) { | |
AsyncStorageService.remove(values.email); | |
setIsUserRegistering(false); | |
navigation.navigate('App'); | |
} | |
formik.setSubmitting(false); | |
}); | |
}; | |
renderPageContent = ({ | |
values, | |
handleSubmit, | |
setFieldValue, | |
setFieldTouched, | |
errors, | |
touched | |
}) => { | |
return ( | |
<View style={styles.container}> | |
<KeyboardAwareScrollView | |
enableOnAndroid={true} | |
extraScrollHeight={ Platform.OS === 'android' ? 100 : 0 } | |
keyboardShouldPersistTaps="never" | |
> | |
<View style={styles.container}> | |
<View style={styles.inputBlock}> | |
<FloatingLabelInput | |
tag="Email" | |
name="email" | |
label="Email address" | |
style={styles.input} | |
onChange={setFieldValue} | |
value={values.email} | |
handleTouch={setFieldTouched} | |
floatingLabel | |
autoCapitalize="none" | |
keyboardType="email-address" | |
returnKeyType="next" | |
onSubmitEditing={() => this.passwordInput.focus()} | |
touched={!!touched.email} | |
error={errors.email} | |
/> | |
<FloatingLabelInput | |
ref={ref => (this.passwordInput = ref)} | |
tag="Password" | |
name="password" | |
label="Password" | |
style={styles.input} | |
value={values.password} | |
onChange={setFieldValue} | |
handleTouch={setFieldTouched} | |
onSubmitEditing={handleSubmit} | |
floatingLabel | |
secureTextEntry | |
autoCapitalize="none" | |
returnKeyType="done" | |
touched={!!touched.password} | |
error={errors.password} | |
/> | |
</View> | |
{!!this.props.errorMessage && ( | |
<Text style={styles.errorMessage}>{this.props.errorMessage}</Text> | |
)} | |
<View style={styles.buttonBlock}> | |
<Button | |
style={[styles.button, styles.buttonLogin]} | |
onPress={handleSubmit} | |
title="SIGN IN" | |
loading={this.props.isLoading} | |
disabled={this.props.isLoading} | |
/> | |
</View> | |
</View> | |
</KeyboardAwareScrollView> | |
</View> | |
); | |
}; | |
render() { | |
return ( | |
<Formik | |
testID="formik" | |
ref={ref => (this.formik = ref)} | |
initialValues={formInitialValues} | |
validationSchema={loginValidationSchema} | |
validateOnChange={false} | |
onSubmit={this.handleSubmit} | |
render={this.renderPageContent} | |
/> | |
); | |
} | |
} | |
const styles = StyleSheet.create({ | |
mainBlock: { | |
width: 315 | |
}, | |
container: { | |
width: '100%', | |
alignItems: 'center' | |
}, | |
button: { | |
flex: 1 | |
}, | |
input: { | |
marginTop: 40 | |
}, | |
inputBlock: { | |
width: 315, | |
marginBottom: 25 | |
}, | |
buttonBlock: { | |
flexDirection: 'row', | |
justifyContent: 'center' | |
}, | |
buttonLogin: { | |
marginTop: 30, | |
marginBottom: 15, | |
backgroundColor: '#ccc' | |
}, | |
errorMessage: { | |
color: color.red, | |
marginTop: 20 | |
} | |
}); | |
const mapStateToProps = state => { | |
return { | |
errorMessage: state.user.error, | |
isLoading: state.user.isFetching, | |
user: state.user | |
}; | |
}; | |
const mapDispatchToProps = dispatch => ({ | |
authenticateUser: (data, cb) => dispatch(authenticateUser(data, cb)), | |
clearError: () => dispatch(clearError()), | |
setIsUserRegistering: status => dispatch(setIsUserRegistering(status)) | |
}); | |
export default connect( | |
mapStateToProps, | |
mapDispatchToProps | |
)(Login); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import MockAsyncStorage from 'mock-async-storage'; | |
import React from 'react'; | |
import { render, fireEvent } from 'react-native-testing-library'; | |
import AsyncStorageService from '../../services/AsyncStorageService'; | |
import { Login } from '../Register/Login'; | |
// Scrollviews are bugged in the current Expo/RN release and won't render in tests | |
// https://github.com/expo/expo/issues/2806#issuecomment-465373231 | |
jest.mock('ScrollView', () => require.requireMock('ScrollViewMock')); | |
// copy/pasted directly from https://github.com/devmetal/mock-async-storage | |
const mock = () => { | |
const mockImpl = new MockAsyncStorage(); | |
jest.mock('AsyncStorage', () => mockImpl); | |
}; | |
mock(); | |
describe('CheckOut', () => { | |
const userObject = { | |
email: '[email protected]', | |
authorizationToken: 'aaaaaaaa', | |
height: { feet: 5, inches: 2 }, | |
weight: 155, | |
profileAspects: { physicalActivity: 'moderate' }, | |
profile: { sex: 'male', birthDay: '2002-01-01', timeZone: 'Europe/Zurich' } | |
}; | |
const props = { | |
errorMessage: 'errorMessage', | |
isLoading: false, | |
user: userObject, | |
navigation: { | |
// that's the only function where we want to know what's happening | |
navigate: jest.fn(), | |
addListener: () => null | |
}, | |
// authenticate needs this in order to know what to do | |
authenticateUser: async (values, authenticateUserCB) => await authenticateUserCB(), | |
clearError: () => null, | |
setIsUserRegistering: () => null, | |
setSubmitting: () => null, | |
authenticateUserCB: () => null | |
}; | |
const invalidProps = { ...props, user: {} }; | |
const goToHome = render(<Login {...props} />); | |
const goToPersonalize = render(<Login {...invalidProps} />); | |
const goToPersonalize2 = render(<Login {...props} />); | |
const goToOnboarding = render(<Login {...props} />); | |
beforeEach(() => { | |
// clean the storage | |
AsyncStorageService.remove('[email protected]'); | |
// clean the nav | |
props.navigation.navigate.mockReset(); | |
}); | |
it('should render', () => { | |
expect(goToHome.getByTestId('formik')).toBeDefined(); | |
}); | |
it('should redirect users to the Homepage', async () => { | |
// await is necessary because of the callback in authenticateUser | |
await fireEvent(goToHome.getByTestId('formik'), 'submit', { | |
email: '[email protected]' | |
}); | |
// has the mock navigation been asked to navigate to 'App'? | |
expect(props.navigation.navigate).toHaveBeenCalledWith('App'); | |
// is the storage for the key '[email protected]' been deleted if necessary? | |
expect(await AsyncStorageService.get('[email protected]')).toBeNull(); | |
}); | |
it('should redirect users to the Personalize page if they signed up from the website', async () => { | |
// one way we require people to fill in the mandatory questions | |
await fireEvent(goToPersonalize.getByTestId('formik'), 'submit', { | |
email: '[email protected]' | |
}); | |
expect(props.navigation.navigate).toHaveBeenCalledWith('Personalize'); | |
expect(await AsyncStorageService.get('[email protected]')).toEqual({ | |
comesFromTheWeb: true | |
}); | |
}); | |
it('should redirect users to the Personalize page if the app was closed while onboarding', async () => { | |
// another way we require people to fill in the mandatory questions | |
await AsyncStorageService.set('[email protected]', { | |
hasAnsweredMandatoryQuestions: false | |
}); | |
await fireEvent(goToPersonalize2.getByTestId('formik'), 'submit', { | |
email: '[email protected]' | |
}); | |
expect(props.navigation.navigate).toHaveBeenCalledWith('Personalize'); | |
expect(await AsyncStorageService.get('[email protected]')).toEqual({ | |
hasAnsweredMandatoryQuestions: false | |
}); | |
}); | |
it('should redirect users to the OnBoarding page if they didn`t answer the onboarding questions', async () => { | |
await AsyncStorageService.set('[email protected]', { | |
hasCompletedOnBoarding: false | |
}); | |
await fireEvent(goToOnboarding.getByTestId('formik'), 'submit', { | |
email: '[email protected]' | |
}); | |
expect(props.navigation.navigate).toHaveBeenCalledWith('OnBoarding'); | |
expect(await AsyncStorageService.get('[email protected]')).toEqual({ | |
hasCompletedOnBoarding: false | |
}); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment