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
*/
import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
import { AppRegistry } from 'react-native';
import App from './src/App';
import { name as appName } from './app.json';
AppRegistry.registerComponent(appName, () => App);

View File

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

View File

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

View File

@@ -1,21 +1,21 @@
import { StyleSheet } from 'react-native';
import { useStore } from './Store';
import store from './store/Store';
export function primaryColor() {
let brandInfo = useStore.getState().brandInfo;
export var primaryColor = '#2194f3';
export var primaryColorStyle = StyleSheet.create({});
function updatePrimaryColorInfo() {
const state = store.getState();
let brandInfo = state.brandInfo.value;
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({
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
// https://majidlotfinia.medium.com/openapi-generator-for-react-native-by-swagger-58847cadd9e8
import 'react-native-url-polyfill/auto';
import {strings} from '../localization/LocalizationStrings';
import { strings } from '../localization/LocalizationStrings';
import axios from 'axios';
import {useStore} from '../Store';
import {showGeneralError} from '../Utils';
import {AuthenticationApiFactory, Configuration as SecurityConfiguration} from './generated/owSecurityApi';
import {DevicesApiFactory, Configuration as GatewayConfiguration} from './generated/owGatewayApi';
import store from '../store/Store';
import { setSystemInfo } from '../store/SystemInfoSlice';
import { showGeneralError } from '../Utils';
import { AuthenticationApiFactory, Configuration as SecurityConfiguration } from './generated/owSecurityApi';
import { DevicesApiFactory, Configuration as GatewayConfiguration } from './generated/owGatewayApi';
const axiosInstance = axios.create({});
axiosInstance.interceptors.request.use(
config => {
const session = useStore.getState().session;
const state = store.getState();
const session = state.session.value;
if (session) {
config.headers.Authorization = 'Bearer ' + useStore.getState().session.access_token;
config.headers.Authorization = 'Bearer ' + session.access_token;
}
return config;
@@ -35,19 +37,19 @@ const authenticationApi = new AuthenticationApiFactory(
const gatewayConfig = new GatewayConfiguration();
var devicesApi = null;
function getDevicesApi() {
if (devicesApi === null) {
let url = getBaseUrlForApi('owgw');
devicesApi = url ? new DevicesApiFactory(gatewayConfig, url, axiosInstance) : null;
}
store.subscribe(generateDevicesApi);
generateDevicesApi();
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
// is needed in order to provide the proper base URIs for the other API systems.
function getBaseUrlForApi(type) {
const systemInfo = useStore.getState().systemInfo;
const state = store.getState();
const systemInfo = state.systemInfo.value;
if (systemInfo && systemInfo.endpoints) {
const endpoints = systemInfo.endpoints;
@@ -67,7 +69,7 @@ function getBaseUrlForApi(type) {
function setApiSystemInfo(systemInfo) {
// 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;
const typesToValidate = ['owgw']; // Include all API types that might be used
@@ -88,6 +90,8 @@ function setApiSystemInfo(systemInfo) {
}
function handleApiError(title, error) {
const state = store.getState();
const session = state.session.value;
let message = strings.errors.unknown;
if (error.response) {
@@ -99,7 +103,7 @@ function handleApiError(title, error) {
case 403:
console.error(error);
if (useStore.getState().session === null) {
if (session === null) {
// If not currently signed in then return a credentials error
message = strings.errors.credentials;
} else {
@@ -128,4 +132,4 @@ function handleApiError(title, error) {
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';
export class BrandItem extends Component {
render() {
return (
<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>
);
}
const BrandItem = props => {
const getCompanyIconUri = () => {
return props.brand.iconUri;
};
getCompanyIconUri() {
return this.props.brand.iconUri;
}
const getCompanyName = () => {
return props.brand.name;
};
getCompanyName() {
return this.props.brand.name;
}
}
return (
<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({
container: {
@@ -41,3 +39,5 @@ const brandItemStyle = StyleSheet.create({
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';
export class DeviceItem extends Component {
render() {
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() {
const DeviceItem = props => {
const getDeviceIcon = () => {
return require('../assets/server-solid.png');
}
};
getDeviceName() {
return this.props.device.compatible;
}
const getDeviceName = () => {
return props.device.compatible;
};
getDeviceType() {
return this.props.device.manufacturer;
}
const getDeviceType = () => {
return props.device.manufacturer;
};
getDeviceStatusIcon() {
const getDeviceStatusIcon = () => {
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({
container: {
@@ -63,3 +61,5 @@ const deviceItemStyle = StyleSheet.create({
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 { useStore } from '../Store';
import { pageStyle, pageItemStyle, primaryColor } from '../AppStyle';
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 {
state = {
loading: false,
brands: [
{
id: 'openwifi',
name: 'OpenWifi',
iconUri: 'https://14oranges-ui.arilia.com/assets/14Oranges_Logo.png',
primaryColor: '#19255f',
},
{
id: 'openwifigreen',
name: 'OpenWifi (Green)',
iconUri: 'https://14oranges-ui.arilia.com/assets/14Oranges_Logo.png',
primaryColor: '#1a3e1b',
},
],
filtered: false,
filteredBrands: [],
};
const BrandSelector = props => {
const dispatch = useDispatch();
const brandInfo = useSelector(selectBrandInfo);
// 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
const [loading, setLoading] = useState(false);
const [brands, setBrands] = useState([
{
id: 'openwifi',
name: 'OpenWifi',
iconUri: 'https://14oranges-ui.arilia.com/assets/14Oranges_Logo.png',
primaryColor: '#19255f',
},
{
id: 'openwifigreen',
name: 'OpenWifi (Green)',
iconUri: 'https://14oranges-ui.arilia.com/assets/14Oranges_Logo.png',
primaryColor: '#1a3e1b',
},
]);
const [filtered, setFiltered] = useState(false);
const [filteredBrands, setFilteredBrands] = useState();
componentDidMount() {
if (useStore.getState().brandInfo !== null) {
this.props.navigation.navigate('SignIn');
useEffect(() => {
if (brandInfo !== null) {
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() {
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 => {
const filterBrands = searchText => {
if (searchText) {
let searchTextLowerCase = searchText.toLowerCase();
this.setState({ filtered: true });
this.setState({
filteredBrands: this.state.brands.filter(b => b.name.toLowerCase().startsWith(searchTextLowerCase)),
});
setFiltered(true);
setFilteredBrands(brands.filter(b => b.name.toLowerCase().startsWith(searchTextLowerCase)));
} else {
this.setState({ filtered: false });
this.setState({ filteredBrands: [] });
setFiltered(false);
setFilteredBrands([]);
}
};
onCompanySelect = async item => {
useStore.getState().setBrandInfo(item);
const onCompanySelect = async item => {
dispatch(setBrandInfo(item));
// 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({
containerBrands: {
@@ -104,3 +105,5 @@ const brandingSelectorStyle = StyleSheet.create({
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 { View, Text } from 'react-native';
export default class DeviceDetails extends Component {
render() {
return (
<View style={pageStyle.container}>
<View style={pageItemStyle.container}>
<Text>Device Details</Text>
</View>
const DeviceDetails = props => {
return (
<View style={pageStyle.container}>
<View style={pageItemStyle.container}>
<Text>Device Details</Text>
</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 { pageStyle, pageItemStyle } from '../AppStyle';
import { View, Text, FlatList } from 'react-native';
import { getDevicesApi, handleApiError } from '../api/apiHandler';
import { DeviceItem } from '../components/DeviceItem';
import { devicesApi, handleApiError } from '../api/apiHandler';
import DeviceItem from '../components/DeviceItem';
export default class DeviceList extends Component {
state = { devices: [] };
const DeviceList = props => {
const [devices, setDevices] = useState([]);
render() {
return (
<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>
);
}
useEffect(() => {
getDevices();
}, []);
onDevicePress = async () => {
this.props.navigation.navigate('DeviceDetails');
const onDevicePress = async () => {
props.navigation.navigate('DeviceDetails');
};
componentDidMount = () => {
this.getDevices();
};
getDevices = async () => {
const getDevices = async () => {
try {
const response = await getDevicesApi().getDeviceList();
this.setState({ devices: response.data.devices });
const response = await devicesApi.getDeviceList();
setDevices(response.data.devices);
console.log(response.data);
} catch (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 { View, Text, TextInput, Button, ActivityIndicator, Alert } from 'react-native';
import { strings } from '../localization/LocalizationStrings';
import { authenticationApi, handleApiError } from '../api/apiHandler';
import { useStore } from '../Store';
export default class ForgotPassword extends Component {
state = {
email: '',
loading: false,
};
const ForgotPassword = props => {
const dispatch = useDispatch();
const [email, setEmail] = useState();
const [loading, setLoading] = useState(false);
render() {
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 validateEmail = () => {
const re = /\S+@\S+\.\S+/;
const valid = re.test(this.state.email);
const valid = re.test(email);
if (!valid) {
Alert.alert(strings.errors.titleForgotPassword, strings.errors.badEmail);
}
return valid;
}
};
const onSubmit = async () => {
if (validateEmail()) {
setLoading(true);
onSubmit = async () => {
if (this.validateEmail()) {
this.setState({ loading: true });
try {
useStore.getState().clearSession();
// Clear the session information
dispatch(clearSession());
const response = await authenticationApi.getAccessToken(
{
userId: this.state.email,
userId: email,
},
undefined,
true,
@@ -82,12 +39,53 @@ export default class ForgotPassword extends Component {
Alert.alert(strings.messages.message, strings.messages.resetEmail);
} catch (error) {
handleApiError(strings.errors.titleForgotPassword, error);
} finally {
setLoading(false);
}
this.setState({ loading: false });
}
};
backToSignin = () => {
this.props.navigation.replace('SignIn');
const backToSignin = () => {
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 { useStore } from '../Store';
import { useDispatch } from 'react-redux';
import { clearSession } from '../store/SessionSlice';
import { pageStyle, pageItemStyle, primaryColor } from '../AppStyle';
import { View, Button } from 'react-native';
export default class Profile extends Component {
render() {
return (
<View style={pageStyle.container}>
<View style={pageItemStyle.containerButton}>
<Button title={strings.buttons.signOut} color={primaryColor()} onPress={this.onSignOutPress} />
</View>
</View>
);
}
const Profile = props => {
const dispatch = useDispatch();
onSignOutPress = async () => {
// Clear the session information and go back to the sign in pageStyle
useStore.getState().clearSession();
this.props.navigation.replace('BrandSelector');
const onSignOutPress = async () => {
// Clear the session information and go back to the start
dispatch(clearSession());
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 { pageStyle, pageItemStyle, primaryColor } from "../AppStyle";
import { pageStyle, pageItemStyle, primaryColor } from '../AppStyle';
import { View, Text, TextInput, Button, Alert, ActivityIndicator } from 'react-native';
import { strings } from '../localization/LocalizationStrings';
import { authenticationApi, handleApiError } from '../api/apiHandler';
@@ -13,7 +13,7 @@ export default function ResetPassword(props) {
useEffect(() => {
console.log(userId, password);
}, []);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const onCancel = () => {
props.navigation.replace('SignIn');
@@ -46,7 +46,7 @@ export default function ResetPassword(props) {
const checkPassword = () => {
const valid = validatePassword(newPassword);
if(newPassword === password) {
if (newPassword === password) {
Alert.alert(strings.errors.titleResetPassword, strings.errors.samePassword);
return false;
}
@@ -54,14 +54,14 @@ export default function ResetPassword(props) {
Alert.alert(strings.errors.titleResetPassword, strings.errors.mismatchPassword);
return false;
}
if(!valid) {
if (!valid) {
Alert.alert(strings.errors.titleResetPassword, strings.errors.badFormat);
return false;
}
return valid && newPassword !== password && newPassword === confirmPassword;
};
const validatePassword = (password) => {
const validatePassword = password => {
const reg = /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/;
return reg.test(password);
};
@@ -102,17 +102,13 @@ export default function ResetPassword(props) {
<View style={pageItemStyle.containerButton}>
<Button
title={strings.buttons.submit}
color={primaryColor()}
color={primaryColor}
onPress={onSubmit}
disabled={loading || !newPassword || !confirmPassword}
/>
</View>
<View style={pageItemStyle.containerButton}>
<Button
title={strings.buttons.cancel}
color={primaryColor()}
onPress={onCancel}
disabled={loading} />
<Button title={strings.buttons.cancel} color={primaryColor} onPress={onCancel} disabled={loading} />
</View>
<View style={pageItemStyle.container}>
<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 { useStore } from '../Store';
import { pageStyle, pageItemStyle, primaryColor, primaryColorStyle } from '../AppStyle';
import { StyleSheet, Text, View, Image, Button, TextInput, ActivityIndicator } from 'react-native';
import { handleApiError, authenticationApi, setApiSystemInfo } from '../api/apiHandler';
export default class SignIn extends Component {
state = {
email: '',
password: '',
loading: false,
};
const SignIn = props => {
const dispatch = useDispatch();
const brandInfo = useSelector(selectBrandInfo);
const [email, setEmail] = useState();
const [password, setPassword] = useState();
const [loading, setLoading] = useState(false);
const passwordRef = createRef();
constructor(props) {
super(props);
this.passwordRef = React.createRef();
}
componentDidMount() {
useEffect(() => {
// If the brand is not selected, then resort back to the brand selector
if (useStore.getState().brandInfo === null) {
this.props.navigation.replace('BrandSelector');
if (brandInfo === null) {
props.navigation.replace('BrandSelector');
}
}
});
render() {
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 () => {
const onSignInPress = async () => {
try {
this.setState({ loading: true });
setLoading(true);
// 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({
userId: this.state.email,
password: this.state.password,
userId: email,
password: password,
});
useStore.getState().setSession(response.data);
dispatch(setSession(response.data));
// must reset password
console.log(JSON.stringify(response.data, null, '\t'));
if (response.data.userMustChangePassword) {
this.props.navigation.navigate('ResetPassword', {
userId: this.state.email,
password: this.state.password,
props.navigation.navigate('ResetPassword', {
userId: email,
password: password,
});
} else {
// Update the system endpoints and navigate to the main view.
this.getSystemEndpointsNavigateToMain();
getSystemEndpointsNavigateToMain();
}
} catch (error) {
// Clear the loading state
this.setState({ loading: false });
setLoading(false);
handleApiError(strings.errors.titleSignIn, error);
}
};
getSystemEndpointsNavigateToMain = async () => {
const getSystemEndpointsNavigateToMain = async () => {
try {
// 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
@@ -129,19 +66,74 @@ export default class SignIn extends Component {
setApiSystemInfo(response.data);
// Replace to the main screen. Use replace to ensure no back button
this.props.navigation.replace('Main');
props.navigation.replace('Main');
} catch (error) {
// Make sure the loading state is done in all cases
this.setState({ loading: false });
setLoading(false);
handleApiError(strings.errors.titleSystemSetup, error);
}
};
onForgotPasswordPress = async () => {
this.props.navigation.navigate('ForgotPassword');
const onForgotPasswordPress = async () => {
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({
containerForm: {
@@ -160,3 +152,5 @@ const signInStyle = StyleSheet.create({
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