From a8bed57729ec459f779ee257bbbdcb318cff0338 Mon Sep 17 00:00:00 2001 From: IZUMI-Zu <274620705z@gmail.com> Date: Fri, 10 Jan 2025 19:48:58 +0800 Subject: [PATCH 1/2] feat: add detail page for mfa item --- App.js | 3 +- EnterAccountDetails.js | 58 +++++++---- HomePage.js | 165 ++++++++++++++++-------------- HomeStackNavigator.js | 40 ++++++++ ItemDetailPage.js | 207 ++++++++++++++++++++++++++++++++++++++ NavigationBar.js | 8 +- SettingsStackNavigator.js | 32 ++++++ locales/ar/data.json | 12 +++ locales/de/data.json | 12 +++ locales/en/data.json | 12 +++ locales/es/data.json | 12 +++ locales/fr/data.json | 12 +++ locales/ja/data.json | 12 +++ locales/ko/data.json | 12 +++ locales/pt/data.json | 12 +++ locales/ru/data.json | 12 +++ locales/th/data.json | 12 +++ locales/uk/data.json | 12 +++ locales/zh/data.json | 12 +++ package-lock.json | 33 +++++- package.json | 4 +- totpUtil.js | 68 ++++++++++++- 22 files changed, 661 insertions(+), 101 deletions(-) create mode 100644 HomeStackNavigator.js create mode 100644 ItemDetailPage.js create mode 100644 SettingsStackNavigator.js diff --git a/App.js b/App.js index 0770876..972b623 100644 --- a/App.js +++ b/App.js @@ -28,7 +28,7 @@ import {ActionSheetProvider} from "@expo/react-native-action-sheet"; import AsyncStorage from "@react-native-async-storage/async-storage"; import "./i18n"; -import Header from "./Header"; + import NavigationBar from "./NavigationBar"; import {db} from "./db/client"; import migrations from "./drizzle/migrations"; @@ -105,7 +105,6 @@ const App = () => { -
diff --git a/EnterAccountDetails.js b/EnterAccountDetails.js index d0747a9..2a7dbd5 100644 --- a/EnterAccountDetails.js +++ b/EnterAccountDetails.js @@ -132,20 +132,26 @@ const EnterAccountDetails = ({onClose, onAdd, validateSecret}) => { } contentStyle={styles.menuContent} > handleMenuItemPress("Time based")} title={t("editAccount.Time based")} /> handleMenuItemPress("Counter based")} title={t("editAccount.Counter based")} /> @@ -210,40 +216,56 @@ const styles = { }, buttonContainer: { flexDirection: "row", - justifyContent: "space-between", - marginTop: 10, + alignItems: "center", + marginTop: 20, + gap: 12, }, menuButton: { - flex: 1, - marginRight: 10, - height: 50, - justifyContent: "center", - fontSize: 12, + width: 80, + height: 45, + borderColor: "#8A7DF7", + borderWidth: 1, + borderRadius: 22, + backgroundColor: "rgba(138, 125, 247, 0.05)", + padding: 0, + margin: 0, }, menuButtonContent: { - height: 50, - justifyContent: "center", + margin: 0, + padding: 0, + }, + icon: { + margin: 0, + padding: 0, }, menuContent: { backgroundColor: "#FFFFFF", - borderRadius: 8, + borderRadius: 12, elevation: 3, shadowColor: "#000000", - shadowOffset: {width: 0, height: 2}, - shadowOpacity: 0.2, - shadowRadius: 3, + shadowOffset: {width: 0, height: 4}, + shadowOpacity: 0.15, + shadowRadius: 8, + marginTop: 4, }, addButton: { flex: 1, backgroundColor: "#8A7DF7", - height: 50, + height: 45, justifyContent: "center", - paddingHorizontal: 5, + borderRadius: 22, + elevation: 2, + shadowColor: "#8A7DF7", + shadowOffset: {width: 0, height: 4}, + shadowOpacity: 0.2, + shadowRadius: 8, }, buttonLabel: { - fontSize: 14, + fontSize: 15, color: "white", textAlign: "center", + fontWeight: "600", + letterSpacing: 0.5, }, }; diff --git a/HomePage.js b/HomePage.js index 5401e4f..14f662c 100644 --- a/HomePage.js +++ b/HomePage.js @@ -27,6 +27,7 @@ import Animated, { withTiming } from "react-native-reanimated"; import {MaterialCommunityIcons} from "@expo/vector-icons"; +import {useNavigation} from "@react-navigation/native"; import SearchBar from "./SearchBar"; import EnterAccountDetails from "./EnterAccountDetails"; @@ -35,8 +36,7 @@ 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 {useTokenRefresh, validateSecret} from "./totpUtil"; import {useAccountSync, useAccounts, useEditAccount} from "./useAccountStore"; const {width, height} = Dimensions.get("window"); @@ -77,6 +77,7 @@ export default function HomePage() { }, () => { setShowScanner(true); }); + const navigation = useNavigation(); useEffect(() => { setCanSync(Boolean(isConnected && userInfo && serverUrl)); @@ -259,6 +260,94 @@ export default function HomePage() { ); }; + const handleItemPress = (item) => { + navigation.navigate("ItemDetailPage", { + item: { + ...item, + changedAt: item.changedAt.toISOString(), + }, + }); + }; + + const ListItem = ({item, onPress}) => { + const {token, timeRemaining} = useTokenRefresh(item.secretKey); + + return ( + + + renderRightActions(progress, dragX, item, handleEditAccount, onAccountDelete) + } + rightThreshold={40} + overshootRight={false} + friction={2} + enableTrackpadTwoFingerGesture + onSwipeableOpen={() => { + if (swipeableRef.current) { + swipeableRef.current.close(); + } + }} + > + + + {item.accountName} + + {token} + + } + left={() => ( + + )} + right={() => ( + + { + setKey(prevKey => prevKey + 1); + return { + shouldRepeat: true, + delay: 0, + newInitialRemainingTime: timeRemaining, + }; + }} + strokeWidth={5} + > + {({remainingTime}) => ( + {remainingTime}s + )} + + + )} + onPress={() => handleItemPress(item)} + /> + + + ); + }; + return ( @@ -271,77 +360,7 @@ export default function HomePage() { } 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={() => } /> diff --git a/HomeStackNavigator.js b/HomeStackNavigator.js new file mode 100644 index 0000000..acf8f33 --- /dev/null +++ b/HomeStackNavigator.js @@ -0,0 +1,40 @@ +// Copyright 2025 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 from "react"; +import {createStackNavigator} from "@react-navigation/stack"; +import HomePage from "./HomePage"; +import ItemDetailPage from "./ItemDetailPage"; +import Header from "./Header"; + +const Stack = createStackNavigator(); + +export default function HomeStackNavigator() { + return ( +
, + }} + > + + ({ + headerShown: false, + })} + /> + + ); +} diff --git a/ItemDetailPage.js b/ItemDetailPage.js new file mode 100644 index 0000000..51f28ea --- /dev/null +++ b/ItemDetailPage.js @@ -0,0 +1,207 @@ +// Copyright 2025 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, {useState} from "react"; +import {StyleSheet, Text, TouchableOpacity, View} from "react-native"; +import {useNavigation} from "@react-navigation/native"; +import Icon from "react-native-vector-icons/MaterialCommunityIcons"; +import {SafeAreaView} from "react-native-safe-area-context"; +import {useTranslation} from "react-i18next"; +import * as Clipboard from "expo-clipboard"; +import {format} from "date-fns"; +import {useTokenRefresh} from "./totpUtil"; + +const ItemDetailPage = ({route}) => { + const {item} = route.params; + const navigation = useNavigation(); + const [copied, setCopied] = useState(false); + const {t} = useTranslation(); + + const {token, timeRemaining} = useTokenRefresh(item.secretKey); + + const copyToClipboard = async(text) => { + await Clipboard.setStringAsync(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( + + + navigation.goBack()}> + + + + + + {token} + copyToClipboard(token)} + > + + + + + + + + + + {timeRemaining}s + + + {/* Details Section */} + + + + + + + + + + + ); +}; + +// Helper component for detail rows +const DetailRow = ({label, value, isSecret = false}) => { + const [isVisible, setIsVisible] = useState(false); + + if (isSecret) { + return ( + + + {label} + setIsVisible(!isVisible)}> + + + + + {isVisible ? value : "••••••••"} + + + ); + } + + return ( + + {label} + + {value} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 20, + }, + backButton: { + position: "absolute", + top: 10, + left: 10, + padding: 10, + zIndex: 1, + }, + tokenSection: { + marginTop: 60, + alignItems: "center", + }, + tokenText: { + fontSize: 32, + fontWeight: "bold", + letterSpacing: 2, + }, + copyButton: { + padding: 8, + }, + detailsContainer: { + marginTop: 40, + backgroundColor: "#f5f5f5", + borderRadius: 12, + padding: 16, + }, + detailRow: { + flexDirection: "column", + paddingVertical: 12, + borderBottomWidth: 1, + borderBottomColor: "#e0e0e0", + }, + detailLabel: { + fontSize: 14, + color: "#666", + marginBottom: 4, + }, + detailValue: { + fontSize: 16, + color: "#000", + }, + detailHeaderRow: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + marginBottom: 4, + }, + tokenContainer: { + flexDirection: "row", + alignItems: "center", + gap: 10, + }, + progressSection: { + marginTop: 20, + paddingHorizontal: 20, + width: "100%", + alignItems: "center", + }, + progressContainer: { + width: "100%", + height: 4, + backgroundColor: "#E0E0E0", + borderRadius: 2, + overflow: "hidden", + }, + progressBar: { + height: "100%", + backgroundColor: "#007AFF", + }, + timeText: { + marginTop: 5, + fontSize: 14, + color: "#666", + textAlign: "center", + }, +}); + +export default ItemDetailPage; diff --git a/NavigationBar.js b/NavigationBar.js index 9d236a6..3161624 100644 --- a/NavigationBar.js +++ b/NavigationBar.js @@ -17,9 +17,9 @@ import React from "react"; 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 HomeStackNavigator from "./HomeStackNavigator"; import {CommonActions} from "@react-navigation/native"; -import SettingPage from "./SettingPage"; +import SettingsStackNavigator from "./SettingsStackNavigator"; import {useTranslation} from "react-i18next"; const Tab = createBottomTabNavigator(); @@ -77,7 +77,7 @@ export default function NavigationBar() { > { @@ -87,7 +87,7 @@ export default function NavigationBar() { /> { diff --git a/SettingsStackNavigator.js b/SettingsStackNavigator.js new file mode 100644 index 0000000..17a4288 --- /dev/null +++ b/SettingsStackNavigator.js @@ -0,0 +1,32 @@ +// Copyright 2025 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 from "react"; +import {createStackNavigator} from "@react-navigation/stack"; +import SettingPage from "./SettingPage"; +import Header from "./Header"; + +const Stack = createStackNavigator(); + +export default function SettingsStackNavigator() { + return ( +
, + }} + > + + + ); +} diff --git a/locales/ar/data.json b/locales/ar/data.json index 0fb1a36..2654948 100644 --- a/locales/ar/data.json +++ b/locales/ar/data.json @@ -94,5 +94,17 @@ "syncLogic": { "Sync failed": "فشل المزامنة", "Access token has expired, please login again": "انتهت صلاحية رمز الوصول، يرجى تسجيل الدخول مرة أخرى" + }, + "itemDetail": { + "token": "رمز", + "accountName": "اسم الحساب", + "issuer": "المصدر", + "lastModified": "آخر تعديل", + "secretKey": "المفتاح السري", + "lastSynced": "آخر مزامنة", + "origin": "المصدر", + "notSpecified": "غير محدد", + "neverSynced": "لم تتم المزامنة", + "copied": "تم النسخ" } } diff --git a/locales/de/data.json b/locales/de/data.json index 64effff..e9acd45 100644 --- a/locales/de/data.json +++ b/locales/de/data.json @@ -94,5 +94,17 @@ "syncLogic": { "Sync failed": "Synchronisierung fehlgeschlagen", "Access token has expired, please login again": "Das Zugriffstoken ist abgelaufen, bitte melden Sie sich erneut an" + }, + "itemDetail": { + "token": "Token", + "accountName": "Kontoname", + "issuer": "Aussteller", + "lastModified": "Zuletzt geändert", + "secretKey": "Geheimer Schlüssel", + "lastSynced": "Zuletzt synchronisiert", + "origin": "Herkunft", + "notSpecified": "Nicht angegeben", + "neverSynced": "Nie synchronisiert", + "copied": "Kopiert" } } diff --git a/locales/en/data.json b/locales/en/data.json index acfdeda..4b9fe38 100644 --- a/locales/en/data.json +++ b/locales/en/data.json @@ -94,5 +94,17 @@ "syncLogic": { "Sync failed": "Sync failed", "Access token has expired, please login again": "Access token has expired, please login again" + }, + "itemDetail": { + "token": "Token", + "accountName": "Account Name", + "issuer": "Issuer", + "lastModified": "Last Modified", + "secretKey": "Secret Key", + "lastSynced": "Last Synced", + "origin": "Origin", + "notSpecified": "Not specified", + "neverSynced": "Never synced", + "copied": "Copied" } } diff --git a/locales/es/data.json b/locales/es/data.json index bddfc35..47b65cf 100644 --- a/locales/es/data.json +++ b/locales/es/data.json @@ -94,5 +94,17 @@ "syncLogic": { "Sync failed": "Error de sincronización", "Access token has expired, please login again": "El token de acceso ha expirado, por favor inicie sesión nuevamente" + }, + "itemDetail": { + "token": "Token", + "accountName": "Nombre de cuenta", + "issuer": "Emisor", + "lastModified": "Última modificación", + "secretKey": "Clave secreta", + "lastSynced": "Última sincronización", + "origin": "Origen", + "notSpecified": "No especificado", + "neverSynced": "Nunca sincronizado", + "copied": "Copiado" } } diff --git a/locales/fr/data.json b/locales/fr/data.json index b4cac8f..41aa963 100644 --- a/locales/fr/data.json +++ b/locales/fr/data.json @@ -94,5 +94,17 @@ "syncLogic": { "Sync failed": "Échec de la synchronisation", "Access token has expired, please login again": "Le jeton d'accès a expiré, veuillez vous reconnecter" + }, + "itemDetail": { + "token": "Jeton", + "accountName": "Nom du compte", + "issuer": "Émetteur", + "lastModified": "Dernière modification", + "secretKey": "Clé secrète", + "lastSynced": "Dernière synchronisation", + "origin": "Origine", + "notSpecified": "Non spécifié", + "neverSynced": "Jamais synchronisé", + "copied": "Copié" } } diff --git a/locales/ja/data.json b/locales/ja/data.json index 8c49d4d..7538274 100644 --- a/locales/ja/data.json +++ b/locales/ja/data.json @@ -94,5 +94,17 @@ "syncLogic": { "Sync failed": "同期に失敗しました", "Access token has expired, please login again": "アクセストークンの有効期限が切れました。再度ログインしてください" + }, + "itemDetail": { + "token": "トークン", + "accountName": "アカウント名", + "issuer": "発行者", + "lastModified": "最終更新", + "secretKey": "シークレットキー", + "lastSynced": "最終同期", + "origin": "出典", + "notSpecified": "未指定", + "neverSynced": "未同期", + "copied": "コピーしました" } } diff --git a/locales/ko/data.json b/locales/ko/data.json index 8e6b05e..0d6a3da 100644 --- a/locales/ko/data.json +++ b/locales/ko/data.json @@ -94,5 +94,17 @@ "syncLogic": { "Sync failed": "동기화 실패", "Access token has expired, please login again": "액세스 토큰이 만료되었습니다. 다시 로그인해주세요" + }, + "itemDetail": { + "token": "토큰", + "accountName": "계정 이름", + "issuer": "발행자", + "lastModified": "마지막 수정", + "secretKey": "비밀 키", + "lastSynced": "마지막 동기화", + "origin": "출처", + "notSpecified": "지정되지 않음", + "neverSynced": "동기화되지 않음", + "copied": "복사됨" } } diff --git a/locales/pt/data.json b/locales/pt/data.json index a364348..6cc6286 100644 --- a/locales/pt/data.json +++ b/locales/pt/data.json @@ -94,5 +94,17 @@ "syncLogic": { "Sync failed": "Sincronização falhou", "Access token has expired, please login again": "O token de acesso expirou, por favor faça login novamente" + }, + "itemDetail": { + "token": "Token", + "accountName": "Nome da conta", + "issuer": "Emissor", + "lastModified": "Última modificação", + "secretKey": "Chave secreta", + "lastSynced": "Última sincronização", + "origin": "Origem", + "notSpecified": "Não especificado", + "neverSynced": "Nunca sincronizado", + "copied": "Copiado" } } diff --git a/locales/ru/data.json b/locales/ru/data.json index 2c5dc1f..f8df0f0 100644 --- a/locales/ru/data.json +++ b/locales/ru/data.json @@ -94,5 +94,17 @@ "syncLogic": { "Sync failed": "Синхронизация не удалась", "Access token has expired, please login again": "Токен доступа истёк, пожалуйста, войдите снова" + }, + "itemDetail": { + "token": "Токен", + "accountName": "Имя аккаунта", + "issuer": "Издатель", + "lastModified": "Последнее изменение", + "secretKey": "Секретный ключ", + "lastSynced": "Последняя синхронизация", + "origin": "Источник", + "notSpecified": "Не указано", + "neverSynced": "Никогда не синхронизировано", + "copied": "Скопировано" } } diff --git a/locales/th/data.json b/locales/th/data.json index 977aa56..bb74fc7 100644 --- a/locales/th/data.json +++ b/locales/th/data.json @@ -94,5 +94,17 @@ "syncLogic": { "Sync failed": "การซิงค์ล้มเหลว", "Access token has expired, please login again": "โทเค็นการเข้าถึงหมดอายุ กรุณาล็อกอินอีกครั้ง" + }, + "itemDetail": { + "token": "โทเค็น", + "accountName": "ชื่อบัญชี", + "issuer": "ผู้ออก", + "lastModified": "แก้ไขล่าสุด", + "secretKey": "รหัสลับ", + "lastSynced": "ซิงค์ล่าสุด", + "origin": "ที่มา", + "notSpecified": "ไม่ระบุ", + "neverSynced": "ไม่เคยซิงค์", + "copied": "คัดลอกแล้ว" } } diff --git a/locales/uk/data.json b/locales/uk/data.json index 132db1e..3578f1d 100644 --- a/locales/uk/data.json +++ b/locales/uk/data.json @@ -94,5 +94,17 @@ "syncLogic": { "Sync failed": "Синхронізація не вдалася", "Access token has expired, please login again": "Токен доступу минув, будь ласка, увійдіть знову" + }, + "itemDetail": { + "token": "Токен", + "accountName": "Назва облікового запису", + "issuer": "Видавець", + "lastModified": "Останнє редагування", + "secretKey": "Секретний ключ", + "lastSynced": "Остання синхронізація", + "origin": "Походження", + "notSpecified": "Не вказано", + "neverSynced": "Ніколи не синхронізовано", + "copied": "Скопійовано" } } diff --git a/locales/zh/data.json b/locales/zh/data.json index 8aa2152..a0098ad 100644 --- a/locales/zh/data.json +++ b/locales/zh/data.json @@ -94,5 +94,17 @@ "syncLogic": { "Sync failed": "同步失败", "Access token has expired, please login again": "访问令牌已过期,请重新登录" + }, + "itemDetail": { + "token": "令牌", + "accountName": "账户名称", + "issuer": "发行方", + "lastModified": "最后修改", + "secretKey": "密钥", + "lastSynced": "最后同步", + "origin": "来源", + "notSpecified": "未指定", + "neverSynced": "从未同步", + "copied": "已复制" } } diff --git a/package-lock.json b/package-lock.json index 3812428..958b5c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,11 +15,13 @@ "@react-native-async-storage/async-storage": "1.23.1", "@react-native-community/masked-view": "^0.1.11", "@react-native-community/netinfo": "11.4.1", - "@react-navigation/bottom-tabs": "^6.5.8", + "@react-navigation/bottom-tabs": "^6.6.1", "@react-navigation/native": "^6.1.7", + "@react-navigation/stack": "^6.4.1", "@shopify/flash-list": "1.7.1", "buffer": "^6.0.3", "casdoor-react-native-sdk": "1.1.0", + "date-fns": "^4.1.0", "drizzle-orm": "^0.37.0", "eslint-plugin-import": "^2.28.1", "expo": "~52.0.18", @@ -5717,6 +5719,25 @@ "nanoid": "^3.1.23" } }, + "node_modules/@react-navigation/stack": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.com/@react-navigation/stack/-/stack-6.4.1.tgz", + "integrity": "sha512-upMEHOKMtuMu4c9gmoPlO/JqI6mDlSqwXg1aXKOTQLXAF8H5koOLRfrmi7AkdiE9A7lDXWUAZoGuD9O88cYvDQ==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^1.3.31", + "color": "^4.2.3", + "warn-once": "^0.1.0" + }, + "peerDependencies": { + "@react-navigation/native": "^6.0.0", + "react": "*", + "react-native": "*", + "react-native-gesture-handler": ">= 1.0.0", + "react-native-safe-area-context": ">= 3.0.0", + "react-native-screens": ">= 3.0.0" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.com/@rtsao/scc/-/scc-1.1.0.tgz", @@ -7917,6 +7938,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.com/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.com/debug/-/debug-4.3.7.tgz", diff --git a/package.json b/package.json index 3a4b54f..0ea03d6 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,13 @@ "@react-native-async-storage/async-storage": "1.23.1", "@react-native-community/masked-view": "^0.1.11", "@react-native-community/netinfo": "11.4.1", - "@react-navigation/bottom-tabs": "^6.5.8", + "@react-navigation/bottom-tabs": "^6.6.1", "@react-navigation/native": "^6.1.7", + "@react-navigation/stack": "^6.4.1", "@shopify/flash-list": "1.7.1", "buffer": "^6.0.3", "casdoor-react-native-sdk": "1.1.0", + "date-fns": "^4.1.0", "drizzle-orm": "^0.37.0", "eslint-plugin-import": "^2.28.1", "expo": "~52.0.18", diff --git a/totpUtil.js b/totpUtil.js index 018b32a..a70a580 100644 --- a/totpUtil.js +++ b/totpUtil.js @@ -13,10 +13,13 @@ // limitations under the License. import totp from "totp-generator"; +import {useEffect, useState} from "react"; export function calculateCountdown(period = 30) { - const now = Math.round(new Date().getTime() / 1000.0); - return period - (now % period); + const now = Date.now() / 1000; + const currentPeriod = Math.floor(now / period); + const nextPeriod = (currentPeriod + 1) * period; + return Math.max(0, Math.round(nextPeriod - now)); } export function validateSecret(secret) { @@ -40,3 +43,64 @@ export function generateToken(secret) { return "Secret Empty"; } } + +export function useTokenRefresh(secretKey, period = 30) { + const [token, setToken] = useState(() => generateToken(secretKey)); + const [timeRemaining, setTimeRemaining] = useState(() => calculateCountdown()); + + useEffect(() => { + let timerRef = null; + let intervalRef = null; + let countdownRef = null; + + const updateToken = () => { + setToken(generateToken(secretKey)); + setTimeRemaining(period); + }; + + const scheduleNextUpdate = () => { + const now = Date.now() / 1000; + const nextUpdate = Math.ceil(now / period) * period; + const delay = Math.max(0, (nextUpdate - now) * 1000); + + if (timerRef) { + clearTimeout(timerRef); + } + if (intervalRef) { + clearInterval(intervalRef); + } + if (countdownRef) { + clearInterval(countdownRef); + } + + timerRef = setTimeout(() => { + updateToken(); + intervalRef = setInterval(updateToken, period * 1000); + }, delay); + + countdownRef = setInterval(() => { + setTimeRemaining(prev => { + const remaining = prev - 1; + return remaining >= 0 ? remaining : period; + }); + }, 1000); + }; + + updateToken(); + scheduleNextUpdate(); + + return () => { + if (timerRef) { + clearTimeout(timerRef); + } + if (intervalRef) { + clearInterval(intervalRef); + } + if (countdownRef) { + clearInterval(countdownRef); + } + }; + }, [secretKey, period]); + + return {token, timeRemaining}; +} From df8e519e2f9767e6a07b4caff4194602aa18ee8a Mon Sep 17 00:00:00 2001 From: IZUMI-Zu <274620705z@gmail.com> Date: Wed, 15 Jan 2025 12:52:36 +0800 Subject: [PATCH 2/2] fix: countdown period in detail page --- totpUtil.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/totpUtil.js b/totpUtil.js index a70a580..e96ffbd 100644 --- a/totpUtil.js +++ b/totpUtil.js @@ -55,7 +55,7 @@ export function useTokenRefresh(secretKey, period = 30) { const updateToken = () => { setToken(generateToken(secretKey)); - setTimeRemaining(period); + setTimeRemaining(calculateCountdown(period)); }; const scheduleNextUpdate = () => {