feat: add QRCode scan feature (#3)
* feat: add QRScan code. * fix: remove 'casdoor:' * fix: add stricter eslint. * fix: use npmjs.
This commit is contained in:
parent
39674fa9c7
commit
8de464cd9a
|
@ -0,0 +1 @@
|
|||
node_modules
|
|
@ -0,0 +1,112 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"parser": "@babel/eslint-parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"requireConfigFile": false,
|
||||
"babelOptions": {
|
||||
"babelrc": false,
|
||||
"configFile": false,
|
||||
"presets": ["@babel/preset-react"]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"plugins": ["unused-imports"],
|
||||
"extends": ["eslint:recommended", "plugin:react/recommended"],
|
||||
"rules": {
|
||||
"semi": ["error", "always"],
|
||||
"indent": ["error", 2],
|
||||
// follow antd's style guide
|
||||
"quotes": ["error", "double"],
|
||||
"jsx-quotes": ["error", "prefer-double"],
|
||||
"space-in-parens": ["error", "never"],
|
||||
"object-curly-spacing": ["error", "never"],
|
||||
"array-bracket-spacing": ["error", "never"],
|
||||
"comma-spacing": ["error", { "before": false, "after": true }],
|
||||
"react/jsx-curly-spacing": [
|
||||
"error",
|
||||
{ "when": "never", "allowMultiline": true, "children": true }
|
||||
],
|
||||
"arrow-spacing": ["error", { "before": true, "after": true }],
|
||||
"space-before-blocks": ["error", "always"],
|
||||
"spaced-comment": ["error", "always"],
|
||||
"react/jsx-tag-spacing": ["error", { "beforeSelfClosing": "always" }],
|
||||
"block-spacing": ["error", "never"],
|
||||
"space-before-function-paren": ["error", "never"],
|
||||
"no-trailing-spaces": ["error", { "ignoreComments": true }],
|
||||
"eol-last": ["error", "always"],
|
||||
"no-var": ["error"],
|
||||
"prefer-const": [
|
||||
"error",
|
||||
{
|
||||
"destructuring": "all"
|
||||
}
|
||||
],
|
||||
"curly": ["error", "all"],
|
||||
"brace-style": ["error", "1tbs", { "allowSingleLine": true }],
|
||||
"no-mixed-spaces-and-tabs": "error",
|
||||
"sort-imports": [
|
||||
"error",
|
||||
{
|
||||
"ignoreDeclarationSort": true
|
||||
}
|
||||
],
|
||||
"no-multiple-empty-lines": [
|
||||
"error",
|
||||
{ "max": 1, "maxBOF": 0, "maxEOF": 0 }
|
||||
],
|
||||
"space-unary-ops": ["error", { "words": true, "nonwords": false }],
|
||||
"space-infix-ops": "error",
|
||||
"key-spacing": ["error", { "beforeColon": false, "afterColon": true }],
|
||||
"comma-style": ["error", "last"],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
{
|
||||
"arrays": "always-multiline",
|
||||
"objects": "always-multiline",
|
||||
"imports": "never",
|
||||
"exports": "never",
|
||||
"functions": "never"
|
||||
}
|
||||
],
|
||||
"no-multi-spaces": ["error", { "ignoreEOLComments": true }],
|
||||
"react/no-unknown-property": [
|
||||
"error",
|
||||
{
|
||||
"ignore": ["css"]
|
||||
}
|
||||
],
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "none",
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"no-unused-vars": "off",
|
||||
"react/no-deprecated": "error",
|
||||
"react/jsx-key": "error",
|
||||
"no-console": "error",
|
||||
"eqeqeq": "error",
|
||||
"keyword-spacing": "error",
|
||||
"react/prop-types": "off",
|
||||
"react/display-name": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"no-case-declarations": "off"
|
||||
}
|
||||
}
|
21
Account.js
21
Account.js
|
@ -12,10 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
|
||||
import {default as hotptotp} from 'hotp-totp';
|
||||
const {totp} = hotptotp;
|
||||
window.Buffer = window.Buffer || require("buffer").Buffer;
|
||||
import totp from "totp-generator";
|
||||
|
||||
class Account {
|
||||
constructor(description, secretCode, onUpdate) {
|
||||
|
@ -23,27 +20,29 @@ class Account {
|
|||
this.secretCode = secretCode;
|
||||
this.countdowns = 30;
|
||||
this.timer = setInterval(this.updateCountdown.bind(this), 1000);
|
||||
this.token = '';
|
||||
this.tokenInterval = setInterval(this.generateAndSetToken.bind(this), 30000);
|
||||
this.token = "";
|
||||
this.onUpdate = onUpdate;
|
||||
}
|
||||
|
||||
generateToken = async () => {
|
||||
let token = await totp(this.secretCode);
|
||||
generateToken() {
|
||||
if (this.secretCode !== null && this.secretCode !== undefined && this.secretCode !== "") {
|
||||
const token = totp(this.secretCode);
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
generateAndSetToken = async () => {
|
||||
this.token = await this.generateToken();
|
||||
generateAndSetToken() {
|
||||
this.token = this.generateToken();
|
||||
this.onUpdate();
|
||||
}
|
||||
|
||||
updateCountdown() {
|
||||
this.countdowns = Math.max(0, this.countdowns - 1);
|
||||
if (this.countdowns === 0) {
|
||||
this.generateAndSetToken();
|
||||
this.countdowns = 30;
|
||||
this.onUpdate();
|
||||
}
|
||||
this.onUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
6
App.js
6
App.js
|
@ -12,10 +12,10 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import * as React from 'react';
|
||||
import {PaperProvider} from 'react-native-paper';
|
||||
import * as React from "react";
|
||||
import {PaperProvider} from "react-native-paper";
|
||||
import NavigationBar from "./NavigationBar";
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import {NavigationContainer} from "@react-navigation/native";
|
||||
import Header from "./Header";
|
||||
|
||||
export default function App() {
|
||||
|
|
|
@ -12,61 +12,100 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, TextInput } from 'react-native';
|
||||
import { Button, IconButton } from 'react-native-paper';
|
||||
import React, {useState} from "react";
|
||||
import {Text, TextInput, View} from "react-native";
|
||||
import {Button, Divider, IconButton, Menu} from "react-native-paper";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
export default function EnterAccountDetails({onClose, onAdd}) {
|
||||
const [description, setDescription] = useState('');
|
||||
const [secretCode, setSecretCode] = useState('');
|
||||
EnterAccountDetails.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const [description, setDescription] = useState("");
|
||||
const [secretCode, setSecretCode] = useState("");
|
||||
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const openMenu = () => setVisible(true);
|
||||
const closeMenu = () => setVisible(false);
|
||||
const [selectedItem, setSelectedItem] = useState("Time based");
|
||||
|
||||
const handleMenuItemPress = (item) => {
|
||||
setSelectedItem(item);
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
const handleAddAccount = () => {
|
||||
onAdd({description, secretCode});
|
||||
setDescription('');
|
||||
setSecretCode('');
|
||||
setDescription("");
|
||||
setSecretCode("");
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<View style={{flex: 1, justifyContent: "center", alignItems: "center"}}>
|
||||
<Text style={{fontSize: 24, marginBottom: 5}}>Add new 2FA account</Text>
|
||||
<div style={{display: 'flex', marginTop: 10}}>
|
||||
<IconButton icon='account-details' size={35}></IconButton>
|
||||
<View style={{flexDirection: "row", alignItems: "center"}}>
|
||||
<IconButton icon="account-details" size={35} />
|
||||
<TextInput
|
||||
placeholder='Description'
|
||||
placeholder="Description"
|
||||
value={description}
|
||||
onChangeText={(text) => setDescription(text)}
|
||||
style={{ borderWidth: 3, borderColor: 'white', margin: 10, width: 230, height: 50, borderRadius: 5, fontSize: 18,
|
||||
color: 'gray', paddingLeft: 10}}
|
||||
style={{borderWidth: 3, borderColor: "white", margin: 10, width: 230, height: 50, borderRadius: 5, fontSize: 18, color: "gray", paddingLeft: 10}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{display: 'flex'}}>
|
||||
<IconButton icon='account-key' size={35}></IconButton>
|
||||
</View>
|
||||
|
||||
<View style={{flexDirection: "row", alignItems: "center"}}>
|
||||
<IconButton icon="account-key" size={35} />
|
||||
<TextInput
|
||||
placeholder='Secret code'
|
||||
placeholder="Secret code"
|
||||
value={secretCode}
|
||||
onChangeText={(text) => setSecretCode(text)}
|
||||
secureTextEntry
|
||||
style={{ borderWidth: 3, borderColor: 'white', margin: 10, width: 230, height: 50, borderRadius: 5, fontSize: 18,
|
||||
color: 'gray', paddingLeft: 10 }}
|
||||
style={{borderWidth: 3, borderColor: "white", margin: 10, width: 230, height: 50, borderRadius: 5, fontSize: 18, color: "gray", paddingLeft: 10}}
|
||||
/>
|
||||
</div>
|
||||
</View>
|
||||
<Button
|
||||
icon='account-plus'
|
||||
icon="account-plus"
|
||||
style={{
|
||||
backgroundColor: '#393544',
|
||||
backgroundColor: "#E6DFF3",
|
||||
borderRadius: 5,
|
||||
margin: 10,
|
||||
alignItems: 'center',
|
||||
position: 'absolute',
|
||||
top: 260,
|
||||
width: 300,
|
||||
// height: 50
|
||||
alignItems: "center",
|
||||
position: "absolute",
|
||||
top: 230,
|
||||
right: 30,
|
||||
width: 90,
|
||||
}}
|
||||
onPress={handleAddAccount}
|
||||
>
|
||||
<Text style={{fontSize: 18}}>Add</Text>
|
||||
</Button>
|
||||
<IconButton icon={'close'} size={30} onPress={onClose} style={{position: 'absolute', top: 5, right: 5}} />
|
||||
<IconButton icon={"close"} size={30} onPress={onClose} style={{position: "absolute", top: 5, right: 5}} />
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#E6DFF3",
|
||||
borderRadius: 5,
|
||||
position: "absolute",
|
||||
left: 30,
|
||||
top: 240,
|
||||
width: 140,
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
visible={visible}
|
||||
onDismiss={closeMenu}
|
||||
anchor={
|
||||
<Button style={{alignItems: "left"}} icon={"chevron-down"} onPress={openMenu}>
|
||||
{selectedItem}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Menu.Item onPress={() => handleMenuItemPress("Time based")} title="Time based" />
|
||||
<Divider />
|
||||
<Menu.Item onPress={() => handleMenuItemPress("Counter based")} title="Counter based" />
|
||||
</Menu>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,14 +12,14 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import * as React from 'react';
|
||||
import {Appbar, Avatar, Text} from 'react-native-paper';
|
||||
import * as React from "react";
|
||||
import {Appbar, Avatar, Text} from "react-native-paper";
|
||||
|
||||
const Header = () => (
|
||||
<Appbar.Header>
|
||||
<Appbar.Content title="Casdoor" />
|
||||
<Avatar.Image size={32} style={{marginRight: '10px', backgroundColor: 'white'}} source={'https://cdn.casbin.com/casdoor/avatar/built-in/admin.jpeg'} />
|
||||
<Text style={{marginRight: '10px'}} variant="titleMedium">Admin</Text>
|
||||
<Avatar.Image size={32} style={{marginRight: 10, backgroundColor: "white"}} source={{uri: "https://cdn.casbin.com/casdoor/avatar/built-in/admin.jpeg"}} />
|
||||
<Text style={{marginRight: 10}} variant="titleMedium">Admin</Text>
|
||||
</Appbar.Header>
|
||||
);
|
||||
|
||||
|
|
112
HomePage.js
112
HomePage.js
|
@ -12,21 +12,33 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import * as React from 'react';
|
||||
import { View, TouchableOpacity, Text, FlatList } from 'react-native';
|
||||
import { Avatar, List, Portal, Modal, IconButton } from 'react-native-paper';
|
||||
import SearchBar from './SearchBar';
|
||||
import * as React from "react";
|
||||
import {Dimensions, FlatList, Text, TouchableOpacity, View} from "react-native";
|
||||
import {IconButton, List, Modal, Portal} from "react-native-paper";
|
||||
import SearchBar from "./SearchBar";
|
||||
|
||||
import EnterAccountDetails from './EnterAccountDetails';
|
||||
import EnterAccountDetails from "./EnterAccountDetails";
|
||||
import Account from "./Account";
|
||||
import ScanQRCode from "./ScanQRCode";
|
||||
|
||||
export default function HomePage() {
|
||||
const [isPlusButton, setIsPlusButton] = React.useState(true);
|
||||
const [showOptions, setShowOptions] = React.useState(false);
|
||||
const [showEnterAccountModal, setShowEnterAccountModal] = React.useState(false);
|
||||
const [accountList, setAccountList] = React.useState([]);
|
||||
const [searchQuery, setSearchQuery] = React.useState('');
|
||||
const [searchQuery, setSearchQuery] = React.useState("");
|
||||
const [filteredData, setFilteredData] = React.useState(accountList);
|
||||
const [showScanner, setShowScanner] = React.useState(false);
|
||||
|
||||
const handleScanPress = () => {
|
||||
setShowScanner(true);
|
||||
setIsPlusButton(true);
|
||||
setShowOptions(false);
|
||||
};
|
||||
|
||||
const handleCloseScanner = () => {
|
||||
setShowScanner(false);
|
||||
};
|
||||
|
||||
const togglePlusButton = () => {
|
||||
setIsPlusButton(!isPlusButton);
|
||||
|
@ -36,6 +48,7 @@ export default function HomePage() {
|
|||
const closeOptions = () => {
|
||||
setIsPlusButton(true);
|
||||
setShowOptions(false);
|
||||
setShowScanner(false);
|
||||
};
|
||||
|
||||
const openEnterAccountModal = () => {
|
||||
|
@ -47,27 +60,22 @@ export default function HomePage() {
|
|||
setShowEnterAccountModal(false);
|
||||
};
|
||||
|
||||
const handleAddAccount = async (accountData) => {
|
||||
const handleAddAccount = (accountData) => {
|
||||
const onUpdate = () => {
|
||||
setAccountList(prevList => [...prevList]);
|
||||
};
|
||||
|
||||
const newAccount = new Account(accountData.description, accountData.secretCode, onUpdate);
|
||||
const token = await newAccount.generateToken();
|
||||
const token = newAccount.generateToken();
|
||||
newAccount.token = token;
|
||||
|
||||
await setAccountList(prevList => [...prevList, newAccount]);
|
||||
setAccountList(prevList => [...prevList, newAccount]);
|
||||
closeEnterAccountModal();
|
||||
};
|
||||
React.useEffect(() => {
|
||||
setAccountList(prevList => [...prevList]);
|
||||
}, [accountList]);
|
||||
|
||||
|
||||
const handleSearch = (query) => {
|
||||
setSearchQuery(query);
|
||||
|
||||
if (query.trim() !== '') {
|
||||
if (query.trim() !== "") {
|
||||
const filteredResults = accountList.filter(item =>
|
||||
item.title.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
|
@ -77,28 +85,31 @@ export default function HomePage() {
|
|||
}
|
||||
};
|
||||
|
||||
const {width, height} = Dimensions.get("window");
|
||||
|
||||
const offsetX = width * 0.45;
|
||||
const offsetY = height * 0.2;
|
||||
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<SearchBar onSearch={handleSearch} />
|
||||
<FlatList
|
||||
// data={accountList}
|
||||
data={searchQuery.trim() !== '' ? filteredData : accountList}
|
||||
data={searchQuery.trim() !== "" ? filteredData : accountList}
|
||||
keyExtractor={(item, index) => index.toString()}
|
||||
renderItem={({item}) => (
|
||||
<List.Item
|
||||
title={
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Text style={{ fontSize: 20, width: 80 }}>{item.title}</Text>
|
||||
<Text style={{ marginLeft: 20, fontSize: 30 }}>{item.token}</Text>
|
||||
<Text style={{ marginLeft: 20, fontSize: 20, width: 20 }}>{item.countdowns}s</Text>
|
||||
<View>
|
||||
<Text style={{fontSize: 20}}>{item.title}</Text>
|
||||
<View style={{flexDirection: "row", alignItems: "center"}}>
|
||||
<Text style={{fontSize: 40, width: 180}}>{item.token}</Text>
|
||||
<Text style={{fontSize: 20, width: 40}}>{item.countdowns}s</Text>
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
left={(props) => (
|
||||
<Avatar.Image
|
||||
size={60}
|
||||
style={{ marginLeft: '20px', backgroundColor: 'rgb(242,242,242)' }}
|
||||
source={'https://cdn.casbin.org/img/social_casdoor.png'}
|
||||
/>
|
||||
<IconButton icon={"account"} size={70} style={{marginLeft: 20}} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
@ -109,72 +120,71 @@ export default function HomePage() {
|
|||
visible={showOptions}
|
||||
onDismiss={closeOptions}
|
||||
contentContainerStyle={{
|
||||
backgroundColor: 'white',
|
||||
backgroundColor: "white",
|
||||
padding: 20,
|
||||
borderRadius: 10,
|
||||
width: 300,
|
||||
height: 150,
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: [{ translateX: '-50%' }, { translateY: '-50%' }],
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: [{translateX: -150}, {translateY: -75}],
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={{ flexDirection: 'row', alignItems: 'center'}}
|
||||
onPress={() => {
|
||||
// Handle scanning QR code operation...
|
||||
// closeOptions();
|
||||
}}
|
||||
style={{flexDirection: "row", alignItems: "center"}}
|
||||
onPress={handleScanPress}
|
||||
>
|
||||
<IconButton icon={'camera'} size={35} />
|
||||
<IconButton icon={"camera"} size={35} />
|
||||
<Text style={{fontSize: 18}} >Scan QR code</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={{ flexDirection: 'row', alignItems: 'center', marginTop: 10 }}
|
||||
style={{flexDirection: "row", alignItems: "center", marginTop: 10}}
|
||||
onPress={openEnterAccountModal}
|
||||
>
|
||||
<IconButton icon={'keyboard'} size={35} />
|
||||
<IconButton icon={"keyboard"} size={35} />
|
||||
<Text style={{fontSize: 18}}>Enter Secret code</Text>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
</Portal>
|
||||
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={showEnterAccountModal}
|
||||
onDismiss={closeEnterAccountModal}
|
||||
contentContainerStyle={{
|
||||
backgroundColor: 'white',
|
||||
backgroundColor: "white",
|
||||
padding: 1,
|
||||
borderRadius: 10,
|
||||
width: '90%',
|
||||
height: '40%',
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: [{ translateX: '-50%' }, { translateY: '-50%' }],
|
||||
width: "90%",
|
||||
height: "40%",
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: [{translateX: -offsetX}, {translateY: -offsetY}],
|
||||
}}
|
||||
>
|
||||
<EnterAccountDetails onClose={closeEnterAccountModal} onAdd={handleAddAccount} />
|
||||
</Modal>
|
||||
</Portal>
|
||||
{showScanner && (
|
||||
<ScanQRCode onClose={handleCloseScanner} showScanner={showScanner} onAdd={handleAddAccount} />
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
position: 'absolute',
|
||||
position: "absolute",
|
||||
bottom: 30,
|
||||
right: 30,
|
||||
width: 70,
|
||||
height: 70,
|
||||
borderRadius: 35,
|
||||
backgroundColor: '#393544',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: "#E6DFF3",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
onPress={togglePlusButton}
|
||||
>
|
||||
<IconButton icon={isPlusButton ? 'plus' : 'close'} size={40} color={'white'} />
|
||||
<IconButton icon={isPlusButton ? "plus" : "close"} size={40} color={"white"} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -12,12 +12,11 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import React from "react";
|
||||
|
||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
import { Text, BottomNavigation } from 'react-native-paper';
|
||||
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import {createBottomTabNavigator} from "@react-navigation/bottom-tabs";
|
||||
import {BottomNavigation} from "react-native-paper";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import HomePage from "./HomePage";
|
||||
import {CommonActions} from "@react-navigation/native";
|
||||
import SettingPage from "./SettingPage";
|
||||
|
@ -36,7 +35,7 @@ export default function NavigationBar() {
|
|||
safeAreaInsets={insets}
|
||||
onTabPress={({route, preventDefault}) => {
|
||||
const event = navigation.emit({
|
||||
type: 'tabPress',
|
||||
type: "tabPress",
|
||||
target: route.key,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
|
@ -76,7 +75,7 @@ export default function NavigationBar() {
|
|||
name="Home"
|
||||
component={HomePage}
|
||||
options={{
|
||||
tabBarLabel: 'Home',
|
||||
tabBarLabel: "Home",
|
||||
tabBarIcon: ({color, size}) => {
|
||||
return <Icon name="home" size={size} color={color} />;
|
||||
},
|
||||
|
@ -86,7 +85,7 @@ export default function NavigationBar() {
|
|||
name="Settings"
|
||||
component={SettingPage}
|
||||
options={{
|
||||
tabBarLabel: 'Settings',
|
||||
tabBarLabel: "Settings",
|
||||
tabBarIcon: ({color, size}) => {
|
||||
return <Icon name="cog" size={size} color={color} />;
|
||||
},
|
||||
|
@ -95,11 +94,3 @@ export default function NavigationBar() {
|
|||
</Tab.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {Dimensions, Text, View} from "react-native";
|
||||
import {IconButton, Modal, Portal} from "react-native-paper";
|
||||
import {BarCodeScanner} from "expo-barcode-scanner";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const ScanQRCode = ({onClose, showScanner, onAdd}) => {
|
||||
ScanQRCode.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
showScanner: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const [hasPermission, setHasPermission] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async() => {
|
||||
const {status} = await BarCodeScanner.requestPermissionsAsync();
|
||||
setHasPermission(status === "granted");
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const closeOptions = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleBarCodeScanned = ({type, data}) => {
|
||||
// type org.iso.QRCode
|
||||
// data otpauth://totp/casdoor:built-in/admin?algorithm=SHA1&digits=6&issuer=casdoor&period=30&secret=DL5XI33M772GSGU73GJPCOIBNJE7TG3J
|
||||
// console.log(`Bar code with type ${type} and data ${data} has been scanned!`);
|
||||
|
||||
const description = data.match(/casdoor:([^?]+)/); // description casdoor:built-in/admin
|
||||
const secretCode = data.match(/secret=([^&]+)/); // secretCode II5UO7HIA3SPVXAB6KPAIXZ33AQP7C3R
|
||||
|
||||
if (description && secretCode) {
|
||||
onAdd({description: description[1], secretCode: secretCode[1]});
|
||||
}
|
||||
|
||||
closeOptions();
|
||||
};
|
||||
|
||||
const {width, height} = Dimensions.get("window");
|
||||
const offsetX = width * 0.5;
|
||||
const offsetY = height * 0.5;
|
||||
|
||||
return (
|
||||
<View style={{marginTop: "50%", flex: 1}} >
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={showScanner}
|
||||
onDismiss={closeOptions}
|
||||
contentContainerStyle={{
|
||||
backgroundColor: "white",
|
||||
width: width,
|
||||
height: height,
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: [{translateX: -offsetX}, {translateY: -offsetY}],
|
||||
}}
|
||||
>
|
||||
{hasPermission === null ? (
|
||||
<Text style={{marginLeft: "20%", marginRight: "20%"}}>Requesting for camera permission</Text>
|
||||
) : hasPermission === false ? (
|
||||
<Text style={{marginLeft: "20%", marginRight: "20%"}}>No access to camera</Text>
|
||||
) : (
|
||||
<BarCodeScanner
|
||||
onBarCodeScanned={handleBarCodeScanned}
|
||||
style={{flex: 1}}
|
||||
/>
|
||||
)}
|
||||
<IconButton icon={"close"} size={40} onPress={onClose} style={{position: "absolute", top: 30, right: 5}} />
|
||||
</Modal>
|
||||
</Portal>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScanQRCode;
|
|
@ -12,17 +12,16 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import * as React from 'react';
|
||||
import { Searchbar } from 'react-native-paper';
|
||||
import * as React from "react";
|
||||
import {Searchbar} from "react-native-paper";
|
||||
|
||||
const SearchBar = ({onSearch}) => {
|
||||
const [searchQuery, setSearchQuery] = React.useState('');
|
||||
const [searchQuery, setSearchQuery] = React.useState("");
|
||||
|
||||
const onChangeSearch = query => {
|
||||
setSearchQuery(query);
|
||||
onSearch(query);
|
||||
}
|
||||
|
||||
};
|
||||
return (
|
||||
<Searchbar
|
||||
placeholder="Search"
|
||||
|
|
|
@ -12,15 +12,16 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import * as React from 'react';
|
||||
import * as React from "react";
|
||||
import {Button} from "react-native-paper";
|
||||
import {View} from "react-native";
|
||||
|
||||
export default function SettingPage() {
|
||||
return (
|
||||
<div>
|
||||
<Button style={{marginTop: "50%"}} icon="login" mode="contained" onPress={() => console.log('Pressed')}>
|
||||
<View>
|
||||
<Button style={{marginTop: "50%", marginLeft: "20%", marginRight: "20%"}} icon="login" mode="contained" onPress={() => {}}>
|
||||
Login with Casdoor
|
||||
</Button>
|
||||
</div>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
module.exports = function(api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
presets: ["babel-preset-expo"],
|
||||
};
|
||||
};
|
||||
|
|
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
|
@ -12,18 +12,28 @@
|
|||
"@expo/webpack-config": "^19.0.0",
|
||||
"@react-navigation/bottom-tabs": "^6.5.8",
|
||||
"@react-navigation/native": "^6.1.7",
|
||||
"buffer": "^6.0.3",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"expo": "~49.0.8",
|
||||
"expo-barcode-scanner": "^12.5.3",
|
||||
"expo-status-bar": "~1.6.0",
|
||||
"hotp-totp": "^1.0.6",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-native": "0.72.4",
|
||||
"react-native-paper": "^5.10.3",
|
||||
"react-native-web": "~0.19.6"
|
||||
"react-native-web": "~0.19.6",
|
||||
"totp-generator": "^0.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0"
|
||||
"@babel/core": "^7.20.0",
|
||||
"@babel/eslint-parser": "^7.18.9",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"eslint": "8.22.0",
|
||||
"eslint-import-resolver-babel-module": "^5.3.2",
|
||||
"eslint-plugin-react": "^7.31.1",
|
||||
"eslint-plugin-unused-imports": "^2.0.0"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue