diff --git a/app/(home)/_layout.tsx b/app/(home)/_layout.tsx index 9451f96..96d9905 100644 --- a/app/(home)/_layout.tsx +++ b/app/(home)/_layout.tsx @@ -2,8 +2,8 @@ import React, { FC } from 'react'; import { Slot } from 'expo-router'; import AddSite from '../../src/components/AddSite'; -import Container from '../../src/ui/Container'; import TopBar from '../../src/components/TopBar'; +import Container from '../../src/ui/Container'; const HomeLayout: FC = () => { return ( diff --git a/app/(home)/index.tsx b/app/(home)/index.tsx index ae4bb20..40f49c3 100644 --- a/app/(home)/index.tsx +++ b/app/(home)/index.tsx @@ -1,19 +1,14 @@ import React, { FC, useEffect, useState } from 'react'; -import { Trans, t } from '@lingui/macro'; -import { ActivityIndicator, FlatList, StyleSheet, View } from 'react-native'; -import { TextInput } from 'react-native-paper'; -import SiteInfo from '../../src/components/SiteInfo'; -import colors from '../../src/constants/colors'; +import { ActivityIndicator } from 'react-native'; +import SitesList from '../../src/components/SitesList'; import Site from '../../src/models/Site'; import { useDB } from '../../src/providers/DatabaseProvider'; -import Text from '../../src/ui/Text'; import Progress from '../../src/ui/Progress'; const Home: FC = () => { const { listSites } = useDB(); const [sites, setSites] = useState([]); - const [search, setSearch] = useState(); const [loading, setLoading] = useState(true); useEffect(() => { @@ -26,68 +21,18 @@ const Home: FC = () => { } }, [listSites, sites.length]); - const regEx = new RegExp(search || '', 'i'); - const filteredSites = !search - ? sites - : sites.filter( - site => site.label.match(regEx) || site.issuer?.match(regEx), - ); + const deleteSite = (site: Site) => { + setSites(sites.filter(s => site.label !== s.label)); + }; if (loading) return ; - if (!sites.length) { - return ( - - No sites have been added yet - - ); - } - return ( <> - - } - placeholder={t`Search...`} - mode="outlined" - theme={{ - colors: { - background: colors.white, - }, - }} - outlineColor={colors.medium} - placeholderTextColor={colors.medium} - /> - - {!!sites.length && !filteredSites.length && ( - - No results matching your search - - )} - } - /> + ); }; -const styles = StyleSheet.create({ - inputWrapper: { - marginBottom: 32, - }, - icon: { - width: 24, - height: 24, - position: 'absolute', - }, - scrollView: { - marginBottom: 32, - }, -}); - export default Home; diff --git a/app/qr/info.tsx b/app/qr/info.tsx index 4b1e3b3..cd472de 100644 --- a/app/qr/info.tsx +++ b/app/qr/info.tsx @@ -20,6 +20,7 @@ const QRInfo: FC = () => { const { push } = useRouter(); const { newSite } = useDB(); const [site, setSite] = useState(); + const [token, setToken] = useState(''); const period = site?.period || DEFAULT_TOTP_PERIOD; @@ -31,12 +32,12 @@ const QRInfo: FC = () => { return ; } - const token = generateTOTP({ + generateTOTP({ algorithm: getAlgorithm(site.algorithm), digits: site.digits, period, secret: site.secret, - }); + }).then(t => setToken(t)); return ( diff --git a/build/latest.apk b/build/latest.apk index 8fdbafd..d801f52 100644 Binary files a/build/latest.apk and b/build/latest.apk differ diff --git a/docs/app.png b/docs/app.png index 6780fc4..e2efd4f 100644 Binary files a/docs/app.png and b/docs/app.png differ diff --git a/package-lock.json b/package-lock.json index 033d11c..b1a3193 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,10 @@ "@react-native-masked-view/masked-view": "0.2.8", "@react-navigation/native": "^6.1.4", "@react-navigation/stack": "^6.3.14", + "@shopify/flash-list": "1.4.0", "dotenv": "^16.0.3", "expo": "~48.0.6", + "expo-background-fetch": "~11.1.1", "expo-barcode-scanner": "~12.3.2", "expo-build-properties": "~0.5.1", "expo-camera": "~13.2.1", @@ -32,6 +34,7 @@ "expo-secure-store": "~12.1.1", "expo-splash-screen": "~0.18.1", "expo-status-bar": "~1.4.4", + "expo-task-manager": "~11.1.1", "make-plural": "^7.2.0", "react": "18.2.0", "react-dom": "18.2.0", @@ -7421,6 +7424,25 @@ "join-component": "^1.1.0" } }, + "node_modules/@shopify/flash-list": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-1.4.0.tgz", + "integrity": "sha512-PvPOyk353LuETFnNA038+QaJsAFlCQ2TYC7DHP3YnYqTX72g2BM6qLoLsPaptXKuoXX+dinOo0MbEm7HDjTy1g==", + "dependencies": { + "recyclerlistview": "4.2.0", + "tslib": "2.4.0" + }, + "peerDependencies": { + "@babel/runtime": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/@shopify/flash-list/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, "node_modules/@sideway/address": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", @@ -12203,6 +12225,17 @@ "url-parse": "^1.5.9" } }, + "node_modules/expo-background-fetch": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/expo-background-fetch/-/expo-background-fetch-11.1.1.tgz", + "integrity": "sha512-5X63ogpCqEqdqXYk4Sl4J8Lt/ZWcQCboS1pq3uprqD1KUi4ICl1P3gwPo+il3rYPV6xYb74Wv1KyyEEYj2vklg==", + "dependencies": { + "expo-task-manager": "~11.1.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-barcode-scanner": { "version": "12.3.2", "resolved": "https://registry.npmjs.org/expo-barcode-scanner/-/expo-barcode-scanner-12.3.2.tgz", @@ -12577,6 +12610,17 @@ "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-1.4.4.tgz", "integrity": "sha512-5DV0hIEWgatSC3UgQuAZBoQeaS9CqeWRZ3vzBR9R/+IUD87Adbi4FGhU10nymRqFXOizGsureButGZIXPs7zEA==" }, + "node_modules/expo-task-manager": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/expo-task-manager/-/expo-task-manager-11.1.1.tgz", + "integrity": "sha512-Ot4wq0fVd8+I1W7MsJz0rNdX0ma/zdnBvAppxDX1Oo0o0exo4qs1FmgrTnh3OBnn18aB4cX3wBJoXIatIgNMZQ==", + "dependencies": { + "unimodules-app-loader": "~4.1.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo/node_modules/@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", @@ -21882,6 +21926,20 @@ "node": ">= 4" } }, + "node_modules/recyclerlistview": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/recyclerlistview/-/recyclerlistview-4.2.0.tgz", + "integrity": "sha512-uuBCi0c+ggqHKwrzPX4Z/mJOzsBbjZEAwGGmlwpD/sD7raXixdAbdJ6BTcAmuWG50Cg4ru9p12M94Njwhr/27A==", + "dependencies": { + "lodash.debounce": "4.0.8", + "prop-types": "15.8.1", + "ts-object-utils": "0.0.5" + }, + "peerDependencies": { + "react": ">= 15.2.1", + "react-native": ">= 0.30.0" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -24121,6 +24179,11 @@ } } }, + "node_modules/ts-object-utils": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/ts-object-utils/-/ts-object-utils-0.0.5.tgz", + "integrity": "sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA==" + }, "node_modules/tslib": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", @@ -24307,6 +24370,11 @@ "node": ">=4" } }, + "node_modules/unimodules-app-loader": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/unimodules-app-loader/-/unimodules-app-loader-4.1.1.tgz", + "integrity": "sha512-K0vuriaD1Zft2dKwYSER/eoiTEPINL6cASed42/QuWeV9jqfQ7Y6OAHM4zIyhXVdro/IS2L6pPnnqZFIH2xoKg==" + }, "node_modules/union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -30590,6 +30658,22 @@ "join-component": "^1.1.0" } }, + "@shopify/flash-list": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-1.4.0.tgz", + "integrity": "sha512-PvPOyk353LuETFnNA038+QaJsAFlCQ2TYC7DHP3YnYqTX72g2BM6qLoLsPaptXKuoXX+dinOo0MbEm7HDjTy1g==", + "requires": { + "recyclerlistview": "4.2.0", + "tslib": "2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } + } + }, "@sideway/address": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", @@ -34446,6 +34530,14 @@ "url-parse": "^1.5.9" } }, + "expo-background-fetch": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/expo-background-fetch/-/expo-background-fetch-11.1.1.tgz", + "integrity": "sha512-5X63ogpCqEqdqXYk4Sl4J8Lt/ZWcQCboS1pq3uprqD1KUi4ICl1P3gwPo+il3rYPV6xYb74Wv1KyyEEYj2vklg==", + "requires": { + "expo-task-manager": "~11.1.0" + } + }, "expo-barcode-scanner": { "version": "12.3.2", "resolved": "https://registry.npmjs.org/expo-barcode-scanner/-/expo-barcode-scanner-12.3.2.tgz", @@ -34722,6 +34814,14 @@ "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-1.4.4.tgz", "integrity": "sha512-5DV0hIEWgatSC3UgQuAZBoQeaS9CqeWRZ3vzBR9R/+IUD87Adbi4FGhU10nymRqFXOizGsureButGZIXPs7zEA==" }, + "expo-task-manager": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/expo-task-manager/-/expo-task-manager-11.1.1.tgz", + "integrity": "sha512-Ot4wq0fVd8+I1W7MsJz0rNdX0ma/zdnBvAppxDX1Oo0o0exo4qs1FmgrTnh3OBnn18aB4cX3wBJoXIatIgNMZQ==", + "requires": { + "unimodules-app-loader": "~4.1.0" + } + }, "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", @@ -41526,6 +41626,16 @@ "tslib": "^2.0.1" } }, + "recyclerlistview": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/recyclerlistview/-/recyclerlistview-4.2.0.tgz", + "integrity": "sha512-uuBCi0c+ggqHKwrzPX4Z/mJOzsBbjZEAwGGmlwpD/sD7raXixdAbdJ6BTcAmuWG50Cg4ru9p12M94Njwhr/27A==", + "requires": { + "lodash.debounce": "4.0.8", + "prop-types": "15.8.1", + "ts-object-utils": "0.0.5" + } + }, "redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -43232,6 +43342,11 @@ "yn": "3.1.1" } }, + "ts-object-utils": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/ts-object-utils/-/ts-object-utils-0.0.5.tgz", + "integrity": "sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA==" + }, "tslib": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", @@ -43356,6 +43471,11 @@ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==" }, + "unimodules-app-loader": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/unimodules-app-loader/-/unimodules-app-loader-4.1.1.tgz", + "integrity": "sha512-K0vuriaD1Zft2dKwYSER/eoiTEPINL6cASed42/QuWeV9jqfQ7Y6OAHM4zIyhXVdro/IS2L6pPnnqZFIH2xoKg==" + }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", diff --git a/package.json b/package.json index 38b464e..2b3b7f2 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "expo-barcode-scanner": "~12.3.2", "expo-build-properties": "~0.5.1", "expo-camera": "~13.2.1", + "expo-clipboard": "~4.1.2", "expo-constants": "~14.2.1", "expo-font": "~11.1.1", "expo-linking": "~4.0.1", @@ -55,7 +56,9 @@ "react-native-vector-icons": "^9.2.0", "react-native-web": "~0.18.11", "totp-generator": "^0.0.14", - "expo-clipboard": "~4.1.2" + "@shopify/flash-list": "1.4.0", + "expo-task-manager": "~11.1.1", + "expo-background-fetch": "~11.1.1" }, "jest": { "preset": "jest-expo", diff --git a/src/components/Page.tsx b/src/components/Page.tsx index 07958a2..51d8dab 100644 --- a/src/components/Page.tsx +++ b/src/components/Page.tsx @@ -14,7 +14,6 @@ import DatabaseProvider from '../providers/DatabaseProvider'; import { FingerprintAuthProvider } from '../providers/FingerprintAuthProvider'; import LocalizationProvider from '../providers/LocalizationProvider'; import { ComponentWithChildren } from '../types'; -import Container from '../ui/Container'; import * as SplashScreen from 'expo-splash-screen'; import NotificationProvider from '../providers/NotificationProvider'; diff --git a/src/components/SiteInfo.tsx b/src/components/SiteInfo.tsx index 505daae..2839440 100644 --- a/src/components/SiteInfo.tsx +++ b/src/components/SiteInfo.tsx @@ -1,61 +1,147 @@ -import { FC, useEffect, useState } from 'react'; -import { StyleSheet, View } from 'react-native'; +import { Trans } from '@lingui/macro'; +import { + Dispatch, + FC, + SetStateAction, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; +import { Animated, StyleSheet, View } from 'react-native'; +import { Swipeable, TouchableOpacity } from 'react-native-gesture-handler'; +import { Button } from 'react-native-paper'; import { DEFAULT_TOTP_PERIOD } from '../constants/app'; import colors from '../constants/colors'; +import { TrashIcon } from '../icons/TrashIcon'; import Site from '../models/Site'; import IssuerIcon from '../ui/IssuerIcon'; import SiteToken from '../ui/SiteToken'; import Text from '../ui/Text'; import { generateTOTP, getAlgorithm } from '../util/generateTotp'; import CopyToClipboard from './CopyToClipboard'; -import { useTimer } from '../hooks/useTimer'; +import { TickIcon } from '../icons/TickIcon'; +import { useDB } from '../providers/DatabaseProvider'; +import { useTimer30 } from '../hooks/useTimer30'; type SiteInfoProps = { site: Site; + timer: number; + visible: boolean; + deleteSite: (site: Site) => void; }; -const SiteInfo: FC = ({ site }) => { - const [token, setToken] = useState(''); +const SiteInfo: FC = ({ site, timer, visible, deleteSite }) => { + const { deleteSite: deleteSiteDB } = useDB(); + const [token, setToken] = useState('------'); + const [showConfirmation, setShowConfirmation] = useState(false); + const [loading, setLoading] = useState(true); + const swipeableRef = useRef(null); const period = site.period || DEFAULT_TOTP_PERIOD; - const timer = useTimer(period, 10000); + const regenerateToken = useCallback(() => { + generateTOTP({ + algorithm: getAlgorithm(site.algorithm), + digits: site.digits, + period, + secret: site.secret, + }).then(t => { + setToken(t); + setLoading(false); + }); + }, [period, site.algorithm, site.digits, site.secret]); useEffect(() => { - if (timer) { - const t = generateTOTP({ - algorithm: getAlgorithm(site.algorithm), - digits: site.digits, - period, - secret: site.secret, - }); - setToken(t); + if (timer >= 0 && visible) { + // Forces update + setLoading(true); + regenerateToken(); } - }, [period, site.algorithm, site.digits, site.secret, timer]); + }, [timer, regenerateToken, site.label, visible]); - return ( - - - - - - - - {site.label} - - + const cancelSwip = () => { + swipeableRef.current?.close(); + setShowConfirmation(false); + }; + + const handleDeleteSite = () => { + deleteSite(site); + deleteSiteDB(site); + }; + + const renderRightActions = () => { + return ( + + + + + + + {!showConfirmation && ( + setShowConfirmation(true)} + style={styles.cancelButton} + > + + + Delete + + + )} + {showConfirmation && ( + handleDeleteSite()} + style={styles.confirmationButton} + > + + Confirm deletion + + + + )} + - + ); + }; + + return ( + + + + + + + + + + {site.label} + + + + + + + ); }; const styles = StyleSheet.create({ + wrapper: { + position: 'relative', + backgroundColor: colors.red, + marginBottom: 16, + borderRadius: 8, + }, card: { padding: 8, backgroundColor: colors.white, borderRadius: 8, - marginBottom: 16, flexDirection: 'row', justifyContent: 'space-between', }, @@ -67,6 +153,39 @@ const styles = StyleSheet.create({ flexDirection: 'row', overflow: 'hidden', }, + swipedRow: { + flexDirection: 'row', + }, + swipeCancel: { + flex: 1, + borderRadius: 8, + backgroundColor: colors.light, + alignItems: 'flex-end', + justifyContent: 'center', + paddingHorizontal: 16, + }, + swipeContainer: { + borderRadius: 8, + backgroundColor: colors.red, + width: '100%', + flexDirection: 'row', + }, + deleteButton: { + borderTopRightRadius: 8, + borderBottomRightRadius: 8, + backgroundColor: colors.red, + padding: 16, + }, + cancelButton: { + alignContent: 'center', + alignItems: 'center', + }, + confirmationButton: { + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'row', + height: '100%', + }, }); export default SiteInfo; diff --git a/src/components/SitesList.tsx b/src/components/SitesList.tsx new file mode 100644 index 0000000..7e4ef6c --- /dev/null +++ b/src/components/SitesList.tsx @@ -0,0 +1,128 @@ +import React, { + Dispatch, + FC, + SetStateAction, + memo, + useCallback, + useEffect, + useState, +} from 'react'; + +import { Trans, t } from '@lingui/macro'; +import { FlashList } from '@shopify/flash-list'; +import { StyleSheet, View, ViewToken } from 'react-native'; +import { TextInput } from 'react-native-paper'; +import { DEFAULT_TOTP_PERIOD } from '../constants/app'; +import colors from '../constants/colors'; +import { useTimer30 } from '../hooks/useTimer30'; +import Site from '../models/Site'; +import Text from '../ui/Text'; +import SiteInfo from './SiteInfo'; + +type SitesListProps = { + sites: Site[]; + deleteSite: (site: Site) => void; +}; + +const SitesList: FC = ({ sites, deleteSite }) => { + const [search, setSearch] = useState(); + const [filteredSites, setFilteredSites] = useState(sites); + const [visibleItems, setVisibleItems] = useState([]); + const timer30 = useTimer30(DEFAULT_TOTP_PERIOD); + + useEffect(() => { + const regEx = new RegExp(search || '', 'i'); + setFilteredSites( + !search + ? sites + : sites.filter( + site => site.label.match(regEx) || site.issuer?.match(regEx), + ), + ); + }, [search, sites]); + + const onViewableItemsChanged = useCallback( + ({ + viewableItems, + }: { + viewableItems: ViewToken[]; + changed: ViewToken[]; + }) => { + setVisibleItems((prevIntes: string[]) => { + return viewableItems.map(token => token.item.label); + }); + }, + [], + ); + + if (!sites.length) { + return ( + + No sites have been added yet + + ); + } + + return ( + <> + + } + placeholder={t`Search...`} + mode="outlined" + theme={{ + colors: { + background: colors.white, + }, + }} + outlineColor={colors.medium} + placeholderTextColor={colors.medium} + /> + + {!!sites.length && !filteredSites.length && ( + + No results matching your search + + )} + + item.id} + renderItem={({ item }) => { + return ( + + ); + }} + onViewableItemsChanged={onViewableItemsChanged} + estimatedItemSize={90} + extraData={{ timer30 }} + /> + + + ); +}; + +const styles = StyleSheet.create({ + inputWrapper: { + marginBottom: 32, + }, + icon: { + width: 24, + height: 24, + position: 'absolute', + }, + scrollView: { + marginBottom: 32, + width: '100%', + flex: 1, + }, +}); + +export default memo(SitesList); diff --git a/src/constants/colors.ts b/src/constants/colors.ts index 1e9c99e..b891b0e 100644 --- a/src/constants/colors.ts +++ b/src/constants/colors.ts @@ -10,4 +10,5 @@ export default { gray: '#9ca5ab', background: '#eef0f3', error: '#b55464', + red: '#E21818', }; diff --git a/src/hooks/useTimer.ts b/src/hooks/useTimer.ts index d8d13a4..bff4399 100644 --- a/src/hooks/useTimer.ts +++ b/src/hooks/useTimer.ts @@ -1,16 +1,22 @@ import { useEffect, useState } from 'react'; -export const useTimer = (period: number, ms = 1000) => { - const [seconds, setSeconds] = useState(period); +export const useTimer = (period?: number) => { + const [value, setValue] = useState(0); useEffect(() => { - const intervalId = setInterval(() => { - const date = new Date(); - setSeconds((date.getSeconds() % period) + date.getMilliseconds() / 1000); - }, ms); + const checkTimeAndUpdateValue = () => { + const now = new Date(); + const seconds = now.getSeconds(); - return () => clearInterval(intervalId); - }, [ms, period]); + setValue(seconds); + }; - return seconds; + const intervalId = setInterval(checkTimeAndUpdateValue, 1000); + + return () => { + clearInterval(intervalId); + }; + }, [period]); + + return value; }; diff --git a/src/hooks/useTimer30.ts b/src/hooks/useTimer30.ts new file mode 100644 index 0000000..d21ae9a --- /dev/null +++ b/src/hooks/useTimer30.ts @@ -0,0 +1,24 @@ +import { useEffect, useState } from 'react'; + +export const useTimer30 = (period: number) => { + const [value, setValue] = useState(0); + + useEffect(() => { + const checkTimeAndUpdateValue = () => { + const now = new Date(); + const seconds = now.getSeconds(); + + if (seconds % period === 0) { + setValue(prevValue => (prevValue === null ? 0 : prevValue + 1)); + } + }; + + const intervalId = setInterval(checkTimeAndUpdateValue, 1000); + + return () => { + clearInterval(intervalId); + }; + }, [period]); + + return value; +}; diff --git a/src/icons/TickIcon.tsx b/src/icons/TickIcon.tsx new file mode 100644 index 0000000..a0b2b1e --- /dev/null +++ b/src/icons/TickIcon.tsx @@ -0,0 +1,21 @@ +import { FC } from 'react'; +import Svg, { Circle, G, Path } from 'react-native-svg'; +import { ComponentWithColor, ComponentWithSize } from '../types'; +import colors from '../constants/colors'; + +export const TickIcon: FC = ({ + width = 48, + height = 48, + color = colors.dark, +}) => { + return ( + + + + ); +}; diff --git a/src/icons/TrashIcon.tsx b/src/icons/TrashIcon.tsx new file mode 100644 index 0000000..bbe4cb0 --- /dev/null +++ b/src/icons/TrashIcon.tsx @@ -0,0 +1,21 @@ +import { FC } from 'react'; +import Svg, { Circle, G, Path } from 'react-native-svg'; +import { ComponentWithColor, ComponentWithSize } from '../types'; +import colors from '../constants/colors'; + +export const TrashIcon: FC = ({ + width = 48, + height = 48, + color = colors.dark, +}) => { + return ( + + + + ); +}; diff --git a/src/providers/DatabaseProvider.tsx b/src/providers/DatabaseProvider.tsx index 495fc82..0036785 100644 --- a/src/providers/DatabaseProvider.tsx +++ b/src/providers/DatabaseProvider.tsx @@ -26,6 +26,7 @@ export interface DBContextInterface { db: Database; newSite: (site: OtpRecord) => Promise; listSites: () => Promise; + deleteSite: (site: Site) => void; } const updateSiteData = (site: Site, otpData: OtpRecord) => { @@ -62,6 +63,12 @@ const newSite = (otpData: OtpRecord) => return site; }); +const deleteSite = (site: Site) => { + db.write(async () => { + site.destroyPermanently(); + }); +}; + const listSites = () => db.collections.get(SITE_TABLE_NAME).query().fetch(); @@ -69,6 +76,7 @@ const initialDBContext: DBContextInterface = { db, newSite, listSites, + deleteSite, }; export const DBContext = createContext(initialDBContext); diff --git a/src/providers/NotificationProvider.tsx b/src/providers/NotificationProvider.tsx index 711dbd6..3b25933 100644 --- a/src/providers/NotificationProvider.tsx +++ b/src/providers/NotificationProvider.tsx @@ -10,16 +10,17 @@ import { ComponentWithChildren } from '../types'; import { Snackbar } from 'react-native-paper'; import Text from '../ui/Text'; -export interface NotificationContextInterface { +export type NotificationContextInterface = { setNotification: (text: string | ReactNode) => void; -} +}; -const initialDBContext: NotificationContextInterface = { +const initialNotificationContext: NotificationContextInterface = { setNotification: () => {}, }; -export const NotificationContext = - createContext(initialDBContext); +export const NotificationContext = createContext( + initialNotificationContext, +); export const useNotification = () => useContext(NotificationContext); const SNACKBAR_DURATION = 3000; diff --git a/src/ui/Progress.tsx b/src/ui/Progress.tsx index 6e12855..a0ca199 100644 --- a/src/ui/Progress.tsx +++ b/src/ui/Progress.tsx @@ -6,8 +6,8 @@ import { useTimer } from '../hooks/useTimer'; import { StyleSheet, View } from 'react-native'; const Progress: FC = () => { - const timer = useTimer(DEFAULT_TOTP_PERIOD); - const progress = timer / DEFAULT_TOTP_PERIOD; + const timer = useTimer(1); + const progress = (timer % DEFAULT_TOTP_PERIOD) / DEFAULT_TOTP_PERIOD; return ( diff --git a/src/ui/SiteToken.tsx b/src/ui/SiteToken.tsx index 43724e6..6399e4b 100644 --- a/src/ui/SiteToken.tsx +++ b/src/ui/SiteToken.tsx @@ -1,27 +1,34 @@ import { FC } from 'react'; import { StyleSheet, View } from 'react-native'; import Text, { TextProps, TextVariantTypes } from './Text'; +import { ActivityIndicator } from 'react-native-paper'; +import colors from '../constants/colors'; type SiteTokenProps = { value: string; variant?: 'primary' | 'tertiary'; size?: TextProps['size']; + loading?: boolean; }; const SiteToken: FC = ({ value, variant = 'primary', size = 'headlineSmall', + loading = false, }) => { return ( - {value.match(/\d{3}/g)?.map((num, index) => ( + {value.match(/.{3}/g)?.map((num, index) => ( {num} ))} + {(loading || value === '------') && ( + + )} ); }; diff --git a/src/util/generateTotp.ts b/src/util/generateTotp.ts index 11f7f37..f5b6c95 100644 --- a/src/util/generateTotp.ts +++ b/src/util/generateTotp.ts @@ -14,8 +14,16 @@ export const getAlgorithm = ( return algorithm as TOTPConfig['algorithm']; }; -export const generateTOTP = ({ secret, ...params }: TOTPConfig): string => { - const token = totp(secret, params); - - return token; +export const generateTOTP = ({ + secret, + ...params +}: TOTPConfig): Promise => { + return new Promise((resolve, reject) => { + try { + const token = totp(secret, params); + resolve(token); + } catch (error) { + reject(error); + } + }); }; diff --git a/src/util/parseOtpUri.ts b/src/util/parseOtpUri.ts index 0c6e79b..ba1e0a0 100644 --- a/src/util/parseOtpUri.ts +++ b/src/util/parseOtpUri.ts @@ -20,7 +20,6 @@ import { * - issuer?: string */ export const parseOtpUri = (uri: string): OtpRecord => { - console.log({ uri }); const url = new URL(uri); const type = url.host; const label = url.pathname.substring(1);