diff --git a/App.js b/App.js
index 1c4ed43..0770876 100644
--- a/App.js
+++ b/App.js
@@ -13,16 +13,19 @@
// limitations under the License.
import React from "react";
+import {SafeAreaView, Text} from "react-native";
+
import {Lato_700Bold, useFonts} from "@expo-google-fonts/lato";
import {Roboto_500Medium} from "@expo-google-fonts/roboto";
+import * as Crypto from "expo-crypto";
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 {ZoomInDownZoomOutUp, createNotifications} from "react-native-notificated";
import {GestureHandlerRootView} from "react-native-gesture-handler";
import {useMigrations} from "drizzle-orm/expo-sqlite/migrator";
import {ActionSheetProvider} from "@expo/react-native-action-sheet";
+import AsyncStorage from "@react-native-async-storage/async-storage";
import "./i18n";
import Header from "./Header";
@@ -37,6 +40,17 @@ const App = () => {
Roboto_500Medium,
});
+ React.useEffect(() => {
+ const storeOrigin = async() => {
+ let origin = await AsyncStorage.getItem("origin");
+ if (!origin) {
+ origin = Crypto.randomUUID();
+ await AsyncStorage.setItem("origin", origin);
+ }
+ };
+ storeOrigin();
+ }, []);
+
const {NotificationsProvider} = createNotifications({
duration: 800,
notificationPosition: "top",
diff --git a/CasdoorLoginPage.js b/CasdoorLoginPage.js
index d830d77..c2a7ce5 100644
--- a/CasdoorLoginPage.js
+++ b/CasdoorLoginPage.js
@@ -17,6 +17,7 @@ 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 AsyncStorage from "@react-native-async-storage/async-storage";
import SDK from "casdoor-react-native-sdk";
import PropTypes from "prop-types";
import EnterCasdoorSdkConfig from "./EnterCasdoorSdkConfig";
@@ -25,6 +26,7 @@ import useStore from "./useStorage";
import DefaultCasdoorSdkConfig from "./DefaultCasdoorSdkConfig";
import {useTranslation} from "react-i18next";
import {useLanguageSync} from "./useLanguageSync";
+import {useEditAccount} from "./useAccountStore";
let sdk = null;
@@ -208,10 +210,20 @@ const styles = StyleSheet.create({
},
});
-export const CasdoorLogout = () => {
- if (sdk) {
- sdk.clearState();
- }
+export const useCasdoorLogout = () => {
+ const {deleteAccountByOrigin} = useEditAccount();
+
+ const logout = async() => {
+ const origin = await AsyncStorage.getItem("origin");
+
+ if (sdk) {
+ sdk.clearState();
+ }
+
+ deleteAccountByOrigin(origin);
+ };
+
+ return logout;
};
export default CasdoorLoginPage;
diff --git a/EditAccountDetails.js b/EditAccountDetails.js
index ad2253b..3dc5c5d 100644
--- a/EditAccountDetails.js
+++ b/EditAccountDetails.js
@@ -13,7 +13,7 @@
// limitations under the License.
import React, {useState} from "react";
-import {Text, TextInput, View} from "react-native";
+import {StyleSheet, Text, TextInput, View} from "react-native";
import {Button, IconButton} from "react-native-paper";
import PropTypes from "prop-types";
import {useTranslation} from "react-i18next";
@@ -26,40 +26,102 @@ export default function EnterAccountDetails({onClose, onEdit, placeholder}) {
};
const {t} = useTranslation();
- const [accountName, setAccountName] = useState("");
+ const [accountName, setAccountName] = useState(placeholder);
const handleConfirm = () => {
onEdit(accountName);
};
return (
-
- {t("editAccount.Enter new account name")}
-
-
+
+
+
+ {t("editAccount.Enter new account name")}
+
+
setAccountName(text)}
+ style={styles.input}
+ mode="outlined"
autoCapitalize="none"
- style={{borderWidth: 3, borderColor: "white", margin: 10, width: 230, height: 50, borderRadius: 5, fontSize: 18, color: "gray", paddingLeft: 10}}
/>
+
+
+
-
-
);
}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ borderRadius: 10,
+ },
+ content: {
+ width: "100%",
+ maxWidth: 500,
+ paddingVertical: 16,
+ paddingHorizontal: 20,
+ },
+ header: {
+ flexDirection: "row",
+ alignItems: "center",
+ position: "relative",
+ minHeight: 32,
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: "bold",
+ color: "#333",
+ position: "absolute",
+ left: 40,
+ right: 40,
+ textAlign: "center",
+ numberOfLines: 1,
+ },
+ closeButton: {
+ marginLeft: "auto",
+ zIndex: 1,
+ },
+ input: {
+ marginVertical: 10,
+ paddingHorizontal: 10,
+ fontSize: 16,
+ height: 50,
+ backgroundColor: "#F5F5F5",
+ borderRadius: 10,
+ },
+ buttonContainer: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ marginTop: 12,
+ },
+ addButton: {
+ flex: 1,
+ backgroundColor: "#8A7DF7",
+ height: 50,
+ justifyContent: "center",
+ paddingHorizontal: 5,
+ },
+ buttonLabel: {
+ fontSize: 14,
+ color: "white",
+ textAlign: "center",
+ },
+});
diff --git a/Header.js b/Header.js
index 2409c02..5471ba6 100644
--- a/Header.js
+++ b/Header.js
@@ -17,7 +17,7 @@ import {Dimensions, StyleSheet, View} from "react-native";
import {Appbar, Avatar, Menu, Text, TouchableRipple} from "react-native-paper";
import {useNotifications} from "react-native-notificated";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
-import CasdoorLoginPage, {CasdoorLogout} from "./CasdoorLoginPage";
+import CasdoorLoginPage, {useCasdoorLogout} from "./CasdoorLoginPage";
import useStore from "./useStorage";
import {useAccountSync} from "./useAccountStore";
import LoginMethodSelector from "./LoginMethodSelector";
@@ -35,6 +35,7 @@ const Header = () => {
const {t} = useTranslation();
const openMenu = () => setMenuVisible(true);
const closeMenu = () => setMenuVisible(false);
+ const logout = useCasdoorLogout();
const handleMenuLogoutClicked = () => {
handleCasdoorLogout();
@@ -57,7 +58,7 @@ const Header = () => {
};
const handleCasdoorLogout = () => {
- CasdoorLogout();
+ logout();
clearAll();
clearSyncError();
};
diff --git a/HomePage.js b/HomePage.js
index 6318c4a..5401e4f 100644
--- a/HomePage.js
+++ b/HomePage.js
@@ -1,413 +1,457 @@
-// 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, useRef, useState} from "react";
-import {Dimensions, InteractionManager, RefreshControl, TouchableOpacity, View} from "react-native";
-import {Divider, IconButton, List, Modal, Portal, Text} from "react-native-paper";
-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 {useNotifications} from "react-native-notificated";
-import {useTranslation} from "react-i18next";
-
-import SearchBar from "./SearchBar";
-import EnterAccountDetails from "./EnterAccountDetails";
-import ScanQRCode from "./ScanQRCode";
-import EditAccountDetails from "./EditAccountDetails";
-import AvatarWithFallback from "./AvatarWithFallback";
-import {useImportManager} from "./ImportManager";
-import useStore from "./useStorage";
-import {calculateCountdown} from "./totpUtil";
-import {generateToken, validateSecret} from "./totpUtil";
-import {useAccountStore, useAccountSync, useEditAccount} from "./useAccountStore";
-
-const {width, height} = Dimensions.get("window");
-const REFRESH_INTERVAL = 10000;
-const OFFSET_X = width * 0.45;
-const OFFSET_Y = height * 0.2;
-
-export default function HomePage() {
- const [isPlusButton, setIsPlusButton] = useState(true);
- const [showOptions, setShowOptions] = useState(false);
- const [showEnterAccountModal, setShowEnterAccountModal] = useState(false);
- const [searchQuery, setSearchQuery] = useState("");
- const [filteredData, setFilteredData] = useState(accounts);
- const [showScanner, setShowScanner] = useState(false);
- const [showEditAccountModal, setShowEditAccountModal] = useState(false);
- const [editingAccount, setEditingAccount] = useState(null);
- const [placeholder, setPlaceholder] = useState("");
- const [refreshing, setRefreshing] = useState(false);
- 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();
- const {t} = useTranslation();
- const {showImportOptions} = useImportManager((data) => {
- handleAddAccount(data);
- }, (err) => {
- notify("error", {
- params: {
- title: t("homepage.Import error"),
- description: err.message,
- },
- });
- }, () => {
- setShowScanner(true);
- });
-
- useEffect(() => {
- refreshAccounts();
- }, []);
-
- useEffect(() => {
- setCanSync(Boolean(isConnected && userInfo && serverUrl));
- }, [isConnected, userInfo, serverUrl]);
-
- useEffect(() => {
- setFilteredData(accounts);
- }, [accounts]);
-
- useEffect(() => {
- const timer = setInterval(() => {
- if (canSync) {
- InteractionManager.runAfterInteractions(() => {
- startSync(userInfo, serverUrl, token);
- refreshAccounts();
- });
- }
- }, REFRESH_INTERVAL);
- return () => clearInterval(timer);
- }, [startSync, canSync, token]);
-
- const onRefresh = async() => {
- setRefreshing(true);
- if (canSync) {
- const syncError = await startSync(userInfo, serverUrl, token);
- if (syncError) {
- notify("error", {
- params: {
- title: "Sync error",
- description: syncError,
- },
- });
- } else {
- notify("success", {
- params: {
- title: "Sync success",
- description: "All your accounts are up to date.",
- },
- });
- }
- }
- refreshAccounts();
- setRefreshing(false);
- };
-
- const handleAddAccount = async(accountDataInput) => {
- if (Array.isArray(accountDataInput)) {
- await insertAccounts(accountDataInput);
- } else {
- await setAccount(accountDataInput);
- await insertAccount();
- closeEnterAccountModal();
- }
- refreshAccounts();
- };
-
- const handleEditAccount = (account) => {
- closeSwipeableMenu();
- setEditingAccount(account);
- setPlaceholder(account.accountName);
- setShowEditAccountModal(true);
- };
-
- const onAccountEdit = async(newAccountName) => {
- if (editingAccount) {
- setAccount({...editingAccount, accountName: newAccountName, oldAccountName: editingAccount.accountName});
- updateAccount();
- refreshAccounts();
- setPlaceholder("");
- setEditingAccount(null);
- closeEditAccountModal();
- }
- };
-
- const onAccountDelete = async(account) => {
- deleteAccount(account.id);
- refreshAccounts();
- };
-
- const closeEditAccountModal = () => setShowEditAccountModal(false);
-
- const handleScanPress = () => {
- setShowScanner(true);
- setIsPlusButton(true);
- setShowOptions(false);
- };
-
- const handleCloseScanner = () => setShowScanner(false);
-
- const handleScanError = (error) => {
- setShowScanner(false);
- notify("error", {
- params: {
- title: t("homepage.Error scanning QR code"),
- description: error,
- },
- });
- };
-
- const togglePlusButton = () => {
- setIsPlusButton(!isPlusButton);
- setShowOptions(!showOptions);
- };
-
- const closeOptions = () => {
- setIsPlusButton(true);
- setShowOptions(false);
- setShowScanner(false);
- };
-
- const openEnterAccountModal = () => {
- setShowEnterAccountModal(true);
- closeOptions();
- };
-
- const openImportAccountModal = () => {
- showImportOptions();
- closeOptions();
- };
-
- const closeEnterAccountModal = () => setShowEnterAccountModal(false);
-
- const closeSwipeableMenu = () => {
- if (swipeableRef.current) {
- swipeableRef.current.close();
- }
- };
-
- const handleSearch = (query) => {
- setSearchQuery(query);
- setFilteredData(query.trim() !== ""
- ? accounts && accounts.filter(item => item.accountName.toLowerCase().includes(query.toLowerCase()))
- : accounts
- );
- };
-
- return (
-
-
- `${item.id}`}
- extraData={key}
- estimatedItemSize={80}
- refreshControl={
-
- }
- renderItem={({item}) => (
-
- (
-
- handleEditAccount(item)}
- >
- {t("common.edit")}
-
- onAccountDelete(item)}
- >
- {t("common.delete")}
-
-
- )}
- >
-
-
- {item.accountName}
-
- {generateToken(item.secretKey)}
-
- }
- left={() => (
-
- )}
- right={() => (
-
- {
- setKey(prevKey => prevKey + 1);
- return {
- shouldRepeat: true,
- delay: 0,
- newInitialRemainingTime: calculateCountdown(),
- };
- }}
- strokeWidth={5}
- >
- {({remainingTime}) => (
- {remainingTime}s
- )}
-
-
- )}
- />
-
-
- )}
- ItemSeparatorComponent={() => }
- />
-
-
-
-
-
-
- {t("homepage.Scan QR Code")}
-
-
-
-
-
- {t("homepage.Enter Secret Code")}
-
-
-
-
-
- {t("homepage.Import from other app")}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {showScanner && (
-
- )}
-
-
-
-
-
- );
-}
+// 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, useRef, useState} from "react";
+import {Dimensions, InteractionManager, RefreshControl, TouchableOpacity, View} from "react-native";
+import {Divider, IconButton, List, Modal, Portal, Text} from "react-native-paper";
+import {GestureHandlerRootView} from "react-native-gesture-handler";
+import Swipeable from "react-native-gesture-handler/ReanimatedSwipeable";
+import {CountdownCircleTimer} from "react-native-countdown-circle-timer";
+import {useNetInfo} from "@react-native-community/netinfo";
+import {FlashList} from "@shopify/flash-list";
+import {useNotifications} from "react-native-notificated";
+import {useTranslation} from "react-i18next";
+import Animated, {
+ useAnimatedStyle,
+ withTiming
+} from "react-native-reanimated";
+import {MaterialCommunityIcons} from "@expo/vector-icons";
+
+import SearchBar from "./SearchBar";
+import EnterAccountDetails from "./EnterAccountDetails";
+import ScanQRCode from "./ScanQRCode";
+import EditAccountDetails from "./EditAccountDetails";
+import AvatarWithFallback from "./AvatarWithFallback";
+import {useImportManager} from "./ImportManager";
+import useStore from "./useStorage";
+import {calculateCountdown} from "./totpUtil";
+import {generateToken, validateSecret} from "./totpUtil";
+import {useAccountSync, useAccounts, useEditAccount} from "./useAccountStore";
+
+const {width, height} = Dimensions.get("window");
+const REFRESH_INTERVAL = 10000;
+const OFFSET_X = width * 0.45;
+const OFFSET_Y = height * 0.2;
+
+export default function HomePage() {
+ const [isPlusButton, setIsPlusButton] = useState(true);
+ const [showOptions, setShowOptions] = useState(false);
+ const [showEnterAccountModal, setShowEnterAccountModal] = useState(false);
+ const [searchQuery, setSearchQuery] = useState("");
+ const [filteredData, setFilteredData] = useState(accounts);
+ const [showScanner, setShowScanner] = useState(false);
+ const [showEditAccountModal, setShowEditAccountModal] = useState(false);
+ const [editingAccount, setEditingAccount] = useState(null);
+ const [placeholder, setPlaceholder] = useState("");
+ const [refreshing, setRefreshing] = useState(false);
+ 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} = useAccounts();
+ const {setAccount, updateAccount, insertAccount, insertAccounts, deleteAccount} = useEditAccount();
+ const {notify} = useNotifications();
+ const {t} = useTranslation();
+ const {showImportOptions} = useImportManager((data) => {
+ handleAddAccount(data);
+ }, (err) => {
+ notify("error", {
+ params: {
+ title: t("homepage.Import error"),
+ description: err.message,
+ },
+ });
+ }, () => {
+ setShowScanner(true);
+ });
+
+ useEffect(() => {
+ setCanSync(Boolean(isConnected && userInfo && serverUrl));
+ }, [isConnected, userInfo, serverUrl]);
+
+ useEffect(() => {
+ setFilteredData(accounts);
+ }, [accounts]);
+
+ useEffect(() => {
+ if (canSync) {
+ startSync(userInfo, serverUrl, token);
+
+ const timer = setInterval(() => {
+ InteractionManager.runAfterInteractions(() => {
+ startSync(userInfo, serverUrl, token);
+ });
+ }, REFRESH_INTERVAL);
+
+ return () => clearInterval(timer);
+ }
+ }, [startSync, canSync, token]);
+
+ const onRefresh = async() => {
+ setRefreshing(true);
+ if (canSync) {
+ const syncError = await startSync(userInfo, serverUrl, token);
+ if (syncError) {
+ notify("error", {
+ params: {
+ title: "Sync error",
+ description: syncError,
+ },
+ });
+ } else {
+ notify("success", {
+ params: {
+ title: "Sync success",
+ description: "All your accounts are up to date.",
+ },
+ });
+ }
+ }
+ setRefreshing(false);
+ };
+
+ const handleAddAccount = async(accountDataInput) => {
+ if (Array.isArray(accountDataInput)) {
+ await insertAccounts(accountDataInput);
+ } else {
+ await setAccount(accountDataInput);
+ await insertAccount();
+ closeEnterAccountModal();
+ }
+ };
+
+ const handleEditAccount = (account) => {
+ closeSwipeableMenu();
+ setEditingAccount(account);
+ setPlaceholder(account.accountName);
+ setShowEditAccountModal(true);
+ };
+
+ const onAccountEdit = async(newAccountName) => {
+ if (editingAccount) {
+ setAccount({...editingAccount, accountName: newAccountName, oldAccountName: editingAccount.accountName});
+ updateAccount();
+ setPlaceholder("");
+ setEditingAccount(null);
+ closeEditAccountModal();
+ }
+ };
+
+ const onAccountDelete = async(account) => {
+ deleteAccount(account.id);
+ };
+
+ const closeEditAccountModal = () => setShowEditAccountModal(false);
+
+ const handleScanPress = () => {
+ setShowScanner(true);
+ setIsPlusButton(true);
+ setShowOptions(false);
+ };
+
+ const handleCloseScanner = () => setShowScanner(false);
+
+ const handleScanError = (error) => {
+ setShowScanner(false);
+ notify("error", {
+ params: {
+ title: t("homepage.Error scanning QR code"),
+ description: error,
+ },
+ });
+ };
+
+ const togglePlusButton = () => {
+ setIsPlusButton(!isPlusButton);
+ setShowOptions(!showOptions);
+ };
+
+ const closeOptions = () => {
+ setIsPlusButton(true);
+ setShowOptions(false);
+ setShowScanner(false);
+ };
+
+ const openEnterAccountModal = () => {
+ setShowEnterAccountModal(true);
+ closeOptions();
+ };
+
+ const openImportAccountModal = () => {
+ showImportOptions();
+ closeOptions();
+ };
+
+ const closeEnterAccountModal = () => setShowEnterAccountModal(false);
+
+ const closeSwipeableMenu = () => {
+ if (swipeableRef.current) {
+ swipeableRef.current.close();
+ }
+ };
+
+ const handleSearch = (query) => {
+ setSearchQuery(query);
+ setFilteredData(query.trim() !== ""
+ ? accounts && accounts.filter(item => item.accountName.toLowerCase().includes(query.toLowerCase()))
+ : accounts
+ );
+ };
+
+ const renderRightActions = (progress, dragX, account, onEdit, onDelete) => {
+ const styleAnimation = useAnimatedStyle(() => {
+ return {
+ transform: [{translateX: dragX.value + 160}],
+ };
+ });
+
+ return (
+
+ {
+ dragX.value = withTiming(0);
+ onEdit(account);
+ }}
+ >
+
+
+ {t("common.edit")}
+
+
+
+ {
+ dragX.value = withTiming(0);
+ onDelete(account);
+ }}
+ >
+
+
+ {t("common.delete")}
+
+
+
+ );
+ };
+
+ return (
+
+
+ `${item.id}`}
+ extraData={key}
+ estimatedItemSize={80}
+ refreshControl={
+
+ }
+ renderItem={({item}) => (
+
+
+ renderRightActions(progress, dragX, item, handleEditAccount, onAccountDelete)
+ }
+ rightThreshold={40}
+ overshootRight={false}
+ friction={2}
+ enableTrackpadTwoFingerGesture
+ onSwipeableOpen={() => {
+ if (swipeableRef.current) {
+ swipeableRef.current.close();
+ }
+ }}
+ >
+
+
+ {item.accountName}
+
+ {generateToken(item.secretKey)}
+
+ }
+ left={() => (
+
+ )}
+ right={() => (
+
+ {
+ setKey(prevKey => prevKey + 1);
+ return {
+ shouldRepeat: true,
+ delay: 0,
+ newInitialRemainingTime: calculateCountdown(),
+ };
+ }}
+ strokeWidth={5}
+ >
+ {({remainingTime}) => (
+ {remainingTime}s
+ )}
+
+
+ )}
+ />
+
+
+ )}
+ ItemSeparatorComponent={() => }
+ />
+
+
+
+
+
+
+ {t("homepage.Scan QR Code")}
+
+
+
+
+
+ {t("homepage.Enter Secret Code")}
+
+
+
+
+
+ {t("homepage.Import from other app")}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {showScanner && (
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/SettingPage.js b/SettingPage.js
index ee3d86f..34e231a 100644
--- a/SettingPage.js
+++ b/SettingPage.js
@@ -13,14 +13,14 @@
// limitations under the License.
import React, {useState} from "react";
-import {Dimensions, ScrollView, StyleSheet, View} from "react-native";
+import {Dimensions, ScrollView, StyleSheet, TouchableOpacity, View} from "react-native";
import {Avatar, Button, IconButton, List, Surface, Text, useTheme} from "react-native-paper";
import {ActionSheetProvider} from "@expo/react-native-action-sheet";
import * as Application from "expo-application";
import {useTranslation} from "react-i18next";
import Constants, {ExecutionEnvironment} from "expo-constants";
-import CasdoorLoginPage, {CasdoorLogout} from "./CasdoorLoginPage";
+import CasdoorLoginPage, {useCasdoorLogout} from "./CasdoorLoginPage";
import LoginMethodSelector from "./LoginMethodSelector";
import useStore from "./useStorage";
import {Language} from "./Language";
@@ -33,7 +33,7 @@ const SettingPage = () => {
const {userInfo, clearAll} = useStore();
const theme = useTheme();
const {t} = useTranslation();
-
+ const logout = useCasdoorLogout();
const {openActionSheet} = LoginMethodSelector({
onSelectMethod: (method) => {
setLoginMethod(method);
@@ -51,7 +51,7 @@ const SettingPage = () => {
};
const handleCasdoorLogout = () => {
- CasdoorLogout();
+ logout();
clearAll();
};
@@ -80,14 +80,18 @@ const SettingPage = () => {
/>
) : (
-
+
+
)}
@@ -151,8 +155,7 @@ const styles = StyleSheet.create({
backgroundColor: "#F2F2F2",
},
profileCard: {
- padding: 12,
- marginBottom: 14,
+ padding: 10,
borderRadius: 14,
},
profileInfo: {
@@ -167,7 +170,6 @@ const styles = StyleSheet.create({
loginButton: {
borderRadius: 8,
alignSelf: "center",
- paddingHorizontal: 20,
},
loginButtonLabel: {
fontSize: 18,
diff --git a/app.json b/app.json
index 89a1fcb..56e2f02 100644
--- a/app.json
+++ b/app.json
@@ -55,7 +55,13 @@
],
"expo-asset",
"expo-font",
- "expo-localization"
+ "expo-localization",
+ [
+ "expo-tracking-transparency",
+ {
+ "userTrackingPermission": "This identifier will be used to check which item is created by the user."
+ }
+ ]
],
"owner": "casdoor"
}
diff --git a/db/schema.js b/db/schema.js
index 66922f2..c99962a 100644
--- a/db/schema.js
+++ b/db/schema.js
@@ -25,6 +25,7 @@ export const accounts = sqliteTable("accounts", {
deletedAt: integer("deleted_at", {mode: "timestamp_ms"}).default(null),
changedAt: integer("changed_at", {mode: "timestamp_ms"}).default(sql`(CURRENT_TIMESTAMP)`),
syncAt: integer("sync_at", {mode: "timestamp_ms"}).default(null),
+ origin: text("origin").default(null),
}, (accounts) => ({
unq: unique().on(accounts.accountName, accounts.issuer),
})
diff --git a/drizzle/0001_dear_scream.sql b/drizzle/0001_dear_scream.sql
new file mode 100644
index 0000000..dfe1f6e
--- /dev/null
+++ b/drizzle/0001_dear_scream.sql
@@ -0,0 +1 @@
+ALTER TABLE `accounts` ADD `device_id` text DEFAULT 'null';
\ No newline at end of file
diff --git a/drizzle/0002_rare_ben_grimm.sql b/drizzle/0002_rare_ben_grimm.sql
new file mode 100644
index 0000000..3a55486
--- /dev/null
+++ b/drizzle/0002_rare_ben_grimm.sql
@@ -0,0 +1 @@
+ALTER TABLE `accounts` RENAME COLUMN "device_id" TO "origin";
\ No newline at end of file
diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json
new file mode 100644
index 0000000..60689e7
--- /dev/null
+++ b/drizzle/meta/0001_snapshot.json
@@ -0,0 +1,113 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "9181a8a8-76a3-4557-b299-41f13fe9d0f6",
+ "prevId": "aaa7b5e3-521e-4c3a-970c-35372e7f05a3",
+ "tables": {
+ "accounts": {
+ "name": "accounts",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "account_name": {
+ "name": "account_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "old_account_name": {
+ "name": "old_account_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'null'"
+ },
+ "secret": {
+ "name": "secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "issuer": {
+ "name": "issuer",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'null'"
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'null'"
+ },
+ "changed_at": {
+ "name": "changed_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(CURRENT_TIMESTAMP)"
+ },
+ "sync_at": {
+ "name": "sync_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'null'"
+ },
+ "device_id": {
+ "name": "device_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'null'"
+ }
+ },
+ "indexes": {
+ "accounts_account_name_issuer_unique": {
+ "name": "accounts_account_name_issuer_unique",
+ "columns": [
+ "account_name",
+ "issuer"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json
new file mode 100644
index 0000000..533c6a1
--- /dev/null
+++ b/drizzle/meta/0002_snapshot.json
@@ -0,0 +1,115 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "98e0dfc5-cfb7-45a6-93ff-fe63a96c9e33",
+ "prevId": "9181a8a8-76a3-4557-b299-41f13fe9d0f6",
+ "tables": {
+ "accounts": {
+ "name": "accounts",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "account_name": {
+ "name": "account_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "old_account_name": {
+ "name": "old_account_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'null'"
+ },
+ "secret": {
+ "name": "secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "issuer": {
+ "name": "issuer",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'null'"
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'null'"
+ },
+ "changed_at": {
+ "name": "changed_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "(CURRENT_TIMESTAMP)"
+ },
+ "sync_at": {
+ "name": "sync_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'null'"
+ },
+ "origin": {
+ "name": "origin",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'null'"
+ }
+ },
+ "indexes": {
+ "accounts_account_name_issuer_unique": {
+ "name": "accounts_account_name_issuer_unique",
+ "columns": [
+ "account_name",
+ "issuer"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {
+ "\"accounts\".\"device_id\"": "\"accounts\".\"origin\""
+ }
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index 1377ff9..af19f31 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -8,6 +8,20 @@
"when": 1724248639995,
"tag": "0000_smooth_owl",
"breakpoints": true
+ },
+ {
+ "idx": 1,
+ "version": "6",
+ "when": 1734441909137,
+ "tag": "0001_dear_scream",
+ "breakpoints": true
+ },
+ {
+ "idx": 2,
+ "version": "6",
+ "when": 1734616589186,
+ "tag": "0002_rare_ben_grimm",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/drizzle/migrations.js b/drizzle/migrations.js
index a5dbbb9..6f0e11f 100644
--- a/drizzle/migrations.js
+++ b/drizzle/migrations.js
@@ -2,10 +2,14 @@
import journal from "./meta/_journal.json";
import m0000 from "./0000_smooth_owl.sql";
+import m0001 from "./0001_dear_scream.sql";
+import m0002 from "./0002_rare_ben_grimm.sql";
export default {
journal,
migrations: {
m0000,
+ m0001,
+ m0002,
},
};
diff --git a/metro.config.js b/metro.config.js
index a09c5b2..cc73a58 100644
--- a/metro.config.js
+++ b/metro.config.js
@@ -1,4 +1,5 @@
const {getDefaultConfig} = require("expo/metro-config");
+const {wrapWithReanimatedMetroConfig} = require("react-native-reanimated/metro-config");
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);
@@ -6,4 +7,4 @@ const config = getDefaultConfig(__dirname);
config.resolver.sourceExts.push("sql");
config.resolver.assetExts.push("proto");
-module.exports = config;
+module.exports = wrapWithReanimatedMetroConfig(config);
diff --git a/package-lock.json b/package-lock.json
index d147e89..3812428 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"@expo-google-fonts/lato": "^0.2.3",
"@expo-google-fonts/roboto": "^0.2.3",
"@expo/react-native-action-sheet": "^4.1.0",
+ "@expo/vector-icons": "^14.0.2",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-community/masked-view": "^0.1.11",
"@react-native-community/netinfo": "11.4.1",
@@ -38,6 +39,7 @@
"expo-sqlite": "~15.0.3",
"expo-status-bar": "~2.0.0",
"expo-system-ui": "~4.0.6",
+ "expo-tracking-transparency": "~5.1.0",
"expo-updates": "~0.26.10",
"hi-base32": "^0.5.1",
"hotp-totp": "^1.0.6",
@@ -9837,6 +9839,16 @@
"integrity": "sha512-6QRLEok1r55gLqj+94mEWUENuU5A6wsr2OoXpyq/CgQ7THWowbHtru/kRGRr6o3AQXrVnZheR60JNgFcpNYIug==",
"license": "MIT"
},
+ "node_modules/expo-tracking-transparency": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.com/expo-tracking-transparency/-/expo-tracking-transparency-5.1.0.tgz",
+ "integrity": "sha512-0blcTlYGCXLDFdBKy61EBETdlQ+YOKLfDVv+Bjohpc+OmWy4or5Rb9QBPubK8hwl/BUSeOHxr/3FUwtasFh+UQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/expo-updates": {
"version": "0.26.10",
"resolved": "https://registry.npmjs.com/expo-updates/-/expo-updates-0.26.10.tgz",
diff --git a/package.json b/package.json
index a4d7843..3a4b54f 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"@expo-google-fonts/lato": "^0.2.3",
"@expo-google-fonts/roboto": "^0.2.3",
"@expo/react-native-action-sheet": "^4.1.0",
+ "@expo/vector-icons": "^14.0.2",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-community/masked-view": "^0.1.11",
"@react-native-community/netinfo": "11.4.1",
@@ -40,6 +41,7 @@
"expo-sqlite": "~15.0.3",
"expo-status-bar": "~2.0.0",
"expo-system-ui": "~4.0.6",
+ "expo-tracking-transparency": "~5.1.0",
"expo-updates": "~0.26.10",
"hi-base32": "^0.5.1",
"hotp-totp": "^1.0.6",
diff --git a/syncLogic.js b/syncLogic.js
index 42d92f9..d6b3f2c 100644
--- a/syncLogic.js
+++ b/syncLogic.js
@@ -34,9 +34,6 @@ function getAccountKey(account) {
async function updateLocalDatabase(db, accounts) {
return db.transaction(async(tx) => {
- // remove all accounts
- // await tx.delete(schema.accounts).run();
-
for (const account of accounts) {
if (account.id) {
if (account.deletedAt === null || account.deletedAt === undefined) {
@@ -45,7 +42,8 @@ async function updateLocalDatabase(db, accounts) {
if (acc.issuer === account.issuer &&
acc.accountName === account.accountName &&
acc.secretKey === account.secretKey &&
- acc.deletedAt === account.deletedAt
+ acc.deletedAt === account.deletedAt &&
+ acc.origin === account.origin
) {
continue;
}
@@ -56,6 +54,7 @@ async function updateLocalDatabase(db, accounts) {
deletedAt: null,
token: generateToken(account.secretKey),
changedAt: new Date(),
+ origin: account.origin,
}).where(eq(schema.accounts.id, account.id));
} else {
await tx.delete(schema.accounts).where(eq(schema.accounts.id, account.id));
@@ -65,6 +64,7 @@ async function updateLocalDatabase(db, accounts) {
issuer: account.issuer || null,
accountName: account.accountName,
secretKey: account.secretKey,
+ origin: account.origin || null,
token: generateToken(account.secretKey),
});
}
@@ -155,18 +155,25 @@ export async function syncWithCloud(db, userInfo, serverUrl, token) {
await updateLocalDatabase(db, mergedAccounts);
- const accountsToSync = mergedAccounts.filter(account => account.deletedAt === null || account.deletedAt === undefined)
- .map(account => ({
- issuer: account.issuer,
- accountName: account.accountName,
- secretKey: account.secretKey,
- }));
-
- const serverAccountsStringified = serverAccounts.map(account => JSON.stringify({
- issuer: account.issuer,
- accountName: account.accountName,
- secretKey: account.secretKey,
- }));
+ const accountsToSync = mergedAccounts
+ .filter(account => account.deletedAt === null || account.deletedAt === undefined)
+ .map(account => {
+ const {issuer, accountName, secretKey, origin} = account;
+ const accountToSync = {issuer, accountName, secretKey};
+ if (origin !== null) {
+ accountToSync.origin = origin;
+ }
+ return accountToSync;
+ });
+
+ const serverAccountsStringified = serverAccounts.map(account => {
+ const {issuer, accountName, secretKey, origin} = account;
+ const accountStringified = {issuer, accountName, secretKey};
+ if (origin !== null) {
+ accountStringified.origin = origin;
+ }
+ return JSON.stringify(accountStringified);
+ });
const accountsToSyncStringified = accountsToSync.map(account => JSON.stringify(account));
diff --git a/useAccountStore.js b/useAccountStore.js
index 5da61ed..91a021b 100644
--- a/useAccountStore.js
+++ b/useAccountStore.js
@@ -12,23 +12,25 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import AsyncStorage from "@react-native-async-storage/async-storage";
+
import {db} from "./db/client";
import * as schema from "./db/schema";
-import {and, eq, isNull} from "drizzle-orm";
+import {and, eq, isNull, not, or} from "drizzle-orm";
import {create} from "zustand";
import {generateToken} from "./totpUtil";
import {syncWithCloud} from "./syncLogic";
+import {useLiveQuery} from "drizzle-orm/expo-sqlite";
-export const useAccountStore = create((set, get) => ({
- accounts: [],
- refreshAccounts: () => {
- const accounts = db.select().from(schema.accounts).where(isNull(schema.accounts.deletedAt)).all();
- set({accounts});
- },
- setAccounts: (accounts) => {
- set({accounts});
- },
-}));
+export const useAccounts = () => {
+ const {data: accounts} = useLiveQuery(
+ db.select().from(schema.accounts).where(isNull(schema.accounts.deletedAt))
+ );
+
+ return {
+ accounts: accounts || [],
+ };
+};
const useEditAccountStore = create((set, get) => ({
account: {id: undefined, issuer: undefined, accountName: undefined, secretKey: undefined, oldAccountName: undefined},
@@ -66,8 +68,9 @@ const useEditAccountStore = create((set, get) => ({
});
},
- insertAccount: () => {
+ insertAccount: async() => {
const {accountName, issuer, secretKey} = get().account;
+ const origin = await AsyncStorage.getItem("origin");
if (!accountName || !secretKey) {return;}
const insertWithDuplicateCheck = (tx, baseAccName) => {
let attemptCount = 0;
@@ -109,6 +112,7 @@ const useEditAccountStore = create((set, get) => ({
issuer: issuer || null,
secretKey,
token: generateToken(secretKey),
+ origin: origin || null,
})
.run();
@@ -131,6 +135,7 @@ const useEditAccountStore = create((set, get) => ({
insertAccounts: async(accounts) => {
try {
+ const origin = await AsyncStorage.getItem("origin");
db.transaction((tx) => {
const insertWithDuplicateCheck = (baseAccName, issuer, secretKey) => {
let attemptCount = 0;
@@ -172,6 +177,7 @@ const useEditAccountStore = create((set, get) => ({
issuer: issuer || null,
secretKey,
token: generateToken(secretKey),
+ origin: origin || null,
})
.run();
@@ -197,6 +203,17 @@ const useEditAccountStore = create((set, get) => ({
.where(eq(schema.accounts.id, id))
.run();
},
+
+ deleteAccountByOrigin: async(origin) => {
+ db.delete(schema.accounts)
+ .where(
+ or(
+ not(eq(schema.accounts.origin, origin)),
+ isNull(schema.accounts.origin)
+ )
+ )
+ .run();
+ },
}));
export const useEditAccount = () => useEditAccountStore(state => ({
@@ -206,6 +223,7 @@ export const useEditAccount = () => useEditAccountStore(state => ({
insertAccount: state.insertAccount,
insertAccounts: state.insertAccounts,
deleteAccount: state.deleteAccount,
+ deleteAccountByOrigin: state.deleteAccountByOrigin,
}));
const useAccountSyncStore = create((set, get) => ({