From 37a03e45ccc7ca49e95ba95f03542ec3644770ed Mon Sep 17 00:00:00 2001 From: IZUMI-Zu <274620705z@gmail.com> Date: Sun, 22 Sep 2024 23:06:20 +0800 Subject: [PATCH] feat: can scan Casdoor QR code to login (#27) --- App.js | 35 +++++++--- CasdoorLoginPage.js | 23 ++++--- EnterAccountDetails.js | 20 +++--- EnterCasdoorSdkConfig.js | 55 ++++++++++++---- Header.js | 134 +++++++++++++++++++++------------------ HomePage.js | 39 ++++++------ ScanLogin.js | 9 +-- SearchBar.js | 38 ++++++++--- api.js | 13 ++++ babel.config.js | 10 ++- package-lock.json | 23 ++++--- package.json | 2 +- 12 files changed, 256 insertions(+), 145 deletions(-) diff --git a/App.js b/App.js index 010fdf8..d8cb384 100644 --- a/App.js +++ b/App.js @@ -17,7 +17,8 @@ import {NavigationContainer} from "@react-navigation/native"; import {PaperProvider} from "react-native-paper"; import {SafeAreaView, Text} from "react-native"; import ContentLoader, {Circle, Rect} from "react-content-loader/native"; -import Toast from "react-native-toast-message"; +import {ZoomInDownZoomOutUp, createNotifications} from "react-native-notificated"; +import {GestureHandlerRootView} from "react-native-gesture-handler"; import {useMigrations} from "drizzle-orm/expo-sqlite/migrator"; import Header from "./Header"; @@ -27,6 +28,21 @@ import migrations from "./drizzle/migrations"; const App = () => { const {success, error} = useMigrations(db, migrations); + const {NotificationsProvider} = createNotifications({ + duration: 800, + notificationPosition: "top", + animationConfig: ZoomInDownZoomOutUp, + isNotch: true, + notificationWidth: 350, + defaultStylesSettings: { + globalConfig: { + borderRadius: 15, + borderWidth: 2, + multiline: 3, + defaultIconType: "no-icon", + }, + }, + }); if (error) { return ( @@ -59,13 +75,16 @@ const App = () => { } return ( - - -
- - - - + + + + +
+ + + + + ); }; export default App; diff --git a/CasdoorLoginPage.js b/CasdoorLoginPage.js index effb71f..8094681 100644 --- a/CasdoorLoginPage.js +++ b/CasdoorLoginPage.js @@ -16,10 +16,9 @@ import React, {useEffect, useState} from "react"; import {WebView} from "react-native-webview"; import {Platform, SafeAreaView, StatusBar, StyleSheet, Text, TouchableOpacity} from "react-native"; import {Portal} from "react-native-paper"; +import {useNotifications} from "react-native-notificated"; 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"; @@ -31,6 +30,7 @@ const CasdoorLoginPage = ({onWebviewClose}) => { onWebviewClose: PropTypes.func.isRequired, }; + const {notify} = useNotifications(); const [casdoorLoginURL, setCasdoorLoginURL] = useState(""); const [showConfigPage, setShowConfigPage] = useState(true); @@ -40,6 +40,7 @@ const CasdoorLoginPage = ({onWebviewClose}) => { redirectPath, appName, organizationName, + token, getCasdoorConfig, setUserInfo, setToken, @@ -65,6 +66,12 @@ const CasdoorLoginPage = ({onWebviewClose}) => { } }, [serverUrl, clientId, redirectPath, appName, organizationName]); + useEffect(() => { + if (token) { + onWebviewClose(); + } + }, [token]); + const onNavigationStateChange = async(navState) => { const {redirectPath} = getCasdoorConfig(); if (navState.url.startsWith(redirectPath)) { @@ -77,11 +84,11 @@ const CasdoorLoginPage = ({onWebviewClose}) => { }; const handleErrorResponse = (error) => { - Toast.show({ - type: "error", - text1: "Error", - text2: error.description, - autoHide: true, + notify("error", { + params: { + text1: "Error", + text2: error.description, + }, }); setShowConfigPage(true); }; @@ -95,7 +102,7 @@ const CasdoorLoginPage = ({onWebviewClose}) => { onWebviewClose={onWebviewClose} /> )} - {!showConfigPage && casdoorLoginURL !== "" && ( + {!showConfigPage && casdoorLoginURL !== "" && !token && ( <> { @@ -25,6 +25,8 @@ const EnterAccountDetails = ({onClose, onAdd, validateSecret}) => { validateSecret: PropTypes.func.isRequired, }; + const {notify} = useNotifications(); + const [accountName, setAccountName] = useState(""); const [secretKey, setSecretKey] = useState(""); const [secretError, setSecretError] = useState(""); @@ -51,21 +53,17 @@ const EnterAccountDetails = ({onClose, onAdd, validateSecret}) => { } if (accountName.trim() === "" || secretKey.trim() === "") { - Toast.show({ - type: "error", - text1: "Error", - text2: "Please fill in all the fields!", - autoHide: true, + notify("error", { + title: "Error", + description: "Please fill in all the fields!", }); return; } if (secretError) { - Toast.show({ - type: "error", - text1: "Invalid Secret Key", - text2: "Please check your secret key and try again.", - autoHide: true, + notify("error", { + title: "Error", + description: "Invalid Secret Key", }); return; } diff --git a/EnterCasdoorSdkConfig.js b/EnterCasdoorSdkConfig.js index c480602..6f52551 100644 --- a/EnterCasdoorSdkConfig.js +++ b/EnterCasdoorSdkConfig.js @@ -15,7 +15,8 @@ import React, {useState} from "react"; import {ScrollView, Text, View} from "react-native"; import {Button, IconButton, Portal, TextInput} from "react-native-paper"; -import Toast from "react-native-toast-message"; +import {useNotifications} from "react-native-notificated"; +import SDK from "casdoor-react-native-sdk"; import DefaultCasdoorSdkConfig from "./DefaultCasdoorSdkConfig"; import PropTypes from "prop-types"; import ScanQRCodeForLogin from "./ScanLogin"; @@ -38,8 +39,13 @@ const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => { setAppName, setOrganizationName, setCasdoorConfig, + getCasdoorConfig, + setToken, + setUserInfo, } = useStore(); + const {notify} = useNotifications(); + const [showScanner, setShowScanner] = useState(false); const closeConfigPage = () => { @@ -49,11 +55,11 @@ const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => { const handleSave = () => { if (!serverUrl || !clientId || !appName || !organizationName || !redirectPath) { - Toast.show({ - type: "error", - text1: "Error", - text2: "Please fill in all the fields!", - autoHide: true, + notify("error", { + params: { + title: "Error", + description: "Please fill in all the fields!", + }, }); return; } @@ -66,11 +72,36 @@ const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => { const handleLogin = (loginInfo) => { setServerUrl(loginInfo.serverUrl); - setClientId(loginInfo.clientId); - setAppName(loginInfo.appName); - setOrganizationName(loginInfo.organizationName); - setShowScanner(false); - onClose(); + setClientId(""); + setAppName(""); + setOrganizationName(""); + + const sdk = new SDK(getCasdoorConfig()); + + try { + const accessToken = loginInfo.accessToken; + const userInfo = sdk.JwtDecode(accessToken); + setToken(accessToken); + setUserInfo(userInfo); + + notify("success", { + params: { + title: "Success", + description: "Logged in successfully!", + }, + }); + + setShowScanner(false); + onClose(); + onWebviewClose(); + } catch (error) { + notify("error", { + params: { + title: "Error in login", + description: error, + }, + }); + } }; const handleUseDefault = () => { @@ -147,7 +178,7 @@ const EnterCasdoorSdkConfig = ({onClose, onWebviewClose}) => { style={[styles.button, styles.outlinedButton]} labelStyle={styles.outlinedButtonLabel} > - Use Casdoor Demo Site + Try with Casdoor Demo Site diff --git a/Header.js b/Header.js index 90a98d1..d2300e8 100644 --- a/Header.js +++ b/Header.js @@ -15,7 +15,8 @@ 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 {useNotifications} from "react-native-notificated"; +import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import CasdoorLoginPage, {CasdoorLogout} from "./CasdoorLoginPage"; import useStore from "./useStorage"; import {useAccountSync} from "./useAccountStore"; @@ -24,9 +25,10 @@ const {width} = Dimensions.get("window"); const Header = () => { const {userInfo, clearAll} = useStore(); - const {syncError, clearSyncError} = useAccountSync(); + const {isSyncing, syncError, clearSyncError} = useAccountSync(); const [showLoginPage, setShowLoginPage] = React.useState(false); const [menuVisible, setMenuVisible] = React.useState(false); + const {notify} = useNotifications(); const openMenu = () => setMenuVisible(true); const closeMenu = () => setMenuVisible(false); @@ -46,32 +48,40 @@ const Header = () => { }; const handleSyncErrorPress = () => { - Toast.show({ - type: "error", - text1: "Sync Error", - text2: syncError || "An unknown error occurred during synchronization.", - autoHide: true, + notify("error", { + params: { + title: "Error", + description: syncError || "An unknown error occurred during synchronization.", + }, }); }; return ( - - - {true && syncError && ( - - )} - + + Casdoor + + } + style={styles.titleWrapper} /> + {userInfo !== null && ( + + )} { style={styles.buttonContainer} > - - {userInfo === null ? "Login" : userInfo.name} - {userInfo !== null && ( )} + + {userInfo === null ? "Login" : userInfo.name} + } @@ -112,63 +120,63 @@ const Header = () => { }; const styles = StyleSheet.create({ - leftContainer: { - position: "absolute", - left: 0, - top: 0, - bottom: 0, - justifyContent: "center", - paddingLeft: width * 0.03, + header: { + backgroundColor: "#F2F2F2", + height: 56, }, rightContainer: { - position: "absolute", - right: 0, - top: 0, - bottom: 0, - justifyContent: "center", - paddingRight: width * 0.03, + flexDirection: "row", + alignItems: "center", + paddingRight: width * 0.04, + }, + titleWrapper: { + alignItems: "flex-start", }, titleContainer: { - position: "absolute", - left: 0, - right: 0, - top: 0, - bottom: 0, - justifyContent: "center", - alignItems: "center", + flexDirection: "row", + alignItems: "baseline", }, - titleText: { - fontSize: Math.max(20, width * 0.045), + titleTextCasdoor: { + fontSize: Math.max(24, width * 0.05), fontWeight: "bold", - textAlign: "center", + color: "#212121", + fontFamily: "Lato-Bold", }, buttonContainer: { - borderRadius: 20, + borderRadius: 24, overflow: "hidden", + borderWidth: 0.5, + borderColor: "#DDDDDD", }, buttonContent: { flexDirection: "row", alignItems: "center", justifyContent: "center", - paddingVertical: 8, - paddingHorizontal: 16, + paddingVertical: 6, + paddingHorizontal: 14, }, buttonText: { - fontSize: Math.max(14, width * 0.035), - fontWeight: "bold", + fontSize: Math.max(14, width * 0.042), + fontWeight: "600", + marginLeft: 8, + color: "#424242", + fontFamily: "Roboto-Medium", }, menuContent: { - backgroundColor: "#FFFFFF", + backgroundColor: "#FAFAFA", borderRadius: 8, - elevation: 3, + elevation: 2, shadowColor: "#000000", - shadowOffset: {width: 0, height: 2}, - shadowOpacity: 0.2, - shadowRadius: 3, + shadowOffset: {width: 0, height: 1}, + shadowOpacity: 0.1, + shadowRadius: 2, }, avatar: { backgroundColor: "transparent", }, + syncIcon: { + marginRight: 12, + }, }); export default Header; diff --git a/HomePage.js b/HomePage.js index 45fcc07..3c602bc 100644 --- a/HomePage.js +++ b/HomePage.js @@ -19,7 +19,7 @@ import {GestureHandlerRootView, Swipeable} from "react-native-gesture-handler"; import {CountdownCircleTimer} from "react-native-countdown-circle-timer"; import {useNetInfo} from "@react-native-community/netinfo"; import {FlashList} from "@shopify/flash-list"; -import Toast from "react-native-toast-message"; +import {useNotifications} from "react-native-notificated"; import SearchBar from "./SearchBar"; import EnterAccountDetails from "./EnterAccountDetails"; @@ -50,12 +50,12 @@ export default function HomePage() { const {isConnected} = useNetInfo(); const [canSync, setCanSync] = useState(false); const [key, setKey] = useState(0); - const swipeableRef = useRef(null); const {userInfo, serverUrl, token} = useStore(); const {startSync} = useAccountSync(); const {accounts, refreshAccounts} = useAccountStore(); const {setAccount, updateAccount, insertAccount, insertAccounts, deleteAccount} = useEditAccount(); + const {notify} = useNotifications(); useEffect(() => { refreshAccounts(); @@ -79,25 +79,25 @@ export default function HomePage() { } }, REFRESH_INTERVAL); return () => clearInterval(timer); - }, [startSync, canSync]); + }, [startSync, canSync, token]); const onRefresh = async() => { setRefreshing(true); if (canSync) { const syncError = await startSync(userInfo, serverUrl, token); if (syncError) { - Toast.show({ - type: "error", - text1: "Sync error", - text2: syncError, - autoHide: true, + notify("error", { + params: { + title: "Sync error", + description: syncError, + }, }); } else { - Toast.show({ - type: "success", - text1: "Sync success", - text2: "All your accounts are up to date.", - autoHide: true, + notify("success", { + params: { + title: "Sync success", + description: "All your accounts are up to date.", + }, }); } } @@ -151,11 +151,11 @@ export default function HomePage() { const handleScanError = (error) => { setShowScanner(false); - Toast.show({ - type: "error", - text1: "Scan error", - text2: error, - autoHide: true, + notify("error", { + params: { + title: "Error scanning QR code", + description: error, + }, }); }; @@ -226,7 +226,8 @@ export default function HomePage() { { }; const isValidLoginQR = (data) => { - return data.startsWith("casdoor-app://login/into?"); + return data.startsWith("casdoor-app://login?"); }; const parseLoginQR = (data) => { @@ -34,10 +34,11 @@ const ScanQRCodeForLogin = ({onClose, showScanner, onLogin}) => { const params = new URLSearchParams(url.search); return { + // clientId: params.get("clientId"), + // appName: params.get("appName"), + // organizationName: params.get("organizationName"), serverUrl: params.get("serverUrl"), - clientId: params.get("clientId"), - appName: params.get("appName"), - organizationName: params.get("organizationName"), + accessToken: params.get("accessToken"), }; }; diff --git a/SearchBar.js b/SearchBar.js index 1561d5b..e6a44a8 100644 --- a/SearchBar.js +++ b/SearchBar.js @@ -13,25 +13,47 @@ // limitations under the License. import * as React from "react"; +import {View} from "react-native"; import {Searchbar} from "react-native-paper"; const SearchBar = ({onSearch}) => { const [searchQuery, setSearchQuery] = React.useState(""); - const onChangeSearch = query => { + const onChangeSearch = (query) => { setSearchQuery(query); onSearch(query); }; return ( - + + + ); }; +const styles = { + container: { + alignItems: "center", + paddingTop: 2, + }, + searchbar: { + height: 56, + backgroundColor: "#E6DFF3", + borderRadius: 99, + width: "95%", + }, + inputStyle: { + minHeight: 0, + textAlignVertical: "center", + justifyContent: "center", + alignItems: "center", + }, +}; + export default SearchBar; diff --git a/api.js b/api.js index c83d954..963e776 100644 --- a/api.js +++ b/api.js @@ -32,6 +32,12 @@ export const getMfaAccounts = async(serverUrl, owner, name, token, timeoutMs = T ]); const res = await result.json(); + + // Check the response status and message + if (res.status === "error") { + throw new Error(res.msg); + } + return {updatedTime: res.data.updatedTime, mfaAccounts: res.data.mfaAccounts}; } catch (error) { if (error.name === "AbortError") { @@ -58,6 +64,7 @@ export const updateMfaAccounts = async(serverUrl, owner, name, newMfaAccounts, t ]); const userData = await getUserResult.json(); + userData.data.mfaAccounts = newMfaAccounts; const updateResult = await Promise.race([ @@ -74,6 +81,12 @@ export const updateMfaAccounts = async(serverUrl, owner, name, newMfaAccounts, t ]); const res = await updateResult.json(); + + // Check the response status and message + if (res.status === "error") { + throw new Error(res.msg); + } + return {status: res.status, data: res.data}; } catch (error) { if (error.name === "AbortError") { diff --git a/babel.config.js b/babel.config.js index dabd037..a6b7063 100644 --- a/babel.config.js +++ b/babel.config.js @@ -2,6 +2,14 @@ module.exports = function(api) { api.cache(true); return { presets: ["babel-preset-expo"], - plugins: [["inline-import", {"extensions": [".sql"]}]], + plugins: [ + [ + "inline-import", + { + "extensions": [".sql"], + }, + ], + "react-native-reanimated/plugin", + ], }; }; diff --git a/package-lock.json b/package-lock.json index 81f71db..7aac8cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,12 +40,12 @@ "react-native": "0.74.5", "react-native-countdown-circle-timer": "^3.2.1", "react-native-gesture-handler": "~2.16.1", + "react-native-notificated": "^0.1.6", "react-native-paper": "^5.10.3", "react-native-reanimated": "~3.10.1", "react-native-safe-area-context": "4.10.5", "react-native-screens": "3.31.1", "react-native-svg": "15.2.0", - "react-native-toast-message": "^2.2.0", "react-native-web": "~0.19.6", "react-native-webview": "13.8.6", "totp-generator": "^0.0.14", @@ -15644,6 +15644,18 @@ "react-native": "*" } }, + "node_modules/react-native-notificated": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/react-native-notificated/-/react-native-notificated-0.1.6.tgz", + "integrity": "sha512-thv+uhQlDHzdKOL2QJcaDC/IT7F9NivdIPutbnr6YZ+MfINr5qeVOeEAuUs+4xIxADvP3MVGehyTGIYnAUU8Og==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-gesture-handler": "^2.9.0", + "react-native-reanimated": "^2.14.4 || ^3.0.0" + } + }, "node_modules/react-native-paper": { "version": "5.10.6", "resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.10.6.tgz", @@ -15807,15 +15819,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/react-native-toast-message": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/react-native-toast-message/-/react-native-toast-message-2.2.0.tgz", - "integrity": "sha512-AFti8VzUk6JvyGAlLm9/BknTNDXrrhqnUk7ak/pM7uCTxDPveAu2ekszU0on6vnUPFnG04H/QfYE2IlETqeaWw==", - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "node_modules/react-native-vector-icons": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.0.0.tgz", diff --git a/package.json b/package.json index ff1f2d4..0fcc210 100644 --- a/package.json +++ b/package.json @@ -42,12 +42,12 @@ "react-native": "0.74.5", "react-native-countdown-circle-timer": "^3.2.1", "react-native-gesture-handler": "~2.16.1", + "react-native-notificated": "^0.1.6", "react-native-paper": "^5.10.3", "react-native-reanimated": "~3.10.1", "react-native-safe-area-context": "4.10.5", "react-native-screens": "3.31.1", "react-native-svg": "15.2.0", - "react-native-toast-message": "^2.2.0", "react-native-web": "~0.19.6", "react-native-webview": "13.8.6", "totp-generator": "^0.0.14",