Converted all components to use functional definition rather than Classes. Removed 'zustand' and replaced with 'react-redux' for state. Some minor adjustments to the project organization.

This commit is contained in:
Dan Lefrancois
2021-10-29 08:50:31 -07:00
parent 8f71945ed0
commit 2a83b00be6
20 changed files with 5725 additions and 5718 deletions

View File

@@ -2,8 +2,8 @@
* @format * @format
*/ */
import {AppRegistry} from 'react-native'; import { AppRegistry } from 'react-native';
import App from './App'; import App from './src/App';
import {name as appName} from './app.json'; import { name as appName } from './app.json';
AppRegistry.registerComponent(appName, () => App); AppRegistry.registerComponent(appName, () => App);

View File

@@ -17,6 +17,7 @@
"@react-navigation/bottom-tabs": "^6.0.9", "@react-navigation/bottom-tabs": "^6.0.9",
"@react-navigation/native": "^6.0.6", "@react-navigation/native": "^6.0.6",
"@react-navigation/native-stack": "^6.2.5", "@react-navigation/native-stack": "^6.2.5",
"@reduxjs/toolkit": "^1.6.2",
"axios": "^0.23.0", "axios": "^0.23.0",
"react": "17.0.2", "react": "17.0.2",
"react-native": "0.66.1", "react-native": "0.66.1",
@@ -24,7 +25,8 @@
"react-native-safe-area-context": "^3.3.2", "react-native-safe-area-context": "^3.3.2",
"react-native-screens": "^3.8.0", "react-native-screens": "^3.8.0",
"react-native-url-polyfill": "^1.3.0", "react-native-url-polyfill": "^1.3.0",
"zustand": "^3.6.0" "react-redux": "^7.2.6",
"redux": "^4.1.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.12.9", "@babel/core": "^7.12.9",

View File

