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
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
import totp from "totp-generator";
|
||||||
import {default as hotptotp} from 'hotp-totp';
|
|
||||||
const {totp} = hotptotp;
|
|
||||||
window.Buffer = window.Buffer || require("buffer").Buffer;
|
|
||||||
|
|
||||||
class Account {
|
class Account {
|
||||||
constructor(description, secretCode, onUpdate) {
|
constructor(description, secretCode, onUpdate) {
|
||||||
|
@ -23,27 +20,29 @@ class Account {
|
||||||
this.secretCode = secretCode;
|
this.secretCode = secretCode;
|
||||||
this.countdowns = 30;
|
this.countdowns = 30;
|
||||||
this.timer = setInterval(this.updateCountdown.bind(this), 1000);
|
this.timer = setInterval(this.updateCountdown.bind(this), 1000);
|
||||||
this.token = '';
|
this.token = "";
|
||||||
this.tokenInterval = setInterval(this.generateAndSetToken.bind(this), 30000);
|
|
||||||
this.onUpdate = onUpdate;
|
this.onUpdate = onUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
generateToken = async () => {
|
generateToken() {
|
||||||
let token = await totp(this.secretCode);
|
if (this.secretCode !== null && this.secretCode !== undefined && this.secretCode !== "") {
|
||||||
|
const token = totp(this.secretCode);
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
generateAndSetToken = async () => {
|
generateAndSetToken() {
|
||||||
this.token = await this.generateToken();
|
this.token = this.generateToken();
|
||||||
this.onUpdate();
|
this.onUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCountdown() {
|
updateCountdown() {
|
||||||
this.countdowns = Math.max(0, this.countdowns - 1);
|
this.countdowns = Math.max(0, this.countdowns - 1);
|
||||||
if (this.countdowns === 0) {
|
if (this.countdowns === 0) {
|
||||||
|
this.generateAndSetToken();
|
||||||
this.countdowns = 30;
|
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
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import {PaperProvider} from 'react-native-paper';
|
import {PaperProvider} from "react-native-paper";
|
||||||
import NavigationBar from "./NavigationBar";
|
import NavigationBar from "./NavigationBar";
|
||||||
import { NavigationContainer } from '@react-navigation/native';
|
import {NavigationContainer} from "@react-navigation/native";
|
||||||
import Header from "./Header";
|
import Header from "./Header";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
|
|
@ -12,61 +12,100 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, {useState} from "react";
|
||||||
import { View, Text, TextInput } from 'react-native';
|
import {Text, TextInput, View} from "react-native";
|
||||||
import { Button, IconButton } from 'react-native-paper';
|
import {Button, Divider, IconButton, Menu} from "react-native-paper";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
export default function EnterAccountDetails({ onClose, onAdd }) {
|
export default function EnterAccountDetails({onClose, onAdd}) {
|
||||||
const [description, setDescription] = useState('');
|
EnterAccountDetails.propTypes = {
|
||||||
const [secretCode, setSecretCode] = useState('');
|
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 = () => {
|
const handleAddAccount = () => {
|
||||||
onAdd({ description, secretCode });
|
onAdd({description, secretCode});
|
||||||
setDescription('');
|
setDescription("");
|
||||||
setSecretCode('');
|
setSecretCode("");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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>
|
<Text style={{fontSize: 24, marginBottom: 5}}>Add new 2FA account</Text>
|
||||||
<div style={{display: 'flex', marginTop: 10}}>
|
<View style={{flexDirection: "row", alignItems: "center"}}>
|
||||||
<IconButton icon='account-details' size={35}></IconButton>
|
<IconButton icon="account-details" size={35} />
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder='Description'
|
placeholder="Description"
|
||||||
value={description}
|
value={description}
|
||||||
onChangeText={(text) => setDescription(text)}
|
onChangeText={(text) => setDescription(text)}
|
||||||
style={{ borderWidth: 3, borderColor: 'white', margin: 10, width: 230, height: 50, borderRadius: 5, fontSize: 18,
|
style={{borderWidth: 3, borderColor: "white", margin: 10, width: 230, height: 50, borderRadius: 5, fontSize: 18, color: "gray", paddingLeft: 10}}
|
||||||
color: 'gray', paddingLeft: 10}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</View>
|
||||||
<div style={{display: 'flex'}}>
|
|
||||||
<IconButton icon='account-key' size={35}></IconButton>
|
<View style={{flexDirection: "row", alignItems: "center"}}>
|
||||||
|
<IconButton icon="account-key" size={35} />
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder='Secret code'
|
placeholder="Secret code"
|
||||||
value={secretCode}
|
value={secretCode}
|
||||||
onChangeText={(text) => setSecretCode(text)}
|
onChangeText={(text) => setSecretCode(text)}
|
||||||
secureTextEntry
|
secureTextEntry
|
||||||
style={{ borderWidth: 3, borderColor: 'white', margin: 10, width: 230, height: 50, borderRadius: 5, fontSize: 18,
|
style={{borderWidth: 3, borderColor: "white", margin: 10, width: 230, height: 50, borderRadius: 5, fontSize: 18, color: "gray", paddingLeft: 10}}
|
||||||
color: 'gray', paddingLeft: 10 }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</View>
|
||||||
<Button
|
<Button
|
||||||
icon='account-plus'
|
icon="account-plus"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#393544',
|
backgroundColor: "#E6DFF3",
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
margin: 10,
|
margin: 10,
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
top: 260,
|
top: 230,
|
||||||
width: 300,
|
right: 30,
|
||||||
// height: 50
|
width: 90,
|
||||||
}}
|
}}
|
||||||
onPress={handleAddAccount}
|
onPress={handleAddAccount}
|
||||||
>
|
>
|
||||||
<Text style={{fontSize: 18}}>Add</Text>
|
<Text style={{fontSize: 18}}>Add</Text>
|
||||||
</Button>
|
</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>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,14 +12,14 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import {Appbar, Avatar, Text} from 'react-native-paper';
|
import {Appbar, Avatar, Text} from "react-native-paper";
|
||||||
|
|
||||||
const Header = () => (
|
const Header = () => (
|
||||||
<Appbar.Header>
|
<Appbar.Header>
|
||||||
<Appbar.Content title="Casdoor" />
|
<Appbar.Content title="Casdoor" />
|
||||||
<Avatar.Image size={32} style={{marginRight: '10px', backgroundColor: 'white'}} source={'https://cdn.casbin.com/casdoor/avatar/built-in/admin.jpeg'} />
|
<Avatar.Image size={32} style={{marginRight: 10, backgroundColor: "white"}} source={{uri: "https://cdn.casbin.com/casdoor/avatar/built-in/admin.jpeg"}} />
|
||||||
<Text style={{marginRight: '10px'}} variant="titleMedium">Admin</Text>
|
<Text style={{marginRight: 10}} variant="titleMedium">Admin</Text>
|
||||||
</Appbar.Header>
|
</Appbar.Header>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
118
HomePage.js
118
HomePage.js
|
@ -12,21 +12,33 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import { View, TouchableOpacity, Text, FlatList } from 'react-native';
|
import {Dimensions, FlatList, Text, TouchableOpacity, View} from "react-native";
|
||||||
import { Avatar, List, Portal, Modal, IconButton } from 'react-native-paper';
|
import {IconButton, List, Modal, Portal} from "react-native-paper";
|
||||||
import SearchBar from './SearchBar';
|
import SearchBar from "./SearchBar";
|
||||||
|
|
||||||
import EnterAccountDetails from './EnterAccountDetails';
|
import EnterAccountDetails from "./EnterAccountDetails";
|
||||||
import Account from "./Account";
|
import Account from "./Account";
|
||||||
|
import ScanQRCode from "./ScanQRCode";
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const [isPlusButton, setIsPlusButton] = React.useState(true);
|
const [isPlusButton, setIsPlusButton] = React.useState(true);
|
||||||
const [showOptions, setShowOptions] = React.useState(false);
|
const [showOptions, setShowOptions] = React.useState(false);
|
||||||
const [showEnterAccountModal, setShowEnterAccountModal] = React.useState(false);
|
const [showEnterAccountModal, setShowEnterAccountModal] = React.useState(false);
|
||||||
const [accountList, setAccountList] = React.useState([]);
|
const [accountList, setAccountList] = React.useState([]);
|
||||||
const [searchQuery, setSearchQuery] = React.useState('');
|
const [searchQuery, setSearchQuery] = React.useState("");
|
||||||
const [filteredData, setFilteredData] = React.useState(accountList);
|
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 = () => {
|
const togglePlusButton = () => {
|
||||||
setIsPlusButton(!isPlusButton);
|
setIsPlusButton(!isPlusButton);
|
||||||
|
@ -36,6 +48,7 @@ export default function HomePage() {
|
||||||
const closeOptions = () => {
|
const closeOptions = () => {
|
||||||
setIsPlusButton(true);
|
setIsPlusButton(true);
|
||||||
setShowOptions(false);
|
setShowOptions(false);
|
||||||
|
setShowScanner(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEnterAccountModal = () => {
|
const openEnterAccountModal = () => {
|
||||||
|
@ -47,27 +60,22 @@ export default function HomePage() {
|
||||||
setShowEnterAccountModal(false);
|
setShowEnterAccountModal(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddAccount = async (accountData) => {
|
const handleAddAccount = (accountData) => {
|
||||||
const onUpdate = () => {
|
const onUpdate = () => {
|
||||||
setAccountList(prevList => [...prevList]);
|
setAccountList(prevList => [...prevList]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const newAccount = new Account(accountData.description, accountData.secretCode, onUpdate);
|
const newAccount = new Account(accountData.description, accountData.secretCode, onUpdate);
|
||||||
const token = await newAccount.generateToken();
|
const token = newAccount.generateToken();
|
||||||
newAccount.token = token;
|
newAccount.token = token;
|
||||||
|
|
||||||
await setAccountList(prevList => [...prevList, newAccount]);
|
setAccountList(prevList => [...prevList, newAccount]);
|
||||||
closeEnterAccountModal();
|
closeEnterAccountModal();
|
||||||
};
|
};
|
||||||
React.useEffect(() => {
|
|
||||||
setAccountList(prevList => [...prevList]);
|
|
||||||
}, [accountList]);
|
|
||||||
|
|
||||||
|
|
||||||
const handleSearch = (query) => {
|
const handleSearch = (query) => {
|
||||||
setSearchQuery(query);
|
setSearchQuery(query);
|
||||||
|
|
||||||
if (query.trim() !== '') {
|
if (query.trim() !== "") {
|
||||||
const filteredResults = accountList.filter(item =>
|
const filteredResults = accountList.filter(item =>
|
||||||
item.title.toLowerCase().includes(query.toLowerCase())
|
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 (
|
return (
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{flex: 1}}>
|
||||||
<SearchBar onSearch={ handleSearch } />
|
<SearchBar onSearch={handleSearch} />
|
||||||
<FlatList
|
<FlatList
|
||||||
// data={accountList}
|
// data={accountList}
|
||||||
data={searchQuery.trim() !== '' ? filteredData : accountList}
|
data={searchQuery.trim() !== "" ? filteredData : accountList}
|
||||||
keyExtractor={(item, index) => index.toString()}
|
keyExtractor={(item, index) => index.toString()}
|
||||||
renderItem={({ item }) => (
|
renderItem={({item}) => (
|
||||||
<List.Item
|
<List.Item
|
||||||
title={
|
title={
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
<View>
|
||||||
<Text style={{ fontSize: 20, width: 80 }}>{item.title}</Text>
|
<Text style={{fontSize: 20}}>{item.title}</Text>
|
||||||
<Text style={{ marginLeft: 20, fontSize: 30 }}>{item.token}</Text>
|
<View style={{flexDirection: "row", alignItems: "center"}}>
|
||||||
<Text style={{ marginLeft: 20, fontSize: 20, width: 20 }}>{item.countdowns}s</Text>
|
<Text style={{fontSize: 40, width: 180}}>{item.token}</Text>
|
||||||
|
<Text style={{fontSize: 20, width: 40}}>{item.countdowns}s</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
left={(props) => (
|
left={(props) => (
|
||||||
<Avatar.Image
|
<IconButton icon={"account"} size={70} style={{marginLeft: 20}} />
|
||||||
size={60}
|
|
||||||
style={{ marginLeft: '20px', backgroundColor: 'rgb(242,242,242)' }}
|
|
||||||
source={'https://cdn.casbin.org/img/social_casdoor.png'}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -109,72 +120,71 @@ export default function HomePage() {
|
||||||
visible={showOptions}
|
visible={showOptions}
|
||||||
onDismiss={closeOptions}
|
onDismiss={closeOptions}
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
backgroundColor: 'white',
|
backgroundColor: "white",
|
||||||
padding: 20,
|
padding: 20,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 150,
|
height: 150,
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
top: '50%',
|
top: "50%",
|
||||||
left: '50%',
|
left: "50%",
|
||||||
transform: [{ translateX: '-50%' }, { translateY: '-50%' }],
|
transform: [{translateX: -150}, {translateY: -75}],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={{ flexDirection: 'row', alignItems: 'center'}}
|
style={{flexDirection: "row", alignItems: "center"}}
|
||||||
onPress={() => {
|
onPress={handleScanPress}
|
||||||
// Handle scanning QR code operation...
|
|
||||||
// closeOptions();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<IconButton icon={'camera'} size={35} />
|
<IconButton icon={"camera"} size={35} />
|
||||||
<Text style={{fontSize: 18}} >Scan QR code</Text>
|
<Text style={{fontSize: 18}} >Scan QR code</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={{ flexDirection: 'row', alignItems: 'center', marginTop: 10 }}
|
style={{flexDirection: "row", alignItems: "center", marginTop: 10}}
|
||||||
onPress={openEnterAccountModal}
|
onPress={openEnterAccountModal}
|
||||||
>
|
>
|
||||||
<IconButton icon={'keyboard'} size={35} />
|
<IconButton icon={"keyboard"} size={35} />
|
||||||
<Text style={{fontSize: 18}}>Enter Secret code</Text>
|
<Text style={{fontSize: 18}}>Enter Secret code</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Portal>
|
</Portal>
|
||||||
|
|
||||||
<Portal>
|
<Portal>
|
||||||
<Modal
|
<Modal
|
||||||
visible={showEnterAccountModal}
|
visible={showEnterAccountModal}
|
||||||
onDismiss={closeEnterAccountModal}
|
onDismiss={closeEnterAccountModal}
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
backgroundColor: 'white',
|
backgroundColor: "white",
|
||||||
padding: 1,
|
padding: 1,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
width: '90%',
|
width: "90%",
|
||||||
height: '40%',
|
height: "40%",
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
top: '50%',
|
top: "50%",
|
||||||
left: '50%',
|
left: "50%",
|
||||||
transform: [{ translateX: '-50%' }, { translateY: '-50%' }],
|
transform: [{translateX: -offsetX}, {translateY: -offsetY}],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EnterAccountDetails onClose={closeEnterAccountModal} onAdd={handleAddAccount} />
|
<EnterAccountDetails onClose={closeEnterAccountModal} onAdd={handleAddAccount} />
|
||||||
</Modal>
|
</Modal>
|
||||||
</Portal>
|
</Portal>
|
||||||
|
{showScanner && (
|
||||||
|
<ScanQRCode onClose={handleCloseScanner} showScanner={showScanner} onAdd={handleAddAccount} />
|
||||||
|
)}
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
bottom: 30,
|
bottom: 30,
|
||||||
right: 30,
|
right: 30,
|
||||||
width: 70,
|
width: 70,
|
||||||
height: 70,
|
height: 70,
|
||||||
borderRadius: 35,
|
borderRadius: 35,
|
||||||
backgroundColor: '#393544',
|
backgroundColor: "#E6DFF3",
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
justifyContent: 'center',
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
onPress={togglePlusButton}
|
onPress={togglePlusButton}
|
||||||
>
|
>
|
||||||
<IconButton icon={isPlusButton ? 'plus' : 'close'} size={40} color={'white'} />
|
<IconButton icon={isPlusButton ? "plus" : "close"} size={40} color={"white"} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
|
@ -12,12 +12,11 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import { View, StyleSheet } from 'react-native';
|
|
||||||
|
|
||||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
import {createBottomTabNavigator} from "@react-navigation/bottom-tabs";
|
||||||
import { Text, BottomNavigation } from 'react-native-paper';
|
import {BottomNavigation} from "react-native-paper";
|
||||||
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
|
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||||
import HomePage from "./HomePage";
|
import HomePage from "./HomePage";
|
||||||
import {CommonActions} from "@react-navigation/native";
|
import {CommonActions} from "@react-navigation/native";
|
||||||
import SettingPage from "./SettingPage";
|
import SettingPage from "./SettingPage";
|
||||||
|
@ -30,13 +29,13 @@ export default function NavigationBar() {
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
}}
|
}}
|
||||||
tabBar={({ navigation, state, descriptors, insets }) => (
|
tabBar={({navigation, state, descriptors, insets}) => (
|
||||||
<BottomNavigation.Bar
|
<BottomNavigation.Bar
|
||||||
navigationState={state}
|
navigationState={state}
|
||||||
safeAreaInsets={insets}
|
safeAreaInsets={insets}
|
||||||
onTabPress={({ route, preventDefault }) => {
|
onTabPress={({route, preventDefault}) => {
|
||||||
const event = navigation.emit({
|
const event = navigation.emit({
|
||||||
type: 'tabPress',
|
type: "tabPress",
|
||||||
target: route.key,
|
target: route.key,
|
||||||
canPreventDefault: true,
|
canPreventDefault: true,
|
||||||
});
|
});
|
||||||
|
@ -50,16 +49,16 @@ export default function NavigationBar() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
renderIcon={({ route, focused, color }) => {
|
renderIcon={({route, focused, color}) => {
|
||||||
const { options } = descriptors[route.key];
|
const {options} = descriptors[route.key];
|
||||||
if (options.tabBarIcon) {
|
if (options.tabBarIcon) {
|
||||||
return options.tabBarIcon({ focused, color, size: 24 });
|
return options.tabBarIcon({focused, color, size: 24});
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}}
|
}}
|
||||||
getLabelText={({ route }) => {
|
getLabelText={({route}) => {
|
||||||
const { options } = descriptors[route.key];
|
const {options} = descriptors[route.key];
|
||||||
const label =
|
const label =
|
||||||
options.tabBarLabel !== undefined
|
options.tabBarLabel !== undefined
|
||||||
? options.tabBarLabel
|
? options.tabBarLabel
|
||||||
|
@ -76,8 +75,8 @@ export default function NavigationBar() {
|
||||||
name="Home"
|
name="Home"
|
||||||
component={HomePage}
|
component={HomePage}
|
||||||
options={{
|
options={{
|
||||||
tabBarLabel: 'Home',
|
tabBarLabel: "Home",
|
||||||
tabBarIcon: ({ color, size }) => {
|
tabBarIcon: ({color, size}) => {
|
||||||
return <Icon name="home" size={size} color={color} />;
|
return <Icon name="home" size={size} color={color} />;
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
@ -86,8 +85,8 @@ export default function NavigationBar() {
|
||||||
name="Settings"
|
name="Settings"
|
||||||
component={SettingPage}
|
component={SettingPage}
|
||||||
options={{
|
options={{
|
||||||
tabBarLabel: 'Settings',
|
tabBarLabel: "Settings",
|
||||||
tabBarIcon: ({ color, size }) => {
|
tabBarIcon: ({color, size}) => {
|
||||||
return <Icon name="cog" size={size} color={color} />;
|
return <Icon name="cog" size={size} color={color} />;
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
@ -95,11 +94,3 @@ export default function NavigationBar() {
|
||||||
</Tab.Navigator>
|
</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;
|
11
SearchBar.js
11
SearchBar.js
|
@ -12,17 +12,16 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import { Searchbar } from 'react-native-paper';
|
import {Searchbar} from "react-native-paper";
|
||||||
|
|
||||||
const SearchBar = ({ onSearch }) => {
|
const SearchBar = ({onSearch}) => {
|
||||||
const [searchQuery, setSearchQuery] = React.useState('');
|
const [searchQuery, setSearchQuery] = React.useState("");
|
||||||
|
|
||||||
const onChangeSearch = query => {
|
const onChangeSearch = query => {
|
||||||
setSearchQuery(query);
|
setSearchQuery(query);
|
||||||
onSearch(query);
|
onSearch(query);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Searchbar
|
<Searchbar
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
|
|
|
@ -12,15 +12,16 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import {Button} from "react-native-paper";
|
import {Button} from "react-native-paper";
|
||||||
|
import {View} from "react-native";
|
||||||
|
|
||||||
export default function SettingPage() {
|
export default function SettingPage() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<View>
|
||||||
<Button style={{marginTop: "50%"}} icon="login" mode="contained" onPress={() => console.log('Pressed')}>
|
<Button style={{marginTop: "50%", marginLeft: "20%", marginRight: "20%"}} icon="login" mode="contained" onPress={() => {}}>
|
||||||
Login with Casdoor
|
Login with Casdoor
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
module.exports = function(api) {
|
module.exports = function(api) {
|
||||||
api.cache(true);
|
api.cache(true);
|
||||||
return {
|
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",
|
"@expo/webpack-config": "^19.0.0",
|
||||||
"@react-navigation/bottom-tabs": "^6.5.8",
|
"@react-navigation/bottom-tabs": "^6.5.8",
|
||||||
"@react-navigation/native": "^6.1.7",
|
"@react-navigation/native": "^6.1.7",
|
||||||
"buffer": "^6.0.3",
|
"eslint-plugin-import": "^2.28.1",
|
||||||
"expo": "~49.0.8",
|
"expo": "~49.0.8",
|
||||||
|
"expo-barcode-scanner": "^12.5.3",
|
||||||
"expo-status-bar": "~1.6.0",
|
"expo-status-bar": "~1.6.0",
|
||||||
"hotp-totp": "^1.0.6",
|
"hotp-totp": "^1.0.6",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-native": "0.72.4",
|
"react-native": "0.72.4",
|
||||||
"react-native-paper": "^5.10.3",
|
"react-native-paper": "^5.10.3",
|
||||||
"react-native-web": "~0.19.6"
|
"react-native-web": "~0.19.6",
|
||||||
|
"totp-generator": "^0.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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
|
"private": true
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue