Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 123 additions & 86 deletions src/app/(tabs)/(password-manager)/password-details.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import FormTextField from "@/components/FormTextField";
import SecureTextField from "@/components/SecureTextField";
import { useToast } from "@/contexts/ToastContext";

import { passwords } from "@/db/schema";
import { getScreenShotSecureScreen } from "@/libs/screenshot_prevention";
import formatDate from "@/utils/formating";

import FontAwesome6 from "@react-native-vector-icons/fontawesome6";
import { eq } from "drizzle-orm";
import { drizzle } from "drizzle-orm/expo-sqlite";
import { Href, router, useLocalSearchParams } from "expo-router";
import { usePreventRemove } from "expo-router/react-navigation";
import { useSQLiteContext } from "expo-sqlite";

import { useEffect, useState } from "react";
import { Keyboard, ScrollView, View } from "react-native";
import { Button } from "react-native-paper";
import { Keyboard, ScrollView, TextInput, View } from "react-native";

import { Button, FAB } from "react-native-paper";
import { SafeAreaView } from "react-native-safe-area-context";

export default function PasswordDetailsScreen() {
Expand All @@ -29,8 +34,23 @@ export default function PasswordDetailsScreen() {

const [isEditing, setIsEditing] = useState(false);

const { showToast } = useToast();

getScreenShotSecureScreen();

/**
* Remove focus from whichever TextInput is currently focused
* and dismiss the keyboard.
*
* This is important because Keyboard.dismiss() alone can leave
* the native TextInput focused, which can leave the focus outline
* visible after leaving edit mode.
*/
function unfocusAllFields() {
TextInput.State.currentlyFocusedInput()?.blur();
Keyboard.dismiss();
}

async function loadAndRefreshPassword() {
const result = await drizzleDb
.select()
Expand All @@ -50,45 +70,75 @@ export default function PasswordDetailsScreen() {
}

async function updatePassword() {
await drizzleDb
.update(passwords)
.set({
domain,
username,
password,
url,
notes,
updatedAt: new Date().toISOString(),
})
.where(eq(passwords.id, Number(id)));

Keyboard.dismiss();
setIsEditing(false);
router.back();
/*
* Blur BEFORE changing isEditing.
*
* This prevents the TextInput from remaining focused when
* it becomes read-only.
*/
unfocusAllFields();

try {
await drizzleDb
.update(passwords)
.set({
domain,
username,
password,
url,
notes,
updatedAt: new Date().toISOString(),
})
.where(eq(passwords.id, Number(id)));

setIsEditing(false);

await loadAndRefreshPassword();

showToast("Password updated successfully");
} catch (error) {
showToast("Failed to update; please try again", "error");
console.error("Failed to update password:", error);
}
}

usePreventRemove(isEditing, () => {
Keyboard.dismiss();
function handleCancel() {
/*
* Remove focus before switching the inputs to read-only.
*/
unfocusAllFields();

setIsEditing(false);

/*
* Restore the values from the database.
*/
loadAndRefreshPassword();
});

showToast("Changes discarded");
}

function handleFabPress() {
if (isEditing) {
updatePassword();
} else {
setIsEditing(true);
}
}

usePreventRemove(isEditing, () => handleCancel());

useEffect(() => {
loadAndRefreshPassword();
}, []);

function handleCancel() {
Keyboard.dismiss();
setIsEditing(false);
loadAndRefreshPassword();
}

return (
<SafeAreaView style={{ flex: 1 }}>
<ScrollView
contentContainerStyle={{
margin: 20,
gap: 24,
paddingBottom: 100,
}}
keyboardShouldPersistTaps="handled"
>
Expand Down Expand Up @@ -146,69 +196,56 @@ export default function PasswordDetailsScreen() {
gap: 4,
}}
>
{!isEditing ? (
<>
<Button
icon={({ size, color }) => (
<FontAwesome6
name="pencil"
size={size}
color={color}
iconStyle="solid"
/>
)}
onPress={() => setIsEditing(true)}
>
Edit
</Button>

<Button
icon={({ size, color }) => (
<FontAwesome6
name="link"
size={size}
color={color}
iconStyle="solid"
/>
)}
onPress={() => router.navigate(url as Href)}
>
Open
</Button>
</>
) : (
<>
<Button
icon={({ size, color }) => (
<FontAwesome6
name="store"
size={size}
color={color}
iconStyle="solid"
/>
)}
onPress={updatePassword}
>
Save
</Button>

<Button
icon={({ size, color }) => (
<FontAwesome6
name="xmark"
size={size}
color={color}
iconStyle="solid"
/>
)}
onPress={handleCancel}
>
Cancel
</Button>
</>
{!isEditing && (
<Button
icon={({ size, color }) => (
<FontAwesome6
name="link"
size={size}
color={color}
iconStyle="solid"
/>
)}
onPress={() => router.navigate(url as Href)}
>
Open
</Button>
)}

{isEditing && (
<Button
icon={({ size, color }) => (
<FontAwesome6
name="xmark"
size={size}
color={color}
iconStyle="solid"
/>
)}
onPress={handleCancel}
>
Cancel
</Button>
)}
</View>
</ScrollView>

<FAB
style={{
position: "absolute",
bottom: 35,
right: 35,
}}
icon={({ size, color }) => (
<FontAwesome6
name={isEditing ? "check" : "pencil"}
size={size}
color={color}
iconStyle="solid"
/>
)}
onPress={handleFabPress}
/>
</SafeAreaView>
);
}
9 changes: 8 additions & 1 deletion src/app/(tabs)/(password-manager)/save-password.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import FormTextField from "@/components/FormTextField";
import SecureTextField from "@/components/SecureTextField";
import { useToast } from "@/contexts/ToastContext";
import { passwords } from "@/db/schema";
import { getScreenShotSecureScreen } from "@/libs/screenshot_prevention";
import FontAwesome6 from "@react-native-vector-icons/fontawesome6";
Expand All @@ -21,6 +22,8 @@ export default function SavePasswordScreen() {
const db = useSQLiteContext();
const drizzleDb = drizzle(db);

const { showToast } = useToast();

getScreenShotSecureScreen();

return (
Expand Down Expand Up @@ -110,9 +113,13 @@ export default function SavePasswordScreen() {
notes,
url,
})
.then(() => router.back())
.then(() => {
showToast("Password saved successfully");
router.back();
})
.catch((err) => {
console.error(err);
showToast("Failed to save, please try again!!", "error");
Alert.alert("Error", "Failed to save password.");
});
}}
Expand Down
5 changes: 2 additions & 3 deletions src/app/(tabs)/password-generator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -312,9 +312,8 @@ function StrengthIndicator({ score }: { score: number }) {
</View>

<View style={styles.strengthLabels}>
<Text variant="labelSmall">Weak</Text>

<Text variant="labelSmall">Strong</Text>
<Text variant="labelSmall">Weak </Text>
<Text variant="labelSmall">Strong </Text>
</View>
</View>
);
Expand Down
5 changes: 4 additions & 1 deletion src/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ScreenHeading from "@/components/ScreenHeading";
import { ToastProvider } from "@/contexts/ToastContext";
import DatabaseProvider from "@/db/provider";
import {
isBiometricsAuthEnabled,
Expand Down Expand Up @@ -97,7 +98,9 @@ function AppContent() {
return (
<ThemeProvider value={isDarkScheme ? DarkTheme : DefaultTheme}>
<PaperProvider>
<Slot />
<ToastProvider>
<Slot />
</ToastProvider>
</PaperProvider>
</ThemeProvider>
);
Expand Down
46 changes: 43 additions & 3 deletions src/components/FormTextField.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,47 @@
import { forwardRef, useEffect, useRef, useState } from "react";
import { TextInput as RNTextInput } from "react-native";
import { TextInput, TextInputProps } from "react-native-paper";

type Props = TextInputProps;

export default function FormTextField({ ...props }: Props) {
return <TextInput mode="outlined" style={{ fontSize: 12 }} {...props} />;
}
const FormTextField = forwardRef<RNTextInput, Props>(
({ editable = true, ...props }, ref) => {
const inputRef = useRef<RNTextInput>(null);

const [internalEditable, setInternalEditable] = useState(editable);

useEffect(() => {
if (editable === internalEditable) {
return;
}

if (!editable) {
inputRef.current?.blur();
}

setInternalEditable(editable);
}, [editable, internalEditable]);

return (
<TextInput
ref={(instance: RNTextInput) => {
inputRef.current = instance;

if (typeof ref === "function") {
ref(instance);
} else if (ref) {
ref.current = instance;
}
}}
mode="outlined"
style={{ fontSize: 12 }}
editable={internalEditable}
{...props}
/>
);
},
);

FormTextField.displayName = "FormTextField";

export default FormTextField;
Loading