@@ -1,16 +1,18 @@
import React from 'react'; import React from 'react';
import store from './store/Store';
import { Provider } from 'react-redux';
import { NavigationContainer } from '@react-navigation/native'; import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Image } from 'react-native'; import { Image } from 'react-native';
import BrandSelector from './src/screens/BrandSelector'; import BrandSelector from './screens/BrandSelector';
import SignIn from './src/screens/SignIn'; import SignIn from './screens/SignIn';
import ForgotPassword from './src/screens/ForgotPassword'; import ForgotPassword from './screens/ForgotPassword';
import ResetPassword from './src/screens/ResetPassword'; import ResetPassword from './screens/ResetPassword';
import DeviceList from './src/screens/DeviceList'; import DeviceList from './screens/DeviceList';
import DeviceDetails from './src/screens/DeviceDetails'; import DeviceDetails from './screens/DeviceDetails';
import Profile from './src/screens/Profile'; import Profile from './screens/Profile';
const Stack = createNativeStackNavigator(); const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator(); const Tab = createBottomTabNavigator();
@@ -18,15 +20,17 @@ const DeviceStack = createNativeStackNavigator();
function App() { function App() {
return ( return (
<NavigationContainer> <Provider store={store}>
<Stack.Navigator> <NavigationContainer>
<Stack.Screen name="BrandSelector" component={BrandSelector} options={{ title: 'Select Brand' }} /> <Stack.Navigator>
<Stack.Screen name="SignIn" component={SignIn} options={{ title: 'Sign In' }} /> <Stack.Screen name="BrandSelector" component={BrandSelector} options={{ title: 'Select Brand' }} />
<Stack.Screen name="ForgotPassword" component={ForgotPassword} options={{ title: 'Forgot Password' }} /> <Stack.Screen name="SignIn" component={SignIn} options={{ title: 'Sign In' }} />
<Stack.Screen name="ResetPassword" component={ResetPassword} options={{ title: 'Password Reset' }} /> <Stack.Screen name="ForgotPassword" component={ForgotPassword} options={{ title: 'Forgot Password' }} />
<Stack.Screen name="Main" component={TabScreens} options={{ headerShown: false }} /> <Stack.Screen name="ResetPassword" component={ResetPassword} options={{ title: 'Password Reset' }} />
</Stack.Navigator> <Stack.Screen name="Main" component={TabScreens} options={{ headerShown: false }} />
</NavigationContainer> </Stack.Navigator>
</NavigationContainer>
</Provider>
); );
} }
@@ -40,7 +44,7 @@ function TabScreens() {
headerShown: false, headerShown: false,
tabBarIcon: ({ tintColor }) => ( tabBarIcon: ({ tintColor }) => (
<Image <Image
source={require('./src/assets/server-solid.png')} source={require('./assets/server-solid.png')}
style={{ width: 26, height: 26, tintColor: tintColor }} style={{ width: 26, height: 26, tintColor: tintColor }}
/> />
), ),
@@ -53,7 +57,7 @@ function TabScreens() {
title: 'Profile', title: 'Profile',
tabBarIcon: ({ tintColor }) => ( tabBarIcon: ({ tintColor }) => (
<Image <Image
source={require('./src/assets/user-solid.png')} source={require('./assets/user-solid.png')}
style={{ width: 26, height: 26, tintColor: tintColor }} style={{ width: 26, height: 26, tintColor: tintColor }}
/> />
), ),

View File

@@ -1,21 +1,21 @@
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import { useStore } from './Store'; import store from './store/Store';
export function primaryColor() { export var primaryColor = '#2194f3';
let brandInfo = useStore.getState().brandInfo; export var primaryColorStyle = StyleSheet.create({});
function updatePrimaryColorInfo() {
const state = store.getState();
let brandInfo = state.brandInfo.value;
if (brandInfo && brandInfo.primaryColor) { if (brandInfo && brandInfo.primaryColor) {
return brandInfo.primaryColor; primaryColor = brandInfo.primaryColor;
return StyleSheet.create({
color: brandInfo.primaryColor,
});
} }
return '#2194f3';
}
export function primaryColorStyle() {
return StyleSheet.create({
color: primaryColor(),
});
} }
store.subscribe(updatePrimaryColorInfo);
export const pageStyle = StyleSheet.create({ export const pageStyle = StyleSheet.create({
container: { container: {

View File

@@ -1,24 +0,0 @@
import create from 'zustand';
export const useStore = create(set => ({
// Session information
session: null,
setSession: state => {
set({ session: state });
},
clearSession: () => set({ session: null }),
// Brand Info
brandInfo: null,
setBrandInfo: state => {
set({ brandInfo: state });
},
clearBrandInfo: () => set({ brandInfo: null }),
// System Info
systemInfo: null,
setSystemInfo: state => {
set({ systemInfo: state });
},
clearSystemInfo: () => set({ systemInfo: null }),
}));

View File

@@ -1,19 +1,21 @@
// Used the following for a basis for generating react-native from OpenAPI // Used the following for a basis for generating react-native from OpenAPI
// https://majidlotfinia.medium.com/openapi-generator-for-react-native-by-swagger-58847cadd9e8 // https://majidlotfinia.medium.com/openapi-generator-for-react-native-by-swagger-58847cadd9e8
import 'react-native-url-polyfill/auto'; import 'react-native-url-polyfill/auto';
import {strings} from '../localization/LocalizationStrings'; import { strings } from '../localization/LocalizationStrings';
import axios from 'axios'; import axios from 'axios';
import {useStore} from '../Store'; import store from '../store/Store';
import {showGeneralError} from '../Utils'; import { setSystemInfo } from '../store/SystemInfoSlice';
import {AuthenticationApiFactory, Configuration as SecurityConfiguration} from './generated/owSecurityApi'; import { showGeneralError } from '../Utils';
import {DevicesApiFactory, Configuration as GatewayConfiguration} from './generated/owGatewayApi'; import { AuthenticationApiFactory, Configuration as SecurityConfiguration } from './generated/owSecurityApi';
import { DevicesApiFactory, Configuration as GatewayConfiguration } from './generated/owGatewayApi';
const axiosInstance = axios.create({}); const axiosInstance = axios.create({});
axiosInstance.interceptors.request.use( axiosInstance.interceptors.request.use(
config => { config => {
const session = useStore.getState().session; const state = store.getState();
const session = state.session.value;
if (session) { if (session) {
config.headers.Authorization = 'Bearer ' + useStore.getState().session.access_token; config.headers.Authorization = 'Bearer ' + session.access_token;
} }
return config; return config;
@@ -35,19 +37,19 @@ const authenticationApi = new AuthenticationApiFactory(
const gatewayConfig = new GatewayConfiguration(); const gatewayConfig = new GatewayConfiguration();
var devicesApi = null; var devicesApi = null;
function getDevicesApi() { store.subscribe(generateDevicesApi);
if (devicesApi === null) { generateDevicesApi();
let url = getBaseUrlForApi('owgw');
devicesApi = url ? new DevicesApiFactory(gatewayConfig, url, axiosInstance) : null;
}
return devicesApi; function generateDevicesApi() {
let url = getBaseUrlForApi('owgw');
devicesApi = url ? new DevicesApiFactory(gatewayConfig, url, axiosInstance) : null;
} }
// Get the base URL from the System Info. This is returned in a call to SystemInfo and it // Get the base URL from the System Info. This is returned in a call to SystemInfo and it
// is needed in order to provide the proper base URIs for the other API systems. // is needed in order to provide the proper base URIs for the other API systems.
function getBaseUrlForApi(type) { function getBaseUrlForApi(type) {
const systemInfo = useStore.getState().systemInfo; const state = store.getState();
const systemInfo = state.systemInfo.value;
if (systemInfo && systemInfo.endpoints) { if (systemInfo && systemInfo.endpoints) {
const endpoints = systemInfo.endpoints; const endpoints = systemInfo.endpoints;
@@ -67,7 +69,7 @@ function getBaseUrlForApi(type) {
function setApiSystemInfo(systemInfo) { function setApiSystemInfo(systemInfo) {
// Set the state, then we can use the getBaseUrlForApi to verify it has the proper information // Set the state, then we can use the getBaseUrlForApi to verify it has the proper information
useStore.getState().setSystemInfo(systemInfo); store.dispatch(setSystemInfo(systemInfo));
let valid = true; let valid = true;
const typesToValidate = ['owgw']; // Include all API types that might be used const typesToValidate = ['owgw']; // Include all API types that might be used
@@ -88,6 +90,8 @@ function setApiSystemInfo(systemInfo) {
} }
function handleApiError(title, error) { function handleApiError(title, error) {
const state = store.getState();
const session = state.session.value;
let message = strings.errors.unknown; let message = strings.errors.unknown;
if (error.response) { if (error.response) {
@@ -99,7 +103,7 @@ function handleApiError(title, error) {
case 403: case 403:
console.error(error); console.error(error);
if (useStore.getState().session === null) { if (session === null) {
// If not currently signed in then return a credentials error // If not currently signed in then return a credentials error
message = strings.errors.credentials; message = strings.errors.credentials;
} else { } else {
@@ -128,4 +132,4 @@ function handleApiError(title, error) {
showGeneralError(title, message); showGeneralError(title, message);
} }
export {authenticationApi, getDevicesApi, handleApiError, setApiSystemInfo}; export { authenticationApi, devicesApi, handleApiError, setApiSystemInfo };

View File

@@ -1,26 +1,24 @@
import React, { Component } from 'react'; import React from 'react';
import { StyleSheet, TouchableOpacity, View, Text, Image } from 'react-native'; import { StyleSheet, TouchableOpacity, View, Text, Image } from 'react-native';
export class BrandItem extends Component { const BrandItem = props => {
render() { const getCompanyIconUri = () => {
return ( return props.brand.iconUri;
<TouchableOpacity onPress={this.props.onPress}> };
<View style={brandItemStyle.container}>
<Image style={brandItemStyle.icon} source={{ uri: this.getCompanyIconUri() }} />
<Text style={brandItemStyle.text}>{this.getCompanyName()}</Text>
</View>
</TouchableOpacity>
);
}
getCompanyIconUri() { const getCompanyName = () => {
return this.props.brand.iconUri; return props.brand.name;
} };
getCompanyName() { return (
return this.props.brand.name; <TouchableOpacity onPress={props.onPress}>
} <View style={brandItemStyle.container}>
} <Image style={brandItemStyle.icon} source={{ uri: getCompanyIconUri() }} />
<Text style={brandItemStyle.text}>{getCompanyName()}</Text>
</View>
</TouchableOpacity>
);
};
const brandItemStyle = StyleSheet.create({ const brandItemStyle = StyleSheet.create({
container: { container: {
@@ -41,3 +39,5 @@ const brandItemStyle = StyleSheet.create({
fontSize: 14, fontSize: 14,
}, },
}); });
export default BrandItem;

View File

@@ -1,42 +1,40 @@
import React, { Component } from 'react'; import React from 'react';
import { StyleSheet, TouchableOpacity, View, Text, Image } from 'react-native'; import { StyleSheet, TouchableOpacity, View, Text, Image } from 'react-native';
export class DeviceItem extends Component { const DeviceItem = props => {
render() { const getDeviceIcon = () => {
return (
<TouchableOpacity onPress={this.props.onPress}>
<View style={deviceItemStyle.container}>
<Image style={deviceItemStyle.icon} source={this.getDeviceIcon()} />
<View style={deviceItemStyle.textContainer}>
<Text>{this.getDeviceName()}</Text>
<Text>{this.getDeviceType()}</Text>
</View>
<Image style={deviceItemStyle.icon} source={this.getDeviceStatusIcon()} />
<Text>&gt;</Text>
</View>
</TouchableOpacity>
);
}
getDeviceIcon() {
return require('../assets/server-solid.png'); return require('../assets/server-solid.png');
} };
getDeviceName() { const getDeviceName = () => {
return this.props.device.compatible; return props.device.compatible;
} };
getDeviceType() { const getDeviceType = () => {
return this.props.device.manufacturer; return props.device.manufacturer;
} };
getDeviceStatusIcon() { const getDeviceStatusIcon = () => {
return require('../assets/wifi-solid.png'); return require('../assets/wifi-solid.png');
} };
}
return (
<TouchableOpacity onPress={props.onPress}>
<View style={deviceItemStyle.container}>
<Image style={deviceItemStyle.icon} source={getDeviceIcon()} />
<View style={deviceItemStyle.textContainer}>
<Text>{getDeviceName()}</Text>
<Text>{getDeviceType()}</Text>
</View>
<Image style={deviceItemStyle.icon} source={getDeviceStatusIcon()} />
<Text>&gt;</Text>
</View>
</TouchableOpacity>
);
};
const deviceItemStyle = StyleSheet.create({ const deviceItemStyle = StyleSheet.create({
container: { container: {
@@ -63,3 +61,5 @@ const deviceItemStyle = StyleSheet.create({
marginLeft: 10, marginLeft: 10,
}, },
}); });
export default DeviceItem;

View File

@@ -1,92 +1,93 @@
import React, { Component } from 'react'; import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { selectBrandInfo, setBrandInfo } from '../store/BrandInfoSlice';
import { strings } from '../localization/LocalizationStrings'; import { strings } from '../localization/LocalizationStrings';
import { useStore } from '../Store';
import { pageStyle, pageItemStyle, primaryColor } from '../AppStyle'; import { pageStyle, pageItemStyle, primaryColor } from '../AppStyle';
import { StyleSheet, View, Text, FlatList, TextInput, ActivityIndicator } from 'react-native'; import { StyleSheet, View, Text, FlatList, TextInput, ActivityIndicator } from 'react-native';
import { BrandItem } from '../components/BrandItem'; import BrandItem from '../components/BrandItem';
export default class BrandSelector extends Component { const BrandSelector = props => {
state = { const dispatch = useDispatch();
loading: false, const brandInfo = useSelector(selectBrandInfo);
brands: [ // Currently this following state does not change, but the expectation is that this information
{ // will come from an API so it is being left as is for this development
id: 'openwifi', const [loading, setLoading] = useState(false);
name: 'OpenWifi', const [brands, setBrands] = useState([
iconUri: 'https://14oranges-ui.arilia.com/assets/14Oranges_Logo.png', {
primaryColor: '#19255f', id: 'openwifi',
}, name: 'OpenWifi',
{ iconUri: 'https://14oranges-ui.arilia.com/assets/14Oranges_Logo.png',
id: 'openwifigreen', primaryColor: '#19255f',
name: 'OpenWifi (Green)', },
iconUri: 'https://14oranges-ui.arilia.com/assets/14Oranges_Logo.png', {
primaryColor: '#1a3e1b', id: 'openwifigreen',
}, name: 'OpenWifi (Green)',
], iconUri: 'https://14oranges-ui.arilia.com/assets/14Oranges_Logo.png',
filtered: false, primaryColor: '#1a3e1b',
filteredBrands: [], },
}; ]);
const [filtered, setFiltered] = useState(false);
const [filteredBrands, setFilteredBrands] = useState();
componentDidMount() { useEffect(() => {
if (useStore.getState().brandInfo !== null) { if (brandInfo !== null) {
this.props.navigation.navigate('SignIn'); props.navigation.navigate('SignIn');
} }
} // No dependencies as this is only to run once on mount. There are plenty of
// hacks around this eslint warning, but disabling it makes the most sense.
}, []); // eslint-disable-line react-hooks/exhaustive-deps
render() { const filterBrands = searchText => {
return (
<View style={pageStyle.container}>
<View style={pageItemStyle.container}>
<Text style={pageItemStyle.title}>{strings.brandSelector.title}</Text>
</View>
<View style={pageItemStyle.container}>
<Text style={pageItemStyle.description}>{strings.brandSelector.description}</Text>
</View>
{this.state.loading ? (
<View style={pageItemStyle.container}>
<ActivityIndicator size="large" color={primaryColor()} animating={this.state.loading} />
</View>
) : (
<View style={pageItemStyle.containerBrands}>
<View style={[pageItemStyle.container, brandingSelectorStyle.containerSearch]}>
<TextInput
style={pageItemStyle.inputText}
placeholder="Search"
onChangeText={search => this.filterBrands(search)}
/>
</View>
<View style={pageItemStyle.container}>
<FlatList
style={brandingSelectorStyle.containerList}
data={this.state.filtered ? this.state.filteredBrands : this.state.brands}
renderItem={({ item }) => <BrandItem brand={item} onPress={this.onCompanySelect.bind(this, item)} />}
/>
</View>
</View>
)}
</View>
);
}
filterBrands = searchText => {
if (searchText) { if (searchText) {
let searchTextLowerCase = searchText.toLowerCase(); let searchTextLowerCase = searchText.toLowerCase();
this.setState({ filtered: true }); setFiltered(true);
this.setState({ setFilteredBrands(brands.filter(b => b.name.toLowerCase().startsWith(searchTextLowerCase)));
filteredBrands: this.state.brands.filter(b => b.name.toLowerCase().startsWith(searchTextLowerCase)),
});
} else { } else {
this.setState({ filtered: false }); setFiltered(false);
this.setState({ filteredBrands: [] }); setFilteredBrands([]);
} }
}; };
onCompanySelect = async item => { const onCompanySelect = async item => {
useStore.getState().setBrandInfo(item); dispatch(setBrandInfo(item));
// Replace to the main screen. Use replace to ensure no back button // Replace to the main screen. Use replace to ensure no back button
this.props.navigation.navigate('SignIn'); props.navigation.navigate('SignIn');
}; };
}
return (
<View style={pageStyle.container}>
<View style={pageItemStyle.container}>
<Text style={pageItemStyle.title}>{strings.brandSelector.title}</Text>
</View>
<View style={pageItemStyle.container}>
<Text style={pageItemStyle.description}>{strings.brandSelector.description}</Text>
</View>
{loading ? (
<View style={pageItemStyle.container}>
<ActivityIndicator size="large" color={primaryColor} animating={loading} />
</View>
) : (
<View style={pageItemStyle.containerBrands}>
<View style={[pageItemStyle.container, brandingSelectorStyle.containerSearch]}>
<TextInput
style={pageItemStyle.inputText}
placeholder="Search"
onChangeText={search => filterBrands(search)}
/>
</View>
<View style={pageItemStyle.container}>
<FlatList
style={brandingSelectorStyle.containerList}
data={filtered ? filteredBrands : brands}
renderItem={({ item }) => <BrandItem brand={item} onPress={() => onCompanySelect(item)} />}
/>
</View>
</View>
)}
</View>
);
};
const brandingSelectorStyle = StyleSheet.create({ const brandingSelectorStyle = StyleSheet.create({
containerBrands: { containerBrands: {
@@ -104,3 +105,5 @@ const brandingSelectorStyle = StyleSheet.create({
width: '100%', width: '100%',
}, },
}); });
export default BrandSelector;

View File

@@ -1,15 +1,15 @@
import React, { Component } from 'react'; import React from 'react';
import { pageStyle, pageItemStyle } from '../AppStyle'; import { pageStyle, pageItemStyle } from '../AppStyle';
import { View, Text } from 'react-native'; import { View, Text } from 'react-native';
export default class DeviceDetails extends Component { const DeviceDetails = props => {
render() { return (
return ( <View style={pageStyle.container}>
<View style={pageStyle.container}> <View style={pageItemStyle.container}>
<View style={pageItemStyle.container}> <Text>Device Details</Text>
<Text>Device Details</Text>
</View>
</View> </View>
); </View>
} );
} };
export default DeviceDetails;

View File

@@ -1,44 +1,41 @@
import React, { Component } from 'react'; import React, { useState, useEffect } from 'react';
import { strings } from '../localization/LocalizationStrings'; import { strings } from '../localization/LocalizationStrings';
import { pageStyle, pageItemStyle } from '../AppStyle'; import { pageStyle, pageItemStyle } from '../AppStyle';
import { View, Text, FlatList } from 'react-native'; import { View, Text, FlatList } from 'react-native';
import { getDevicesApi, handleApiError } from '../api/apiHandler'; import { devicesApi, handleApiError } from '../api/apiHandler';
import { DeviceItem } from '../components/DeviceItem'; import DeviceItem from '../components/DeviceItem';
export default class DeviceList extends Component { const DeviceList = props => {
state = { devices: [] }; const [devices, setDevices] = useState([]);
render() { useEffect(() => {
return ( getDevices();
<View style={pageStyle.container}> }, []);
<View style={pageItemStyle.container}>
<Text style={{ fontSize: 24, fontWeight: 'bold' }}>Devices</Text>
</View>
<View style={pageItemStyle.container}>
<FlatList
data={this.state.devices}
renderItem={({ item }) => <DeviceItem device={item} onPress={this.onDevicePress} />}
/>
</View>
</View>
);
}
onDevicePress = async () => { const onDevicePress = async () => {
this.props.navigation.navigate('DeviceDetails'); props.navigation.navigate('DeviceDetails');
}; };
componentDidMount = () => { const getDevices = async () => {
this.getDevices();
};
getDevices = async () => {
try { try {
const response = await getDevicesApi().getDeviceList(); const response = await devicesApi.getDeviceList();
this.setState({ devices: response.data.devices }); setDevices(response.data.devices);
console.log(response.data); console.log(response.data);
} catch (error) { } catch (error) {
handleApiError(strings.errors.titleDeviceList, error); handleApiError(strings.errors.titleDeviceList, error);
} }
}; };
}
return (
<View style={pageStyle.container}>
<View style={pageItemStyle.container}>
<Text style={{ fontSize: 24, fontWeight: 'bold' }}>Devices</Text>
</View>
<View style={pageItemStyle.container}>
<FlatList data={devices} renderItem={({ item }) => <DeviceItem device={item} onPress={onDevicePress} />} />
</View>
</View>
);
};
export default DeviceList;

View File

@@ -1,79 +1,36 @@
import React, { Component } from 'react'; import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { clearSession } from '../store/SessionSlice';
import { pageStyle, pageItemStyle, primaryColor } from '../AppStyle'; import { pageStyle, pageItemStyle, primaryColor } from '../AppStyle';
import { View, Text, TextInput, Button, ActivityIndicator, Alert } from 'react-native'; import { View, Text, TextInput, Button, ActivityIndicator, Alert } from 'react-native';
import { strings } from '../localization/LocalizationStrings'; import { strings } from '../localization/LocalizationStrings';
import { authenticationApi, handleApiError } from '../api/apiHandler'; import { authenticationApi, handleApiError } from '../api/apiHandler';
import { useStore } from '../Store';
export default class ForgotPassword extends Component { const ForgotPassword = props => {
state = { const dispatch = useDispatch();
email: '', const [email, setEmail] = useState();
loading: false, const [loading, setLoading] = useState(false);
};
render() { const validateEmail = () => {
return (
<View style={pageStyle.container}>
<View style={pageItemStyle.container}>
<Text>Forgot Password</Text>
</View>
<View style={pageItemStyle.container}>
<ActivityIndicator size="large" animating={this.state.loading} />
</View>
<View style={pageItemStyle.container}>
<TextInput
style={pageItemStyle.inputText}
placeholder={strings.placeholders.email}
autoComplete="email"
autoCapitalize="none"
autoFocus={true}
keyboardType="email-address"
textContentType="emailAddress"
returnKeyType="go"
onChangeText={text => this.setState({ email: text })}
onSubmitEditing={() => {
this.state.email && this.onSubmit;
}}
/>
</View>
<View style={pageItemStyle.containerButton}>
<Button
title={strings.buttons.sendEmail}
color={primaryColor()}
onPress={this.onSubmit}
disabled={this.state.loading || !this.state.email}
/>
</View>
<View style={pageItemStyle.containerButton}>
<Button
title={strings.buttons.signIn}
color={primaryColor()}
onPress={this.backToSignin}
disabled={this.state.loading}
/>
</View>
</View>
);
}
validateEmail = () => {
const re = /\S+@\S+\.\S+/; const re = /\S+@\S+\.\S+/;
const valid = re.test(this.state.email); const valid = re.test(email);
if (!valid) { if (!valid) {
Alert.alert(strings.errors.titleForgotPassword, strings.errors.badEmail); Alert.alert(strings.errors.titleForgotPassword, strings.errors.badEmail);
} }
return valid; return valid;
} };
const onSubmit = async () => {
if (validateEmail()) {
setLoading(true);
onSubmit = async () => {
if (this.validateEmail()) {
this.setState({ loading: true });
try { try {
useStore.getState().clearSession(); // Clear the session information
dispatch(clearSession());
const response = await authenticationApi.getAccessToken( const response = await authenticationApi.getAccessToken(
{ {
userId: this.state.email, userId: email,
}, },
undefined, undefined,
true, true,
@@ -82,12 +39,53 @@ export default class ForgotPassword extends Component {
Alert.alert(strings.messages.message, strings.messages.resetEmail); Alert.alert(strings.messages.message, strings.messages.resetEmail);
} catch (error) { } catch (error) {
handleApiError(strings.errors.titleForgotPassword, error); handleApiError(strings.errors.titleForgotPassword, error);
} finally {
setLoading(false);
} }
this.setState({ loading: false });
} }
}; };
backToSignin = () => { const backToSignin = () => {
this.props.navigation.replace('SignIn'); props.navigation.replace('SignIn');
}; };
}
return (
<View style={pageStyle.container}>
<View style={pageItemStyle.container}>
<Text>Forgot Password</Text>
</View>
<View style={pageItemStyle.container}>
<ActivityIndicator size="large" animating={loading} />
</View>
<View style={pageItemStyle.container}>
<TextInput
style={pageItemStyle.inputText}
placeholder={strings.placeholders.email}
autoComplete="email"
autoCapitalize="none"
autoFocus={true}
keyboardType="email-address"
textContentType="emailAddress"
returnKeyType="go"
onChangeText={text => setEmail(text)}
onSubmitEditing={() => {
email && onSubmit;
}}
/>
</View>
<View style={pageItemStyle.containerButton}>
<Button
title={strings.buttons.sendEmail}
color={primaryColor}
onPress={onSubmit}
disabled={loading || !email}
/>
</View>
<View style={pageItemStyle.containerButton}>
<Button title={strings.buttons.signIn} color={primaryColor} onPress={backToSignin} disabled={loading} />
</View>
</View>
);
};
export default ForgotPassword;

View File

@@ -1,23 +1,27 @@
import React, { Component } from 'react'; import React from 'react';
import { strings } from '../localization/LocalizationStrings'; import { strings } from '../localization/LocalizationStrings';
import { useStore } from '../Store'; import { useDispatch } from 'react-redux';
import { clearSession } from '../store/SessionSlice';
import { pageStyle, pageItemStyle, primaryColor } from '../AppStyle'; import { pageStyle, pageItemStyle, primaryColor } from '../AppStyle';
import { View, Button } from 'react-native'; import { View, Button } from 'react-native';
export default class Profile extends Component { const Profile = props => {
render() { const dispatch = useDispatch();
return (
<View style={pageStyle.container}>
<View style={pageItemStyle.containerButton}>
<Button title={strings.buttons.signOut} color={primaryColor()} onPress={this.onSignOutPress} />
</View>
</View>
);
}
onSignOutPress = async () => { const onSignOutPress = async () => {
// Clear the session information and go back to the sign in pageStyle // Clear the session information and go back to the start
useStore.getState().clearSession(); dispatch(clearSession());
this.props.navigation.replace('BrandSelector');
props.navigation.replace('BrandSelector');
}; };
}
return (
<View style={pageStyle.container}>
<View style={pageItemStyle.containerButton}>
<Button title={strings.buttons.signOut} color={primaryColor} onPress={onSignOutPress} />
</View>
</View>
);
};
export default Profile;

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { pageStyle, pageItemStyle, primaryColor } from "../AppStyle"; import { pageStyle, pageItemStyle, primaryColor } from '../AppStyle';
import { View, Text, TextInput, Button, Alert, ActivityIndicator } from 'react-native'; import { View, Text, TextInput, Button, Alert, ActivityIndicator } from 'react-native';
import { strings } from '../localization/LocalizationStrings'; import { strings } from '../localization/LocalizationStrings';
import { authenticationApi, handleApiError } from '../api/apiHandler'; import { authenticationApi, handleApiError } from '../api/apiHandler';
@@ -13,7 +13,7 @@ export default function ResetPassword(props) {
useEffect(() => { useEffect(() => {
console.log(userId, password); console.log(userId, password);
}, []); }, []); // eslint-disable-line react-hooks/exhaustive-deps
const onCancel = () => { const onCancel = () => {
props.navigation.replace('SignIn'); props.navigation.replace('SignIn');
@@ -46,7 +46,7 @@ export default function ResetPassword(props) {
const checkPassword = () => { const checkPassword = () => {
const valid = validatePassword(newPassword); const valid = validatePassword(newPassword);
if(newPassword === password) { if (newPassword === password) {
Alert.alert(strings.errors.titleResetPassword, strings.errors.samePassword); Alert.alert(strings.errors.titleResetPassword, strings.errors.samePassword);
return false; return false;
} }
@@ -54,14 +54,14 @@ export default function ResetPassword(props) {
Alert.alert(strings.errors.titleResetPassword, strings.errors.mismatchPassword); Alert.alert(strings.errors.titleResetPassword, strings.errors.mismatchPassword);
return false; return false;
} }
if(!valid) { if (!valid) {
Alert.alert(strings.errors.titleResetPassword, strings.errors.badFormat); Alert.alert(strings.errors.titleResetPassword, strings.errors.badFormat);
return false; return false;
} }
return valid && newPassword !== password && newPassword === confirmPassword; return valid && newPassword !== password && newPassword === confirmPassword;
}; };
const validatePassword = (password) => { const validatePassword = password => {
const reg = /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/; const reg = /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/;
return reg.test(password); return reg.test(password);
}; };
@@ -102,17 +102,13 @@ export default function ResetPassword(props) {
<View style={pageItemStyle.containerButton}> <View style={pageItemStyle.containerButton}>
<Button <Button
title={strings.buttons.submit} title={strings.buttons.submit}
color={primaryColor()} color={primaryColor}
onPress={onSubmit} onPress={onSubmit}
disabled={loading || !newPassword || !confirmPassword} disabled={loading || !newPassword || !confirmPassword}
/> />
</View> </View>
<View style={pageItemStyle.containerButton}> <View style={pageItemStyle.containerButton}>
<Button <Button title={strings.buttons.cancel} color={primaryColor} onPress={onCancel} disabled={loading} />
title={strings.buttons.cancel}
color={primaryColor()}
onPress={onCancel}
disabled={loading} />
</View> </View>
<View style={pageItemStyle.container}> <View style={pageItemStyle.container}>
<View> <View>

View File

@@ -1,123 +1,60 @@
import React, { Component } from 'react'; import React, { useState, useEffect, createRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { clearSession, setSession } from '../store/SessionSlice';
import { selectBrandInfo } from '../store/BrandInfoSlice';
import { strings } from '../localization/LocalizationStrings'; import { strings } from '../localization/LocalizationStrings';
import { useStore } from '../Store';
import { pageStyle, pageItemStyle, primaryColor, primaryColorStyle } from '../AppStyle'; import { pageStyle, pageItemStyle, primaryColor, primaryColorStyle } from '../AppStyle';
import { StyleSheet, Text, View, Image, Button, TextInput, ActivityIndicator } from 'react-native'; import { StyleSheet, Text, View, Image, Button, TextInput, ActivityIndicator } from 'react-native';
import { handleApiError, authenticationApi, setApiSystemInfo } from '../api/apiHandler'; import { handleApiError, authenticationApi, setApiSystemInfo } from '../api/apiHandler';
export default class SignIn extends Component { const SignIn = props => {
state = { const dispatch = useDispatch();
email: '', const brandInfo = useSelector(selectBrandInfo);
password: '', const [email, setEmail] = useState();
loading: false, const [password, setPassword] = useState();
}; const [loading, setLoading] = useState(false);
const passwordRef = createRef();
constructor(props) { useEffect(() => {
super(props);
this.passwordRef = React.createRef();
}
componentDidMount() {
// If the brand is not selected, then resort back to the brand selector // If the brand is not selected, then resort back to the brand selector
if (useStore.getState().brandInfo === null) { if (brandInfo === null) {
this.props.navigation.replace('BrandSelector'); props.navigation.replace('BrandSelector');
} }
} });
render() { const onSignInPress = async () => {
return (
<View style={pageStyle.container}>
<View style={pageItemStyle.container}>
<Image style={signInStyle.headerImage} source={{ uri: useStore.getState().brandInfo.iconUri }} />
</View>
{this.state.loading ? (
<View style={pageItemStyle.container}>
<ActivityIndicator size="large" color={primaryColor()} animating={this.state.loading} />
</View>
) : (
<View style={signInStyle.containerForm}>
<View style={pageItemStyle.container}>
<Text style={pageItemStyle.description}>{strings.signIn.description}</Text>
</View>
<View style={pageItemStyle.container}>
<TextInput
style={pageItemStyle.inputText}
placeholder={strings.placeholders.username}
autoComplete="email"
autoCapitalize="none"
autoFocus={true}
keyboardType="email-address"
textContentType="username"
returnKeyType="next"
value={this.state.email}
onChangeText={text => this.setState({ email: text })}
onSubmitEditing={() => this.passwordRef.current.focus()}
/>
</View>
<View style={pageItemStyle.container}>
<TextInput
style={pageItemStyle.inputText}
ref={this.passwordRef}
placeholder={strings.placeholders.password}
secureTextEntry={true}
autoCapitalize="none"
textContentType="password"
returnKeyType="go"
onChangeText={text => this.setState({ password: text })}
onSubmitEditing={() => this.onSignInPress()}
/>
</View>
<View style={pageItemStyle.containerButton}>
<Text style={[pageItemStyle.buttonText, primaryColorStyle()]} onPress={this.onForgotPasswordPress}>
{strings.buttons.forgotPassword}
</Text>
</View>
<View style={pageItemStyle.containerButton}>
<Button title={strings.buttons.signIn} color={primaryColor()} onPress={this.onSignInPress} />
</View>
{/*<View style={pageItemStyle.containerButton}>
<Button title={"Reset"}
onPress={() => this.props.navigation.navigate("ResetPassword", {userId: this.state.email, password: this.state.password})} />
</View>*/}
</View>
)}
</View>
);
}
onSignInPress = async () => {
try { try {
this.setState({ loading: true }); setLoading(true);
// Make sure to clear any session information, this ensures error messaging is handled properly as well // Make sure to clear any session information, this ensures error messaging is handled properly as well
useStore.getState().clearSession(); dispatch(clearSession());
const response = await authenticationApi.getAccessToken({ const response = await authenticationApi.getAccessToken({
userId: this.state.email, userId: email,
password: this.state.password, password: password,
}); });
useStore.getState().setSession(response.data); dispatch(setSession(response.data));
// must reset password // must reset password
console.log(JSON.stringify(response.data, null, '\t')); console.log(JSON.stringify(response.data, null, '\t'));
if (response.data.userMustChangePassword) { if (response.data.userMustChangePassword) {
this.props.navigation.navigate('ResetPassword', { props.navigation.navigate('ResetPassword', {
userId: this.state.email, userId: email,
password: this.state.password, password: password,
}); });
} else { } else {
// Update the system endpoints and navigate to the main view. // Update the system endpoints and navigate to the main view.
this.getSystemEndpointsNavigateToMain(); getSystemEndpointsNavigateToMain();
} }
} catch (error) { } catch (error) {
// Clear the loading state // Clear the loading state
this.setState({ loading: false }); setLoading(false);
handleApiError(strings.errors.titleSignIn, error); handleApiError(strings.errors.titleSignIn, error);
} }
}; };
getSystemEndpointsNavigateToMain = async () => { const getSystemEndpointsNavigateToMain = async () => {
try { try {
// The system info is necessary before moving on to the next view as it'll provide // The system info is necessary before moving on to the next view as it'll provide
// the endpoints needed for communicating with the other systems // the endpoints needed for communicating with the other systems
@@ -129,19 +66,74 @@ export default class SignIn extends Component {
setApiSystemInfo(response.data); setApiSystemInfo(response.data);
// Replace to the main screen. Use replace to ensure no back button // Replace to the main screen. Use replace to ensure no back button
this.props.navigation.replace('Main'); props.navigation.replace('Main');
} catch (error) { } catch (error) {
// Make sure the loading state is done in all cases // Make sure the loading state is done in all cases
this.setState({ loading: false }); setLoading(false);
handleApiError(strings.errors.titleSystemSetup, error); handleApiError(strings.errors.titleSystemSetup, error);
} }
}; };
onForgotPasswordPress = async () => { const onForgotPasswordPress = async () => {
this.props.navigation.navigate('ForgotPassword'); props.navigation.navigate('ForgotPassword');
}; };
}
return (
<View style={pageStyle.container}>
<View style={pageItemStyle.container}>
<Image style={signInStyle.headerImage} source={{ uri: brandInfo.iconUri }} />
</View>
{loading ? (
<View style={pageItemStyle.container}>
<ActivityIndicator size="large" color={primaryColor} animating={loading} />
</View>
) : (
<View style={signInStyle.containerForm}>
<View style={pageItemStyle.container}>
<Text style={pageItemStyle.description}>{strings.signIn.description}</Text>
</View>
<View style={pageItemStyle.container}>
<TextInput
style={pageItemStyle.inputText}
placeholder={strings.placeholders.username}
autoComplete="email"
autoCapitalize="none"
autoFocus={true}
keyboardType="email-address"
textContentType="username"
returnKeyType="next"
value={email}
onChangeText={text => setEmail(text)}
onSubmitEditing={() => passwordRef.current.focus()}
/>
</View>
<View style={pageItemStyle.container}>
<TextInput
style={pageItemStyle.inputText}
ref={passwordRef}
placeholder={strings.placeholders.password}
secureTextEntry={true}
autoCapitalize="none"
textContentType="password"
returnKeyType="go"
onChangeText={text => setPassword(text)}
onSubmitEditing={() => onSignInPress()}
/>
</View>
<View style={pageItemStyle.containerButton}>
<Text style={[pageItemStyle.buttonText, primaryColorStyle]} onPress={onForgotPasswordPress}>
{strings.buttons.forgotPassword}
</Text>
</View>
<View style={pageItemStyle.containerButton}>
<Button title={strings.buttons.signIn} color={primaryColor} onPress={onSignInPress} />
</View>
</View>
)}
</View>
);
};
const signInStyle = StyleSheet.create({ const signInStyle = StyleSheet.create({
containerForm: { containerForm: {
@@ -160,3 +152,5 @@ const signInStyle = StyleSheet.create({
marginBottom: 10, marginBottom: 10,
}, },
}); });
export default SignIn;

View File

@@ -0,0 +1,20 @@
import { createSlice } from '@reduxjs/toolkit';
export const brandInfoSlice = createSlice({
name: 'brandInfo',
initialState: {
value: null,
},
reducers: {
setBrandInfo: (state, action) => {
state.value = action.payload;
},
clearBrandInfo: state => {
state.value -= null;
},
},
});
export const selectBrandInfo = state => state.brandInfo.value;
export const { setBrandInfo, clearBrandInfo } = brandInfoSlice.actions;
export default brandInfoSlice.reducer;

20
src/store/SessionSlice.js Normal file
View File

@@ -0,0 +1,20 @@
import { createSlice } from '@reduxjs/toolkit';
export const sessionSlice = createSlice({
name: 'session',
initialState: {
value: null,
},
reducers: {
setSession: (state, action) => {
state.value = action.payload;
},
clearSession: state => {
state.value -= null;
},
},
});
export const selectSession = state => state.session.value;
export const { setSession, clearSession } = sessionSlice.actions;
export default sessionSlice.reducer;

16
src/store/Store.js Normal file
View File

@@ -0,0 +1,16 @@
import { configureStore } from '@reduxjs/toolkit';
import brandInfoReducer from './BrandInfoSlice';
import sessionReducer from './SessionSlice';
import systemInfoReducer from './SystemInfoSlice';
export default configureStore({
reducer: {
brandInfo: brandInfoReducer,
session: sessionReducer,
systemInfo: systemInfoReducer,
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
serializableCheck: false,
}),
});

View File

@@ -0,0 +1,20 @@
import { createSlice } from '@reduxjs/toolkit';
export const systemInfoSlice = createSlice({
name: 'systemInfo',
initialState: {
value: null,
},
reducers: {
setSystemInfo: (state, action) => {
state.value = action.payload;
},
clearSystemInfo: state => {
state.value -= null;
},
},
});
export const selectSystemInfo = state => state.systemInfo.value;
export const { setSystemInfo, clearSystemInfo } = systemInfoSlice.actions;
export default systemInfoSlice.reducer;

10535
yarn.lock

File diff suppressed because it is too large Load Diff