Dunja Djokic пре 4 година
комит
dc42a97376

+ 4
- 0
.expo-shared/assets.json Прегледај датотеку

@@ -0,0 +1,4 @@
{
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
}

+ 13
- 0
.gitignore Прегледај датотеку

@@ -0,0 +1,13 @@
node_modules/
.expo/
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/

# macOS
.DS_Store

+ 60
- 0
App.js Прегледај датотеку

@@ -0,0 +1,60 @@
import 'react-native-gesture-handler';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { Provider } from 'react-redux';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import store from "./store";
// import { Counter } from './screens/Counter'
import { LogIn } from './screens/LogIn';
import { createStackNavigator } from '@react-navigation/stack';


const A = () => <View><Text>Home</Text></View>
const B = () => <View><Text>Settings</Text></View>
const C = () => <View><Text>C</Text></View>

const Tab = createBottomTabNavigator();
const Stack = createStackNavigator();

const Home = () => {
return (
<Tab.Navigator>
<Tab.Screen name="A" component={A} />
<Tab.Screen name="B" component={B} />
</Tab.Navigator>
)
}

function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="Profile" component={C} />
</Stack.Navigator>
</NavigationContainer>
);
}

const AppWrapper = () => {
return (
<Provider store={store}>
<SafeAreaProvider>
<App />
</SafeAreaProvider>
</Provider>
);
};


const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
export default AppWrapper;

+ 32
- 0
app.json Прегледај датотеку

@@ -0,0 +1,32 @@
{
"expo": {
"name": "native",
"slug": "native",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#FFFFFF"
}
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}

BIN
assets/adaptive-icon.png Прегледај датотеку


BIN
assets/favicon.png Прегледај датотеку



BIN
assets/splash.png Прегледај датотеку


+ 6
- 0
babel.config.js Прегледај датотеку

@@ -0,0 +1,6 @@
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
};
};

+ 40
- 0
components/Buttons/Button.jsx Прегледај датотеку

@@ -0,0 +1,40 @@
import React from 'react'
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
import { heightPercentageToDP as hp, widthPercentageToDP as wp } from "react-native-responsive-screen";

export default Button = ({ onPress, title, style }) => {
return (
<TouchableOpacity
onPress={onPress}
style={[{ paddingHorizontal: hp("2.6%"), justifyContent: "space-between" }, styles.widgetBox, style]}
>
<View style={[styles.alignCenter, styles.row]}>
<Text trunk={true} numberOfLines={2} style={{ color: "#4C5A81", fontSize: hp("2.3%"), textAlign: 'center' }}>{title}</Text>
</View>
</TouchableOpacity>
);
}

const styles = StyleSheet.create({
alignCenter: {
alignItems: "center",
},
row: {
flexDirection: "row",
},
text: {
color: "#4C5A81",
fontSize: hp("2.3%"),
paddingLeft: wp("5%"),
width: wp("60%"),
},
widgetBox: {
backgroundColor: "#fff",
borderRadius: wp("3%"),
marginVertical: hp("1.3%"),
paddingVertical: hp("2%"),
position: "relative",
flexDirection: "row",
alignItems: "center",
},
});

+ 16
- 0
metro.config.js Прегледај датотеку

@@ -0,0 +1,16 @@
const { getDefaultConfig } = require("expo/metro-config");

module.exports = (async () => {
const {
resolver: { sourceExts, assetExts }
} = await getDefaultConfig(__dirname);
return {
transformer: {
babelTransformerPath: require.resolve("react-native-svg-transformer")
},
resolver: {
assetExts: assetExts.filter(ext => ext !== "svg"),
sourceExts: [...sourceExts, "svg"]
}
};
})();

+ 20921
- 0
package-lock.json
Разлика између датотеке није приказан због своје велике величине
Прегледај датотеку


+ 37
- 0
package.json Прегледај датотеку

@@ -0,0 +1,37 @@
{
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"eject": "expo eject"
},
"dependencies": {
"@react-native-community/masked-view": "0.1.10",
"@react-navigation/bottom-tabs": "^5.11.11",
"@react-navigation/native": "^5.9.4",
"@reduxjs/toolkit": "^1.5.1",
"axios": "^0.21.1",
"expo": "~41.0.1",
"expo-status-bar": "~1.0.4",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-native": "https://github.com/expo/react-native/archive/sdk-41.0.0.tar.gz",
"react-native-gesture-handler": "~1.10.2",
"react-native-reanimated": "~2.1.0",
"react-native-responsive-screen": "^1.4.2",
"react-native-safe-area-context": "3.2.0",
"react-native-screens": "~3.0.0",
"react-native-svg": "12.1.0",
"react-native-svg-transformer": "^0.14.3",
"react-native-web": "~0.13.12",
"react-navigation-stack": "^2.10.4",
"react-redux": "^7.2.4",
"redux-thunk": "^2.3.0"
},
"devDependencies": {
"@babel/core": "^7.9.0"
},
"private": true
}

