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:
ChenWenpeng 2023-09-13 00:20:43 +08:00 committed by GitHub
parent 39674fa9c7
commit 8de464cd9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 2703 additions and 251 deletions

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
node_modules

112
.eslintrc Normal file
View File

@ -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"
}
}

View File

@ -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);
return token;
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
View File

@ -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() {

View File

@ -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('');
export default function EnterAccountDetails({onClose, onAdd}) {
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('');
onAdd({description, secretCode});
setDescription("");
setSecretCode("");
};
return (
<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>
<TextInput
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}}
/>
</div>
<div style={{display: 'flex'}}>
<IconButton icon='account-key' size={35}></IconButton>
<TextInput
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 }}
/>
</div>
<Button
icon='account-plus'
style={{
backgroundColor: '#393544',
borderRadius: 5,
margin: 10,
alignItems: 'center',
position: 'absolute',
top: 260,
width: 300,
// height: 50
}}
onPress={handleAddAccount}
>
<Text style={{fontSize: 18}}>Add</Text>
</Button>
<IconButton icon={'close'} size={30} onPress={onClose} style={{position: 'absolute', top: 5, right: 5}} />
<View style={{flex: 1, justifyContent: "center", alignItems: "center"}}>
<Text style={{fontSize: 24, marginBottom: 5}}>Add new 2FA account</Text>
<View style={{flexDirection: "row", alignItems: "center"}}>
<IconButton icon="account-details" size={35} />
<TextInput
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}}
/>
</View>
<View style={{flexDirection: "row", alignItems: "center"}}>
<IconButton icon="account-key" size={35} />
<TextInput
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}}
/>
</View>
<Button
icon="account-plus"
style={{
backgroundColor: "#E6DFF3",
borderRadius: 5,
margin: 10,
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}} />
<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 File

@ -12,15 +12,15 @@
// 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>
);
export default Header;
export default Header;

View File

@ -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,29 +60,24 @@ 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())
item.title.toLowerCase().includes(query.toLowerCase())
);
setFilteredData(filteredResults);
} else {
@ -77,105 +85,107 @@ 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}
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>
}
left={(props) => (
<Avatar.Image
size={60}
style={{ marginLeft: '20px', backgroundColor: 'rgb(242,242,242)' }}
source={'https://cdn.casbin.org/img/social_casdoor.png'}
/>
)}
/>
<View style={{flex: 1}}>
<SearchBar onSearch={handleSearch} />
<FlatList
// data={accountList}
data={searchQuery.trim() !== "" ? filteredData : accountList}
keyExtractor={(item, index) => index.toString()}
renderItem={({item}) => (
<List.Item
title={
<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) => (
<IconButton icon={"account"} size={70} style={{marginLeft: 20}} />
)}
/>
/>
)}
/>
<Portal>
<Modal
visible={showOptions}
onDismiss={closeOptions}
contentContainerStyle={{
backgroundColor: 'white',
padding: 20,
borderRadius: 10,
width: 300,
height: 150,
position: 'absolute',
top: '50%',
left: '50%',
transform: [{ translateX: '-50%' }, { translateY: '-50%' }],
}}
>
<TouchableOpacity
style={{ flexDirection: 'row', alignItems: 'center'}}
onPress={() => {
// Handle scanning QR code operation...
// closeOptions();
}}
>
<IconButton icon={'camera'} size={35} />
<Text style={{fontSize: 18}} >Scan QR code</Text>
</TouchableOpacity>
<TouchableOpacity
style={{ flexDirection: 'row', alignItems: 'center', marginTop: 10 }}
onPress={openEnterAccountModal}
>
<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',
padding: 1,
borderRadius: 10,
width: '90%',
height: '40%',
position: 'absolute',
top: '50%',
left: '50%',
transform: [{ translateX: '-50%' }, { translateY: '-50%' }],
}}
>
<EnterAccountDetails onClose={closeEnterAccountModal} onAdd={handleAddAccount} />
</Modal>
</Portal>
<TouchableOpacity
style={{
position: 'absolute',
bottom: 30,
right: 30,
width: 70,
height: 70,
borderRadius: 35,
backgroundColor: '#393544',
alignItems: 'center',
justifyContent: 'center',
}}
onPress={togglePlusButton}
<Portal>
<Modal
visible={showOptions}
onDismiss={closeOptions}
contentContainerStyle={{
backgroundColor: "white",
padding: 20,
borderRadius: 10,
width: 300,
height: 150,
position: "absolute",
top: "50%",
left: "50%",
transform: [{translateX: -150}, {translateY: -75}],
}}
>
<IconButton icon={isPlusButton ? 'plus' : 'close'} size={40} color={'white'} />
</TouchableOpacity>
</View>
<TouchableOpacity
style={{flexDirection: "row", alignItems: "center"}}
onPress={handleScanPress}
>
<IconButton icon={"camera"} size={35} />
<Text style={{fontSize: 18}} >Scan QR code</Text>
</TouchableOpacity>
<TouchableOpacity
style={{flexDirection: "row", alignItems: "center", marginTop: 10}}
onPress={openEnterAccountModal}
>
<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",
padding: 1,
borderRadius: 10,
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",
bottom: 30,
right: 30,
width: 70,
height: 70,
borderRadius: 35,
backgroundColor: "#E6DFF3",
alignItems: "center",
justifyContent: "center",
}}
onPress={togglePlusButton}
>
<IconButton icon={isPlusButton ? "plus" : "close"} size={40} color={"white"} />
</TouchableOpacity>
</View>
);
}

View File

@ -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";
@ -30,13 +29,13 @@ export default function NavigationBar() {
screenOptions={{
headerShown: false,
}}
tabBar={({ navigation, state, descriptors, insets }) => (
tabBar={({navigation, state, descriptors, insets}) => (
<BottomNavigation.Bar
navigationState={state}
safeAreaInsets={insets}
onTabPress={({ route, preventDefault }) => {
onTabPress={({route, preventDefault}) => {
const event = navigation.emit({
type: 'tabPress',
type: "tabPress",
target: route.key,
canPreventDefault: true,
});
@ -50,16 +49,16 @@ export default function NavigationBar() {
});
}
}}
renderIcon={({ route, focused, color }) => {
const { options } = descriptors[route.key];
renderIcon={({route, focused, color}) => {
const {options} = descriptors[route.key];
if (options.tabBarIcon) {
return options.tabBarIcon({ focused, color, size: 24 });
return options.tabBarIcon({focused, color, size: 24});
}
return null;
}}
getLabelText={({ route }) => {
const { options } = descriptors[route.key];
getLabelText={({route}) => {
const {options} = descriptors[route.key];
const label =
options.tabBarLabel !== undefined
? options.tabBarLabel
@ -76,8 +75,8 @@ export default function NavigationBar() {
name="Home"
component={HomePage}
options={{
tabBarLabel: 'Home',
tabBarIcon: ({ color, size }) => {
tabBarLabel: "Home",
tabBarIcon: ({color, size}) => {
return <Icon name="home" size={size} color={color} />;
},
}}
@ -86,8 +85,8 @@ export default function NavigationBar() {
name="Settings"
component={SettingPage}
options={{
tabBarLabel: 'Settings',
tabBarIcon: ({ color, size }) => {
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',
},
});

93
ScanQRCode.js Normal file
View File

@ -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;

View File

@ -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 SearchBar = ({onSearch}) => {
const [searchQuery, setSearchQuery] = React.useState("");
const onChangeSearch = query => {
setSearchQuery(query);
onSearch(query);
}
};
return (
<Searchbar
placeholder="Search"

View File

@ -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>
);
}

View File

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

2259
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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
}