diff --git a/App.js b/App.js
index 9061e7f..fb58301 100644
--- a/App.js
+++ b/App.js
@@ -17,6 +17,7 @@ import {PaperProvider} from "react-native-paper";
import {NavigationContainer} from "@react-navigation/native";
import {BulletList} from "react-content-loader/native";
import {SQLiteProvider} from "expo-sqlite";
+import Toast from "react-native-toast-message";
import Header from "./Header";
import NavigationBar from "./NavigationBar";
import {migrateDb} from "./TotpDatabase";
@@ -31,6 +32,7 @@ const App = () => {
+
);
diff --git a/CasdoorLoginPage.js b/CasdoorLoginPage.js
index 2c4b1b1..effb71f 100644
--- a/CasdoorLoginPage.js
+++ b/CasdoorLoginPage.js
@@ -14,10 +14,12 @@
import React, {useEffect, useState} from "react";
import {WebView} from "react-native-webview";
-import {View} from "react-native";
+import {Platform, SafeAreaView, StatusBar, StyleSheet, Text, TouchableOpacity} from "react-native";
import {Portal} from "react-native-paper";
import SDK from "casdoor-react-native-sdk";
import PropTypes from "prop-types";
+import Toast from "react-native-toast-message";
+
import EnterCasdoorSdkConfig from "./EnterCasdoorSdkConfig";
import useStore from "./useStorage";
// import {LogBox} from "react-native";
@@ -28,6 +30,7 @@ const CasdoorLoginPage = ({onWebviewClose}) => {
CasdoorLoginPage.propTypes = {
onWebviewClose: PropTypes.func.isRequired,
};
+
const [casdoorLoginURL, setCasdoorLoginURL] = useState("");
const [showConfigPage, setShowConfigPage] = useState(true);
@@ -45,6 +48,11 @@ const CasdoorLoginPage = ({onWebviewClose}) => {
const handleHideConfigPage = () => {
setShowConfigPage(false);
};
+
+ const handleShowConfigPage = () => {
+ setShowConfigPage(true);
+ };
+
const getCasdoorSignInUrl = async() => {
const signinUrl = await sdk.getSigninUrl();
setCasdoorLoginURL(signinUrl);
@@ -68,24 +76,71 @@ const CasdoorLoginPage = ({onWebviewClose}) => {
}
};
+ const handleErrorResponse = (error) => {
+ Toast.show({
+ type: "error",
+ text1: "Error",
+ text2: error.description,
+ autoHide: true,
+ });
+ setShowConfigPage(true);
+ };
+
return (
-
- {showConfigPage && }
- {!showConfigPage && casdoorLoginURL !== "" && (
-
+ {showConfigPage && (
+
)}
-
+ {!showConfigPage && casdoorLoginURL !== "" && (
+ <>
+
+ Back to Config
+
+ {
+ const {nativeEvent} = syntheticEvent;
+ handleErrorResponse(nativeEvent);
+ }}
+ style={styles.webview}
+ mixedContentMode="always"
+ javaScriptEnabled={true}
+ />
+ >
+ )}
+
);
};
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: "white",
+ paddingTop: Platform.OS === "android" ? StatusBar.currentHeight : 0,
+ },
+ webview: {
+ flex: 1,
+ },
+ backButton: {
+ padding: 10,
+ backgroundColor: "#007AFF",
+ alignItems: "center",
+ },
+ backButtonText: {
+ color: "white",
+ fontWeight: "bold",
+ },
+});
+
export const CasdoorLogout = () => {
if (sdk) {sdk.clearState();}
};
diff --git a/EnterAccountDetails.js b/EnterAccountDetails.js
index 4fd2ba9..f831b0e 100644
--- a/EnterAccountDetails.js
+++ b/EnterAccountDetails.js
@@ -12,104 +12,230 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import React, {useState} from "react";
-import {Text, TextInput, View} from "react-native";
-import {Button, Divider, IconButton, Menu} from "react-native-paper";
+import React, {useCallback, useState} from "react";
+import {View} from "react-native";
+import {Button, IconButton, Menu, Text, TextInput} from "react-native-paper";
+import Toast from "react-native-toast-message";
import PropTypes from "prop-types";
-export default function EnterAccountDetails({onClose, onAdd}) {
+const EnterAccountDetails = ({onClose, onAdd, validateSecret}) => {
EnterAccountDetails.propTypes = {
onClose: PropTypes.func.isRequired,
onAdd: PropTypes.func.isRequired,
+ validateSecret: PropTypes.func.isRequired,
};
const [accountName, setAccountName] = useState("");
const [secretKey, setSecretKey] = useState("");
+ const [secretError, setSecretError] = useState("");
+ const [accountNameError, setAccountNameError] = useState("");
+ const [visible, setVisible] = useState(false);
+ const [selectedItem, setSelectedItem] = useState("Time based");
+ const [showPassword, setShowPassword] = useState(false);
- const [visible, setVisible] = React.useState(false);
const openMenu = () => setVisible(true);
const closeMenu = () => setVisible(false);
- const [selectedItem, setSelectedItem] = useState("Time based");
- const handleMenuItemPress = (item) => {
+ const handleMenuItemPress = useCallback((item) => {
setSelectedItem(item);
closeMenu();
- };
+ }, []);
+
+ const handleAddAccount = useCallback(() => {
+ if (accountName.trim() === "") {
+ setAccountNameError("Account Name is required");
+ }
+
+ if (secretKey.trim() === "") {
+ setSecretError("Secret Key is required");
+ }
+
+ if (accountName.trim() === "" || secretKey.trim() === "") {
+ Toast.show({
+ type: "error",
+ text1: "Error",
+ text2: "Please fill in all the fields!",
+ autoHide: true,
+ });
+ return;
+ }
+
+ if (secretError) {
+ Toast.show({
+ type: "error",
+ text1: "Invalid Secret Key",
+ text2: "Please check your secret key and try again.",
+ autoHide: true,
+ });
+ return;
+ }
- const handleAddAccount = () => {
onAdd({accountName, secretKey});
setAccountName("");
setSecretKey("");
- };
+ setAccountNameError("");
+ setSecretError("");
+ }, [accountName, secretKey, secretError, onAdd]);
+
+ const handleSecretKeyChange = useCallback((text) => {
+ setSecretKey(text);
+ if (validateSecret) {
+ const isValid = validateSecret(text);
+ setSecretError(isValid || text.trim() === "" ? "" : "Invalid Secret Key");
+ }
+ }, [validateSecret]);
+
+ const handleAccountNameChange = useCallback((text) => {
+ setAccountName(text);
+ if (accountNameError) {
+ setAccountNameError("");
+ }
+ }, [accountNameError]);
return (
-
- Add new 2FA account
-
-
+
+
+
+ Add Account
+
+
setAccountName(text)}
- style={{borderWidth: 3, borderColor: "white", margin: 10, width: 230, height: 50, borderRadius: 5, fontSize: 18, color: "gray", paddingLeft: 10}}
+ onChangeText={handleAccountNameChange}
+ error={!!accountNameError}
+ style={styles.input}
+ mode="outlined"
/>
-
-
-
-
setSecretKey(text)}
- secureTextEntry
- style={{borderWidth: 3, borderColor: "white", margin: 10, width: 230, height: 50, borderRadius: 5, fontSize: 18, color: "gray", paddingLeft: 10}}
- />
-
-
-
-
-
+ />
+
+
+
+
);
-}
+};
+
+const styles = {
+ container: {
+ flex: 1,
+ justifyContent: "center",
+ alignItems: "center",
+ },
+ content: {
+ width: "100%",
+ borderRadius: 10,
+ padding: 20,
+ backgroundColor: "#F5F5F5",
+ shadowColor: "#000",
+ shadowOffset: {width: 0, height: 2},
+ shadowOpacity: 0.1,
+ shadowRadius: 10,
+ elevation: 5,
+ },
+ header: {
+ position: "relative",
+ alignItems: "center",
+ marginBottom: 20,
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: "bold",
+ color: "#333",
+ textAlign: "center",
+ },
+ closeButton: {
+ position: "absolute",
+ right: 0,
+ top: -8,
+ },
+ input: {
+ marginVertical: 10,
+ fontSize: 16,
+ backgroundColor: "white",
+ },
+ buttonContainer: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ marginTop: 10,
+ },
+ menuButton: {
+ flex: 1,
+ marginRight: 10,
+ height: 50,
+ justifyContent: "center",
+ fontSize: 12,
+ },
+ menuButtonContent: {
+ height: 50,
+ justifyContent: "center",
+ },
+ menuContent: {
+ backgroundColor: "#FFFFFF",
+ borderRadius: 8,
+ elevation: 3,
+ shadowColor: "#000000",
+ shadowOffset: {width: 0, height: 2},
+ shadowOpacity: 0.2,
+ shadowRadius: 3,
+ },
+ addButton: {
+ flex: 1,
+ backgroundColor: "#8A7DF7",
+ height: 50,
+ justifyContent: "center",
+ paddingHorizontal: 5,
+ },
+ buttonLabel: {
+ fontSize: 14,
+ color: "white",
+ textAlign: "center",
+ },
+};
+
+export default EnterAccountDetails;
diff --git a/EnterCasdoorSdkConfig.js b/EnterCasdoorSdkConfig.js
index 2de356b..b7a2c29 100644
--- a/EnterCasdoorSdkConfig.js
+++ b/EnterCasdoorSdkConfig.js
@@ -13,8 +13,9 @@
// limitations under the License.
import React from "react";
-import {Alert, Text, View} from "react-native";
+import {ScrollView, Text, View} from "react-native";
import {Button, IconButton, Portal, TextInput} from "react-native-paper";
+import Toast from "react-native-toast-message";
import DefaultCasdoorSdkConfig from "./DefaultCasdoorSdkConfig";
import PropTypes from "prop-types";
import useStore from "./useStorage";
@@ -22,6 +23,7 @@ import useStore from "./useStorage";
const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => {
EnterCasdoorSdkConfig.propTypes = {
onClose: PropTypes.func.isRequired,
+ onWebviewClose: PropTypes.func.isRequired,
};
const {
@@ -44,12 +46,26 @@ const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => {
const handleSave = () => {
if (!serverUrl || !clientId || !appName || !organizationName || !redirectPath) {
- Alert.alert("Please fill in all the fields!");
+ Toast.show({
+ type: "error",
+ text1: "Error",
+ text2: "Please fill in all the fields!",
+ autoHide: true,
+ });
return;
}
onClose();
};
+ const handleScanToLogin = () => {
+ Toast.show({
+ type: "info",
+ text1: "Info",
+ text2: "Scan to Login functionality not implemented yet.",
+ autoHide: true,
+ });
+ };
+
const handleUseDefault = () => {
setCasdoorConfig(DefaultCasdoorSdkConfig);
onClose();
@@ -57,115 +73,155 @@ const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => {
return (
-
-
- Casdoor server
+
+
+
+ Casdoor server
+
+
+
+
+
+
-
-
-
+
);
};
+const styles = {
+ scrollContainer: {
+ flexGrow: 1,
+ width: "100%",
+ justifyContent: "center",
+ alignItems: "center",
+ backgroundColor: "rgba(255, 255, 255, 0.5)",
+ },
+ content: {
+ width: "95%",
+ borderRadius: 10,
+ padding: 20,
+ backgroundColor: "#F5F5F5",
+ shadowColor: "#000",
+ shadowOffset: {width: 0, height: 2},
+ shadowOpacity: 0.1,
+ shadowRadius: 10,
+ elevation: 5,
+ },
+ input: {
+ marginVertical: 10,
+ fontSize: 16,
+ backgroundColor: "white",
+ },
+ buttonRow: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ marginTop: 14,
+ marginBottom: 12,
+ },
+ button: {
+ borderRadius: 5,
+ paddingVertical: 8,
+ },
+ confirmButton: {
+ backgroundColor: "#6200EE",
+ flex: 1,
+ marginRight: 5,
+ },
+ scanButton: {
+ backgroundColor: "#03DAC6",
+ flex: 1,
+ marginLeft: 5,
+ },
+ buttonLabel: {
+ fontSize: 16,
+ color: "white",
+ },
+ outlinedButton: {
+ borderColor: "#6200EE",
+ borderWidth: 1,
+ width: "100%",
+ },
+ outlinedButtonLabel: {
+ color: "#6200EE",
+ fontSize: 16,
+ textAlign: "center",
+ },
+ header: {
+ position: "relative",
+ alignItems: "center",
+ marginBottom: 20,
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: "bold",
+ color: "#333",
+ textAlign: "center",
+ },
+ closeButton: {
+ position: "absolute",
+ right: 0,
+ top: -8,
+ },
+};
+
export default EnterCasdoorSdkConfig;
diff --git a/Header.js b/Header.js
index e814259..224960a 100644
--- a/Header.js
+++ b/Header.js
@@ -15,13 +15,16 @@
import * as React from "react";
import {Dimensions, StyleSheet, View} from "react-native";
import {Appbar, Avatar, Menu, Text, TouchableRipple} from "react-native-paper";
+import Toast from "react-native-toast-message";
import CasdoorLoginPage, {CasdoorLogout} from "./CasdoorLoginPage";
import useStore from "./useStorage";
+import useSyncStore from "./useSyncStore";
const {width} = Dimensions.get("window");
const Header = () => {
const {userInfo, clearAll} = useStore();
+ const syncError = useSyncStore(state => state.syncError);
const [showLoginPage, setShowLoginPage] = React.useState(false);
const [menuVisible, setMenuVisible] = React.useState(false);
@@ -41,8 +44,27 @@ const Header = () => {
clearAll();
};
+ const handleSyncErrorPress = () => {
+ Toast.show({
+ type: "error",
+ text1: "Sync Error",
+ text2: syncError || "An unknown error occurred during synchronization.",
+ autoHide: true,
+ });
+ };
+
return (
-
+
+
+ {true && syncError && (
+
+ )}
+
state.syncError);
useEffect(() => {
if (db) {
@@ -89,11 +92,29 @@ export default function HomePage() {
const onRefresh = async() => {
setRefreshing(true);
- if (canSync) {await startSync(db, userInfo, serverUrl, token);}
+ if (canSync) {
+ await startSync(db, userInfo, serverUrl, token);
+ if (syncError) {
+ Toast.show({
+ type: "error",
+ text1: "Sync error",
+ text2: syncError,
+ autoHide: true,
+ });
+ } else {
+ Toast.show({
+ type: "success",
+ text1: "Sync success",
+ text2: "All your accounts are up to date.",
+ autoHide: true,
+ });
+ }
+ }
setRefreshing(false);
};
const handleAddAccount = async(accountData) => {
+ setKey(prevKey => prevKey + 1);
await TotpDatabase.insertAccount(db, accountData);
closeEnterAccountModal();
};
@@ -218,6 +239,7 @@ export default function HomePage() {
right={() => (
{
TotpDatabase.updateToken(db, item.id);
- return {shouldRepeat: true, delay: 0};
+ return {
+ shouldRepeat: true,
+ delay: 0,
+ newInitialRemainingTime: TotpDatabase.calculateCountdown(),
+ };
}}
strokeWidth={5}
>
@@ -292,7 +318,7 @@ export default function HomePage() {
transform: [{translateX: -OFFSET_X}, {translateY: -OFFSET_Y}],
}}
>
-
+
diff --git a/ScanQRCode.js b/ScanQRCode.js
index 7178382..781c817 100644
--- a/ScanQRCode.js
+++ b/ScanQRCode.js
@@ -13,8 +13,8 @@
// 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 {Text, View} from "react-native";
+import {IconButton, Portal} from "react-native-paper";
import {Camera, CameraView} from "expo-camera";
import PropTypes from "prop-types";
@@ -54,41 +54,23 @@ const ScanQRCode = ({onClose, showScanner, onAdd}) => {
closeOptions();
};
- const {width, height} = Dimensions.get("window");
- const offsetX = width * 0.5;
- const offsetY = height * 0.5;
-
return (
-
- {hasPermission === null ? (
- Requesting for camera permission
- ) : hasPermission === false ? (
- No access to camera
- ) : (
-
- )}
-
-
+ {hasPermission === null ? (
+ Requesting for camera permission
+ ) : hasPermission === false ? (
+ No access to camera
+ ) : (
+
+ )}
+
);
diff --git a/SettingPage.js b/SettingPage.js
index 336ee71..69503e8 100644
--- a/SettingPage.js
+++ b/SettingPage.js
@@ -13,14 +13,15 @@
// limitations under the License.
import * as React from "react";
-import {Button} from "react-native-paper";
-import {View} from "react-native";
+import {StyleSheet, View, useWindowDimensions} from "react-native";
+import {Button, Surface, Text} from "react-native-paper";
import CasdoorLoginPage, {CasdoorLogout} from "./CasdoorLoginPage";
import useStore from "./useStorage";
const SettingPage = () => {
const [showLoginPage, setShowLoginPage] = React.useState(false);
const {userInfo, clearAll} = useStore();
+ const {width} = useWindowDimensions();
const handleCasdoorLogin = () => setShowLoginPage(true);
const handleHideLoginPage = () => setShowLoginPage(false);
@@ -30,16 +31,42 @@ const SettingPage = () => {
clearAll();
};
+ const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: "center",
+ alignItems: "center",
+ padding: 16,
+ },
+ surface: {
+ padding: 16,
+ width: width > 600 ? 400 : "100%",
+ maxWidth: 400,
+ alignItems: "center",
+ },
+ title: {
+ fontSize: 24,
+ marginBottom: 24,
+ },
+ button: {
+ marginTop: 16,
+ width: "100%",
+ },
+ });
+
return (
-
-
+
+
+ Account Settings
+
+
{showLoginPage && }
);
diff --git a/TotpDatabase.js b/TotpDatabase.js
index 0c7b5bb..ac1d30c 100644
--- a/TotpDatabase.js
+++ b/TotpDatabase.js
@@ -45,6 +45,17 @@ CREATE TABLE accounts (
await db.execAsync(`PRAGMA user_version = ${DATABASE_VERSION}`);
}
+export async function clearDatabase(db) {
+ try {
+ await db.execAsync("DELETE FROM accounts");
+ await db.execAsync("DELETE FROM sqlite_sequence WHERE name='accounts'");
+ await db.execAsync("PRAGMA user_version = 0");
+ return true;
+ } catch (error) {
+ return false;
+ }
+}
+
const generateToken = (secretKey) => {
if (secretKey !== null && secretKey !== undefined && secretKey !== "") {
try {
@@ -147,7 +158,6 @@ export async function getAllAccounts(db) {
const mappedAccount = {
...account,
accountName: account.account_name,
- secretKey: account.secret,
};
return mappedAccount;
});
@@ -173,10 +183,18 @@ async function updateSyncTimeForAll(db) {
}
export function calculateCountdown() {
- const now = Math.floor(Date.now() / 1000);
+ const now = Math.round(new Date().getTime() / 1000.0);
return 30 - (now % 30);
}
+export function validateSecret(secret) {
+ const base32Regex = /^[A-Z2-7]+=*$/i;
+ if (!secret || secret.length % 8 !== 0) {
+ return false;
+ }
+ return base32Regex.test(secret);
+}
+
async function updateLocalDatabase(db, mergedAccounts) {
for (const account of mergedAccounts) {
if (account.id) {
diff --git a/app.json b/app.json
index 2515267..f870a5f 100644
--- a/app.json
+++ b/app.json
@@ -1,7 +1,7 @@
{
"expo": {
"name": "Casdoor",
- "slug": "Casdoor",
+ "slug": "casdoor-app",
"version": "1.2.0",
"orientation": "portrait",
"icon": "./assets/icon.png",