+ 47
- 0
precheck.js Прегледај датотеку

@@ -0,0 +1,47 @@
const npmCheck = require('npm-check')
const compareVersions = require('compare-versions');
const exec = require('child_process').exec;

npmCheck()
.then(currentState => {
packages = currentState.get('packages')
packages.forEach(p => {
const { moduleName, latest, installed } = p
if (moduleName === 'expo' && compareVersions(latest, installed) >= 1) {
console.log('\x1b[31m', `Latest version of expo is ${latest}, but you are running ${installed}. Please run: expo upgrade`, '\x1b[0m')
}
})
})
.then(() => new Promise((resolve) => {
exec('node -v', (err, nodeVersion, stdErr) => {
nodeVersion = nodeVersion.trim().substring(1)
const supportedNode = '14.17.0'
if (compareVersions(supportedNode, nodeVersion) >= 1) {
console.log('\x1b[31m', `You should have node version ${supportedNode} or later. Please download the latest version`, '\x1b[0m')
process.exit(1)
}
resolve()
})
}))
.then(() => new Promise((resolve) => {
exec('npm -v', (err, npmVersion, stdErr) => {
npmVersion = npmVersion.trim()
const supportedNpm = '7.16.0'
if (compareVersions(supportedNpm, npmVersion) >= 1) {
console.log('\x1b[31m', `You should have npm version ${supportedNpm} or later. Please run: npm install -g npm@latest`, '\x1b[0m')
process.exit(1)
}
resolve()
})
}))
.then(() => new Promise((resolve) => {
exec('expo-cli --version', (err, expoCliVersion, stdErr) => {
const supportedExpoCli = '4.5.2'
expoCliVersion = expoCliVersion.trim()
if (err || compareVersions(supportedExpoCli, expoCliVersion) >= 1) {
console.log('\x1b[31m', `You should have globally installed expo-cli version ${supportedExpoCli} or later. Please run: npm i -g expo-cli@latest`, '\x1b[0m')
process.exit(1)
}
resolve()
})
}))

+ 49
- 0
screens/Counter.jsx Прегледај датотеку

@@ -0,0 +1,49 @@
import React from "react";
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
import { useDispatch, useSelector } from "react-redux";
import { decrement, increment } from "../store/actions";

export const Counter = () => {
const dispatch = useDispatch()
const value = useSelector(s => s.test.value)

return (
<View style={[{ width: '75%' }]}>
<Text style={[styles.textAlignCenter]}>{value}</Text>
<View style={[styles.row, styles.justifySpaceBetween]}>
<TouchableOpacity style={[styles.button, styles.justifyCenter]} onPress={() => dispatch(increment())}>
<Text style={[styles.textAlignCenter]}>Increment</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.button, styles.justifyCenter]} onPress={() => dispatch(decrement())}>
<Text style={[styles.textAlignCenter]}>Decrement</Text>
</TouchableOpacity>
</View>
</View>
)
}

const styles = StyleSheet.create({
row: {
flexDirection: "row",
},
flex: {
flex: 1
},
justifySpaceBetween: {
justifyContent: 'space-between'
},
button: {
backgroundColor: "#D9F1EC",
padding: '3%',
marginTop: '10%'
},
textAlignCenter: {
textAlign: 'center'
},
justifyCenter: {
justifyContent: 'center'
},
alignCenter: {
alignItems: 'center'
}
})

+ 33
- 0
screens/LogIn.jsx Прегледај датотеку

@@ -0,0 +1,33 @@
import React from "react";
import { useState } from "react";
import { View, TextInput } from "react-native";
import Button from "../components/Buttons/Button";
import { login } from "../thunks/user.thunk";

export const LogIn = () => {

const [username, setUsername] = useState()
const [password, setPassword] = useState()
const [error, setError] = useState()

return (
<View>
<TextInput focus={true}
style={{ marginTop: 20 }}
placeholder='Username' />
<TextInput focus={true}
style={{ marginTop: 20 }}
placeholder='Password'
secureTextEntry />
<Button onPress={() => dispatch(
login(username, password, function (result) {
if (!result.OK) {
setUsername("");
setPassword("");
setError(result.data.Message ? result.data.Message : "Error occurred during Login,please try again!");
}
})
)} title='Log In' style={{ backgroundColor: 'red' }} />
</View>
)
}

