mobile: migrate app lock

This commit is contained in:
Ammar Ahmed
2024-01-17 12:23:34 +05:00
parent dd03f1321f
commit ceeb6f271e
17 changed files with 689 additions and 1733 deletions

View File

@@ -53,10 +53,11 @@ import { themeTrpcClient } from "./screens/settings/theme-selector";
const App = () => {
const init = useAppEvents();
useEffect(() => {
const { appLockMode } = SettingsService.get();
if (appLockMode && appLockMode !== "none") {
const { appLockEnabled, appLockMode } = SettingsService.get();
if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
init();

View File

@@ -87,7 +87,8 @@ const BaseDialog = ({
useEffect(() => {
if (useUserStore.getState().disableAppLockRequests) return;
if (SettingsService.get().appLockMode === "background") {
if (SettingsService.canLockAppInBackground()) {
if (appState === "background") {
setIntervalVisible(false);
if (useUserStore.getState().appLocked) {

View File

@@ -42,6 +42,7 @@ type DialogInfo = {
inputPlaceholder: string;
defaultValue: string;
context: "global" | "local";
secureTextEntry?: boolean;
};
export function presentDialog(data: Partial<DialogInfo>): void {

View File

@@ -36,6 +36,10 @@ import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import Navigation from "../../services/navigation";
import { eOpenLoginDialog } from "../../utils/events";
import { AuthMode } from "../auth";
import { eSendEvent } from "../../services/event-manager";
const Intro = ({ navigation }) => {
const { colors } = useThemeColors();
@@ -193,9 +197,15 @@ const Intro = ({ navigation }) => {
<Button
width={250}
onPress={async () => {
navigation.navigate("AppLock", {
welcome: true
});
eSendEvent(eOpenLoginDialog, AuthMode.welcomeSignup);
setTimeout(() => {
SettingsService.set({
introCompleted: true
});
Navigation.replace("Notes", {
canGoBack: false
});
}, 1000);
}}
style={{
paddingHorizontal: 24,

View File

@@ -91,7 +91,7 @@ const SheetWrapper = ({
useEffect(() => {
if (useUserStore.getState().disableAppLockRequests) return;
if (SettingsService.get().appLockMode === "background") {
if (SettingsService.canLockAppInBackground()) {
if (appState === "background") {
const ref = fwdRef || localRef;
ref?.current?.hide();

View File

@@ -480,15 +480,20 @@ export const useAppEvents = () => {
const onAppStateChanged = async (state: AppStateStatus) => {
if (state === "active") {
if (SettingsService.shouldLockAppOnEnterForeground()) {
console.log("Locked app on enter foreground");
useUserStore.getState().lockApp(true);
}
notifee.setBadgeCount(0);
updateStatusBarColor();
if (
SettingsService.get().appLockMode !== "background" &&
!SettingsService.canLockAppInBackground() &&
!SettingsService.get().privacyScreen
) {
enabled(false);
}
if (SettingsService.get().appLockMode === "background") {
if (SettingsService.canLockAppInBackground()) {
if (useSettingStore.getState().requestBiometrics) {
useSettingStore.getState().setRequestBiometrics(false);
return;
@@ -510,22 +515,23 @@ export const useAppEvents = () => {
}
}
} else {
SettingsService.appEnteredBackground();
const id = useEditorStore.getState().currentEditingNote;
const note = id ? await db.notes.note(id) : undefined;
if (
note?.locked &&
SettingsService.get().appLockMode === "background"
) {
if (note?.locked && SettingsService.canLockAppInBackground()) {
eSendEvent(eClearEditor);
}
await saveEditorState();
if (
SettingsService.get().appLockMode === "background" &&
SettingsService.canLockAppInBackground() &&
!useSettingStore.getState().requestBiometrics &&
!useUserStore.getState().appLocked &&
!useUserStore.getState().disableAppLockRequests
) {
useUserStore.getState().lockApp(true);
if (SettingsService.getProperty("appLockTimer") === 0) {
useUserStore.getState().lockApp(true);
}
if (Platform.OS === "ios") {
editorController.current?.commands.blur();
Keyboard.dismiss();
@@ -533,7 +539,7 @@ export const useAppEvents = () => {
}
if (
SettingsService.get().privacyScreen ||
SettingsService.get().appLockMode === "background"
SettingsService.canLockAppInBackground()
) {
!useSettingStore.getState().requestBiometrics ? enabled(true) : null;
}

View File

@@ -69,7 +69,6 @@ const IntroStackNavigator = () => {
initialRouteName={"Intro"}
>
<NativeStack.Screen name="Intro" component={Intro} />
<NativeStack.Screen name="AppLock" component={AppLock} />
</IntroStack.Navigator>
);
};

View File

@@ -17,36 +17,24 @@ 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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Platform, ScrollView, View } from "react-native";
import Animated, { FadeInDown, FadeOutUp } from "react-native-reanimated";
import Animated from "react-native-reanimated";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { AuthMode } from "../../components/auth";
import { Button } from "../../components/ui/button";
import { PressableButton } from "../../components/ui/pressable";
import Seperator from "../../components/ui/seperator";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import BiometicService from "../../services/biometrics";
import { DDS } from "../../services/device-detection";
import {
ToastManager,
eSendEvent,
presentSheet
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { ToastManager, presentSheet } from "../../services/event-manager";
import SettingsService from "../../services/settings";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { useUserStore } from "../../stores/use-user-store";
import { getElevationStyle } from "../../utils/elevation";
import { eOpenLoginDialog } from "../../utils/events";
import { SIZE } from "../../utils/size";
const AppLock = ({ route }) => {
const AppLock = () => {
const { colors } = useThemeColors();
const appLockMode = useSettingStore((state) => state.settings.appLockMode);
const welcome = route?.params?.welcome;
const deviceMode = useSettingStore((state) => state.deviceMode);
const modes = [
{
@@ -76,128 +64,51 @@ const AppLock = ({ route }) => {
return (
<>
<Animated.View
exiting={!welcome ? undefined : FadeOutUp}
entering={!welcome ? undefined : FadeInDown}
style={{
height: !welcome ? undefined : "100%",
width: !welcome ? undefined : "100%"
}}
>
<Animated.View>
<>
{!welcome ? (
<View
style={{
flexDirection: "row",
alignItems: "flex-end",
justifyContent: "space-between",
width: "95%",
paddingVertical: 12,
paddingHorizontal: 0,
alignSelf: "center",
minHeight: 125,
borderBottomWidth: 1,
borderBottomColor: colors.secondary.background
}}
>
<Icon
name="shield-lock"
color={colors.secondary.icon}
size={100}
style={{
position: "absolute",
right: 0,
top: 6,
opacity: 0.2
}}
/>
<View
style={{
flexDirection: "row",
alignItems: "flex-end",
justifyContent: "space-between",
width: DDS.isTab && welcome ? "50%" : "95%",
paddingVertical: 12,
paddingHorizontal: 0,
alignSelf: "center",
minHeight: 125,
borderBottomWidth: 1,
borderBottomColor: welcome
? "transparent"
: colors.secondary.background
width: "100%"
}}
>
<Icon
name="shield-lock"
color={colors.secondary.icon}
size={100}
style={{
position: "absolute",
right: 0,
top: 6,
opacity: 0.2
}}
/>
<View
style={{
alignItems: !welcome ? undefined : "center",
width: "100%"
}}
>
<Heading>Protect your notes</Heading>
<Paragraph
style={{
textAlign: !welcome ? undefined : "center"
}}
size={SIZE.md}
>
Choose how you want to secure your notes locally.
</Paragraph>
</View>
</View>
) : (
<View
style={{
flexGrow: 1,
justifyContent: "flex-end",
paddingHorizontal: 20,
backgroundColor: colors.secondary.background,
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
borderWidth: deviceMode !== "mobile" ? 1 : null,
borderColor:
deviceMode !== "mobile" ? colors.primary.border : null,
borderRadius: deviceMode !== "mobile" ? 20 : null,
marginTop: deviceMode !== "mobile" ? 50 : null,
width: deviceMode === "mobile" ? null : "50%",
minHeight: 180
}}
>
<View
style={{
flexDirection: "row"
}}
>
<View
style={{
width: 100,
height: 5,
backgroundColor: colors.primary.accent,
borderRadius: 2,
marginRight: 7
}}
/>
<View
style={{
width: 20,
height: 5,
backgroundColor: colors.secondary.background,
borderRadius: 2
}}
/>
</View>
<Heading
style={{
marginTop: 10
}}
extraBold
size={SIZE.xxl}
>
Protect your notes
</Heading>
<Paragraph
style={{
marginBottom: 25
}}
>
<Heading>Protect your notes</Heading>
<Paragraph size={SIZE.md}>
Choose how you want to secure your notes locally.
</Paragraph>
</View>
)}
</View>
<Seperator />
<ScrollView
style={{
paddingHorizontal: 12,
width: DDS.isTab && welcome ? "50%" : "100%",
width: "100%",
alignSelf: "center",
flexGrow: 1
}}
@@ -284,34 +195,6 @@ const AppLock = ({ route }) => {
</Paragraph>
</PressableButton>
))}
{welcome && (
<Button
fontSize={SIZE.md}
width={250}
onPress={async () => {
eSendEvent(eOpenLoginDialog, AuthMode.welcomeSignup);
setTimeout(() => {
SettingsService.set({
introCompleted: true
});
Navigation.replace("Notes", {
canGoBack: false
});
}, 1000);
}}
style={{
paddingHorizontal: 24,
alignSelf: "center",
...getElevationStyle(5),
marginTop: 30,
borderRadius: 100,
marginBottom: 30
}}
type="accent"
title="Next"
/>
)}
</ScrollView>
</>
</Animated.View>
@@ -319,10 +202,9 @@ const AppLock = ({ route }) => {
);
};
AppLock.present = async (isWelcome) => {
AppLock.present = async () => {
presentSheet({
component: <AppLock welcome={isWelcome} s={0} />,
disableClosing: isWelcome
component: <AppLock />
});
};

View File

@@ -31,7 +31,8 @@ import {
FontPicker,
TimeFormatPicker,
TrashIntervalPicker,
BackupReminderPicker
BackupReminderPicker,
ApplockTimerPicker
} from "./picker/pickers";
import ThemeSelector from "./theme-selector";
@@ -49,5 +50,6 @@ export const components: { [name: string]: ReactElement } = {
"title-format": <TitleFormat />,
"date-format-selector": <DateFormatPicker />,
"time-format-selector": <TimeFormatPicker />,
"theme-selector": <ThemeSelector />
"theme-selector": <ThemeSelector />,
"applock-timer": <ApplockTimerPicker />
};

View File

@@ -138,3 +138,23 @@ export const BackupReminderPicker = createSettingsPicker({
return item !== "useroff";
}
});
export const ApplockTimerPicker = createSettingsPicker({
getValue: () => useSettingStore.getState().settings.appLockTimer,
updateValue: (item) => {
SettingsService.set({ appLockTimer: item });
},
formatValue: (item) => {
return item === -1
? "Never"
: item === 0 || item === undefined
? "Immediately"
: item === 1
? "1 minute"
: item + " minutes";
},
getItemKey: (item) => item.toString(),
options: [-1, 0, 1, 5, 15, 30],
compareValue: (current, item) => current === item,
premium: true
});

View File

@@ -69,6 +69,7 @@ import { verifyUser } from "./functions";
import { SettingSection } from "./types";
import { getTimeLeft } from "./user-section";
import { AppLockPassword } from "../../components/dialogs/applock-password";
import { validateAppLockPassword } from "../../common/database/encryption";
type User = any;
export const settingsGroups: SettingSection[] = [
@@ -767,14 +768,105 @@ export const settingsGroups: SettingSection[] = [
sections: [
{
id: "app-lock-mode",
name: "App lock mode",
name: "Enable app lock",
description:
"Select the mode for the desired level of app lock security.",
icon: "fingerprint",
modifer: () => {
AppLock.present();
icon: "lock",
type: "switch",
property: "appLockEnabled",
onChange: async () => {
if (!SettingsService.getProperty("appLockEnabled")) {
if (SettingsService.getProperty("appLockHasPasswordSecurity")) {
presentDialog({
title: "Verify it's you",
input: true,
inputPlaceholder: "Enter app lock pin",
paragraph:
"Please enter your app lock pin to disable app lock",
positiveText: "Disable",
secureTextEntry: true,
negativeText: "Cancel",
positivePress: async (value) => {
try {
let verified = await validateAppLockPassword(value);
if (!verified) {
SettingsService.setProperty("appLockEnabled", true);
return false;
} else {
SettingsService.setProperty("appLockEnabled", false);
}
} catch (e) {
SettingsService.setProperty("appLockEnabled", true);
return false;
}
}
});
} else {
if (await BiometicService.isBiometryAvailable()) {
const verified = await BiometicService.validateUser(
"Verify it's you"
);
if (!verified) {
SettingsService.setProperty("appLockEnabled", true);
return;
} else {
SettingsService.setProperty("appLockEnabled", false);
}
} else if (useUserStore.getState().user) {
let verified = false;
verifyUser(
null,
() => {
SettingsService.setProperty("appLockEnabled", false);
verified = true;
},
false,
() => {
if (!verified)
SettingsService.setProperty("appLockEnabled", true);
}
);
}
}
return;
}
if (
!(await BiometicService.isBiometryAvailable()) &&
!useUserStore.getState().user &&
!SettingsService.getProperty("appLockHasPasswordSecurity")
) {
ToastManager.show({
heading: "Biometrics not enrolled",
type: "error",
message:
"To use app lock, you must enable biometrics such as Fingerprint lock or Face ID on your phone or create an account."
});
SettingsService.setProperty("appLockEnabled", false);
return;
}
if (!SettingsService.getProperty("appLockHasPasswordSecurity")) {
const verified = await BiometicService.validateUser(
"Verify it's you"
);
if (verified) {
SettingsService.setProperty("biometricsAuthEnabled", true);
} else {
SettingsService.setProperty("appLockEnabled", false);
return;
}
}
}
},
{
id: "app-lock-timer",
name: "App lock timeout",
description:
"Set the time after which the app should lock in background",
type: "component",
component: "applock-timer"
},
{
id: "app-lock-pin",
name: "Setup app lock pin",
@@ -836,7 +928,8 @@ export const settingsGroups: SettingSection[] = [
);
SettingsService.setProperty("biometricsAuthEnabled", false);
}
}
},
icon: "fingerprint"
}
]
}

View File

@@ -27,6 +27,7 @@ import {
} from "../stores/use-setting-store";
import { NotesnookModule } from "../utils/notesnook-module";
import { scale, updateSize } from "../utils/size";
import { DatabaseLogger } from "../common/database";
function reset() {
const settings = get();
if (settings.reminder !== "off" && settings.reminder !== "useroff") {
@@ -44,6 +45,25 @@ function resetSettings() {
init();
}
function migrateAppLock() {
const appLockMode = get().appLockMode;
if (appLockMode === "none") return;
if (appLockMode === "background") {
set({
appLockEnabled: true,
appLockTimer: 0,
appLockMode: "none"
});
} else if (appLockMode === "launch") {
set({
appLockEnabled: true,
appLockTimer: -1,
appLockMode: "none"
});
}
DatabaseLogger.debug("App lock Migrated");
}
function init() {
scale.fontScale = 1;
const settingsJson = MMKV.getString("appSettings");
@@ -56,17 +76,17 @@ function init() {
...JSON.parse(settingsJson)
};
}
if (settings.fontScale) {
scale.fontScale = settings.fontScale;
}
setTimeout(() => setPrivacyScreen(settings), 1);
updateSize();
useSettingStore.getState().setSettings({ ...settings });
migrateAppLock();
}
function setPrivacyScreen(settings: SettingStore["settings"]) {
if (settings.privacyScreen || settings.appLockMode === "background") {
if (settings.privacyScreen || canLockAppInBackground()) {
if (Platform.OS === "android") {
NotesnookModule.setSecureMode(true);
} else {
@@ -153,6 +173,27 @@ function checkOrientation() {
//});
}
function canLockAppInBackground() {
return get().appLockEnabled && get().appLockTimer !== -1;
}
let backgroundEnterTime = 0;
function appEnteredBackground() {
if (canLockAppInBackground()) {
backgroundEnterTime = Date.now();
}
}
function shouldLockAppOnEnterForeground() {
const settings = get();
if (!settings.appLockEnabled) return false;
if (settings.appLockTimer === -1) return false;
if (settings.appLockTimer === 0) return true;
const time = Date.now();
const diff = time - backgroundEnterTime;
return diff > settings.appLockTimer * 60000;
}
export const SettingsService = {
init,
set,
@@ -163,7 +204,10 @@ export const SettingsService = {
reset,
getProperty,
setProperty,
resetSettings
resetSettings,
shouldLockAppOnEnterForeground,
canLockAppInBackground,
appEnteredBackground
};
init();

View File

@@ -39,7 +39,9 @@ export type Settings = {
sortOrder?: string;
screenshotMode?: boolean;
privacyScreen?: boolean;
appLockMode?: string;
appLockTimer: number;
appLockEnabled?: boolean;
appLockMode?: "none" | "background" | "launch";
telemetry?: boolean;
notebooksListMode?: "normal" | "compact";
notesListMode?: "normal" | "compact";
@@ -118,6 +120,7 @@ export interface SettingStore extends State {
const { width, height } = Dimensions.get("window");
export const defaultSettings: SettingStore["settings"] = {
appLockTimer: 0,
showToolbarOnTop: false,
showKeyboardOnOpen: false,
fontScale: 1,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4055,7 +4055,8 @@
"ws": {
"version": "7.5.9",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q=="
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
"requires": {}
}
}
},
@@ -6310,7 +6311,8 @@
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"dev": true
"dev": true,
"requires": {}
},
"xml-name-validator": {
"version": "4.0.0",

View File

@@ -4358,7 +4358,7 @@
"version": "15.7.11",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
"dev": true
"devOptional": true
},
"node_modules/@types/q": {
"version": "1.5.8",
@@ -4382,7 +4382,7 @@
"version": "18.2.39",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz",
"integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==",
"dev": true,
"devOptional": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -4417,7 +4417,7 @@
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
"dev": true
"devOptional": true
},
"node_modules/@types/semver": {
"version": "7.5.6",
@@ -9713,7 +9713,7 @@
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
"dev": true,
"devOptional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -17758,6 +17758,20 @@
"is-typedarray": "^1.0.0"
}
},
"node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/unbox-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",