Skip to content

Instantly share code, notes, and snippets.

@jmporchet
Created March 22, 2019 22:14
Show Gist options
  • Save jmporchet/c29f49498c4244bc7b8cbe1e20ccc867 to your computer and use it in GitHub Desktop.
Save jmporchet/c29f49498c4244bc7b8cbe1e20ccc867 to your computer and use it in GitHub Desktop.
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;
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);
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