mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-07-09 20:09:36 +02:00
Merge branch 'master' into beta
This commit is contained in:
@@ -140,7 +140,7 @@ android {
|
||||
if (project.hasProperty("prBuildNumber")) {
|
||||
versionCode Integer.parseInt(prBuildNumber())
|
||||
} else {
|
||||
versionCode 3103
|
||||
versionCode 3104
|
||||
}
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
|
||||
@@ -22,7 +22,7 @@ import { Attachment, Note, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { RefObject, useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { ActionSheetRef } from "react-native-actions-sheet";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import { db } from "../../common/database";
|
||||
@@ -59,6 +59,7 @@ import Paragraph from "../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { createFormRef, validators } from "../ui/input/form-input";
|
||||
|
||||
const Actions = ({
|
||||
attachment,
|
||||
@@ -153,50 +154,100 @@ const Actions = ({
|
||||
{
|
||||
name: strings.rename(),
|
||||
onPress: () => {
|
||||
presentDialog({
|
||||
input: true,
|
||||
title: strings.renameFile(),
|
||||
defaultValue: attachment.filename,
|
||||
positivePress: async (value) => {
|
||||
if (value && value.length > 0) {
|
||||
await db.attachments.add({
|
||||
hash: attachment.hash,
|
||||
filename: value
|
||||
});
|
||||
setFilename(value);
|
||||
setAttachments();
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
positiveText: strings.rename()
|
||||
});
|
||||
close?.();
|
||||
setTimeout(() => {
|
||||
presentDialog({
|
||||
title: strings.renameFile(),
|
||||
form: {
|
||||
formRef: createFormRef({
|
||||
name: attachment.filename
|
||||
}),
|
||||
items: [
|
||||
{
|
||||
name: "name",
|
||||
defaultValue: attachment.filename,
|
||||
placeholder: strings.enterTitle(),
|
||||
ref: React.createRef<TextInput | null>(),
|
||||
validators: [validators.required(strings.nameIsRequired())]
|
||||
}
|
||||
],
|
||||
onFormSubmit: async (form) => {
|
||||
try {
|
||||
const value = form.getValue("name");
|
||||
await db.attachments.add({
|
||||
hash: attachment.hash,
|
||||
filename: value
|
||||
});
|
||||
setFilename(value);
|
||||
setAttachments();
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
ToastManager.show({
|
||||
message: `Attachment renamed to ${value}`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
form.setError("name", (e as Error).message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
positiveText: strings.rename()
|
||||
});
|
||||
}, 500);
|
||||
},
|
||||
icon: "form-textbox"
|
||||
},
|
||||
{
|
||||
name: strings.delete(),
|
||||
onPress: async () => {
|
||||
const relations = await db.relations.to(attachment, "note").get();
|
||||
await db.attachments.remove(attachment.hash, false);
|
||||
setAttachments();
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
relations
|
||||
.map((relation) => relation.fromId)
|
||||
.forEach(async (id) => {
|
||||
useTabStore.getState().forEachNoteTab(id, async (tab) => {
|
||||
const isFocused = useTabStore.getState().currentTab === tab.id;
|
||||
if (isFocused) {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: await db.notes.note(id),
|
||||
forced: true
|
||||
});
|
||||
} else {
|
||||
editorController.current.commands.setLoading(true, tab.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
close?.();
|
||||
setTimeout(() => {
|
||||
presentDialog({
|
||||
title: strings.deleteAttachment(),
|
||||
paragraph: strings.deleteAttachmentConfirm(),
|
||||
positiveText: strings.yes(),
|
||||
negativeText: strings.no(),
|
||||
positiveType: "errorShade",
|
||||
positivePress: async () => {
|
||||
try {
|
||||
const relations = await db.relations
|
||||
.to(attachment, "note")
|
||||
.get();
|
||||
await db.attachments.remove(attachment.hash, false);
|
||||
ToastManager.show({
|
||||
type: "success",
|
||||
message: strings.attachmentDeleted()
|
||||
});
|
||||
setAttachments();
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
relations
|
||||
.map((relation) => relation.fromId)
|
||||
.forEach(async (id) => {
|
||||
useTabStore.getState().forEachNoteTab(id, async (tab) => {
|
||||
const isFocused =
|
||||
useTabStore.getState().currentTab === tab.id;
|
||||
if (isFocused) {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: await db.notes.note(id),
|
||||
forced: true
|
||||
});
|
||||
} else {
|
||||
editorController.current.commands.setLoading(
|
||||
true,
|
||||
tab.id
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
},
|
||||
icon: "delete-outline"
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
@@ -26,42 +27,44 @@ import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eOpenRecoveryKeyDialog } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Dialog } from "../dialog";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import FormInput, { createFormRef, validators } from "../ui/input/form-input";
|
||||
import { Notice } from "../ui/notice";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { TextInput } from "react-native-gesture-handler";
|
||||
|
||||
export const ChangePassword = () => {
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
const password = useRef<string>(undefined);
|
||||
const { colors } = useThemeColors();
|
||||
const formRef = useRef(
|
||||
createFormRef({
|
||||
oldPassword: "",
|
||||
password: ""
|
||||
})
|
||||
);
|
||||
const oldPasswordInputRef = useRef<TextInput>(null);
|
||||
const oldPassword = useRef<string>(undefined);
|
||||
|
||||
const [error, setError] = useState(false);
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
const user = useUserStore((state) => state.user);
|
||||
|
||||
const changePassword = async () => {
|
||||
setError(undefined);
|
||||
formRef.current.clearErrors();
|
||||
|
||||
if (!user?.isEmailConfirmed) {
|
||||
ToastManager.show({
|
||||
heading: strings.emailNotConfirmed(),
|
||||
message: strings.emailNotConfirmedDesc(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
setError(strings.emailNotConfirmedDesc());
|
||||
return;
|
||||
}
|
||||
if (error || !oldPassword.current || !password.current) {
|
||||
ToastManager.show({
|
||||
heading: strings.allFieldsRequired(),
|
||||
message: strings.allFieldsRequiredDesc(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
if (!formRef.current.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const values = formRef.current.getValues();
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await BackupService.run(
|
||||
@@ -74,8 +77,8 @@ export const ChangePassword = () => {
|
||||
}
|
||||
|
||||
const passwordChanged = await db.user.changePassword(
|
||||
oldPassword.current,
|
||||
password.current
|
||||
values.oldPassword,
|
||||
values.password
|
||||
);
|
||||
|
||||
if (!passwordChanged) {
|
||||
@@ -91,15 +94,15 @@ export const ChangePassword = () => {
|
||||
Navigation.goBack();
|
||||
eSendEvent(eOpenRecoveryKeyDialog);
|
||||
} catch (e) {
|
||||
const message = (e as Error).message;
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: strings.passwordChangeFailed(),
|
||||
message: (e as Error).message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
|
||||
if (/old password/i.test(message)) {
|
||||
formRef.current.setError("oldPassword", message);
|
||||
} else {
|
||||
setError(message);
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -110,36 +113,61 @@ export const ChangePassword = () => {
|
||||
}}
|
||||
>
|
||||
<Dialog context="change-password-dialog" />
|
||||
<Input
|
||||
<FormInput
|
||||
name="oldPassword"
|
||||
formRef={formRef}
|
||||
fwdRef={oldPasswordInputRef}
|
||||
onChangeText={(value) => {
|
||||
oldPassword.current = value;
|
||||
}}
|
||||
loading={loading}
|
||||
validators={[validators.required(strings.currentPasswordRequired())]}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.oldPassword()}
|
||||
placeholder={strings.currentPassword()}
|
||||
onSubmitEditing={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
<FormInput
|
||||
name="password"
|
||||
formRef={formRef}
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
loading={loading}
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
validationType="password"
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.newPassword()}
|
||||
onSubmitEditing={() => {
|
||||
changePassword();
|
||||
}}
|
||||
/>
|
||||
|
||||
{error ? (
|
||||
<Paragraph
|
||||
numberOfLines={4}
|
||||
onPress={() => {}}
|
||||
color={colors.error.accent}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
color={colors.error.accent}
|
||||
name="alert-circle-outline"
|
||||
size={AppFontSize.sm - 1}
|
||||
/>{" "}
|
||||
{error}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
<Notice text={strings.changePasswordNotice()} type="alert" />
|
||||
|
||||
<View style={{ height: 10 }} />
|
||||
|
||||
@@ -27,7 +27,7 @@ import { useThemeColors } from "@notesnook/theme";
|
||||
import DialogHeader from "../dialog/dialog-header";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
import FormInput, { createFormRef, validators } from "../ui/input/form-input";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
@@ -36,21 +36,22 @@ import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const ForgotPassword = ({ userEmail }: { userEmail: string }) => {
|
||||
const { colors } = useThemeColors("sheet");
|
||||
const email = useRef<string>(userEmail);
|
||||
const formRef = useRef(
|
||||
createFormRef({
|
||||
email: userEmail || ""
|
||||
})
|
||||
);
|
||||
const emailInputRef = useRef<TextInput>(null);
|
||||
const [error, setError] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sent, setSent] = useState(false);
|
||||
|
||||
const sendRecoveryEmail = async () => {
|
||||
if (!email.current || error) {
|
||||
ToastManager.show({
|
||||
heading: strings.emailRequired(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
if (formRef.current.validateField("email")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const values = formRef.current.getValues();
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const lastRecoveryEmailTime = SettingsService.get().lastRecoveryEmailTime;
|
||||
@@ -60,7 +61,7 @@ export const ForgotPassword = ({ userEmail }: { userEmail: string }) => {
|
||||
) {
|
||||
throw new Error(strings.pleaseWaitBeforeSendEmail());
|
||||
}
|
||||
await db.user.recoverAccount(email.current.toLowerCase());
|
||||
await db.user.recoverAccount(values.email.toLowerCase());
|
||||
SettingsService.set({
|
||||
lastRecoveryEmailTime: Date.now()
|
||||
});
|
||||
@@ -75,12 +76,7 @@ export const ForgotPassword = ({ userEmail }: { userEmail: string }) => {
|
||||
setSent(true);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: strings.recoveryEmailFailed(),
|
||||
message: (e as Error).message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
formRef.current.setError("email", (e as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -126,22 +122,25 @@ export const ForgotPassword = ({ userEmail }: { userEmail: string }) => {
|
||||
<DialogHeader title={strings.accountRecovery()} />
|
||||
<Seperator />
|
||||
|
||||
<Input
|
||||
<FormInput
|
||||
name="email"
|
||||
formRef={formRef}
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
defaultValue={email.current}
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
loading={loading}
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
keyboardType="email-address"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage={strings.emailInvalid()}
|
||||
placeholder={strings.email()}
|
||||
onSubmit={() => {}}
|
||||
validators={[
|
||||
validators.required(strings.emailRequired()),
|
||||
validators.email(strings.enterAValidEmailAddress())
|
||||
]}
|
||||
onSubmitEditing={() => {
|
||||
sendRecoveryEmail();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -37,8 +37,9 @@ import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Dialog } from "../dialog";
|
||||
import { Progress } from "../sheets/progress";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import FormInput, { validators } from "../ui/input/form-input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { hideAuth } from "./common";
|
||||
@@ -63,14 +64,13 @@ export const Login = ({
|
||||
const {
|
||||
step,
|
||||
setStep,
|
||||
password,
|
||||
email,
|
||||
emailInputRef,
|
||||
passwordInputRef,
|
||||
loading,
|
||||
setLoading,
|
||||
setError,
|
||||
login
|
||||
login,
|
||||
error,
|
||||
formRef
|
||||
} = useLogin(async () => {
|
||||
eSendEvent(eUserLoggedIn, true);
|
||||
await sleep(500);
|
||||
@@ -93,6 +93,11 @@ export const Login = ({
|
||||
});
|
||||
const { width, height } = useWindowDimensions();
|
||||
const isTablet = width > 600;
|
||||
|
||||
const onContinue = () => {
|
||||
login();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async () => {
|
||||
setStep(LoginSteps.emailAuth);
|
||||
@@ -198,27 +203,27 @@ export const Login = ({
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
<FormInput
|
||||
name="email"
|
||||
formRef={formRef}
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
testID="input.email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
keyboardType="email-address"
|
||||
marginBottom={0}
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage={strings.emailInvalid()}
|
||||
placeholder={strings.email()}
|
||||
defaultValue={email.current}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
onSubmit={() => {
|
||||
validators={[
|
||||
validators.required(strings.emailRequired()),
|
||||
validators.email(strings.enterAValidEmailAddress())
|
||||
]}
|
||||
onSubmitEditing={() => {
|
||||
if (step === LoginSteps.emailAuth) {
|
||||
login();
|
||||
onContinue();
|
||||
} else {
|
||||
passwordInputRef.current?.focus();
|
||||
}
|
||||
@@ -227,11 +232,10 @@ export const Login = ({
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<>
|
||||
<Input
|
||||
<FormInput
|
||||
name="password"
|
||||
formRef={formRef}
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
testID="input.password"
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="done"
|
||||
@@ -242,9 +246,9 @@ export const Login = ({
|
||||
placeholder={strings.password()}
|
||||
marginBottom={0}
|
||||
editable={!loading}
|
||||
defaultValue={password.current}
|
||||
onSubmit={() => {
|
||||
login();
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
onSubmitEditing={() => {
|
||||
onContinue();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
@@ -255,9 +259,13 @@ export const Login = ({
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading || !email.current) return;
|
||||
if (loading) return;
|
||||
presentSheet({
|
||||
component: <ForgotPassword userEmail={email.current} />
|
||||
component: (
|
||||
<ForgotPassword
|
||||
userEmail={formRef.current.getValue("email")}
|
||||
/>
|
||||
)
|
||||
});
|
||||
}}
|
||||
textStyle={{
|
||||
@@ -273,8 +281,7 @@ export const Login = ({
|
||||
<Button
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
login();
|
||||
onContinue();
|
||||
}}
|
||||
style={{
|
||||
width: "100%"
|
||||
@@ -328,6 +335,25 @@ export const Login = ({
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<Paragraph
|
||||
numberOfLines={4}
|
||||
onPress={() => {}}
|
||||
color={colors.error.accent}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
color={colors.error.accent}
|
||||
name="alert-circle-outline"
|
||||
size={AppFontSize.sm - 1}
|
||||
/>{" "}
|
||||
{error.message}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -43,29 +43,26 @@ import SheetProvider from "../sheet-provider";
|
||||
import { Toast } from "../toast";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { LoginSteps, useLogin } from "./use-login";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { getObfuscatedEmail } from "../../utils/functions";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import FormInput, { validators } from "../ui/input/form-input";
|
||||
|
||||
export const SessionExpired = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [focused, setFocused] = useState(false);
|
||||
const { step, password, email, passwordInputRef, loading, login } = useLogin(
|
||||
() => {
|
||||
eSendEvent(eUserLoggedIn, true);
|
||||
setVisible(false);
|
||||
setFocused(false);
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
},
|
||||
true
|
||||
);
|
||||
const { step, passwordInputRef, loading, login, formRef } = useLogin(() => {
|
||||
eSendEvent(eUserLoggedIn, true);
|
||||
setVisible(false);
|
||||
setFocused(false);
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
}, true);
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
@@ -104,7 +101,7 @@ export const SessionExpired = () => {
|
||||
if (!complete) {
|
||||
const user = await db.user.getUser();
|
||||
if (!user) return;
|
||||
email.current = user.email;
|
||||
formRef.current.setValue("email", user.email);
|
||||
setVisible(true);
|
||||
setFocused(false);
|
||||
return;
|
||||
@@ -117,14 +114,14 @@ export const SessionExpired = () => {
|
||||
} catch (e) {
|
||||
const user = await db.user.getUser();
|
||||
if (!user) return;
|
||||
email.current = user.email;
|
||||
formRef.current.setValue("email", user.email);
|
||||
setFocused(false);
|
||||
setVisible(true);
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: true
|
||||
});
|
||||
}
|
||||
}, [email]);
|
||||
}, [formRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = eSubscribeEvent(eLoginSessionExpired, open);
|
||||
@@ -155,6 +152,7 @@ export const SessionExpired = () => {
|
||||
enableSheetKeyboardHandler={true}
|
||||
visible={true}
|
||||
>
|
||||
<Dialog context="two_factor_verify" />
|
||||
<View
|
||||
style={{
|
||||
width: focused ? "100%" : "99.9%",
|
||||
@@ -192,17 +190,17 @@ export const SessionExpired = () => {
|
||||
}}
|
||||
>
|
||||
{strings.sessionExpiredDesc(
|
||||
getObfuscatedEmail(email.current as string)
|
||||
getObfuscatedEmail(formRef.current.getValue("email") as string)
|
||||
)}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
{step === LoginSteps.passwordAuth ? (
|
||||
<Input
|
||||
<FormInput
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
formRef={formRef}
|
||||
name="password"
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
@@ -210,7 +208,7 @@ export const SessionExpired = () => {
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.password()}
|
||||
onSubmit={() => {
|
||||
onSubmitEditing={() => {
|
||||
login();
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { clearMessage, setEmailVerifyMessage } from "../../services/message";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
@@ -39,13 +38,14 @@ import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Loading } from "../loading";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import FormInput, { createFormRef, validators } from "../ui/input/form-input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { AuthHeader } from "./header";
|
||||
import { SignupContext } from "./signup-context";
|
||||
import { RouteParams } from "../../stores/use-navigation-store";
|
||||
import SettingsService from "../../services/settings";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
|
||||
const SignupSteps = {
|
||||
signup: 0,
|
||||
@@ -62,13 +62,17 @@ export const Signup = ({
|
||||
}) => {
|
||||
const [currentStep, setCurrentStep] = useState(SignupSteps.signup);
|
||||
const { colors } = useThemeColors();
|
||||
const email = useRef<string>(undefined);
|
||||
const formRef = useRef(
|
||||
createFormRef({
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: ""
|
||||
})
|
||||
);
|
||||
const emailInputRef = useRef<TextInput>(null);
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
const password = useRef<string>(undefined);
|
||||
const confirmPasswordInputRef = useRef<TextInput>(null);
|
||||
const confirmPassword = useRef<string>(undefined);
|
||||
const [error, setError] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
const setLastSynced = useUserStore((state) => state.setLastSynced);
|
||||
@@ -76,29 +80,17 @@ export const Signup = ({
|
||||
const isTablet = width > 600;
|
||||
const route = useRoute<RouteProp<RouteParams, "Auth">>();
|
||||
|
||||
const validateInfo = () => {
|
||||
if (!password.current || !email.current || !confirmPassword.current) {
|
||||
ToastManager.show({
|
||||
heading: strings.allFieldsRequired(),
|
||||
message: strings.allFieldsRequiredDesc(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const signup = async () => {
|
||||
if (!validateInfo() || error) return;
|
||||
setErrorMessage(undefined);
|
||||
if (!formRef.current.validate()) return;
|
||||
if (loading) return;
|
||||
|
||||
const values = formRef.current.getValues();
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
setCurrentStep(SignupSteps.createAccount);
|
||||
await db.user.signup(email.current!.toLowerCase(), password.current!);
|
||||
await db.user.signup(values.email.toLowerCase(), values.password);
|
||||
const user = await db.user.getUser();
|
||||
setUser(user);
|
||||
setLastSynced(await db.lastSynced());
|
||||
@@ -115,12 +107,14 @@ export const Signup = ({
|
||||
} catch (e) {
|
||||
setCurrentStep(SignupSteps.signup);
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: strings.signupFailed(),
|
||||
message: (e as Error).message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
if (
|
||||
(e as Error).message === "Unable to create an account on this email."
|
||||
) {
|
||||
formRef.current.setError("email", (e as Error).message);
|
||||
} else {
|
||||
setErrorMessage((e as Error).message);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -215,37 +209,35 @@ export const Signup = ({
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
<FormInput
|
||||
name="email"
|
||||
formRef={formRef}
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
defaultValue={email.current}
|
||||
loading={loading}
|
||||
testID="input.email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
keyboardType="email-address"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage={strings.emailInvalid()}
|
||||
placeholder={strings.email()}
|
||||
blurOnSubmit={false}
|
||||
onSubmit={() => {
|
||||
if (!email.current) return;
|
||||
validators={[
|
||||
validators.required(strings.emailRequired()),
|
||||
validators.email(strings.enterAValidEmailAddress())
|
||||
]}
|
||||
onSubmitEditing={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
<FormInput
|
||||
name="password"
|
||||
formRef={formRef}
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
defaultValue={password.current}
|
||||
loading={loading}
|
||||
testID="input.password"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
@@ -254,20 +246,18 @@ export const Signup = ({
|
||||
blurOnSubmit={false}
|
||||
autoCorrect={false}
|
||||
placeholder={strings.password()}
|
||||
onSubmit={() => {
|
||||
if (!password.current) return;
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
onSubmitEditing={() => {
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
<FormInput
|
||||
name="confirmPassword"
|
||||
formRef={formRef}
|
||||
fwdRef={confirmPasswordInputRef}
|
||||
onChangeText={(value) => {
|
||||
confirmPassword.current = value;
|
||||
}}
|
||||
defaultValue={confirmPassword.current}
|
||||
loading={loading}
|
||||
testID="input.confirmPassword"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Signup"
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
@@ -275,17 +265,22 @@ export const Signup = ({
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
blurOnSubmit={false}
|
||||
validationType="confirmPassword"
|
||||
customValidator={() => password.current || ""}
|
||||
placeholder={strings.confirmPassword()}
|
||||
marginBottom={12}
|
||||
onSubmit={() => {
|
||||
validators={[
|
||||
validators.required(strings.confirmPasswordRequired()),
|
||||
validators.matchField(
|
||||
"password",
|
||||
strings.passwordNotMatched()
|
||||
)
|
||||
]}
|
||||
onSubmitEditing={() => {
|
||||
signup();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={!loading ? "Continue" : null}
|
||||
title={!loading ? strings.continue() : null}
|
||||
type="accent"
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
@@ -319,6 +314,25 @@ export const Signup = ({
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
|
||||
{errorMessage ? (
|
||||
<Paragraph
|
||||
numberOfLines={4}
|
||||
onPress={() => {}}
|
||||
color={colors.error.accent}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
color={colors.error.accent}
|
||||
name="alert-circle-outline"
|
||||
size={AppFontSize.sm - 1}
|
||||
/>{" "}
|
||||
{errorMessage}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<View
|
||||
|
||||
@@ -28,6 +28,7 @@ import SettingsService from "../../services/settings";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eCloseSimpleDialog } from "../../utils/events";
|
||||
import TwoFactorVerification from "./two-factor";
|
||||
import { createFormRef } from "../ui/input/form-input";
|
||||
|
||||
export const LoginSteps = {
|
||||
emailAuth: 1,
|
||||
@@ -39,45 +40,33 @@ export const useLogin = (
|
||||
onFinishLogin?: () => void,
|
||||
sessionExpired = false
|
||||
) => {
|
||||
const [error, setError] = useState(false);
|
||||
const [error, setError] = useState<Error>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
const [step, setStep] = useState(LoginSteps.emailAuth);
|
||||
const email = useRef<string>(undefined);
|
||||
const password = useRef<string>(undefined);
|
||||
const emailInputRef = useRef<TextInput>(null);
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
|
||||
const validateInfo = () => {
|
||||
if (
|
||||
(!password.current && step === LoginSteps.passwordAuth) ||
|
||||
(!email.current && step === LoginSteps.emailAuth)
|
||||
) {
|
||||
ToastManager.show({
|
||||
heading: strings.allFieldsRequired(),
|
||||
message: strings.allFieldsRequiredDesc(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
const formRef = useRef(
|
||||
createFormRef({
|
||||
email: "",
|
||||
password: ""
|
||||
})
|
||||
);
|
||||
|
||||
const login = async () => {
|
||||
if (!validateInfo() || error) return;
|
||||
try {
|
||||
if (loading) return;
|
||||
setError(undefined);
|
||||
setLoading(true);
|
||||
switch (step) {
|
||||
case LoginSteps.emailAuth: {
|
||||
if (!email.current) {
|
||||
if (formRef.current.validateField("email")) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const mfaInfo = await db.user.authenticateEmail(email.current);
|
||||
const mfaInfo = await db.user.authenticateEmail(
|
||||
formRef.current.getValue("email")
|
||||
);
|
||||
|
||||
if (mfaInfo) {
|
||||
TwoFactorVerification.present(
|
||||
@@ -122,13 +111,14 @@ export const useLogin = (
|
||||
break;
|
||||
}
|
||||
case LoginSteps.passwordAuth: {
|
||||
if (!email.current || !password.current) {
|
||||
if (!formRef.current.validate()) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const values = formRef.current.getValues();
|
||||
await db.user.authenticatePassword(
|
||||
email.current,
|
||||
password.current,
|
||||
values.email,
|
||||
values.password,
|
||||
undefined,
|
||||
sessionExpired
|
||||
);
|
||||
@@ -145,12 +135,11 @@ export const useLogin = (
|
||||
const finishWithError = async (e: Error) => {
|
||||
if (e.message === "invalid_grant") setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: strings.loginFailed(),
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
if (e.message === "Password is incorrect.") {
|
||||
formRef.current.setError("password", e.message);
|
||||
} else {
|
||||
setError(e);
|
||||
}
|
||||
};
|
||||
|
||||
const finishLogin = async () => {
|
||||
@@ -177,13 +166,12 @@ export const useLogin = (
|
||||
login,
|
||||
step,
|
||||
setStep,
|
||||
email,
|
||||
password,
|
||||
passwordInputRef,
|
||||
emailInputRef,
|
||||
loading,
|
||||
setLoading,
|
||||
error,
|
||||
setError
|
||||
setError,
|
||||
formRef
|
||||
};
|
||||
};
|
||||
|
||||
@@ -136,7 +136,7 @@ const BaseDialog = ({
|
||||
? background
|
||||
: transparent
|
||||
? "transparent"
|
||||
: "rgba(0,0,0,0.3)"
|
||||
: "rgba(0,0,0,0.1)"
|
||||
}}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
|
||||
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import { ActivityIndicator, StyleSheet, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import { getColorLinearShade } from "../../utils/colors";
|
||||
@@ -79,6 +79,11 @@ const DialogButtons = ({
|
||||
/>
|
||||
<Paragraph color={colors.primary.accent}>{" " + doneText}</Paragraph>
|
||||
</View>
|
||||
) : loading ? (
|
||||
<ActivityIndicator
|
||||
size={AppFontSize.lg}
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
) : (
|
||||
<View />
|
||||
)}
|
||||
@@ -105,7 +110,6 @@ const DialogButtons = ({
|
||||
style={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
loading={loading}
|
||||
bold
|
||||
type={positiveType || "transparent"}
|
||||
title={positiveTitle}
|
||||
|
||||
@@ -17,10 +17,12 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { KeyboardTypeOptions } from "react-native";
|
||||
import { KeyboardTypeOptions, TextInput } from "react-native";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { eCloseSimpleDialog, eOpenSimpleDialog } from "../../utils/events";
|
||||
import { ButtonProps } from "../ui/button";
|
||||
import { FieldValidator, FormRef } from "../ui/input/form-input";
|
||||
import { RefObject } from "react";
|
||||
|
||||
export type DialogInfo = {
|
||||
title?: string;
|
||||
@@ -43,6 +45,18 @@ export type DialogInfo = {
|
||||
| "errorShade";
|
||||
icon?: string;
|
||||
paragraphColor: string;
|
||||
form?: {
|
||||
formRef: FormRef;
|
||||
items: {
|
||||
name: string;
|
||||
placeholder: string;
|
||||
label?: string;
|
||||
validators: FieldValidator[];
|
||||
defaultValue?: string;
|
||||
ref: RefObject<TextInput | null>;
|
||||
}[];
|
||||
onFormSubmit?: (form: FormRef) => Promise<boolean>;
|
||||
};
|
||||
input: boolean;
|
||||
inputPlaceholder: string;
|
||||
defaultValue: string;
|
||||
|
||||
@@ -18,7 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
RefObject
|
||||
} from "react";
|
||||
import { TextInput, View, ViewStyle } from "react-native";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import {
|
||||
@@ -34,6 +40,7 @@ import { sleep } from "../../utils/time";
|
||||
import { Toast } from "../toast";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import { FormInput, type FormRef } from "../ui/input/form-input";
|
||||
import { Notice } from "../ui/notice";
|
||||
import Seperator from "../ui/seperator";
|
||||
import BaseDialog from "./base-dialog";
|
||||
@@ -53,15 +60,38 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
});
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const [dialogInfo, setDialogInfo] = useState<DialogInfo>();
|
||||
const formRef = useRef(dialogInfo?.form?.formRef);
|
||||
formRef.current = dialogInfo?.form?.formRef;
|
||||
|
||||
const onPressPositive = async () => {
|
||||
if (dialogInfo?.positivePress) {
|
||||
// Handle form submission if form is available
|
||||
if (dialogInfo?.form && formRef.current) {
|
||||
inputRef.current?.blur();
|
||||
try {
|
||||
const isValid = await formRef.current.validate();
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
if (dialogInfo.form.onFormSubmit) {
|
||||
setLoading(true);
|
||||
const result = await dialogInfo.form.onFormSubmit(formRef.current);
|
||||
if (result === false) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
/** Empty */
|
||||
}
|
||||
setLoading(false);
|
||||
} else if (dialogInfo?.positivePress) {
|
||||
// Handle old input-based submission
|
||||
inputRef.current?.blur();
|
||||
setLoading(true);
|
||||
let result = false;
|
||||
try {
|
||||
result = await dialogInfo.positivePress(
|
||||
values.current.inputValue || dialogInfo.defaultValue,
|
||||
values.current.inputValue,
|
||||
checked
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -76,6 +106,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
|
||||
setChecked(false);
|
||||
values.current.inputValue = undefined;
|
||||
formRef.current = undefined;
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
@@ -85,6 +116,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
if (data.context !== context) return;
|
||||
setDialogInfo(data);
|
||||
setChecked(data.check?.defaultValue);
|
||||
formRef.current = data?.form?.formRef;
|
||||
values.current.inputValue = data.defaultValue;
|
||||
setVisible(true);
|
||||
},
|
||||
@@ -94,6 +126,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
const hide = React.useCallback(() => {
|
||||
setChecked(false);
|
||||
values.current.inputValue = undefined;
|
||||
formRef.current = undefined;
|
||||
setVisible(false);
|
||||
setDialogInfo(undefined);
|
||||
dialogInfo?.onClose?.();
|
||||
@@ -134,19 +167,30 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
? false
|
||||
: dialogInfo.statusBarTranslucent
|
||||
}
|
||||
bounce={!dialogInfo.input}
|
||||
bounce={!dialogInfo.input && !dialogInfo.form}
|
||||
closeOnTouch={!dialogInfo.disableBackdropClosing}
|
||||
background={dialogInfo.background}
|
||||
transparent={
|
||||
dialogInfo.transparent === undefined ? true : dialogInfo.transparent
|
||||
dialogInfo.transparent === undefined ? false : dialogInfo.transparent
|
||||
}
|
||||
onShow={async () => {
|
||||
if (dialogInfo.input) {
|
||||
if (dialogInfo.input && !dialogInfo.form) {
|
||||
inputRef.current?.setNativeProps({
|
||||
text: dialogInfo.defaultValue
|
||||
});
|
||||
await sleep(300);
|
||||
inputRef.current?.focus();
|
||||
} else if (dialogInfo.form) {
|
||||
const items = dialogInfo.form?.items;
|
||||
const firstItem = items[0];
|
||||
for (const item of items) {
|
||||
if (item.defaultValue) {
|
||||
item.ref.current?.setNativeProps({
|
||||
text: dialogInfo.defaultValue
|
||||
});
|
||||
}
|
||||
}
|
||||
firstItem?.ref?.current?.focus();
|
||||
}
|
||||
}}
|
||||
visible={true}
|
||||
@@ -170,7 +214,36 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
/>
|
||||
<Seperator half />
|
||||
|
||||
{dialogInfo.input ? (
|
||||
{dialogInfo.form ? (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
gap: DefaultAppStyles.GAP / 2
|
||||
}}
|
||||
>
|
||||
{dialogInfo.form.items.map((item, index) => (
|
||||
<FormInput
|
||||
key={item.name}
|
||||
fwdRef={item.ref}
|
||||
name={item.name}
|
||||
autoFocus={index === 0}
|
||||
placeholder={item.placeholder}
|
||||
formRef={formRef as RefObject<FormRef>}
|
||||
validators={item.validators}
|
||||
defaultValue={item.defaultValue}
|
||||
secureTextEntry={dialogInfo.secureTextEntry}
|
||||
onSubmitEditing={() => {
|
||||
const nextItem = dialogInfo?.form?.items?.[index + 1];
|
||||
if (nextItem) {
|
||||
nextItem?.ref.current?.focus();
|
||||
} else {
|
||||
onPressPositive();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
) : dialogInfo.input ? (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
@@ -184,7 +257,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
}}
|
||||
testID="input-value"
|
||||
secureTextEntry={dialogInfo.secureTextEntry}
|
||||
//defaultValue={dialogInfo.defaultValue}
|
||||
defaultValue={dialogInfo.defaultValue}
|
||||
onSubmit={() => {
|
||||
onPressPositive();
|
||||
}}
|
||||
@@ -237,7 +310,10 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
|
||||
|
||||
<DialogButtons
|
||||
onPressNegative={onNegativePress}
|
||||
onPressPositive={dialogInfo.positivePress && onPressPositive}
|
||||
onPressPositive={
|
||||
(dialogInfo.positivePress || dialogInfo.form?.onFormSubmit) &&
|
||||
onPressPositive
|
||||
}
|
||||
loading={loading}
|
||||
positiveTitle={dialogInfo.positiveText}
|
||||
negativeTitle={dialogInfo.negativeText}
|
||||
|
||||
@@ -69,8 +69,6 @@ export const SectionHeader = React.memo<
|
||||
dataType as "note" | "notebook" | "searchResult"
|
||||
);
|
||||
|
||||
console.log(item);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
|
||||
@@ -323,7 +323,6 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
|
||||
}
|
||||
|
||||
function getDate(item: Notebook | Note, groupType?: GroupingKey): number {
|
||||
console.log(groupType);
|
||||
return (
|
||||
getSortValue(
|
||||
groupType
|
||||
|
||||
411
apps/mobile/app/components/ui/input/form-input.tsx
Normal file
411
apps/mobile/app/components/ui/input/form-input.tsx
Normal file
@@ -0,0 +1,411 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { RefObject, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
ColorValue,
|
||||
TextInput,
|
||||
TextInputProps,
|
||||
TextStyle,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle
|
||||
} from "react-native";
|
||||
import isEmail from "validator/lib/isEmail";
|
||||
import isURL from "validator/lib/isURL";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { IconButton } from "../icon-button";
|
||||
import Paragraph from "../typography/paragraph";
|
||||
import AppIcon from "../AppIcon";
|
||||
|
||||
export type FormValues = Record<string, string>;
|
||||
export type FormErrors = Partial<Record<string, string>>;
|
||||
export type FieldValidator = (
|
||||
value: string,
|
||||
values: FormValues
|
||||
) => string | undefined;
|
||||
export type ValidationSchema = Partial<Record<string, FieldValidator[]>>;
|
||||
|
||||
export interface FormRef {
|
||||
values: FormValues;
|
||||
errors: FormErrors;
|
||||
setValue: (name: string, value: string) => void;
|
||||
getValue: (name: string) => string;
|
||||
getValues: () => FormValues;
|
||||
setError: (name: string, error?: string) => void;
|
||||
getError: (name: string) => string | undefined;
|
||||
clearErrors: () => void;
|
||||
registerField: (name: string, validators?: FieldValidator[]) => void;
|
||||
unregisterField: (name: string) => void;
|
||||
validateField: (name: string) => string | undefined;
|
||||
validate: () => boolean;
|
||||
subscribe: (listener: () => void) => () => void;
|
||||
}
|
||||
|
||||
export function createFormRef(initialValues: FormValues = {}): FormRef {
|
||||
const listeners = new Set<() => void>();
|
||||
const values: FormValues = { ...initialValues };
|
||||
const errors: FormErrors = {};
|
||||
const schema: ValidationSchema = {};
|
||||
|
||||
const notify = () => {
|
||||
listeners.forEach((listener) => listener());
|
||||
};
|
||||
|
||||
return {
|
||||
values,
|
||||
errors,
|
||||
setValue(name, value) {
|
||||
values[name] = value;
|
||||
if (errors[name]) {
|
||||
delete errors[name];
|
||||
notify();
|
||||
}
|
||||
},
|
||||
getValue(name) {
|
||||
return values[name] ?? "";
|
||||
},
|
||||
getValues() {
|
||||
return { ...values };
|
||||
},
|
||||
setError(name, error) {
|
||||
if (!error) {
|
||||
delete errors[name];
|
||||
} else {
|
||||
errors[name] = error;
|
||||
}
|
||||
notify();
|
||||
},
|
||||
getError(name) {
|
||||
return errors[name];
|
||||
},
|
||||
clearErrors() {
|
||||
Object.keys(errors).forEach((key) => delete errors[key]);
|
||||
notify();
|
||||
},
|
||||
registerField(name, validators = []) {
|
||||
schema[name] = validators;
|
||||
if (values[name] === undefined) values[name] = "";
|
||||
},
|
||||
unregisterField(name) {
|
||||
delete schema[name];
|
||||
delete errors[name];
|
||||
notify();
|
||||
},
|
||||
validateField(name) {
|
||||
const fieldValidators = schema[name] || [];
|
||||
const value = values[name] ?? "";
|
||||
for (const validator of fieldValidators) {
|
||||
const error = validator(value, values);
|
||||
if (error) {
|
||||
errors[name] = error;
|
||||
notify();
|
||||
return error;
|
||||
}
|
||||
}
|
||||
delete errors[name];
|
||||
notify();
|
||||
return undefined;
|
||||
},
|
||||
validate() {
|
||||
const nextErrors = validateForm(values, schema);
|
||||
Object.keys(errors).forEach((key) => delete errors[key]);
|
||||
Object.keys(nextErrors).forEach((key) => {
|
||||
errors[key] = nextErrors[key];
|
||||
});
|
||||
notify();
|
||||
return !hasFormErrors(nextErrors);
|
||||
},
|
||||
subscribe(listener) {
|
||||
listeners.add(listener);
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function validateForm(
|
||||
values: FormValues,
|
||||
schema: ValidationSchema
|
||||
): FormErrors {
|
||||
const errors: FormErrors = {};
|
||||
|
||||
Object.keys(schema).forEach((field) => {
|
||||
const fieldValidators = schema[field] || [];
|
||||
const value = values[field] ?? "";
|
||||
for (const validator of fieldValidators) {
|
||||
const error = validator(value, values);
|
||||
if (error) {
|
||||
errors[field] = error;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function hasFormErrors(errors: FormErrors) {
|
||||
return Object.keys(errors).length > 0;
|
||||
}
|
||||
|
||||
export const validators = {
|
||||
required:
|
||||
(message = "This field is required") =>
|
||||
(value: string) =>
|
||||
value?.trim() ? undefined : message,
|
||||
|
||||
email:
|
||||
(message = "Please enter a valid email") =>
|
||||
(value: string) =>
|
||||
!value?.trim() || isEmail(value.trim()) ? undefined : message,
|
||||
|
||||
minLength: (length: number, message?: string) => (value: string) =>
|
||||
!value || value.length >= length
|
||||
? undefined
|
||||
: message || `Must be at least ${length} characters`,
|
||||
|
||||
url:
|
||||
(message = "Please enter a valid URL") =>
|
||||
(value: string) =>
|
||||
!value?.trim() || isURL(value.trim(), { allow_underscores: true })
|
||||
? undefined
|
||||
: message,
|
||||
|
||||
matchField:
|
||||
(fieldName: string, message = "Values do not match") =>
|
||||
(value: string, values: FormValues) =>
|
||||
value === values[fieldName] ? undefined : message
|
||||
};
|
||||
|
||||
interface FormInputProps extends TextInputProps {
|
||||
name: string;
|
||||
formRef: RefObject<FormRef>;
|
||||
validators?: FieldValidator[];
|
||||
fwdRef?: RefObject<TextInput | null>;
|
||||
loading?: boolean;
|
||||
error?: string;
|
||||
customColor?: ColorValue;
|
||||
marginBottom?: number;
|
||||
marginRight?: number;
|
||||
button?: {
|
||||
icon: string;
|
||||
color: ColorValue;
|
||||
onPress: () => void;
|
||||
testID?: string;
|
||||
size?: number;
|
||||
};
|
||||
buttons?: React.ReactNode;
|
||||
buttonLeft?: React.ReactNode;
|
||||
height?: number;
|
||||
fontSize?: number;
|
||||
inputStyle?: TextInputProps["style"];
|
||||
containerStyle?: ViewStyle;
|
||||
wrapperStyle?: ViewStyle;
|
||||
errorStyle?: TextStyle;
|
||||
}
|
||||
|
||||
export function FormInput({
|
||||
name,
|
||||
formRef,
|
||||
validators: fieldValidators = [],
|
||||
fwdRef,
|
||||
loading,
|
||||
error,
|
||||
secureTextEntry,
|
||||
customColor,
|
||||
marginBottom = 10,
|
||||
marginRight,
|
||||
button,
|
||||
buttonLeft,
|
||||
buttons,
|
||||
height = 45,
|
||||
fontSize = AppFontSize.sm,
|
||||
inputStyle = {},
|
||||
containerStyle = {},
|
||||
wrapperStyle = {},
|
||||
onFocus,
|
||||
onBlur,
|
||||
onPress,
|
||||
onChangeText,
|
||||
errorStyle,
|
||||
...restProps
|
||||
}: FormInputProps) {
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const [focused, setFocused] = useState(false);
|
||||
const [secureEntry, setSecureEntry] = useState(true);
|
||||
const [, setVersion] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const form = formRef.current;
|
||||
form.registerField(name, fieldValidators);
|
||||
const unsubscribe = form.subscribe(() => {
|
||||
setVersion((v) => v + 1);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
form.unregisterField(name);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [formRef, name]);
|
||||
|
||||
const value = formRef.current.getValue(name);
|
||||
const fieldError = error || formRef.current.getError(name);
|
||||
|
||||
const borderColor = useMemo(() => {
|
||||
if (fieldError) return colors.error.accent;
|
||||
if (focused) return customColor || colors.selected.border;
|
||||
return colors.primary.border;
|
||||
}, [colors, customColor, fieldError, focused]);
|
||||
|
||||
const style: ViewStyle = {
|
||||
borderWidth: 1,
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderColor,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
...containerStyle
|
||||
};
|
||||
|
||||
const textStyle: TextInputProps["style"] = {
|
||||
paddingHorizontal: 0,
|
||||
fontSize,
|
||||
color:
|
||||
onPress && loading ? colors.primary.accent : colors.primary.paragraph,
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
paddingBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
fontFamily: "Inter-Regular",
|
||||
...(inputStyle as ViewStyle)
|
||||
};
|
||||
|
||||
const handleChangeText = (nextValue: string) => {
|
||||
formRef.current.setValue(name, nextValue);
|
||||
onChangeText?.(nextValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
importantForAccessibility="yes"
|
||||
style={{
|
||||
marginBottom,
|
||||
marginRight,
|
||||
...wrapperStyle
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
disabled={!loading}
|
||||
onPress={onPress}
|
||||
activeOpacity={1}
|
||||
style={style}
|
||||
>
|
||||
{buttonLeft}
|
||||
|
||||
<TextInput
|
||||
{...restProps}
|
||||
defaultValue={value}
|
||||
ref={fwdRef}
|
||||
editable={!loading && restProps.editable !== false}
|
||||
onChangeText={handleChangeText}
|
||||
onFocus={(e) => {
|
||||
setFocused(true);
|
||||
onFocus?.(e);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
setFocused(false);
|
||||
onBlur?.(e);
|
||||
}}
|
||||
keyboardAppearance={isDark ? "dark" : "light"}
|
||||
style={textStyle}
|
||||
secureTextEntry={secureTextEntry && secureEntry}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
height: 35 > height ? height : 35,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{secureTextEntry && (
|
||||
<IconButton
|
||||
name="eye"
|
||||
size={20}
|
||||
top={10}
|
||||
bottom={10}
|
||||
onPress={() => {
|
||||
fwdRef?.current?.blur();
|
||||
setSecureEntry(!secureEntry);
|
||||
}}
|
||||
style={{
|
||||
width: 25,
|
||||
marginLeft: 5
|
||||
}}
|
||||
color={
|
||||
secureEntry ? colors.secondary.icon : colors.primary.accent
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{buttons}
|
||||
|
||||
{button && (
|
||||
<IconButton
|
||||
testID={button.testID}
|
||||
name={button.icon}
|
||||
size={button.size || AppFontSize.xl}
|
||||
top={10}
|
||||
bottom={10}
|
||||
onPress={button.onPress}
|
||||
color={button.color}
|
||||
style={{
|
||||
marginRight: -8
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{fieldError ? (
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
style={[{ marginTop: 5, color: colors.error.icon }, errorStyle]}
|
||||
>
|
||||
<AppIcon
|
||||
color={colors.error.accent}
|
||||
name="alert-circle-outline"
|
||||
size={AppFontSize.sm - 1}
|
||||
/>{" "}
|
||||
{fieldError}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default FormInput;
|
||||
@@ -121,8 +121,8 @@ const Input = ({
|
||||
const color = error
|
||||
? colors.error.border
|
||||
: focus
|
||||
? customColor || colors.selected.border
|
||||
: colors.primary.border;
|
||||
? customColor || colors.selected.border
|
||||
: colors.primary.border;
|
||||
|
||||
const validate = async (value: string) => {
|
||||
if (!validationType) return;
|
||||
|
||||
@@ -370,7 +370,14 @@ export const useActions = ({
|
||||
inputPlaceholder: strings.name(),
|
||||
defaultValue: item.title,
|
||||
positivePress: async (value) => {
|
||||
if (!value || value.trim().length === 0) return;
|
||||
if (!value || value.trim().length === 0) {
|
||||
ToastManager.error(
|
||||
new Error(strings.nameIsRequired()),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
await db.colors.add({
|
||||
id: item.id,
|
||||
title: value
|
||||
|
||||
@@ -97,6 +97,7 @@ import { BETA } from "../utils/constants";
|
||||
import {
|
||||
eAfterSync,
|
||||
eCloseSheet,
|
||||
eCloseSimpleDialog,
|
||||
eEditorReset,
|
||||
eLoginSessionExpired,
|
||||
eOnLoadNote,
|
||||
@@ -300,7 +301,10 @@ const onUserSubscriptionStatusChanged = async (
|
||||
subscription: subscription
|
||||
}
|
||||
});
|
||||
Walkthrough.present("prouser", false, true);
|
||||
eSendEvent(eCloseSimpleDialog);
|
||||
setTimeout(() => {
|
||||
Walkthrough.present("prouser", false, true);
|
||||
}, 500);
|
||||
}
|
||||
await PremiumService.setPremiumStatus();
|
||||
useMessageStore.getState().setAnnouncement();
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Plan, SubscriptionPlan } from "@notesnook/core";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useAsync } from "react-async-hook";
|
||||
import { Platform } from "react-native";
|
||||
import Config from "react-native-config";
|
||||
@@ -181,6 +181,8 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
|
||||
const [cancelPromo, setCancelPromo] = useState(false);
|
||||
const [userCanRequestTrial, setUserCanRequestTrial] = useState(false);
|
||||
const [webPricingPlans, setWebPricingPlans] = useState<Plan[]>([]);
|
||||
const plansRef = useRef(plans);
|
||||
plansRef.current = plans;
|
||||
|
||||
const getProduct = React.useCallback(
|
||||
(planId: string, skuId: string) => {
|
||||
@@ -190,11 +192,11 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
|
||||
);
|
||||
|
||||
return (
|
||||
plans.find((p) => p.id === planId)?.subscriptions?.[skuId] ||
|
||||
plans.find((p) => p.id === planId)?.products?.[skuId]
|
||||
plansRef.current.find((p) => p.id === planId)?.subscriptions?.[skuId] ||
|
||||
plansRef.current.find((p) => p.id === planId)?.products?.[skuId]
|
||||
);
|
||||
},
|
||||
[plans, webPricingPlans]
|
||||
[webPricingPlans]
|
||||
);
|
||||
|
||||
const getProductAndroid = (planId: string, skuId: string) => {
|
||||
@@ -253,35 +255,41 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
|
||||
|
||||
useEffect(() => {
|
||||
const loadPlans = async () => {
|
||||
const items = await PremiumService.loadProductsAndSubs();
|
||||
pricingPlans.forEach((plan) => {
|
||||
plan.subscriptions = {};
|
||||
plan.products = {};
|
||||
plan.subscriptionSkuList.forEach((sku) => {
|
||||
if (!plan.subscriptions) plan.subscriptions = {};
|
||||
plan.subscriptions[sku] = items.subs.find((p) => p.productId === sku);
|
||||
try {
|
||||
const items = await PremiumService.loadProductsAndSubs();
|
||||
pricingPlans.forEach((plan) => {
|
||||
plan.subscriptions = {};
|
||||
plan.products = {};
|
||||
plan.subscriptionSkuList.forEach((sku) => {
|
||||
if (!plan.subscriptions) plan.subscriptions = {};
|
||||
plan.subscriptions[sku] = items.subs.find(
|
||||
(p) => p.productId === sku
|
||||
);
|
||||
});
|
||||
plan.productSkuList.forEach((sku) => {
|
||||
if (!plan.products) plan.products = {};
|
||||
plan.products[sku] = items.products.find(
|
||||
(p) => p.productId === sku
|
||||
);
|
||||
});
|
||||
});
|
||||
plan.productSkuList.forEach((sku) => {
|
||||
if (!plan.products) plan.products = {};
|
||||
plan.products[sku] = items.products.find((p) => p.productId === sku);
|
||||
});
|
||||
});
|
||||
setPlans([...pricingPlans]);
|
||||
setUserCanRequestTrial(hasTrialOffer());
|
||||
if (
|
||||
Config.GITHUB_RELEASE === "true" &&
|
||||
!SettingsService.getProperty("serverUrls")
|
||||
) {
|
||||
try {
|
||||
const products = WebPlanCache || (await db.pricing.products());
|
||||
WebPlanCache = products;
|
||||
setWebPricingPlans(products);
|
||||
} catch (e) {
|
||||
/**
|
||||
setPlans([...pricingPlans]);
|
||||
setUserCanRequestTrial(hasTrialOffer());
|
||||
if (
|
||||
Config.GITHUB_RELEASE === "true" &&
|
||||
!SettingsService.getProperty("serverUrls")
|
||||
) {
|
||||
try {
|
||||
const products = WebPlanCache || (await db.pricing.products());
|
||||
WebPlanCache = products;
|
||||
setWebPricingPlans(products);
|
||||
} catch (e) {
|
||||
/**
|
||||
empty */
|
||||
}
|
||||
}
|
||||
}
|
||||
setLoadingPlans(false);
|
||||
setLoadingPlans(false);
|
||||
} catch (e) {}
|
||||
};
|
||||
loadPlans();
|
||||
}, [options?.promoOffer, cancelPromo, hasTrialOffer]);
|
||||
|
||||
@@ -35,7 +35,6 @@ import { db } from "../../common/database";
|
||||
import { Dialog } from "../../components/dialog";
|
||||
import { Header } from "../../components/header";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import Input from "../../components/ui/input";
|
||||
import { ReminderTime } from "../../components/ui/reminder-time";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
@@ -59,6 +58,11 @@ import { TimeSince } from "../../components/ui/time-since";
|
||||
import Heading from "../../components/ui/typography/heading";
|
||||
import { eOnLoadNote } from "../../utils/events";
|
||||
import { fluidTabsRef } from "../../utils/global-refs";
|
||||
import FormInput, {
|
||||
createFormRef,
|
||||
validators
|
||||
} from "../../components/ui/input/form-input";
|
||||
import AppIcon from "../../components/ui/AppIcon";
|
||||
|
||||
const ReminderModes =
|
||||
Platform.OS === "ios"
|
||||
@@ -113,7 +117,12 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
const [repeatFrequency, setRepeatFrequency] = useState(1);
|
||||
const referencedItem = reference ? (reference as Note) : null;
|
||||
const recurringReminderFeature = useIsFeatureAvailable("recurringReminders");
|
||||
|
||||
const formRef = useRef(
|
||||
createFormRef({
|
||||
title: reminder?.title || referencedItem?.title || "",
|
||||
description: reminder?.description || referencedItem?.headline || ""
|
||||
})
|
||||
);
|
||||
const title = useRef<string | undefined>(
|
||||
!reminder ? referencedItem?.title : reminder?.title
|
||||
);
|
||||
@@ -132,6 +141,8 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
: null,
|
||||
[reminder?.id]
|
||||
);
|
||||
const [dateError, setDateError] = useState<string>();
|
||||
const [selectDayError, setSelectDayError] = useState<string>();
|
||||
|
||||
const showDatePicker = () => {
|
||||
setDatePickerVisibility(true);
|
||||
@@ -143,6 +154,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
|
||||
const handleConfirm = (date: Date) => {
|
||||
timer.current = setTimeout(() => {
|
||||
setDateError(undefined);
|
||||
hideDatePicker();
|
||||
setDate(date);
|
||||
}, 10);
|
||||
@@ -172,26 +184,27 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
|
||||
async function saveReminder() {
|
||||
try {
|
||||
if (!(await Notifications.checkAndRequestPermissions(true)))
|
||||
throw new Error(strings.noNotificationPermission());
|
||||
if (!date && reminderMode !== ReminderModes.Permanent) return;
|
||||
if (!formRef.current.validate()) return;
|
||||
if (date.getTime() < Date.now() && reminderMode === "once") {
|
||||
setDateError(strings.dateError());
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
reminderMode === ReminderModes.Repeat &&
|
||||
recurringMode !== "day" &&
|
||||
recurringMode !== "year" &&
|
||||
selectedDays.length === 0
|
||||
)
|
||||
throw new Error(strings.selectDayError());
|
||||
|
||||
if (!title.current) throw new Error(strings.setTitleError());
|
||||
if (
|
||||
date.getTime() < Date.now() &&
|
||||
reminderMode === "once" &&
|
||||
!props.route.params.reminder
|
||||
) {
|
||||
throw new Error(strings.dateError());
|
||||
setSelectDayError(strings.selectDayError());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!date && reminderMode !== ReminderModes.Permanent) return;
|
||||
|
||||
if (!(await Notifications.checkAndRequestPermissions(true)))
|
||||
throw new Error(strings.noNotificationPermission());
|
||||
|
||||
date.setSeconds(0, 0);
|
||||
|
||||
const reminderId = await db.reminders?.add({
|
||||
@@ -261,7 +274,10 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
keyboardDismissMode="interactive"
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<Input
|
||||
<FormInput
|
||||
name="title"
|
||||
validators={[validators.required(strings.titleIsRequired())]}
|
||||
formRef={formRef}
|
||||
fwdRef={titleRef}
|
||||
defaultValue={reminder?.title || referencedItem?.title}
|
||||
placeholder={strings.remindeMeOf()}
|
||||
@@ -270,12 +286,15 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
wrapperStyle={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
onSubmit={() => {
|
||||
onSubmitEditing={() => {
|
||||
descriptionRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
<FormInput
|
||||
name="description"
|
||||
validators={[]}
|
||||
formRef={formRef}
|
||||
defaultValue={
|
||||
reminder ? reminder?.description : referencedItem?.headline
|
||||
}
|
||||
@@ -461,6 +480,22 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
{selectDayError ? (
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
color: colors.error.icon
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
color={colors.error.accent}
|
||||
name="alert-circle-outline"
|
||||
size={AppFontSize.sm - 1}
|
||||
/>{" "}
|
||||
{selectDayError}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
@@ -476,16 +511,23 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
<DateTimePickerModal
|
||||
isVisible={isDatePickerVisible}
|
||||
mode="datetime"
|
||||
minimumDate={
|
||||
reminderMode === "once" ? dayjs().toDate() : new Date(0)
|
||||
}
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={hideDatePicker}
|
||||
isDarkModeEnabled={isDark}
|
||||
firstDayOfWeek={weekFormat === "Mon" ? 1 : 0}
|
||||
is24Hour={db.settings.getTimeFormat() === "24-hour"}
|
||||
date={date || new Date(Date.now())}
|
||||
themeVariant={isDark ? "dark" : "light"}
|
||||
/>
|
||||
|
||||
<DatePicker
|
||||
date={date}
|
||||
minimumDate={
|
||||
reminderMode === "once" ? dayjs().toDate() : new Date(0)
|
||||
}
|
||||
maximumDate={dayjs(date).add(3, "months").toDate()}
|
||||
onDateChange={handleConfirm}
|
||||
theme={isDark ? "dark" : "light"}
|
||||
@@ -519,6 +561,23 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{dateError ? (
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
color: colors.error.icon
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
color={colors.error.accent}
|
||||
name="alert-circle-outline"
|
||||
size={AppFontSize.sm - 1}
|
||||
/>{" "}
|
||||
{dateError}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import { sanitizeFilename, useIsFeatureAvailable } from "@notesnook/common";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors, VariantsWithStaticColors } from "@notesnook/theme";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import isMobilePhone from "validator/lib/isMobilePhone";
|
||||
import React, {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
@@ -28,7 +29,13 @@ import React, {
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { ActivityIndicator, Linking, Platform, View } from "react-native";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Linking,
|
||||
Platform,
|
||||
TextInput,
|
||||
View
|
||||
} from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { FlatList } from "react-native-gesture-handler";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
@@ -37,7 +44,10 @@ import filesystem from "../../common/filesystem";
|
||||
import DialogHeader from "../../components/dialog/dialog-header";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { IconButton } from "../../components/ui/icon-button";
|
||||
import Input from "../../components/ui/input";
|
||||
import FormInput, {
|
||||
createFormRef,
|
||||
validators
|
||||
} from "../../components/ui/input/form-input";
|
||||
import { Pressable } from "../../components/ui/pressable";
|
||||
import Seperator from "../../components/ui/seperator";
|
||||
import { SvgView } from "../../components/ui/svg";
|
||||
@@ -183,27 +193,60 @@ export const MFASetup = ({
|
||||
}: MFAStepProps) => {
|
||||
const { colors } = useThemeColors();
|
||||
const user = useUserStore((state) => state.user);
|
||||
const methodId = method?.id;
|
||||
const formRef = useRef(
|
||||
createFormRef({
|
||||
target: "",
|
||||
code: ""
|
||||
})
|
||||
);
|
||||
const targetInputRef = useRef<TextInput>(null);
|
||||
const codeInputRef = useRef<TextInput>(null);
|
||||
const [authenticatorDetails, setAuthenticatorDetails] = useState({
|
||||
sharedKey: null,
|
||||
authenticatorUri: null
|
||||
});
|
||||
const code = useRef<string>(undefined);
|
||||
const phoneNumber = useRef<string>(undefined);
|
||||
const { seconds, setId, start } = useTimer(method?.id);
|
||||
|
||||
const [loading, setLoading] = useState(method?.id === "app" ? true : false);
|
||||
const [enabling, setEnabling] = useState(false);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const [generalError, setGeneralError] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
if (method?.id === "app") {
|
||||
db.mfa?.setup("app").then((data) => {
|
||||
setAuthenticatorDetails(data);
|
||||
setLoading(false);
|
||||
});
|
||||
if (methodId === "app") {
|
||||
setLoading(true);
|
||||
db.mfa
|
||||
?.setup("app")
|
||||
.then((data) => {
|
||||
setAuthenticatorDetails(data);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
setLoading(false);
|
||||
setGeneralError(error.message);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}, [method?.id]);
|
||||
|
||||
setLoading(false);
|
||||
}, [methodId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!methodId) return;
|
||||
|
||||
formRef.current.clearErrors();
|
||||
formRef.current.setValue(
|
||||
"target",
|
||||
methodId === "email"
|
||||
? user?.email || ""
|
||||
: methodId === "app"
|
||||
? authenticatorDetails.sharedKey || ""
|
||||
: formRef.current.getValue("target")
|
||||
);
|
||||
formRef.current.setValue("code", "");
|
||||
setGeneralError(undefined);
|
||||
}, [authenticatorDetails.sharedKey, methodId, user?.email]);
|
||||
|
||||
const codeHelpText = {
|
||||
app: "After putting the above code in authenticator app, the app will display a code that you can enter below.",
|
||||
@@ -212,15 +255,43 @@ export const MFASetup = ({
|
||||
"You will receive a 2FA code on your email address which you can enter below"
|
||||
};
|
||||
|
||||
const targetValidators =
|
||||
method?.id === "sms"
|
||||
? [
|
||||
validators.required(strings.phoneNumberNotEntered()),
|
||||
(value: string) =>
|
||||
isMobilePhone(value, "any", {
|
||||
strictMode: true
|
||||
})
|
||||
? undefined
|
||||
: strings.enterValidPhone()
|
||||
]
|
||||
: method?.id === "email"
|
||||
? [
|
||||
validators.required(strings.emailRequired()),
|
||||
validators.email(strings.enterValidEmail())
|
||||
]
|
||||
: [];
|
||||
|
||||
const codeValidators = [
|
||||
validators.required(strings.enterSixDigitCode()),
|
||||
(value: string) =>
|
||||
/^\d{6}$/.test(value.trim()) ? undefined : strings.enterSixDigitCode()
|
||||
];
|
||||
|
||||
const onNext = async () => {
|
||||
if (!code.current || code.current.length !== 6) return;
|
||||
if (formRef.current.validateField("code")) return;
|
||||
|
||||
try {
|
||||
if (!method) return;
|
||||
const code = formRef.current.getValue("code").trim();
|
||||
|
||||
setGeneralError(undefined);
|
||||
setEnabling(true);
|
||||
if (recovery) {
|
||||
await db.mfa.enableFallback(method.id, code.current);
|
||||
await db.mfa.enableFallback(method.id, code);
|
||||
} else {
|
||||
await db.mfa.enable(method.id, code.current);
|
||||
await db.mfa.enable(method.id, code);
|
||||
}
|
||||
|
||||
const user = await db.user.fetchUser();
|
||||
@@ -229,14 +300,18 @@ export const MFASetup = ({
|
||||
setEnabling(false);
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
ToastManager.error(error, "Error submitting 2fa code");
|
||||
formRef.current.setError("code", error.message);
|
||||
setEnabling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onSendCode = async () => {
|
||||
if (error) return;
|
||||
if (!method || sending) return;
|
||||
|
||||
if (method.id !== "app" && formRef.current.validateField("target")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (method.id === "app" && authenticatorDetails.sharedKey) {
|
||||
Clipboard.setString(authenticatorDetails.sharedKey);
|
||||
if (authenticatorDetails.authenticatorUri) {
|
||||
@@ -254,30 +329,37 @@ export const MFASetup = ({
|
||||
}
|
||||
|
||||
try {
|
||||
if (seconds) throw new Error(strings.resendCodeWait());
|
||||
if (method.id === "sms" && !phoneNumber.current)
|
||||
throw new Error(strings.phoneNumberNotEntered());
|
||||
const target = formRef.current.getValue("target").trim();
|
||||
|
||||
setGeneralError(undefined);
|
||||
if (seconds) {
|
||||
setGeneralError(strings.resendCodeWait());
|
||||
return;
|
||||
}
|
||||
|
||||
setSending(true);
|
||||
await db.mfa.setup(method?.id, phoneNumber.current);
|
||||
await db.mfa.setup(method.id, method.id === "sms" ? target : undefined);
|
||||
|
||||
if (method.id === "sms") {
|
||||
setId(method.id + phoneNumber.current);
|
||||
setId(method.id + target);
|
||||
}
|
||||
await sleep(300);
|
||||
start(
|
||||
60,
|
||||
method.id === "sms" ? method.id + phoneNumber.current : method.id
|
||||
);
|
||||
start(60, method.id === "sms" ? method.id + target : method.id);
|
||||
setSending(false);
|
||||
ToastManager.show({
|
||||
heading: strings["2faCodeSentVia"](method.id),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
codeInputRef.current?.focus();
|
||||
} catch (e) {
|
||||
setSending(false);
|
||||
const error = e as Error;
|
||||
ToastManager.error(error, strings.errorSend2fa());
|
||||
if (method.id === "sms" || method.id === "email") {
|
||||
formRef.current.setError("target", error.message);
|
||||
} else {
|
||||
setGeneralError(error.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -316,55 +398,54 @@ export const MFASetup = ({
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<Input
|
||||
loading={method?.id !== "sms" ? true : false}
|
||||
value={
|
||||
method?.id === "email"
|
||||
? user?.email
|
||||
: method?.id === "app"
|
||||
? authenticatorDetails?.sharedKey || ""
|
||||
: undefined
|
||||
<FormInput
|
||||
key={`${method.id}-${authenticatorDetails.sharedKey || user?.email || ""}`}
|
||||
name="target"
|
||||
formRef={formRef}
|
||||
fwdRef={targetInputRef}
|
||||
loading={method?.id !== "sms"}
|
||||
editable={method.id === "sms"}
|
||||
defaultValue={
|
||||
method.id === "email"
|
||||
? user?.email || ""
|
||||
: method.id === "app"
|
||||
? authenticatorDetails.sharedKey || ""
|
||||
: undefined
|
||||
}
|
||||
multiline={method.id === "app"}
|
||||
onChangeText={(value) => {
|
||||
phoneNumber.current = value;
|
||||
onChangeText={() => {
|
||||
setGeneralError(undefined);
|
||||
}}
|
||||
placeholder={
|
||||
method?.id === "email"
|
||||
method.id === "email"
|
||||
? strings.enterEmailAddress()
|
||||
: "+1234567890"
|
||||
}
|
||||
onSubmit={() => {
|
||||
onSendCode();
|
||||
}}
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
validationType={method?.id === "email" ? "email" : "phonenumber"}
|
||||
onSubmitEditing={onSendCode}
|
||||
validators={targetValidators}
|
||||
keyboardType={
|
||||
method.id == "email" ? "email-address" : "phone-pad"
|
||||
}
|
||||
errorMessage={
|
||||
method?.id === "email"
|
||||
? strings.enterValidEmail()
|
||||
: strings.enterValidPhone()
|
||||
method.id === "email" ? "email-address" : "phone-pad"
|
||||
}
|
||||
buttons={
|
||||
error ? null : (
|
||||
<Button
|
||||
onPress={onSendCode}
|
||||
loading={sending}
|
||||
title={
|
||||
sending
|
||||
? null
|
||||
: method.id === "app"
|
||||
<Button
|
||||
onPress={onSendCode}
|
||||
loading={sending}
|
||||
style={{
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
title={
|
||||
sending
|
||||
? null
|
||||
: method.id === "app"
|
||||
? strings.copy()
|
||||
: `${
|
||||
seconds
|
||||
? strings.resendCode(seconds as number)
|
||||
: strings.sendCode()
|
||||
}`
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -373,13 +454,22 @@ export const MFASetup = ({
|
||||
</Heading>
|
||||
<Paragraph>{codeHelpText[method?.id]}</Paragraph>
|
||||
<Seperator />
|
||||
<Input
|
||||
<FormInput
|
||||
name="code"
|
||||
formRef={formRef}
|
||||
fwdRef={codeInputRef}
|
||||
placeholder="xxxxxx"
|
||||
maxLength={6}
|
||||
loading={loading}
|
||||
textAlign="center"
|
||||
keyboardType="numeric"
|
||||
onChangeText={(value) => (code.current = value)}
|
||||
onChangeText={() => {
|
||||
setGeneralError(undefined);
|
||||
}}
|
||||
onSubmitEditing={onNext}
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="done"
|
||||
validators={codeValidators}
|
||||
inputStyle={{
|
||||
fontSize: AppFontSize.lg,
|
||||
height: 60,
|
||||
@@ -392,7 +482,25 @@ export const MFASetup = ({
|
||||
borderWidth: 0,
|
||||
width: undefined
|
||||
}}
|
||||
errorStyle={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
/>
|
||||
|
||||
{generalError ? (
|
||||
<Paragraph
|
||||
size={AppFontSize.sm}
|
||||
style={{
|
||||
color: colors.error.icon,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
textAlign: "center",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
{generalError}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
<Seperator />
|
||||
<Button
|
||||
title={enabling ? null : strings.next()}
|
||||
@@ -517,7 +625,7 @@ export const MFARecoveryCodes = ({
|
||||
ToastManager.show({
|
||||
heading: strings.codesCopied(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
context: "local"
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
|
||||
@@ -16,15 +16,20 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { db } from "../../../common/database";
|
||||
import { eSendEvent, ToastManager } from "../../../services/event-manager";
|
||||
import { eUserLoggedIn } from "../../../utils/events";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import Input from "../../../components/ui/input";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import FormInput, {
|
||||
createFormRef,
|
||||
validators
|
||||
} from "../../../components/ui/input/form-input";
|
||||
import { eSendEvent, ToastManager } from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { eUserLoggedIn } from "../../../utils/events";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
|
||||
enum EmailChangeSteps {
|
||||
verify,
|
||||
@@ -32,14 +37,16 @@ enum EmailChangeSteps {
|
||||
}
|
||||
|
||||
export const ChangeEmail = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const [step, setStep] = useState(EmailChangeSteps.verify);
|
||||
const emailChangeData = useRef<{
|
||||
email?: string;
|
||||
password?: string;
|
||||
code?: string;
|
||||
}>({});
|
||||
const formRef = useRef(
|
||||
createFormRef({
|
||||
email: "",
|
||||
password: "",
|
||||
code: ""
|
||||
})
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const emailInputRef = useRef<TextInput>(null);
|
||||
const passInputRef = useRef<TextInput>(null);
|
||||
const codeInputRef = useRef<TextInput>(null);
|
||||
@@ -47,44 +54,52 @@ export const ChangeEmail = () => {
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
if (step === EmailChangeSteps.verify) {
|
||||
if (
|
||||
!emailChangeData.current.email ||
|
||||
!emailChangeData.current.password ||
|
||||
error
|
||||
)
|
||||
return;
|
||||
const hasEmailError = formRef.current.validateField("email");
|
||||
const hasPasswordError = formRef.current.validateField("password");
|
||||
if (hasEmailError || hasPasswordError) return;
|
||||
|
||||
const { email, password } = formRef.current.getValues();
|
||||
|
||||
setLoading(true);
|
||||
const verified = await db.user?.verifyPassword(
|
||||
emailChangeData.current.password
|
||||
);
|
||||
const verified = await db.user?.verifyPassword(password);
|
||||
if (!verified) throw new Error(strings.passwordIncorrect());
|
||||
await db.user?.sendVerificationEmail(emailChangeData.current.email);
|
||||
await db.user?.sendVerificationEmail(email);
|
||||
setStep(EmailChangeSteps.changeEmail);
|
||||
formRef.current.clearErrors();
|
||||
formRef.current.setValue("code", "");
|
||||
setLoading(false);
|
||||
} else {
|
||||
if (
|
||||
!emailChangeData.current.email ||
|
||||
!emailChangeData.current.password ||
|
||||
error ||
|
||||
!emailChangeData.current.code
|
||||
)
|
||||
return;
|
||||
await db.user?.changeEmail(
|
||||
emailChangeData.current.email,
|
||||
emailChangeData.current.password,
|
||||
emailChangeData.current.code
|
||||
);
|
||||
const hasCodeError = formRef.current.validateField("code");
|
||||
if (hasCodeError) return;
|
||||
|
||||
const { email, password, code } = formRef.current.getValues();
|
||||
|
||||
setLoading(true);
|
||||
await db.user?.changeEmail(email, password, code);
|
||||
eSendEvent(eUserLoggedIn);
|
||||
close?.();
|
||||
ToastManager.show({
|
||||
heading: strings.emailUpdated(emailChangeData.current.email),
|
||||
heading: strings.emailUpdated(email),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
Navigation.goBack();
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastManager.error(e as Error);
|
||||
const error = e as Error;
|
||||
|
||||
if (step === EmailChangeSteps.verify) {
|
||||
if (error.message === strings.passwordIncorrect()) {
|
||||
formRef.current.setError("password", error.message);
|
||||
} else {
|
||||
formRef.current.setError("email", error.message);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
formRef.current.setError("code", error.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -97,34 +112,54 @@ export const ChangeEmail = () => {
|
||||
>
|
||||
{step === EmailChangeSteps.verify ? (
|
||||
<>
|
||||
<Input
|
||||
<FormInput
|
||||
name="email"
|
||||
formRef={formRef}
|
||||
fwdRef={emailInputRef}
|
||||
placeholder={strings.enterNewEmail()}
|
||||
validationType="email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
onChangeText={(email) => {
|
||||
emailChangeData.current.email = email;
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
validators={[
|
||||
validators.required(strings.emailRequired()),
|
||||
validators.email(strings.enterValidEmail())
|
||||
]}
|
||||
onSubmitEditing={() => {
|
||||
passInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
<FormInput
|
||||
name="password"
|
||||
formRef={formRef}
|
||||
fwdRef={passInputRef}
|
||||
placeholder={strings.enterAccountPassword()}
|
||||
secureTextEntry
|
||||
onChangeText={(pass) => {
|
||||
emailChangeData.current.password = pass;
|
||||
}}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
autoComplete="password"
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
onSubmitEditing={onSubmit}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Input
|
||||
key="code-input"
|
||||
<FormInput
|
||||
name="code"
|
||||
formRef={formRef}
|
||||
fwdRef={codeInputRef}
|
||||
placeholder={strings.code()}
|
||||
defaultValue=""
|
||||
onChangeText={(code) => {
|
||||
emailChangeData.current.code = code;
|
||||
}}
|
||||
keyboardType="number-pad"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
maxLength={6}
|
||||
validators={[
|
||||
validators.required(strings.enterSixDigitCode()),
|
||||
(value: string) =>
|
||||
/^\d{6}$/.test(value.trim())
|
||||
? undefined
|
||||
: strings.enterSixDigitCode()
|
||||
]}
|
||||
onSubmitEditing={onSubmit}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -135,8 +170,8 @@ export const ChangeEmail = () => {
|
||||
loading
|
||||
? undefined
|
||||
: step === EmailChangeSteps.verify
|
||||
? strings.verify()
|
||||
: strings.changeEmail()
|
||||
? strings.verify()
|
||||
: strings.changeEmail()
|
||||
}
|
||||
type="accent"
|
||||
style={{
|
||||
|
||||
@@ -47,6 +47,14 @@ export async function verifyUser(
|
||||
negativeText: closeText || strings.cancel(),
|
||||
positivePress: async (value) => {
|
||||
try {
|
||||
if (!value || !value.trim()) {
|
||||
ToastManager.error(
|
||||
new Error(strings.passwordNotEntered()),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
const user = await db.user.getUser();
|
||||
let verified = !user ? true : await db.user.verifyPassword(value);
|
||||
if (verified) {
|
||||
@@ -95,6 +103,14 @@ export async function verifyUserWithApplock() {
|
||||
keyboardType: keyboardType,
|
||||
positivePress: async (value) => {
|
||||
try {
|
||||
if (!value || !value.trim()) {
|
||||
ToastManager.error(
|
||||
new Error(strings.passwordNotEntered()),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
const verified = await validateAppLockPassword(value);
|
||||
if (!verified) {
|
||||
ToastManager.show({
|
||||
|
||||
@@ -31,8 +31,8 @@ import dayjs from "dayjs";
|
||||
import React from "react";
|
||||
import { Appearance, Linking, Platform } from "react-native";
|
||||
import { getVersion } from "react-native-device-info";
|
||||
import { TextInput } from "react-native-gesture-handler";
|
||||
import * as RNIap from "react-native-iap";
|
||||
import ScreenGuardModule from "react-native-screenguard";
|
||||
import { DatabaseLogger, db } from "../../common/database";
|
||||
import filesystem from "../../common/filesystem";
|
||||
import { presentDialog } from "../../components/dialog/functions";
|
||||
@@ -42,43 +42,46 @@ import ExportNotesSheet from "../../components/sheets/export-notes";
|
||||
import { Issue } from "../../components/sheets/github/issue";
|
||||
import { Progress } from "../../components/sheets/progress";
|
||||
import { Update } from "../../components/sheets/update";
|
||||
import {
|
||||
createFormRef,
|
||||
validators
|
||||
} from "../../components/ui/input/form-input";
|
||||
import { VaultStatusType, useVaultStatus } from "../../hooks/use-vault-status";
|
||||
import { BackgroundSync } from "../../services/background-sync";
|
||||
import BackupService from "../../services/backup";
|
||||
import BiometricService from "../../services/biometrics";
|
||||
import {
|
||||
ToastManager,
|
||||
VaultRequestType,
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
openVault,
|
||||
presentSheet,
|
||||
VaultRequestType
|
||||
presentSheet
|
||||
} from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import Notifications from "../../services/notifications";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
import Sync from "../../services/sync";
|
||||
import { clearAllStores } from "../../stores";
|
||||
import { refreshAllStores } from "../../stores/create-db-collection-store";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { EDITOR_LINE_HEIGHT } from "../../utils/constants";
|
||||
import {
|
||||
eAfterSync,
|
||||
eCloseSheet,
|
||||
eOpenRecoveryKeyDialog
|
||||
} from "../../utils/events";
|
||||
import { NotesnookModule } from "../../utils/notesnook-module";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { resetTabStore } from "../editor/tiptap/use-tab-store";
|
||||
import { MFARecoveryCodes, MFASheet } from "./2fa";
|
||||
import { useDragState } from "./editor/state";
|
||||
import { verifyUser, verifyUserWithApplock } from "./functions";
|
||||
import { logoutUser } from "./logout";
|
||||
import { SettingSection } from "./types";
|
||||
import { getTimeLeft } from "./user-section";
|
||||
import { EDITOR_LINE_HEIGHT } from "../../utils/constants";
|
||||
import { MMKV } from "../../common/database/mmkv";
|
||||
import { resetTabStore } from "../editor/tiptap/use-tab-store";
|
||||
import { clearAllStores } from "../../stores";
|
||||
import { refreshAllStores } from "../../stores/create-db-collection-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
|
||||
export const settingsGroups: SettingSection[] = [
|
||||
@@ -235,18 +238,29 @@ export const settingsGroups: SettingSection[] = [
|
||||
presentDialog({
|
||||
title: strings.redeemGiftCode(),
|
||||
paragraph: strings.redeemGiftCodeDesc(),
|
||||
input: true,
|
||||
inputPlaceholder: strings.code(),
|
||||
positiveText: strings.redeem(),
|
||||
positivePress: async (value) => {
|
||||
db.subscriptions.redeemCode(value).catch((e) => {
|
||||
ToastManager.show({
|
||||
heading: "Error redeeming code",
|
||||
message: (e as Error).message,
|
||||
type: "error"
|
||||
});
|
||||
});
|
||||
}
|
||||
form: {
|
||||
formRef: createFormRef({
|
||||
code: ""
|
||||
}),
|
||||
items: [
|
||||
{
|
||||
name: "code",
|
||||
placeholder: strings.code(),
|
||||
ref: React.createRef<TextInput | null>(),
|
||||
validators: [validators.required(strings.giftCodeRequired())]
|
||||
}
|
||||
],
|
||||
onFormSubmit: async (form) => {
|
||||
try {
|
||||
await db.subscriptions.redeemCode(form.getValue("code"));
|
||||
return true;
|
||||
} catch (e) {
|
||||
form.setError("code", (e as Error).message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
positiveText: strings.redeem()
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -516,6 +530,14 @@ export const settingsGroups: SettingSection[] = [
|
||||
positiveText: strings.delete(),
|
||||
positivePress: async (value) => {
|
||||
try {
|
||||
if (!value || !value.trim()) {
|
||||
ToastManager.error(
|
||||
new Error(strings.passwordNotEntered()),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
const verified = await db.user?.verifyPassword(value);
|
||||
if (verified) {
|
||||
setTimeout(async () => {
|
||||
|
||||
@@ -29,6 +29,10 @@ import ImagePicker from "react-native-image-crop-picker";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import { presentDialog } from "../../components/dialog/functions";
|
||||
import {
|
||||
createFormRef,
|
||||
validators
|
||||
} from "../../components/ui/input/form-input";
|
||||
import { PlanLimits } from "../../components/sheets/plan-limits";
|
||||
import AppIcon from "../../components/ui/AppIcon";
|
||||
import { Button } from "../../components/ui/button";
|
||||
@@ -197,19 +201,34 @@ const SettingsUserSection = ({ item }) => {
|
||||
title: strings.setFullName(),
|
||||
paragraph: strings.setFullNameDesc(),
|
||||
positiveText: strings.save(),
|
||||
input: true,
|
||||
inputPlaceholder: strings.enterFullName(),
|
||||
defaultValue: userProfile?.fullName,
|
||||
positivePress: async (value) => {
|
||||
db.settings
|
||||
.setProfile({
|
||||
fullName: value
|
||||
})
|
||||
.then(async () => {
|
||||
form: {
|
||||
formRef: createFormRef({
|
||||
fullName: userProfile?.fullName || ""
|
||||
}),
|
||||
items: [
|
||||
{
|
||||
name: "fullName",
|
||||
placeholder: strings.enterFullName(),
|
||||
defaultValue: userProfile?.fullName,
|
||||
validators: [
|
||||
validators.required(strings.nameIsRequired())
|
||||
]
|
||||
}
|
||||
],
|
||||
onFormSubmit: async (form) => {
|
||||
try {
|
||||
await db.settings.setProfile({
|
||||
fullName: form.getValue("fullName")
|
||||
});
|
||||
useUserStore.setState({
|
||||
profile: db.settings.getProfile()
|
||||
});
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
form.setError("fullName", e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
} from "../stores/use-setting-store";
|
||||
import { NotesnookModule } from "../utils/notesnook-module";
|
||||
import { scale, updateSize } from "../utils/size";
|
||||
import { DatabaseLogger } from "../common/database";
|
||||
import { useUserStore } from "../stores/use-user-store";
|
||||
import ScreenGuardModule from "react-native-screenguard";
|
||||
ScreenGuardModule.initSettings();
|
||||
@@ -89,7 +88,6 @@ function migrateAppLock() {
|
||||
biometricsAuthEnabled: true
|
||||
});
|
||||
}
|
||||
DatabaseLogger.debug("App lock Migrated");
|
||||
}
|
||||
|
||||
function migrateSettings(settings: SettingStore["settings"]) {
|
||||
|
||||
@@ -65,6 +65,14 @@ export async function unlockVault({
|
||||
paragraph: paragraph,
|
||||
inputPlaceholder: strings.enterPassword(),
|
||||
positivePress: async (value) => {
|
||||
if (!value || !value.trim()) {
|
||||
ToastManager.error(
|
||||
new Error(strings.passwordNotEntered()),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
const unlocked = await db.vault.unlock(value);
|
||||
if (!unlocked) {
|
||||
ToastManager.show({
|
||||
|
||||
Binary file not shown.
@@ -2263,14 +2263,35 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- react-native-screenguard (1.0.0):
|
||||
- react-native-screenguard (2.0.0-beta5):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
- fmt
|
||||
- glog
|
||||
- hermes-engine
|
||||
- RCT-Folly
|
||||
- RCT-Folly/Fabric
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-Codegen
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-ImageManager
|
||||
- React-jsi
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-renderercss
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- SDWebImage (~> 5.11.1)
|
||||
- SDWebImage (~> 5.11)
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- react-native-share-extension (2.9.5):
|
||||
- React
|
||||
- react-native-sodium (1.6.8):
|
||||
@@ -3125,8 +3146,6 @@ PODS:
|
||||
- RNNotifee/NotifeeCore (= 7.4.12)
|
||||
- RNNotifee/NotifeeCore (7.4.12):
|
||||
- React-Core
|
||||
- RNPrivacySnapshot (1.0.0):
|
||||
- React-Core
|
||||
- RNReanimated (4.2.0):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
@@ -3602,7 +3621,6 @@ DEPENDENCIES:
|
||||
- RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`)
|
||||
- RNKeychain (from `../node_modules/react-native-keychain`)
|
||||
- "RNNotifee (from `../node_modules/@ammarahmed/notifee-react-native`)"
|
||||
- RNPrivacySnapshot (from `../node_modules/react-native-privacy-snapshot`)
|
||||
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
||||
- RNScreens (from `../node_modules/react-native-screens`)
|
||||
- RNSecureRandom (from `../node_modules/react-native-securerandom`)
|
||||
@@ -3870,8 +3888,6 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-keychain"
|
||||
RNNotifee:
|
||||
:path: "../node_modules/@ammarahmed/notifee-react-native"
|
||||
RNPrivacySnapshot:
|
||||
:path: "../node_modules/react-native-privacy-snapshot"
|
||||
RNReanimated:
|
||||
:path: "../node_modules/react-native-reanimated"
|
||||
RNScreens:
|
||||
@@ -3974,7 +3990,7 @@ SPEC CHECKSUMS:
|
||||
react-native-pdf: edc236298f13f1609e42d41e45b8b6ea88ed10f9
|
||||
react-native-quick-sqlite: 1ed8d3db1e22a8604d006be69f06053382e93bb0
|
||||
react-native-safe-area-context: c6e2edd1c1da07bdce287fa9d9e60c5f7b514616
|
||||
react-native-screenguard: 9fc3b4ad5b97783fc0832638fae0dce51272c661
|
||||
react-native-screenguard: 975d4612dce0c348b19b08a48aa23e9b68584d98
|
||||
react-native-share-extension: fdc6aaab51591a2d445df239c446aaa3a99658ec
|
||||
react-native-sodium: 066f76e46c9be13e9260521e3fa994937c4cdab4
|
||||
react-native-theme-switch-animation: 449d6db7a760f55740505e7403ae8061debc9a7e
|
||||
@@ -4027,7 +4043,6 @@ SPEC CHECKSUMS:
|
||||
RNImageCropPicker: 5fd4ceaead64d8c53c787e4e559004f97bc76df7
|
||||
RNKeychain: ffd0513e676445c637410b47249460cbf56bc9cb
|
||||
RNNotifee: dea82c9ec44684eeeac9da85deb5eeb8ebe5937f
|
||||
RNPrivacySnapshot: ccad3a548338c2f526bb7b1789af3fb0618b7d1d
|
||||
RNReanimated: f1868b36f4b2b52a0ed00062cfda69506f75eaee
|
||||
RNScreens: ffbb0296608eb3560de641a711bbdb663ed1f6b4
|
||||
RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc
|
||||
|
||||
@@ -231,15 +231,40 @@ export async function restoreBackupFile(backupFile: File) {
|
||||
}
|
||||
await db.initCollections();
|
||||
} else {
|
||||
const { createUnzipIterator } = await import(
|
||||
"../utils/streams/unzip-stream"
|
||||
);
|
||||
|
||||
let skipAttachments = false;
|
||||
if (!useUserStore.getState().isLoggedIn) {
|
||||
let hasAttachments = false;
|
||||
for await (const entry of createUnzipIterator(backupFile)) {
|
||||
if (
|
||||
entry.name.startsWith("attachments/") &&
|
||||
entry.name !== "attachments/.attachments_key"
|
||||
) {
|
||||
hasAttachments = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasAttachments) {
|
||||
const result = await ConfirmDialog.show({
|
||||
title: strings.loginToRestoreAttachments(),
|
||||
message: strings.loginToRestoreAttachmentsDesc(),
|
||||
positiveButtonText: strings.yes(),
|
||||
negativeButtonText: strings.no()
|
||||
});
|
||||
if (!result) return;
|
||||
skipAttachments = true;
|
||||
}
|
||||
}
|
||||
|
||||
const error = await TaskManager.startTask<Error | void>({
|
||||
title: strings.restoringBackup(),
|
||||
subtitle: strings.restoringBackupDesc(),
|
||||
type: "modal",
|
||||
action: async (report) => {
|
||||
const { createUnzipIterator } = await import(
|
||||
"../utils/streams/unzip-stream"
|
||||
);
|
||||
|
||||
let cachedPassword: string | undefined = undefined;
|
||||
let cachedKey: string | undefined = undefined;
|
||||
// const { read, totalFiles } = await Reader(backupFile);
|
||||
@@ -254,13 +279,13 @@ export async function restoreBackupFile(backupFile: File) {
|
||||
isValid = true;
|
||||
continue;
|
||||
}
|
||||
if (entry.name === "attachments/.attachments_key")
|
||||
if (!skipAttachments && entry.name === "attachments/.attachments_key")
|
||||
attachmentsKey = JSON.parse(await entry.text()) as
|
||||
| SerializedKey
|
||||
| Cipher<"base64">;
|
||||
else if (entry.name.startsWith("attachments/"))
|
||||
else if (!skipAttachments && entry.name.startsWith("attachments/"))
|
||||
attachments.push(entry);
|
||||
else entries.push(entry);
|
||||
else if (!entry.name.startsWith("attachments/")) entries.push(entry);
|
||||
}
|
||||
if (!isValid)
|
||||
console.warn(
|
||||
@@ -339,6 +364,8 @@ export async function restoreBackupFile(backupFile: File) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
showToast("error", `${strings.restoreFailed()}: ${error.message}`);
|
||||
} else {
|
||||
showToast("success", strings.backupRestored());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ export function DayPicker(props: DayPickerProps) {
|
||||
key={month.month + year}
|
||||
value={month.month}
|
||||
data-date={month.$date.toDateString()}
|
||||
disabled={month.disabled}
|
||||
>
|
||||
{month.month}
|
||||
</option>
|
||||
@@ -121,6 +122,7 @@ export function DayPicker(props: DayPickerProps) {
|
||||
key={year.year}
|
||||
value={year.year}
|
||||
data-date={year.$date.toDateString()}
|
||||
disabled={year.disabled}
|
||||
>
|
||||
{year.year}
|
||||
</option>
|
||||
|
||||
@@ -105,6 +105,7 @@ import ListItem from "../list-item";
|
||||
import { PublishDialog } from "../publish-view";
|
||||
import TimeAgo from "../time-ago";
|
||||
import { NoteExpiryDateDialog } from "../../dialogs/note-expiry-date-dialog";
|
||||
import { withFeatureCheck } from "../../common";
|
||||
|
||||
type NoteProps = NoteResolvedData & {
|
||||
item: NoteType;
|
||||
@@ -666,11 +667,11 @@ export const noteMenuItems: (
|
||||
title: strings.setExpiry(),
|
||||
icon: Destruct.path,
|
||||
premium: !features.expiringNotes.isAllowed,
|
||||
onClick: async () => {
|
||||
onClick: withFeatureCheck(features.expiringNotes, async () => {
|
||||
await NoteExpiryDateDialog.show({
|
||||
noteId: note.id
|
||||
});
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
|
||||
@@ -44,7 +44,7 @@ export const AddNotebookDialog = DialogManager.register(
|
||||
|
||||
const onSubmit = useCallback(async () => {
|
||||
if (!title.current.trim())
|
||||
return showToast("error", strings.allFieldsRequired());
|
||||
return showToast("error", strings.titleIsRequired());
|
||||
|
||||
const id = await db.notebooks.add({
|
||||
id: props.notebook?.id,
|
||||
|
||||
@@ -47,6 +47,7 @@ import { FlexScrollContainer } from "../../components/scroll-container";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
DropdownSettingComponent,
|
||||
Section,
|
||||
SectionGroup,
|
||||
SectionKeys,
|
||||
Setting,
|
||||
@@ -254,9 +255,15 @@ export const SettingsDialog = DialogManager.register(function SettingsDialog(
|
||||
overflow: "auto"
|
||||
}}
|
||||
>
|
||||
{activeSettings.map((group) => (
|
||||
<SettingsGroupComponent item={group} />
|
||||
))}
|
||||
{activeSettings.length > 0 ? (
|
||||
activeSettings.map((group) => (
|
||||
<SettingsGroupComponent item={group} />
|
||||
))
|
||||
) : (
|
||||
<Text variant="body" sx={{ color: "paragraph-secondary" }}>
|
||||
{strings.noResultsFound()}
|
||||
</Text>
|
||||
)}
|
||||
</FlexScrollContainer>
|
||||
</Flex>
|
||||
</Dialog>
|
||||
@@ -317,12 +324,17 @@ function SettingsSideBar(props: SettingsSideBarProps) {
|
||||
SettingsGroups.filter((g) => g.section === route)
|
||||
);
|
||||
|
||||
const groups: SettingsGroup[] = [];
|
||||
let groups: SettingsGroup[] = [];
|
||||
for (const group of SettingsGroups) {
|
||||
const section = findSection(group.section);
|
||||
if (section?.isHidden?.() || group.isHidden?.()) continue;
|
||||
|
||||
const isTitleMatch =
|
||||
typeof group.header === "string" &&
|
||||
group.header.toLowerCase().includes(query);
|
||||
const isSectionMatch = group.section.includes(query);
|
||||
const isSectionMatch = group.section
|
||||
.toLowerCase()
|
||||
.includes(query);
|
||||
|
||||
if (isTitleMatch || isSectionMatch) {
|
||||
groups.push(group);
|
||||
@@ -347,6 +359,18 @@ function SettingsSideBar(props: SettingsSideBarProps) {
|
||||
if (!settings.length) continue;
|
||||
groups.push({ ...group, settings });
|
||||
}
|
||||
const matchedSections = findSections(query);
|
||||
if (matchedSections.length > 0) {
|
||||
const matchedGroups = SettingsGroups.filter((g) =>
|
||||
matchedSections.some((s) => s.key === g.section)
|
||||
);
|
||||
// remove groups whose sections were matched to avoid duplicate
|
||||
// entries.
|
||||
groups = groups.filter(
|
||||
(g) => !matchedGroups.some((mg) => mg.section === g.section)
|
||||
);
|
||||
groups.push(...matchedGroups);
|
||||
}
|
||||
onNavigate(groups);
|
||||
}}
|
||||
/>
|
||||
@@ -717,3 +741,22 @@ function NumberInput({
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
function findSection(key: SectionKeys) {
|
||||
for (const group of sectionGroups) {
|
||||
const section = group.sections.find((s) => s.key === key);
|
||||
if (section) return section;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findSections(query: string) {
|
||||
const sections: Section[] = [];
|
||||
for (const group of sectionGroups) {
|
||||
for (const section of group.sections) {
|
||||
if (section.isHidden?.()) continue;
|
||||
if (section.title.toLowerCase().includes(query)) sections.push(section);
|
||||
}
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
} from "@notesnook/core";
|
||||
import { logger } from "../utils/logger";
|
||||
import { newQueue } from "@henrygd/queue";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export const ABYTES = 17;
|
||||
const CHUNK_SIZE = 512 * 1024;
|
||||
@@ -499,12 +500,20 @@ async function downloadFile(
|
||||
{ type: "download", hash: filename }
|
||||
);
|
||||
|
||||
const signedUrl = (
|
||||
await axios.get(url, {
|
||||
headers,
|
||||
responseType: "text"
|
||||
})
|
||||
).data;
|
||||
const signedUrlResponse = await axios
|
||||
.get(url, { headers, responseType: "text" })
|
||||
.catch((e) => {
|
||||
if (e.response?.status === 401) {
|
||||
showToast("error", strings.pleaseLoginToDownloadAttachments());
|
||||
return null;
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
if (!signedUrlResponse) {
|
||||
reportProgress(undefined, { type: "download", hash: filename });
|
||||
return false;
|
||||
}
|
||||
const signedUrl = signedUrlResponse.data;
|
||||
|
||||
logger.debug("Got attachment signed url", { filename });
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Login to restore attachments in backup.
|
||||
description: We require users to be logged in to restore attachments in backup.
|
||||
---
|
||||
|
||||
# Login to restore attachments in backup.
|
||||
|
||||
We require users to be logged in to restore attachments in backup. This is because attachments are encrypted using a sub-key derived from your database encryption key. Without a login, we cannot encrypt/upload/sync attachments.
|
||||
@@ -91,3 +91,4 @@ navigation:
|
||||
- path: faqs/what-are-merge-conflicts.md
|
||||
- path: faqs/is-there-an-eta.md
|
||||
- path: faqs/login-to-upload-attachments.md
|
||||
- path: faqs/login-to-restore-attachments-in-backup.md
|
||||
|
||||
3
fastlane/metadata/android/en-US/changelogs/15519.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/15519.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -564,18 +564,24 @@ class UserManager {
|
||||
|
||||
const email = newEmail.toLowerCase();
|
||||
|
||||
await http.patch(
|
||||
`${constants.AUTH_HOST}${ENDPOINTS.patchUser}`,
|
||||
{
|
||||
type: "change_email",
|
||||
new_email: newEmail,
|
||||
password: await this.db.storage().hash(password, email, {
|
||||
usesFallback: await this.usesFallbackPWHash(password)
|
||||
}),
|
||||
verification_code: code
|
||||
},
|
||||
token
|
||||
);
|
||||
try {
|
||||
await http.patch(
|
||||
`${constants.AUTH_HOST}${ENDPOINTS.patchUser}`,
|
||||
{
|
||||
type: "change_email",
|
||||
new_email: newEmail,
|
||||
password: await this.db.storage().hash(password, email, {
|
||||
usesFallback: await this.usesFallbackPWHash(password)
|
||||
}),
|
||||
verification_code: code
|
||||
},
|
||||
token
|
||||
);
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
if (error.message === "Invalid token.") throw new Error("Invalid code.");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
recoverAccount(email: string) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2026-04-20 11:33+0500\n"
|
||||
"POT-Creation-Date: 2026-05-11 12:10+0500\n"
|
||||
"POT-Creation-Date: 2026-05-11 12:10+0500\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
@@ -975,6 +976,10 @@ msgstr "Are you sure you want to clear all logs from {key}?"
|
||||
msgid "Are you sure you want to clear trash?"
|
||||
msgstr "Are you sure you want to clear trash?"
|
||||
|
||||
#: src/strings.ts:2734
|
||||
msgid "Are you sure you want to delete this attachment?"
|
||||
msgstr "Are you sure you want to delete this attachment?"
|
||||
|
||||
#: src/strings.ts:1544
|
||||
msgid "Are you sure you want to logout and clear all data stored on THIS DEVICE?"
|
||||
msgstr "Are you sure you want to logout and clear all data stored on THIS DEVICE?"
|
||||
@@ -1032,6 +1037,10 @@ msgstr "attachment"
|
||||
msgid "Attachment"
|
||||
msgstr "Attachment"
|
||||
|
||||
#: src/strings.ts:2735
|
||||
msgid "Attachment deleted"
|
||||
msgstr "Attachment deleted"
|
||||
|
||||
#: src/strings.ts:2460
|
||||
msgid "Attachment manager"
|
||||
msgstr "Attachment manager"
|
||||
@@ -1830,6 +1839,10 @@ msgstr "Confirm new password"
|
||||
msgid "Confirm password"
|
||||
msgstr "Confirm password"
|
||||
|
||||
#: src/strings.ts:2729
|
||||
msgid "Confirm password required"
|
||||
msgstr "Confirm password required"
|
||||
|
||||
#: src/strings.ts:1489
|
||||
msgid "Confirm pin"
|
||||
msgstr "Confirm pin"
|
||||
@@ -2040,10 +2053,15 @@ msgstr "Curate the toolbar that fits your needs and matches your personality."
|
||||
msgid "Current note"
|
||||
msgstr "Current note"
|
||||
|
||||
#: src/strings.ts:1481
|
||||
#: src/strings.ts:1487
|
||||
msgid "Current password"
|
||||
msgstr "Current password"
|
||||
|
||||
#: src/strings.ts:2738
|
||||
msgid "Current password required"
|
||||
msgstr "Current password required"
|
||||
|
||||
#: src/strings.ts:1231
|
||||
msgid "Current path: {path}"
|
||||
msgstr "Current path: {path}"
|
||||
@@ -2222,6 +2240,10 @@ msgstr "Delete"
|
||||
msgid "Delete account"
|
||||
msgstr "Delete account"
|
||||
|
||||
#: src/strings.ts:2732
|
||||
msgid "Delete attachment"
|
||||
msgstr "Delete attachment"
|
||||
|
||||
#: src/strings.ts:1321
|
||||
msgid "Delete collapsed section"
|
||||
msgstr "Delete collapsed section"
|
||||
@@ -3230,6 +3252,10 @@ msgstr "Getting information"
|
||||
msgid "Getting recovery codes"
|
||||
msgstr "Getting recovery codes"
|
||||
|
||||
#: src/strings.ts:2731
|
||||
msgid "Gift code required"
|
||||
msgstr "Gift code required"
|
||||
|
||||
#: src/strings.ts:2174
|
||||
msgid "GNU GENERAL PUBLIC LICENSE Version 3"
|
||||
msgstr "GNU GENERAL PUBLIC LICENSE Version 3"
|
||||
@@ -3937,6 +3963,10 @@ msgstr "Login failed"
|
||||
msgid "Login required"
|
||||
msgstr "Login required"
|
||||
|
||||
#: src/strings.ts:2739
|
||||
msgid "Login required to restore attachments"
|
||||
msgstr "Login required to restore attachments"
|
||||
|
||||
#: src/strings.ts:779
|
||||
msgid "Login successful"
|
||||
msgstr "Login successful"
|
||||
@@ -4187,6 +4217,10 @@ msgstr "Multi-layer encryption to most important notes"
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
|
||||
#: src/strings.ts:2737
|
||||
msgid "Name is required."
|
||||
msgstr "Name is required."
|
||||
|
||||
#: src/strings.ts:1690
|
||||
msgid "Native high-performance encryption"
|
||||
msgstr "Native high-performance encryption"
|
||||
@@ -4568,10 +4602,6 @@ msgstr "Okay"
|
||||
msgid "Old - new"
|
||||
msgstr "Old - new"
|
||||
|
||||
#: src/strings.ts:1481
|
||||
msgid "Old password"
|
||||
msgstr "Old password"
|
||||
|
||||
#: src/strings.ts:1837
|
||||
msgid "Older version"
|
||||
msgstr "Older version"
|
||||
@@ -4725,6 +4755,10 @@ msgstr "Password not entered"
|
||||
msgid "Password protection"
|
||||
msgstr "Password protection"
|
||||
|
||||
#: src/strings.ts:2728
|
||||
msgid "Password required"
|
||||
msgstr "Password required"
|
||||
|
||||
#: src/strings.ts:809
|
||||
msgid "Password updated"
|
||||
msgstr "Password updated"
|
||||
@@ -4836,6 +4870,7 @@ msgid "Please enter a key name"
|
||||
msgstr "Please enter a key name"
|
||||
|
||||
#: src/strings.ts:1514
|
||||
#: src/strings.ts:2730
|
||||
msgid "Please enter a valid email address"
|
||||
msgstr "Please enter a valid email address"
|
||||
|
||||
@@ -4899,6 +4934,10 @@ msgstr "Please fill all the fields to continue."
|
||||
msgid "Please grant notifications permission to add new reminders."
|
||||
msgstr "Please grant notifications permission to add new reminders."
|
||||
|
||||
#: src/strings.ts:2745
|
||||
msgid "Please login to download attachments."
|
||||
msgstr "Please login to download attachments."
|
||||
|
||||
#: src/strings.ts:873
|
||||
msgid "Please make sure you have saved the recovery key. Tap one more time to confirm."
|
||||
msgstr "Please make sure you have saved the recovery key. Tap one more time to confirm."
|
||||
@@ -6856,6 +6895,10 @@ msgstr "Title"
|
||||
msgid "Title format"
|
||||
msgstr "Title format"
|
||||
|
||||
#: src/strings.ts:2736
|
||||
msgid "Title is required"
|
||||
msgstr "Title is required"
|
||||
|
||||
#: src/strings.ts:1190
|
||||
msgid "To use app lock, you must enable biometrics such as Fingerprint lock or Face ID on your phone."
|
||||
msgstr "To use app lock, you must enable biometrics such as Fingerprint lock or Face ID on your phone."
|
||||
@@ -7681,6 +7724,16 @@ msgstr "You have unsynced notes. Take a backup or sync your notes to avoid losin
|
||||
msgid "You must log out in order to change/reset server URLs."
|
||||
msgstr "You must log out in order to change/reset server URLs."
|
||||
|
||||
#: src/strings.ts:2741
|
||||
msgid ""
|
||||
"You need to login to restore attachments from a backup file. [Read more](https://help.notesnook.com/faqs/login-to-restore-attachments-in-backup).\n"
|
||||
" \n"
|
||||
"Continue without attachments?"
|
||||
msgstr ""
|
||||
"You need to login to restore attachments from a backup file. [Read more](https://help.notesnook.com/faqs/login-to-restore-attachments-in-backup).\n"
|
||||
" \n"
|
||||
"Continue without attachments?"
|
||||
|
||||
#: src/strings.ts:836
|
||||
msgid "You simply cannot get any better of a note taking app than @notesnook. The UI is clean and slick, it is feature rich, encrypted, reasonably priced (esp. for students & educators) & open source"
|
||||
msgstr "You simply cannot get any better of a note taking app than @notesnook. The UI is clean and slick, it is feature rich, encrypted, reasonably priced (esp. for students & educators) & open source"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2026-04-20 11:33+0500\n"
|
||||
"POT-Creation-Date: 2026-05-11 12:10+0500\n"
|
||||
"POT-Creation-Date: 2026-05-11 12:10+0500\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
@@ -973,6 +974,10 @@ msgstr ""
|
||||
|
||||
#: src/strings.ts:1326
|
||||
msgid "Are you sure you want to clear trash?"
|
||||
msgstr "<<<<<<< HEAD"
|
||||
|
||||
#: src/strings.ts:2734
|
||||
msgid "Are you sure you want to delete this attachment?"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1544
|
||||
@@ -1030,6 +1035,10 @@ msgstr ""
|
||||
#: src/strings.ts:300
|
||||
#: src/strings.ts:2356
|
||||
msgid "Attachment"
|
||||
msgstr "<<<<<<< HEAD"
|
||||
|
||||
#: src/strings.ts:2735
|
||||
msgid "Attachment deleted"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2460
|
||||
@@ -1817,6 +1826,10 @@ msgstr ""
|
||||
|
||||
#: src/strings.ts:1485
|
||||
msgid "Confirm password"
|
||||
msgstr "<<<<<<< HEAD"
|
||||
|
||||
#: src/strings.ts:2729
|
||||
msgid "Confirm password required"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1489
|
||||
@@ -2027,12 +2040,17 @@ msgstr ""
|
||||
|
||||
#: src/strings.ts:1838
|
||||
msgid "Current note"
|
||||
msgstr ""
|
||||
msgstr "<<<<<<< HEAD"
|
||||
|
||||
#: src/strings.ts:1481
|
||||
#: src/strings.ts:1487
|
||||
msgid "Current password"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2738
|
||||
msgid "Current password required"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1231
|
||||
msgid "Current path: {path}"
|
||||
msgstr ""
|
||||
@@ -2209,6 +2227,10 @@ msgstr ""
|
||||
|
||||
#: src/strings.ts:1078
|
||||
msgid "Delete account"
|
||||
msgstr "<<<<<<< HEAD"
|
||||
|
||||
#: src/strings.ts:2732
|
||||
msgid "Delete attachment"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1321
|
||||
@@ -3210,7 +3232,11 @@ msgstr ""
|
||||
|
||||
#: src/strings.ts:400
|
||||
msgid "Getting recovery codes"
|
||||
msgstr "<<<<<<< HEAD"
|
||||
msgstr "<<<<<<< HEAD<<<<<<< HEAD"
|
||||
|
||||
#: src/strings.ts:2731
|
||||
msgid "Gift code required"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2174
|
||||
msgid "GNU GENERAL PUBLIC LICENSE Version 3"
|
||||
@@ -3917,6 +3943,10 @@ msgstr ""
|
||||
msgid "Login required"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2739
|
||||
msgid "Login required to restore attachments"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:779
|
||||
msgid "Login successful"
|
||||
msgstr ""
|
||||
@@ -4165,6 +4195,10 @@ msgstr ""
|
||||
|
||||
#: src/strings.ts:887
|
||||
msgid "Name"
|
||||
msgstr "<<<<<<< HEAD"
|
||||
|
||||
#: src/strings.ts:2737
|
||||
msgid "Name is required."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1690
|
||||
@@ -4540,11 +4574,7 @@ msgstr ""
|
||||
|
||||
#: src/strings.ts:641
|
||||
msgid "Old - new"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1481
|
||||
msgid "Old password"
|
||||
msgstr ""
|
||||
msgstr "<<<<<<< HEAD"
|
||||
|
||||
#: src/strings.ts:1837
|
||||
msgid "Older version"
|
||||
@@ -4699,6 +4729,10 @@ msgstr ""
|
||||
msgid "Password protection"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2728
|
||||
msgid "Password required"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:809
|
||||
msgid "Password updated"
|
||||
msgstr "<<<<<<< HEAD"
|
||||
@@ -4807,9 +4841,10 @@ msgstr "<<<<<<< HEAD"
|
||||
|
||||
#: src/strings.ts:2674
|
||||
msgid "Please enter a key name"
|
||||
msgstr ""
|
||||
msgstr "<<<<<<< HEAD"
|
||||
|
||||
#: src/strings.ts:1514
|
||||
#: src/strings.ts:2730
|
||||
msgid "Please enter a valid email address"
|
||||
msgstr ""
|
||||
|
||||
@@ -4873,6 +4908,10 @@ msgstr ""
|
||||
msgid "Please grant notifications permission to add new reminders."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2745
|
||||
msgid "Please login to download attachments."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:873
|
||||
msgid "Please make sure you have saved the recovery key. Tap one more time to confirm."
|
||||
msgstr ""
|
||||
@@ -6813,6 +6852,10 @@ msgstr ""
|
||||
|
||||
#: src/strings.ts:1159
|
||||
msgid "Title format"
|
||||
msgstr "<<<<<<< HEAD"
|
||||
|
||||
#: src/strings.ts:2736
|
||||
msgid "Title is required"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1190
|
||||
@@ -7623,6 +7666,13 @@ msgstr ""
|
||||
msgid "You must log out in order to change/reset server URLs."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2741
|
||||
msgid ""
|
||||
"You need to login to restore attachments from a backup file. [Read more](https://help.notesnook.com/faqs/login-to-restore-attachments-in-backup).\n"
|
||||
" \n"
|
||||
"Continue without attachments?"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:836
|
||||
msgid "You simply cannot get any better of a note taking app than @notesnook. The UI is clean and slick, it is feature rich, encrypted, reasonably priced (esp. for students & educators) & open source"
|
||||
msgstr ""
|
||||
@@ -7826,7 +7876,7 @@ msgstr "<<<<<<< HEAD"
|
||||
|
||||
#: src/strings.ts:2055
|
||||
msgid "Your support request has been forwarded"
|
||||
msgstr ""
|
||||
msgstr "<<<<<<< HEAD"
|
||||
|
||||
#: src/strings.ts:2064
|
||||
msgid "Your support request has been forwarded to our support team. We will get back to you via email as soon as possible. If you don't receive an email from us within 24-48 hours, please send us an email directly at support@notesnook.com."
|
||||
|
||||
@@ -1478,7 +1478,7 @@ $day$: Current day (eg. Monday)`,
|
||||
someNotesPublished: () => t`Some notes are published`,
|
||||
unpublishToDelete: () => t`Unpublish notes to delete them`,
|
||||
filterAttachments: () => t`Filter attachments by filename, type or hash`,
|
||||
oldPassword: () => t`Old password`,
|
||||
oldPassword: () => t`Current password`,
|
||||
newPassword: () => t`New password`,
|
||||
email: () => t`Email`,
|
||||
emailInvalid: () => t`Invalid email`,
|
||||
@@ -2724,5 +2724,23 @@ Use this if changes from other devices are not appearing on this device. This wi
|
||||
valueMustBeBetween: (min: number, max: number) =>
|
||||
t`Value must be between ${min} and ${max}`,
|
||||
pgpPrivateKeyProtected: () =>
|
||||
t`Private key is passphrase-protected. Please provide the decrypted key or a key without a passphrase.`
|
||||
t`Private key is passphrase-protected. Please provide the decrypted key or a key without a passphrase.`,
|
||||
passwordRequired: () => t`Password required`,
|
||||
confirmPasswordRequired: () => t`Confirm password required`,
|
||||
enterAValidEmailAddress: () => t`Please enter a valid email address`,
|
||||
giftCodeRequired: () => t`Gift code required`,
|
||||
deleteAttachment: () => t`Delete attachment`,
|
||||
deleteAttachmentConfirm: () =>
|
||||
t`Are you sure you want to delete this attachment?`,
|
||||
attachmentDeleted: () => t`Attachment deleted`,
|
||||
titleIsRequired: () => t`Title is required`,
|
||||
nameIsRequired: () => t`Name is required.`,
|
||||
currentPasswordRequired: () => t`Current password required`,
|
||||
loginToRestoreAttachments: () => t`Login required to restore attachments`,
|
||||
loginToRestoreAttachmentsDesc: () =>
|
||||
t`You need to login to restore attachments from a backup file. [Read more](https://help.notesnook.com/faqs/login-to-restore-attachments-in-backup).
|
||||
|
||||
Continue without attachments?`,
|
||||
pleaseLoginToDownloadAttachments: () =>
|
||||
t`Please login to download attachments.`
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user