Compare commits
6 Commits
Author | SHA1 | Date |
---|---|---|
![]() |
c593330a21 | |
|
ea4f542b0f | |
![]() |
4fef2a2e30 | |
|
308b546395 | |
![]() |
e3f7ab5203 | |
|
a1295e09ac |
47
App.js
47
App.js
|
@ -12,7 +12,9 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import * as React from "react";
|
||||
import React from "react";
|
||||
import {Lato_700Bold, useFonts} from "@expo-google-fonts/lato";
|
||||
import {Roboto_500Medium} from "@expo-google-fonts/roboto";
|
||||
import {NavigationContainer} from "@react-navigation/native";
|
||||
import {PaperProvider} from "react-native-paper";
|
||||
import {SafeAreaView, Text} from "react-native";
|
||||
|
@ -29,6 +31,11 @@ import migrations from "./drizzle/migrations";
|
|||
|
||||
const App = () => {
|
||||
const {success, error} = useMigrations(db, migrations);
|
||||
const [fontsLoaded] = useFonts({
|
||||
Lato_700Bold,
|
||||
Roboto_500Medium,
|
||||
});
|
||||
|
||||
const {NotificationsProvider} = createNotifications({
|
||||
duration: 800,
|
||||
notificationPosition: "top",
|
||||
|
@ -53,25 +60,27 @@ const App = () => {
|
|||
);
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
if (!success || !fontsLoaded) {
|
||||
return (
|
||||
<ContentLoader
|
||||
speed={2}
|
||||
width={400}
|
||||
height={150}
|
||||
viewBox="0 0 400 150"
|
||||
backgroundColor="#f3f3f3"
|
||||
foregroundColor="#ecebeb"
|
||||
>
|
||||
<Circle cx="10" cy="20" r="8" />
|
||||
<Rect x="25" y="15" rx="5" ry="5" width="220" height="10" />
|
||||
<Circle cx="10" cy="50" r="8" />
|
||||
<Rect x="25" y="45" rx="5" ry="5" width="220" height="10" />
|
||||
<Circle cx="10" cy="80" r="8" />
|
||||
<Rect x="25" y="75" rx="5" ry="5" width="220" height="10" />
|
||||
<Circle cx="10" cy="110" r="8" />
|
||||
<Rect x="25" y="105" rx="5" ry="5" width="220" height="10" />
|
||||
</ContentLoader>
|
||||
<SafeAreaView style={{flex: 1, justifyContent: "center", alignItems: "center"}}>
|
||||
<ContentLoader
|
||||
speed={2}
|
||||
width={300}
|
||||
height={150}
|
||||
viewBox="0 0 300 150"
|
||||
backgroundColor="#f3f3f3"
|
||||
foregroundColor="#ecebeb"
|
||||
>
|
||||
<Circle cx="10" cy="20" r="8" />
|
||||
<Rect x="25" y="15" rx="5" ry="5" width="220" height="10" />
|
||||
<Circle cx="10" cy="50" r="8" />
|
||||
<Rect x="25" y="45" rx="5" ry="5" width="220" height="10" />
|
||||
<Circle cx="10" cy="80" r="8" />
|
||||
<Rect x="25" y="75" rx="5" ry="5" width="220" height="10" />
|
||||
<Circle cx="10" cy="110" r="8" />
|
||||
<Rect x="25" y="105" rx="5" ry="5" width="220" height="10" />
|
||||
</ContentLoader>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,31 +12,30 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React, {useState} from "react";
|
||||
import {View} from "react-native";
|
||||
import React from "react";
|
||||
import {Image} from "expo-image";
|
||||
|
||||
const AvatarWithFallback = ({source, fallbackSource, size, style}) => {
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
const handleImageError = () => {
|
||||
if (!hasError) {
|
||||
setHasError(true);
|
||||
}
|
||||
};
|
||||
function AvatarWithFallback({source, fallbackSource, size, style}) {
|
||||
const [imageSource, setImageSource] = React.useState(source);
|
||||
|
||||
return (
|
||||
<View style={{overflow: "hidden", borderRadius: 9999, width: size, height: size, ...style}}>
|
||||
<Image
|
||||
style={{width: "100%", height: "100%"}}
|
||||
source={hasError ? fallbackSource : source}
|
||||
onError={handleImageError}
|
||||
contentFit="cover"
|
||||
transition={300}
|
||||
cachePolicy={"memory-disk"}
|
||||
/>
|
||||
</View>
|
||||
<Image
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
borderRadius: 9999,
|
||||
width: size,
|
||||
height: size,
|
||||
...style,
|
||||
}}
|
||||
source={imageSource}
|
||||
onError={() => setImageSource(fallbackSource)}
|
||||
placeholder={fallbackSource}
|
||||
placeholderContentFit="cover"
|
||||
contentFit="cover"
|
||||
transition={300}
|
||||
cachePolicy="memory-disk"
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default AvatarWithFallback;
|
||||
|
|
|
@ -127,6 +127,7 @@ function CasdoorLoginPage({onWebviewClose, initialMethod}) {
|
|||
<EnterCasdoorSdkConfig
|
||||
onClose={() => handleLogin(initialMethod)}
|
||||
onWebviewClose={onWebviewClose}
|
||||
usePortal={false}
|
||||
/>
|
||||
),
|
||||
scanner: (
|
||||
|
|
|
@ -19,7 +19,7 @@ import {useNotifications} from "react-native-notificated";
|
|||
import PropTypes from "prop-types";
|
||||
import useStore from "./useStorage";
|
||||
|
||||
function EnterCasdoorSdkConfig({onClose, onWebviewClose}) {
|
||||
function EnterCasdoorSdkConfig({onClose, onWebviewClose, usePortal = true}) {
|
||||
const {
|
||||
serverUrl,
|
||||
clientId,
|
||||
|
@ -52,72 +52,73 @@ function EnterCasdoorSdkConfig({onClose, onWebviewClose}) {
|
|||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>Casdoor Configuration</Text>
|
||||
<View style={styles.formContainer}>
|
||||
<TextInput
|
||||
label="Endpoint"
|
||||
value={serverUrl}
|
||||
onChangeText={setServerUrl}
|
||||
autoCapitalize="none"
|
||||
style={styles.input}
|
||||
mode="outlined"
|
||||
/>
|
||||
<TextInput
|
||||
label="Client ID"
|
||||
value={clientId}
|
||||
onChangeText={setClientId}
|
||||
autoCapitalize="none"
|
||||
style={styles.input}
|
||||
mode="outlined"
|
||||
/>
|
||||
<TextInput
|
||||
label="App Name"
|
||||
value={appName}
|
||||
onChangeText={setAppName}
|
||||
autoCapitalize="none"
|
||||
style={styles.input}
|
||||
mode="outlined"
|
||||
/>
|
||||
<TextInput
|
||||
label="Organization Name"
|
||||
value={organizationName}
|
||||
onChangeText={setOrganizationName}
|
||||
autoCapitalize="none"
|
||||
style={styles.input}
|
||||
mode="outlined"
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={closeConfigPage}
|
||||
style={styles.button}
|
||||
labelStyle={styles.buttonLabel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={handleSave}
|
||||
style={styles.button}
|
||||
labelStyle={styles.buttonLabel}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</View>
|
||||
const content = (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>Casdoor Configuration</Text>
|
||||
<View style={styles.formContainer}>
|
||||
<TextInput
|
||||
label="Endpoint"
|
||||
value={serverUrl}
|
||||
onChangeText={setServerUrl}
|
||||
autoCapitalize="none"
|
||||
style={styles.input}
|
||||
mode="outlined"
|
||||
/>
|
||||
<TextInput
|
||||
label="Client ID"
|
||||
value={clientId}
|
||||
onChangeText={setClientId}
|
||||
autoCapitalize="none"
|
||||
style={styles.input}
|
||||
mode="outlined"
|
||||
/>
|
||||
<TextInput
|
||||
label="App Name"
|
||||
value={appName}
|
||||
onChangeText={setAppName}
|
||||
autoCapitalize="none"
|
||||
style={styles.input}
|
||||
mode="outlined"
|
||||
/>
|
||||
<TextInput
|
||||
label="Organization Name"
|
||||
value={organizationName}
|
||||
onChangeText={setOrganizationName}
|
||||
autoCapitalize="none"
|
||||
style={styles.input}
|
||||
mode="outlined"
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={closeConfigPage}
|
||||
style={styles.button}
|
||||
labelStyle={styles.buttonLabel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={handleSave}
|
||||
style={styles.button}
|
||||
labelStyle={styles.buttonLabel}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</Portal>
|
||||
</View>
|
||||
);
|
||||
|
||||
return usePortal ? <Portal>{content}</Portal> : content;
|
||||
}
|
||||
|
||||
EnterCasdoorSdkConfig.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onWebviewClose: PropTypes.func.isRequired,
|
||||
usePortal: PropTypes.bool,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
@ -142,7 +143,7 @@ const styles = StyleSheet.create({
|
|||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: "bold",
|
||||
fontFamily: "Lato-Bold",
|
||||
fontFamily: "Lato_700Bold",
|
||||
color: "#212121",
|
||||
textAlign: "center",
|
||||
marginBottom: 16,
|
||||
|
|
|
@ -159,7 +159,7 @@ const styles = StyleSheet.create({
|
|||
fontSize: Math.max(24, width * 0.05),
|
||||
fontWeight: "bold",
|
||||
color: "#212121",
|
||||
fontFamily: "Lato-Bold",
|
||||
fontFamily: "Lato_700Bold",
|
||||
},
|
||||
buttonContainer: {
|
||||
borderRadius: 24,
|
||||
|
@ -179,7 +179,7 @@ const styles = StyleSheet.create({
|
|||
fontWeight: "600",
|
||||
marginLeft: 8,
|
||||
color: "#424242",
|
||||
fontFamily: "Roboto-Medium",
|
||||
fontFamily: "Roboto_500Medium",
|
||||
},
|
||||
menuContent: {
|
||||
backgroundColor: "#FAFAFA",
|
||||
|
|
29
HomePage.js
29
HomePage.js
|
@ -26,6 +26,7 @@ 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";
|
||||
|
@ -57,6 +58,16 @@ export default function HomePage() {
|
|||
const {setAccount, updateAccount, insertAccount, insertAccounts, deleteAccount} = useEditAccount();
|
||||
const {notify} = useNotifications();
|
||||
|
||||
const {showImportOptions} = useImportManager((data) => {
|
||||
handleAddAccount(data);
|
||||
}, (err) => {
|
||||
notify("error", {
|
||||
params: {title: "Import error", description: err.message},
|
||||
});
|
||||
}, () => {
|
||||
setShowScanner(true);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
refreshAccounts();
|
||||
}, []);
|
||||
|
@ -107,7 +118,7 @@ export default function HomePage() {
|
|||
|
||||
const handleAddAccount = async(accountDataInput) => {
|
||||
if (Array.isArray(accountDataInput)) {
|
||||
insertAccounts(accountDataInput);
|
||||
await insertAccounts(accountDataInput);
|
||||
} else {
|
||||
await setAccount(accountDataInput);
|
||||
await insertAccount();
|
||||
|
@ -175,6 +186,11 @@ export default function HomePage() {
|
|||
closeOptions();
|
||||
};
|
||||
|
||||
const openImportAccountModal = () => {
|
||||
showImportOptions();
|
||||
closeOptions();
|
||||
};
|
||||
|
||||
const closeEnterAccountModal = () => setShowEnterAccountModal(false);
|
||||
|
||||
const closeSwipeableMenu = () => {
|
||||
|
@ -291,11 +307,11 @@ export default function HomePage() {
|
|||
padding: 20,
|
||||
borderRadius: 10,
|
||||
width: 300,
|
||||
height: 150,
|
||||
height: 225,
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: [{translateX: -150}, {translateY: -75}],
|
||||
transform: [{translateX: -150}, {translateY: -112.5}],
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
|
@ -312,6 +328,13 @@ export default function HomePage() {
|
|||
<IconButton icon={"keyboard"} size={35} />
|
||||
<Text style={{fontSize: 18}}>Enter Secret code</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={{flexDirection: "row", alignItems: "center", marginTop: 10}}
|
||||
onPress={openImportAccountModal}
|
||||
>
|
||||
<IconButton icon={"import"} size={35} />
|
||||
<Text style={{fontSize: 18}}>Import from other app</Text>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
</Portal>
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright 2024 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 {useActionSheet} from "@expo/react-native-action-sheet";
|
||||
import {importFromMSAuth} from "./MSAuthImportLogic";
|
||||
|
||||
const importApps = [
|
||||
{name: "Google Authenticator", useScanner: true},
|
||||
{name: "Microsoft Authenticator", importFunction: importFromMSAuth},
|
||||
];
|
||||
|
||||
export const useImportManager = (onImportComplete, onError, onOpenScanner) => {
|
||||
const {showActionSheetWithOptions} = useActionSheet();
|
||||
|
||||
const showImportOptions = () => {
|
||||
const options = [...importApps.map(app => app.name), "Cancel"];
|
||||
const cancelButtonIndex = options.length - 1;
|
||||
|
||||
showActionSheetWithOptions(
|
||||
{
|
||||
options,
|
||||
cancelButtonIndex,
|
||||
title: "Select app to import from",
|
||||
},
|
||||
(selectedIndex) => {
|
||||
if (selectedIndex !== cancelButtonIndex) {
|
||||
const selectedApp = importApps[selectedIndex];
|
||||
if (selectedApp.useScanner) {
|
||||
onOpenScanner();
|
||||
} else if (selectedApp.importFunction) {
|
||||
selectedApp.importFunction()
|
||||
.then(result => {
|
||||
if (result) {onImportComplete(result);}
|
||||
})
|
||||
.catch(onError);
|
||||
} else {
|
||||
onError(new Error(`Import function not implemented for ${selectedApp.name}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return {showImportOptions};
|
||||
};
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright 2024 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 * as DocumentPicker from "expo-document-picker";
|
||||
import * as FileSystem from "expo-file-system";
|
||||
import {openDatabaseSync} from "expo-sqlite/next";
|
||||
|
||||
const SQLITE_DIR = `${FileSystem.documentDirectory}SQLite`;
|
||||
|
||||
const getRandomDBName = () => {
|
||||
const randomString = Math.random().toString(36).substring(2, 15);
|
||||
const timestamp = Date.now();
|
||||
return `${randomString}_${timestamp}.db`;
|
||||
};
|
||||
|
||||
const createDirectory = async(dir) => {
|
||||
try {
|
||||
if (!(await FileSystem.getInfoAsync(dir)).exists) {
|
||||
await FileSystem.makeDirectoryAsync(dir, {intermediates: true});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating directory: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const queryMicrosoftAuthenticatorDatabase = async(db) => {
|
||||
return await db.getAllAsync("SELECT name, username, oath_secret_key FROM accounts WHERE account_type = 0");
|
||||
};
|
||||
|
||||
const formatMicrosoftAuthenticatorData = (rows) => {
|
||||
return rows.map(row => {
|
||||
const {name, username, oath_secret_key} = row;
|
||||
return {issuer: name, accountName: username, secretKey: oath_secret_key};
|
||||
});
|
||||
};
|
||||
|
||||
export const importFromMSAuth = async() => {
|
||||
const dbName = getRandomDBName();
|
||||
const internalDbName = `${SQLITE_DIR}/${dbName}`;
|
||||
try {
|
||||
const result = await DocumentPicker.getDocumentAsync({
|
||||
multiple: false,
|
||||
copyToCacheDirectory: false,
|
||||
});
|
||||
|
||||
if (!result.canceled) {
|
||||
const file = result.assets[0];
|
||||
if ((await FileSystem.getInfoAsync(file.uri)).exists) {
|
||||
await createDirectory(SQLITE_DIR);
|
||||
await FileSystem.copyAsync({from: file.uri, to: internalDbName});
|
||||
|
||||
try {
|
||||
const db = openDatabaseSync(dbName);
|
||||
|
||||
const rows = await queryMicrosoftAuthenticatorDatabase(db);
|
||||
if (rows.length === 0) {
|
||||
throw new Error("No data found in Microsoft Authenticator database");
|
||||
}
|
||||
return formatMicrosoftAuthenticatorData(rows);
|
||||
} catch (dbError) {
|
||||
if (dbError.message.includes("file is not a database")) {
|
||||
throw new Error("file is not a database");
|
||||
}
|
||||
throw new Error(dbError.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Error importing from Microsoft Authenticator: ${error.message}`);
|
||||
} finally {
|
||||
await FileSystem.deleteAsync(internalDbName, {idempotent: true});
|
||||
}
|
||||
};
|
5
api.js
5
api.js
|
@ -38,7 +38,10 @@ export const getMfaAccounts = async(serverUrl, owner, name, token, timeoutMs = T
|
|||
throw new Error(res.msg);
|
||||
}
|
||||
|
||||
return {updatedTime: res.data.updatedTime, mfaAccounts: res.data.mfaAccounts};
|
||||
return {
|
||||
updatedTime: res.data.updatedTime,
|
||||
mfaAccounts: res.data.mfaAccounts || [],
|
||||
};
|
||||
} catch (error) {
|
||||
if (error.name === "AbortError") {
|
||||
throw new Error("Request timed out");
|
||||
|
|
9
app.json
9
app.json
|
@ -2,7 +2,7 @@
|
|||
"expo": {
|
||||
"name": "Casdoor",
|
||||
"slug": "casdoor-app",
|
||||
"version": "1.8.0",
|
||||
"version": "1.11.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "light",
|
||||
|
@ -19,7 +19,7 @@
|
|||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "org.casdoor.casdoorapp",
|
||||
"buildNumber": "1.8.0"
|
||||
"buildNumber": "1.11.0"
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
|
@ -27,7 +27,7 @@
|
|||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"package": "org.casdoor.casdoorapp",
|
||||
"versionCode": 510010800
|
||||
"versionCode": 510011100
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
|
@ -50,7 +50,8 @@
|
|||
"photosPermission": "The app accesses your photos to add Totp account."
|
||||
}
|
||||
],
|
||||
"expo-asset"
|
||||
"expo-asset",
|
||||
"expo-font"
|
||||
],
|
||||
"owner": "casdoor"
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
|
@ -10,6 +10,8 @@
|
|||
"release": "npx -p semantic-release-expo -p semantic-release -p @semantic-release/git -p @semantic-release/changelog -p @semantic-release/exec semantic-release"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo-google-fonts/lato": "^0.2.3",
|
||||
"@expo-google-fonts/roboto": "^0.2.3",
|
||||
"@expo/react-native-action-sheet": "^4.1.0",
|
||||
"@react-native-async-storage/async-storage": "1.23.1",
|
||||
"@react-native-community/masked-view": "^0.1.11",
|
||||
|
@ -21,18 +23,20 @@
|
|||
"casdoor-react-native-sdk": "1.1.0",
|
||||
"drizzle-orm": "^0.33.0",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"expo": "~51.0.34",
|
||||
"expo": "~51.0.38",
|
||||
"expo-asset": "~10.0.10",
|
||||
"expo-camera": "~15.0.16",
|
||||
"expo-crypto": "~13.0.2",
|
||||
"expo-dev-client": "~4.0.27",
|
||||
"expo-dev-client": "~4.0.28",
|
||||
"expo-document-picker": "~12.0.2",
|
||||
"expo-drizzle-studio-plugin": "^0.0.2",
|
||||
"expo-font": "~12.0.10",
|
||||
"expo-image": "~1.13.0",
|
||||
"expo-image-picker": "~15.0.7",
|
||||
"expo-sqlite": "^14.0.6",
|
||||
"expo-status-bar": "~1.12.1",
|
||||
"expo-system-ui": "~3.0.7",
|
||||
"expo-updates": "~0.25.25",
|
||||
"expo-updates": "~0.25.27",
|
||||
"hi-base32": "^0.5.1",
|
||||
"hotp-totp": "^1.0.6",
|
||||
"prop-types": "^15.8.1",
|
||||
|
|
|
@ -129,7 +129,7 @@ const useEditAccountStore = create((set, get) => ({
|
|||
}
|
||||
},
|
||||
|
||||
insertAccounts: (accounts) => {
|
||||
insertAccounts: async(accounts) => {
|
||||
try {
|
||||
db.transaction((tx) => {
|
||||
const insertWithDuplicateCheck = (baseAccName, issuer, secretKey) => {
|
||||
|
|
Loading…
Reference in New Issue