+ 1
- 0
service/endpointDef.js Прегледај датотеку

@@ -0,0 +1 @@
export const API_ENDPOINT = __DEV__ ? "https://mystratadevapi.kalla.co/" : "https://my.kalla.co/";

+ 111
- 0
service/index.js Прегледај датотеку

@@ -0,0 +1,111 @@
import axios from "axios";
import { setConnectionError } from "../store/actions";
import store from "../store/store";
import { API_ENDPOINT } from './endpointDef';
import { refreshTokens } from "./tokenService/tokenApiClient";

const axiosApiInstance = axios.create();
const globalLog = false;

const defaultOptions = { log: false }

export const Get = (url, options) => {
return request("GET", url, { ...defaultOptions, ...options });
}

export const Post = (url, data, options) => {
return request("POST", url, { ...defaultOptions, ...options, data });
}

export const Put = (url, data, options) => {
return request("PUT", url, { ...defaultOptions, ...options, data });
}

export const Delete = (url, options) => {
return request("DELETE", url, { ...defaultOptions, ...options });
}

const isLogging = (log) => {
return (globalLog || log);
}

const request = (method, url, options) => {
const { data, log } = options;
if (isLogging(log)) console.log("REQUEST URL : ", url);
const requestObject = {
method,
url: API_ENDPOINT + url,
timeout: 60000
}
if (data) {
if (isLogging(log)) console.log(`DATA FOR : ${url}`, data);
requestObject.data = data;
}
else {
requestObject.data = undefined;
}
// console.log("url", url)
return axiosApiInstance(requestObject).then((response) => {
if (isLogging(log)) console.log(`RESPONSE for ${url} : `, response);

return { ...response, OK: true };
})
.catch((error) => {
if (isLogging(log)) console.log(`RESPONSE for catch ${url} : `, (error.response ? error.response : error));

//if we get a request timeout error
if (!error.response) {
store.dispatch(setConnectionError(error));
return { error, OK: false };
}

//if we get a response other than OK
if (error.response) {
return { ...error.response, OK: false };
}

return { OK: false };
})
}

axiosApiInstance.interceptors.request.use(
(config) => {
// console.log('sending request')
const accessToken = store.getState().user.token;
config.headers = {
"Authorization": `Bearer ${accessToken}`,
"Accept": "application/json",
"Content-Type": "application/json",
"Referer": config.url
};
return config;
},
(error) => {
Promise.reject(error);
}
);

axiosApiInstance.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
// console.log('********************************************************************', error)
const originalRequest = error.config
if (originalRequest._retried) {
console.log("Request second attempt failed.")
}
if (!originalRequest._retried && error.response && error.response.status === 401) {
try {
await refreshTokens()
}
catch (e) {
throw error;
}
return await axiosApiInstance({ ...originalRequest, _retried: true })
}
throw error;
}
);

export default axiosApiInstance;

+ 22
- 0
service/tokenService/index.js Прегледај датотеку

@@ -0,0 +1,22 @@
import store from '../../store/store';
import { refreshTokens } from './tokenApiClient'
import { validateAccessToken } from './validator'

let lock;

export async function getAccessToken() {
if (lock) {
await lock;
}
else {
lock = new Promise((resolve, reject) => {

})
}
if (!validateAccessToken()) {
console.log("REFRESHING TOKEN")
await refreshTokens();

}
return store.getState().user.token
}

+ 36
- 0
service/tokenService/tokenApiClient.js Прегледај датотеку

@@ -0,0 +1,36 @@
import axios from "axios";
import { batchActions } from 'redux-batched-actions';
import { logOut, setRefreshToken, setToken } from "../../store/actions";
import store from "../../store/store";
import { API_ENDPOINT } from '../endpointDef';

let refreshingPromise = null

export async function refreshTokens() {
// console.log("RefreshTokens")
if (!refreshingPromise) {
refreshingPromise = new Promise(async (resolve, reject) => {
// console.log("Sending the requst for new tokens")
const refreshToken = store.getState().user.refreshToken
const username = store.getState().user.username
try {
const { data } = await axios.post(`${API_ENDPOINT}api/token/refresh`, { Token: refreshToken, Username: username, });
// console.log("Dispatch from refresh", data.Data.AccessToken, data.Data.RefreshToken)
store.dispatch(batchActions([setToken(data.Data.AccessToken), setRefreshToken(data.Data.RefreshToken)]))
resolve()
}
catch (e) {
// console.log("Could not refresh token.")
store.dispatch(logOut())
reject(e)
}
finally {
refreshingPromise = null
}
})
}
// else {
// console.log("Someone is refreshing...")
// }
await refreshingPromise
}

+ 17
- 0
service/tokenService/validator.js Прегледај датотеку

@@ -0,0 +1,17 @@
import store from "../../store/store";
import jwt_decode from "jwt-decode";

export function validateAccessToken() {
const accessToken = store.getState().user.token
if (!accessToken) {
return false;
}
const decoded = jwt_decode(accessToken);
// console.log("DECODED", decoded)
const exp = decoded.exp * 1000
const isExpired = exp - (Date.now() + 60 * 1000)
// console.log("isExpired", isExpired)
if (isExpired <= 0)
return false
return true
}

+ 6
- 0
service/user.js Прегледај датотеку

@@ -0,0 +1,6 @@
import { Post } from './index';

export const loginAPI = async (username, password) => {
const body = JSON.stringify({ Username: username, Password: password });
return Post(`api/login`, body);
};

+ 14
- 0
store/actions/index.js Прегледај датотеку

@@ -0,0 +1,14 @@
import { createAction } from "@reduxjs/toolkit";

const createTestAction = (name) => createAction(`TEST_${name}`)
export const increment = createTestAction('INCREMENT')
export const decrement = createTestAction('DECREMENT')

const createUserAction = (name) => createAction(`USER_${name}`)
export const setToken = createUserAction('TOKEN')
export const setRefreshToken = createUserAction('REFRESHTOKEN')
export const authLoaded = createUserAction('AUTH_LOADED')
export const logOut = createUserAction('LOGOUT')
export const setUsername = createUserAction('USERNAME')
export const logInSuccess = createUserAction('LOGIN_SUCCESS')
export const setConnectionError = createUserAction('SET_CONNECTION_ERROR')

+ 79
- 0
store/authPersistor.js Прегледај датотеку

@@ -0,0 +1,79 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { batchActions } from 'redux-batched-actions';
import { authLoaded, setRefreshToken, setToken, setUsername } from './actions'

// AsyncStorage.getAllKeys()
// .then((keys) => AsyncStorage.multiGet(keys)
// .then((data) => console.log(data)));

async function safeSetStorage(key, value) {
if (value) {
await AsyncStorage.setItem(key, value)
}
else {
await AsyncStorage.removeItem(key)
}
}

const accessTokenKey = 'userToken'
const refreshTokenKey = 'refreshToken'
const userNameKey = 'userName'

function subscibeOnAuthChanges(store) {
let lastAccessToken = store.getState().user.token;
let lastRefreshToken = store.getState().user.refreshToken;
let lastUsername = store.getState().user.username;
// console.log("In subscribe function", lastAccessToken, lastRefreshToken, lastUsername)

store.subscribe(async () => {
const state = store.getState();
// console.log("In subscribe...")
const newAccessToken = state.user.token;
const newRefreshToken = state.user.refreshToken;
const newUsername = state.user.username;
// console.log("In subscribe", newAccessToken, newRefreshToken, newUsername)

if (lastAccessToken !== newAccessToken) {
lastAccessToken = newAccessToken
await safeSetStorage(accessTokenKey, newAccessToken)
}

if (lastRefreshToken !== newRefreshToken) {
lastRefreshToken = newRefreshToken
await safeSetStorage(refreshTokenKey, newRefreshToken)
}

if (lastUsername !== newUsername) {
lastUsername = newUsername
await safeSetStorage(userNameKey, newUsername)
}
})
}

async function loadFromStorage(store) {
try {
// leave comments this for testing
// console.log("getting Token from storage");
// await AsyncStorage.setItem(accessTokenKey, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI4MmVlNjNkMi0yZDRjLTRkZjQtYTQ0MS1hZWRkZjhiNGEwMmYiLCJzdWIiOiJpbGlqYSt1Y2lAcHJvZml0b3B0aWNzLmNvbSIsInVuaXF1ZV9uYW1lIjoiaWxpamErdWNpQHByb2ZpdG9wdGljcy5jb20iLCJVdGNPZmZzZXRNaW51dGVzIjoiLTI0MCIsIkFncmVlbWVudEFjY2VwdGVkIjoiVHJ1ZSIsIk9yZ2FuaXphdGlvbklkIjoiMTkiLCJPcmdhbml6YXRpb25Ib3N0bmFtZSI6Im15c3RyYXRhZGV2Mi5wcm9maXRvcHRpY3MuY29tIiwibmJmIjoxNjE2Njk0NTI2LCJleHAiOjE2MTY2OTYzMjYsImlzcyI6InN0cmF0YWNsZWFyLmNvbSIsImF1ZCI6IkNsaWVudCJ9.kk0TQENidNJfQpkLy6uVUZULHp1r46gA7IyhAMHkqsI");
userToken = await AsyncStorage.getItem(accessTokenKey);
// leave this for testing
// await AsyncStorage.setItem(refreshTokenKey, "IBFRYNvviwI0+C3CVy1sViQlvFDTXsTUZWa8fgzv6oLQtYTng3A+8xc4gMqpX+y1lR8WIhN4uDlEV19uu+prWEVpi8qkjp999Li0t+eiEoe7OdeK+vQwecowvvJZRt1WK/Qb3biMC76IhElZta/riy2yZ9jQRBhiEfPs+yK+GJI=");
// await AsyncStorage.setItem(refreshTokenKey, "TV+VsT+Qwb9hh1oi8mG4CBX0gnN9y9aNfXT/QUPZsPwCJ3n1svOO/KwT14Z5UZxhU+8h5ocuvipj4dIGDJa7wJHkV7j5/mBKfSgVqMvX43VdFXkp19Dg0h4Tg9Elh6GpR0N6FSCDJl3igv83W3OpC4UWXylQKW+0gr+tjOPmT78=");
refreshToken = await AsyncStorage.getItem(refreshTokenKey);
// leave this for testing
// await AsyncStorage.setItem(userNameKey, "ilija+uci@profitoptics.com");
userName = await AsyncStorage.getItem(userNameKey);

// console.log('Loaded from storage', userToken)
store.dispatch(batchActions([setToken(userToken), setRefreshToken(refreshToken), setUsername(userName), authLoaded()]))
} catch (eror) {
console.error("There was an error getting token from storage", eror);
}
}

function init(store) {
loadFromStorage(store)
.then(() => subscibeOnAuthChanges(store))
}

export { init };

+ 9
- 0
store/index.js Прегледај датотеку

@@ -0,0 +1,9 @@
import { createStore, applyMiddleware } from "redux";
import { enableBatching } from 'redux-batched-actions';
import thunk from "redux-thunk";
import reducers from "./reducers";
import { init } from './authPersistor'

const store = createStore(enableBatching(reducers), applyMiddleware(thunk));
init(store)
export default store;

+ 12
- 0
store/reducers/index.js Прегледај датотеку

@@ -0,0 +1,12 @@
import { combineReducers } from "redux";
// import { persistReducer } from "redux-persist";
// import immutableTransform from "redux-persist-transform-immutable";
import test from "./test";
import user from "./user";

const rootReducer = combineReducers({
test,
user
});

export default rootReducer;

+ 14
- 0
store/reducers/test.js Прегледај датотеку

@@ -0,0 +1,14 @@
import { createReducer } from '@reduxjs/toolkit'
import { increment, decrement } from '../actions/index'

export default createReducer({
value: 0
}, builder => {
builder
.addCase(increment, (state) => {
state.value += 1
})
.addCase(decrement, (state) => {
state.value -= 1
})
})

+ 36
- 0
store/reducers/user.js Прегледај датотеку

@@ -0,0 +1,36 @@
import { setToken, setRefreshToken, authLoaded, logOut, setUsername, logInSuccess, setConnectionError } from "../actions/index";


export default createReducer({
token: null,
refreshToken: null,
username: "",
logInSuccess: "",
connectionError: "",
isAuthLoadedFromStorage: false
}, builder => {
builder
.addCase(setToken, (state, { payload }) => {
state.token = payload
})
.addCase(setRefreshToken, (state, { payload }) => {
state.refreshToken = payload
})
.addCase(authLoaded, (state) => {
state.isAuthLoadedFromStorage = true
})
.addCase(logOut, (state) => {
state.username = null;
state.token = null;
state.refreshToken = null;
})
.addCase(setUsername, (state, { payload }) => {
state.username = payload
})
.addCase(logInSuccess, (state, { payload }) => {
state.logInSuccess = payload
})
.addCase(setConnectionError, (state) => {
state.connectionError = payload
})
})

+ 17
- 0
thunks/user.thunk.js Прегледај датотеку

@@ -0,0 +1,17 @@
import { loginAPI, } from "../service/user";

export const login = (username, password, callback) => {
return (dispatch) =>
loginAPI(username, password)
.then((responseJson) => {
if (responseJson.OK && responseJson.data.Data.AccessToken != null) {
dispatch(batchActions([
setToken(responseJson.data.Data.AccessToken),
setRefreshToken(responseJson.data.Data.RefreshToken),
setUsername(username)]))
} else callback(responseJson);
})
.catch((error) => {
console.log("error", error);
});
};

Loading…
Откажи
Сачувај