migrate settings to new ui

This commit is contained in:
ammarahm-ed
2022-04-07 04:20:13 +05:00
parent 6ad63e2d34
commit 96d0ea5973
10 changed files with 890 additions and 251 deletions

View File

@@ -151,7 +151,7 @@ type MFAStepProps = {
method?: MFAMethod;
isSetup?: boolean;
};
const MFAMethodsPickerStep = ({ recovery, onSuccess }: MFAStepProps) => {
export const MFAMethodsPickerStep = ({ recovery, onSuccess }: MFAStepProps) => {
const colors = useThemeStore(state => state.colors);
const user = useUserStore(state => state.user);
@@ -209,7 +209,7 @@ const MFAMethodsPickerStep = ({ recovery, onSuccess }: MFAStepProps) => {
);
};
const MFASetup = ({ method, onSuccess, setStep, recovery }: MFAStepProps) => {
export const MFASetup = ({ method, onSuccess, setStep, recovery }: MFAStepProps) => {
const colors = useThemeStore(state => state.colors);
const user = useUserStore(state => state.user);
const [authenticatorDetails, setAuthenticatorDetails] = useState({
@@ -437,7 +437,7 @@ const MFASetup = ({ method, onSuccess, setStep, recovery }: MFAStepProps) => {
);
};
const MFARecoveryCodes = ({ method, recovery, onSuccess, isSetup = true }: MFAStepProps) => {
export const MFARecoveryCodes = ({ method, recovery, onSuccess, isSetup = true }: MFAStepProps) => {
const colors = useThemeStore(state => state.colors);
const [codes, setCodes] = useState([]);
const [loading, setLoading] = useState(true);
@@ -647,7 +647,7 @@ const MFASuccess = ({ recovery }: MFAStepProps) => {
);
};
const MFASheet = ({ recovery }: { recovery?: boolean }) => {
export const MFASheet = ({ recovery }: { recovery?: boolean }) => {
const [step, setStep] = useState<MFAStep>({
id: 'mfapick',
props: {

View File

@@ -212,6 +212,82 @@ const SettingsAppearanceSection = () => {
);
};
export const HomagePageSelector = () => {
const colors = useThemeStore(state => state.colors);
const settings = useSettingStore(state => state.settings);
const menuRef = useRef();
const [width, setWidth] = useState(0);
return (
<View
onLayout={event => {
setWidth(event.nativeEvent.layout.width);
}}
style={{
width: '100%'
}}
>
<Menu
ref={menuRef}
animationDuration={200}
style={{
borderRadius: 5,
backgroundColor: colors.bg,
width: width,
marginTop: 60
}}
button={
<PressableButton
onPress={async () => {
await PremiumService.verify(menuRef.current?.show);
}}
type="grayBg"
customStyle={{
flexDirection: 'row',
alignItems: 'center',
marginTop: 10,
width: '100%',
justifyContent: 'space-between',
padding: 12
}}
>
<Paragraph>{settings.homepage}</Paragraph>
<Icon color={colors.icon} name="menu-down" size={SIZE.md} />
</PressableButton>
}
>
{MenuItemsList.slice(0, MenuItemsList.length - 1).map(
item =>
item.name !== 'Monographs' && (
<MenuItem
key={item.name}
onPress={async () => {
menuRef.current?.hide();
await SettingsService.set({ homepage: item.name });
ToastEvent.show({
heading: 'Homepage set to ' + item.name,
message: 'Restart the app for changes to take effect.',
type: 'success'
});
}}
style={{
backgroundColor: settings.homepage === item.name ? colors.nav : 'transparent',
width: '100%',
maxWidth: width
}}
textStyle={{
fontSize: SIZE.md,
color: settings.homepage === item.name ? colors.accent : colors.pri
}}
>
{item.name}
</MenuItem>
)
)}
</Menu>
</View>
);
};
export const AccentColorPicker = ({ settings = true, wrap = false }) => {
const colors = useThemeStore(state => state.colors);
function changeAccentColor(color) {

View File

@@ -1,17 +1,16 @@
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import { Platform, TouchableOpacity, View } from 'react-native';
import ToggleSwitch from 'toggle-switch-react-native';
import Seperator from '../../components/ui/seperator';
import Paragraph from '../../components/ui/typography/paragraph';
import { useThemeStore } from '../../stores/theme';
import { useSettingStore, useUserStore } from '../../stores/stores';
import Backup from '../../services/backup';
import { eSendEvent, presentSheet, ToastEvent } from '../../services/event-manager';
import PremiumService from '../../services/premium';
import SettingsService from '../../services/settings';
import { useSettingStore, useUserStore } from '../../stores/stores';
import { useThemeStore } from '../../stores/theme';
import { eCloseProgressDialog, eOpenLoginDialog, eOpenRestoreDialog } from '../../utils/events';
import { openLinkInBrowser } from '../../utils/functions';
import { MMKV } from '../../utils/database/mmkv';
import { SIZE } from '../../utils/size';
import { sleep } from '../../utils/time';
import { CustomButton } from './button';
@@ -137,12 +136,6 @@ const SettingsBackupAndRestore = ({ isSheet }) => {
await SettingsService.set({ encryptedBackup: !settings.encryptedBackup });
};
const updateAskForBackup = async () => {
SettingsService.set({
nextBackupRequestTime: Date.now() + 86400000 * 3
});
};
return (
<>
{!isSheet && (
@@ -182,72 +175,7 @@ const SettingsBackupAndRestore = ({ isSheet }) => {
Backup your data once every week or daily automatically.
</Paragraph>
<Seperator half />
<View
style={{
flexDirection: 'row',
borderRadius: 5,
overflow: 'hidden',
flexShrink: 1,
width: '100%'
}}
>
{[
{
title: 'Never',
value: 'useroff'
},
{
title: 'Daily',
value: 'daily'
},
{
title: 'Weekly',
value: 'weekly'
},
{
title: 'Monthly',
value: 'monthly'
}
].map((item, index) => (
<TouchableOpacity
activeOpacity={0.9}
onPress={async () => {
if (item.value === 'useroff') {
await SettingsService.set({ reminder: item.value });
} else {
await PremiumService.verify(async () => {
if (Platform.OS === 'android') {
let granted = await Backup.checkBackupDirExists();
if (!granted) {
console.log('returning');
return;
}
}
await SettingsService.set({ reminder: item.value });
});
}
updateAskForBackup();
}}
key={item.value}
style={{
backgroundColor: settings.reminder === item.value ? colors.accent : colors.nav,
justifyContent: 'center',
alignItems: 'center',
width: '25%',
height: 35,
borderRightWidth: index !== 3 ? 1 : 0,
borderRightColor: colors.border
}}
>
<Paragraph
color={settings.reminder === item.value ? 'white' : colors.icon}
size={SIZE.sm - 1}
>
{item.title}
</Paragraph>
</TouchableOpacity>
))}
</View>
<AutomaticBackupsSelector />
</View>
{!isSheet && (
@@ -273,4 +201,84 @@ const SettingsBackupAndRestore = ({ isSheet }) => {
);
};
export const AutomaticBackupsSelector = () => {
const colors = useThemeStore(state => state.colors);
const settings = useSettingStore(state => state.settings);
const user = useUserStore(state => state.user);
const updateAskForBackup = async () => {
SettingsService.set({
nextBackupRequestTime: Date.now() + 86400000 * 3
});
};
return (
<View
style={{
flexDirection: 'row',
borderRadius: 5,
overflow: 'hidden',
flexShrink: 1,
width: '100%'
}}
>
{[
{
title: 'Never',
value: 'useroff'
},
{
title: 'Daily',
value: 'daily'
},
{
title: 'Weekly',
value: 'weekly'
},
{
title: 'Monthly',
value: 'monthly'
}
].map((item, index) => (
<TouchableOpacity
activeOpacity={0.9}
onPress={async () => {
if (item.value === 'useroff') {
await SettingsService.set({ reminder: item.value });
} else {
await PremiumService.verify(async () => {
if (Platform.OS === 'android') {
let granted = await Backup.checkBackupDirExists();
if (!granted) {
console.log('returning');
return;
}
}
await SettingsService.set({ reminder: item.value });
});
}
updateAskForBackup();
}}
key={item.value}
style={{
backgroundColor: settings.reminder === item.value ? colors.accent : colors.nav,
justifyContent: 'center',
alignItems: 'center',
width: '25%',
height: 35,
borderRightWidth: index !== 3 ? 1 : 0,
borderRightColor: colors.border
}}
>
<Paragraph
color={settings.reminder === item.value ? 'white' : colors.icon}
size={SIZE.sm - 1}
>
{item.title}
</Paragraph>
</TouchableOpacity>
))}
</View>
);
};
export default SettingsBackupAndRestore;

View File

@@ -1,16 +1,42 @@
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import React from 'react';
import { FlatList, Linking, View } from 'react-native';
import { FlatList, Linking, Platform, View } from 'react-native';
import { enabled } from 'react-native-privacy-snapshot';
import { APP_VERSION } from '../../../version';
import { ChangePassword } from '../../components/auth/change-password';
import { ContainerHeader } from '../../components/container/containerheader';
import { Header } from '../../components/header';
import { Issue } from '../../components/sheets/github/issue';
import { presentSheet } from '../../services/event-manager';
import { useSettingStore } from '../../stores/stores';
import { Progress } from '../../components/sheets/progress';
import BackupService from '../../services/backup';
import { eSendEvent, openVault, presentSheet, ToastEvent } from '../../services/event-manager';
import Notifications from '../../services/notifications';
import PremiumService from '../../services/premium';
import SettingsService from '../../services/settings';
import { useSettingStore, useUserStore } from '../../stores/stores';
import { useThemeStore } from '../../stores/theme';
import { AndroidModule } from '../../utils';
import { toggleDarkMode } from '../../utils/color-scheme/utils';
import * as RNIap from 'react-native-iap';
import {
eCloseProgressDialog,
eOpenAttachmentsDialog,
eOpenLoginDialog,
eOpenRecoveryKeyDialog,
eOpenRestoreDialog
} from '../../utils/events';
import { openLinkInBrowser } from '../../utils/functions';
import { useVaultStatus } from '../../utils/hooks/use-vault-status';
import { sleep } from '../../utils/time';
import AppLock from './app-lock';
import { verifyUser } from './functions';
import { SectionGroup } from './section-group';
import { RouteParams, SettingSection } from './types';
import SettingsUserSection from './user-section';
import Sync from '../../services/sync';
import { MFARecoveryCodes, MFASheet } from './2fa';
import { db } from '../../utils/database';
const format = (ver: number) => {
let parts = ver.toString().split('');
return `v${parts[0]}.${parts[1]}.${parts[2]?.startsWith('0') ? '' : parts[2]}${
@@ -19,6 +45,163 @@ const format = (ver: number) => {
};
const groups: SettingSection[] = [
{
name: 'account',
sections: [
{
type: 'screen',
name: 'Account Settings',
icon: 'account-cog',
description: 'Manage account',
sections: [
{
name: 'Save data recovery key',
modifer: async () => {
verifyUser(null, async () => {
await sleep(300);
eSendEvent(eOpenRecoveryKeyDialog);
});
},
description: 'Recover your data using the recovery key if your password is lost.'
},
{
name: 'Manage attachments',
icon: 'attachment',
modifer: () => {
eSendEvent(eOpenAttachmentsDialog);
},
description: 'Manage all attachments in one place.'
},
{
name: 'Change password',
modifer: async () => {
ChangePassword.present();
},
description: 'Setup a new password for your account.'
},
{
type: 'screen',
name: 'Two factor authentication',
description: 'Manage 2FA settings',
icon: 'two-factor-authentication',
sections: [
{
name: 'Enable two-factor authentication',
modifer: () => {
verifyUser('global', async () => {
MFASheet.present();
});
},
hidden: () => {
let user = useUserStore.getState().user;
return !!user?.mfa?.isEnabled;
},
description: 'Increased security for your account'
},
{
name: 'Add fallback 2FA method',
hidden: () => {
let user = useUserStore.getState().user;
return !!user?.mfa?.secondaryMethod || !user?.mfa?.isEnabled;
},
modifer: () => {
verifyUser('global', async () => {
MFASheet.present(true);
});
},
description:
'You can use fallback 2FA method incase you are unable to login via primary method'
},
{
name: 'Reconfigure fallback 2FA method',
hidden: () => {
let user = useUserStore.getState().user;
return !user?.mfa?.secondaryMethod || !user?.mfa?.isEnabled;
},
modifer: () => {
verifyUser('global', async () => {
MFASheet.present(true);
});
},
description:
'You can use fallback 2FA method incase you are unable to login via primary method'
},
{
name: 'View recovery codes',
modifer: () => {
verifyUser('global', async () => {
MFARecoveryCodes.present('sms');
});
},
hidden: () => {
let user = useUserStore.getState().user;
return !user?.mfa?.isEnabled;
},
description: 'View and save recovery codes for to recover your account'
},
{
name: 'Disable two-factor authentication',
modifer: () => {
verifyUser('global', async () => {
await db.mfa?.disable();
let user = await db.user?.fetchUser();
useUserStore.getState().setUser(user);
});
},
hidden: () => {
let user = useUserStore.getState().user;
return !user?.mfa?.isEnabled;
},
description: 'Decreased security for your account'
}
]
},
{
name: 'Subscription not activated?',
hidden: () => Platform.OS !== 'ios',
modifer: async () => {
if (Platform.OS === 'android') return;
presentSheet({
title: 'Loading subscriptions',
paragraph: `Please wait while we fetch your subscriptions.`
});
let subscriptions = await RNIap.getPurchaseHistory();
subscriptions.sort((a, b) => b.transactionDate - a.transactionDate);
let currentSubscription = subscriptions[0];
presentSheet({
title: 'Notesnook Pro',
paragraph: `You subscribed to Notesnook Pro on ${new Date(
currentSubscription.transactionDate
).toLocaleString()}. Verify this subscription?`,
action: async () => {
presentSheet({
title: 'Verifying subscription',
paragraph: `Please wait while we verify your subscription.`
});
await PremiumService.subscriptions.verify(currentSubscription);
eSendEvent(eCloseProgressDialog);
},
icon: 'information-outline',
actionText: 'Verify'
});
},
description: 'Verify your subscription to Notesnook Pro'
}
]
},
{
name: 'Having problems with sync',
description: 'Try force sync to resolve issues with syncing',
icon: 'sync-alert',
modifer: async () => {
Progress.present();
await Sync.run('global', true);
eSendEvent(eCloseProgressDialog);
}
}
]
},
{
name: 'Customize',
sections: [
@@ -36,7 +219,7 @@ const groups: SettingSection[] = [
},
{
type: 'switch',
name: 'Follow system theme',
name: 'Use system theme',
description: 'Automatically switch to dark mode when system theme changes',
property: 'useSystemTheme',
icon: 'circle-half'
@@ -48,11 +231,7 @@ const groups: SettingSection[] = [
property: 'theme',
icon: 'brightness-6',
modifer: () => {
let current = { ...useSettingStore.getState().settings.theme };
current.dark = !current.dark;
return {
theme: current
};
toggleDarkMode();
},
getter: () => useSettingStore.getState().settings.theme.dark
},
@@ -68,7 +247,15 @@ const groups: SettingSection[] = [
{
type: 'screen',
name: 'Behaviour',
description: 'Change app homepage'
description: 'Change app homepage',
sections: [
{
type: 'component',
name: 'Homepage',
description: 'Default screen to open on app startup',
component: 'homeselector'
}
]
}
]
},
@@ -85,11 +272,373 @@ const groups: SettingSection[] = [
},
{
type: 'screen',
name: 'Vault'
name: 'Vault',
description: 'Multi-layer encryption to most important notes',
icon: 'key',
sections: [
{
name: 'Create vault',
description: 'Set a password to create vault and lock notes.',
icon: 'key',
useHook: useVaultStatus,
hidden: current => current?.exists,
modifer: () => {
PremiumService.verify(() => {
openVault({
item: {},
novault: false,
title: 'Create vault',
description: 'Set a password to create vault and lock notes.'
});
});
}
},
{
useHook: useVaultStatus,
name: 'Change vault password',
description: 'Setup a new password for your vault.',
hidden: current => !current?.exists,
modifer: () =>
openVault({
item: {},
changePassword: true,
novault: true,
title: 'Change vault password',
description: 'Set a new password for your vault.'
})
},
{
useHook: useVaultStatus,
name: 'Clear vault',
description: 'Unlock all locked notes',
hidden: current => !current?.exists,
modifer: () => {
openVault({
item: {},
clearVault: true,
novault: true,
title: 'Clear vault',
description: 'Enter vault password to unlock and remove all notes from the vault.'
});
}
},
{
name: 'Delete vault',
description: 'Delete vault (and optionally remove all notes).',
useHook: useVaultStatus,
hidden: current => !current?.exists,
modifer: () => {
openVault({
item: {},
deleteVault: true,
novault: true,
title: 'Delete vault',
description: 'Enter your account password to delete your vault.'
});
}
},
{
type: 'switch',
name: 'Biometric unlocking',
icon: 'fingerprint',
useHook: useVaultStatus,
description: 'Access notes in vault using biometrics',
hidden: current => !current?.exists || !current?.isBiometryAvailable,
getter: current => current?.biometryEnrolled,
modifer: current => {
openVault({
item: {},
fingerprintAccess: !current.biometryEnrolled,
revokeFingerprintAccess: current.biometryEnrolled,
novault: true,
title: current.biometryEnrolled
? 'Revoke biometric unlocking'
: 'Enable biometery unlock',
description: current.biometryEnrolled
? 'Disable biometric unlocking for notes in vault'
: 'Enable biometric unlocking for notes in vault'
});
}
}
]
},
{
type: 'switch',
icon: 'eye-off-outline',
name: 'Privacy mode',
description: `Hide app contents when you switch to other apps. This will also disable screenshot taking in the app.`,
modifer: () => {
const settings = SettingsService.get();
Platform.OS === 'android'
? AndroidModule.setSecureMode(!settings.privacyScreen)
: enabled(true);
SettingsService.set({ privacyScreen: !settings.privacyScreen });
},
property: 'privacyScreen'
},
{
name: 'App lock',
description: 'Change app lock mode to suit your needs',
icon: 'fingerprint',
modifer: () => {
AppLock.present();
}
}
]
},
{
name: 'Backup and restore',
sections: [
{
type: 'screen',
name: 'App lock'
name: 'Backups',
icon: 'backup-restore',
description: 'Create a backup or change backup settings',
sections: [
{
name: 'Backup now',
description: 'Create a backup of your data',
modifer: async () => {
const user = useUserStore.getState().user;
if (!user) {
await BackupService.run(true);
return;
}
verifyUser(null, () => BackupService.run(true));
}
},
{
type: 'component',
name: 'Automatic backups',
description: 'Backup your data once every week or daily automatically.',
component: 'autobackups'
},
{
name: 'Select backup directory',
description: 'Select directory to store backups',
icon: 'folder',
hidden: () =>
!!SettingsService.get().backupDirectoryAndroid || Platform.OS !== 'android',
property: 'backupDirectoryAndroid',
modifer: async () => {
let dir;
try {
let dir = await BackupService.checkBackupDirExists(true);
} catch (e) {
} finally {
if (!dir) {
ToastEvent.show({
heading: 'No directory selected',
type: 'error'
});
}
}
}
},
{
name: 'Change backup directory',
description: () => SettingsService.get().backupDirectoryAndroid?.name || '',
icon: 'folder',
hidden: () =>
!SettingsService.get().backupDirectoryAndroid || Platform.OS !== 'android',
property: 'backupDirectoryAndroid',
modifer: async () => {
let dir;
try {
dir = await BackupService.checkBackupDirExists(true);
} catch (e) {
} finally {
if (!dir) {
ToastEvent.show({
heading: 'No directory selected',
type: 'error'
});
}
}
}
},
{
type: 'switch',
name: 'Backup encryption',
description: 'Encrypt all your backups.',
icon: 'lock',
property: 'encryptedBackup',
modifer: async () => {
const user = useUserStore.getState().user;
const settings = SettingsService.get();
if (!user) {
ToastEvent.show({
heading: 'Login required to enable encryption',
type: 'error',
func: () => {
eSendEvent(eOpenLoginDialog);
},
actionText: 'Login'
});
return;
}
await SettingsService.set({ encryptedBackup: !settings.encryptedBackup });
}
}
]
},
{
name: 'Restore backup',
description: `Restore backup from phone storage.`,
modifer: () => {
const user = useUserStore.getState().user;
if (!user || !user?.email) {
ToastEvent.show({
heading: 'Login required',
message: 'Please log in to your account to restore backup',
type: 'error',
context: 'global'
});
return;
}
eSendEvent(eOpenRestoreDialog);
}
}
]
},
{
name: 'Productivity',
hidden: () => Platform.OS !== 'android',
sections: [
{
type: 'switch',
name: 'Notes in notifications',
description: `Add quick notes from notifications without opening the app.`,
property: 'notifNotes',
icon: 'form-textbox',
modifer: () => {
const settings = SettingsService.get();
if (settings.notifNotes) {
Notifications.unpinQuickNote();
} else {
Notifications.pinQuickNote(false);
}
SettingsService.toggle('notifNotes');
}
}
]
},
{
name: 'Help and support',
sections: [
{
name: 'Report an issue',
icon: 'bug',
modifer: () => {
presentSheet({
component: <Issue />
});
},
description: 'Faced an issue or have a suggestion? Click here to create a bug report'
},
{
name: 'Documentation',
modifer: async () => {
Linking.openURL('https://docs.notesnook.com');
},
description: 'Learn about every feature and how it works.'
}
]
},
{
name: 'community',
sections: [
{
name: 'Join our Telegram group',
description: "We are on telegram, let's talk",
icon: 'telegram',
modifer: () => {
Linking.openURL('https://t.me/notesnook').catch(console.log);
}
},
{
name: 'Join our Discord community',
icon: 'discord',
modifer: async () => {
presentSheet({
title: 'Join our Discord Community',
iconColor: 'discord',
paragraph: 'We are not ghosts, chat with us and share your experience.',
valueArray: [
'Talk with us anytime.',
'Follow the development process',
'Give suggestions and report issues.',
'Get early access to new features',
'Meet other people using Notesnook'
],
icon: 'discord',
action: async () => {
try {
Linking.openURL('https://discord.gg/zQBK97EE22').catch(console.log);
} catch (e) {}
},
actionText: 'Join Now'
});
},
description: 'We are not ghosts, chat with us and share your experience.'
}
]
},
{
name: 'legal',
sections: [
{
name: 'Terms of service',
modifer: async () => {
try {
await Linking.openURL('https://notesnook.com/tos');
} catch (e) {}
},
description: 'Read our terms of service'
},
{
name: 'Privacy policy',
modifer: async () => {
try {
await Linking.openURL('https://notesnook.com/privacy');
} catch (e) {}
},
description: 'Read our privacy policy'
}
]
},
{
name: 'about',
sections: [
{
name: 'Download on desktop',
icon: 'monitor',
modifer: async () => {
try {
await Linking.openURL('https://notesnook.com');
} catch (e) {}
},
description: 'Notesnook app can be downloaded on all platforms'
},
{
name: 'Roadmap',
icon: 'chart-timeline',
modifer: async () => {
try {
await Linking.openURL('https://docs.notesnook.com/roadmap/');
} catch (e) {}
},
description: 'See what the future of Notesnook is going to be like.'
},
{
name: 'About Notesnook',
modifer: async () => {
try {
await Linking.openURL('https://notesnook.com');
} catch (e) {}
},
description: format(APP_VERSION)
}
]
}
@@ -167,15 +716,7 @@ const Home = ({ navigation }: NativeStackScreenProps<RouteParams, 'SettingsHome'
},
desc: 'Notesnook app can be downloaded on all platforms'
},
{
name: 'Documentation',
func: async () => {
try {
await openLinkInBrowser('https://docs.notesnook.com', colors);
} catch (e) {}
},
desc: 'Learn about every feature and how it works.'
},
{
name: 'Roadmap',
func: async () => {
@@ -196,19 +737,19 @@ const Home = ({ navigation }: NativeStackScreenProps<RouteParams, 'SettingsHome'
}
];
const renderItem = ({ item, index }: { item: SettingSection; index: number }) => (
<SectionGroup item={item} />
);
const renderItem = ({ item, index }: { item: SettingSection; index: number }) =>
item.name === 'account' ? <SettingsUserSection item={item} /> : <SectionGroup item={item} />;
return (
<View>
<ContainerHeader>
<Header title="Settings" isBack={true} screen="Settings" />
<Header title="Settings" isBack={false} screen="Settings" />
</ContainerHeader>
<FlatList
data={groups}
keyExtractor={(item, index) => item.name || index.toString()}
ListFooterComponent={<View style={{ height: 200 }} />}
renderItem={renderItem}
/>
</View>

View File

@@ -8,7 +8,8 @@ import { SettingSection } from './types';
export const SectionGroup = ({ item }: { item: SettingSection }) => {
const colors = useThemeStore(state => state.colors);
return (
const isHidden = item.hidden && item.hidden(null);
return isHidden ? null : (
<View
style={{
marginVertical: item.sections ? 10 : 0
@@ -19,10 +20,10 @@ export const SectionGroup = ({ item }: { item: SettingSection }) => {
style={{
paddingHorizontal: 12
}}
color={colors.icon}
size={SIZE.sm}
color={colors.accent}
size={SIZE.xs}
>
{item.name}
{item.name.toUpperCase()}
</Heading>
) : null}

View File

@@ -1,37 +1,43 @@
import { NavigationProp, useNavigation } from '@react-navigation/native';
import { NavigationProp, StackActions, useNavigation } from '@react-navigation/native';
import React, { ReactElement } from 'react';
import { View } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import ToggleSwitch from 'toggle-switch-react-native';
import { PressableButton } from '../../components/ui/pressable';
import Seperator from '../../components/ui/seperator';
import Paragraph from '../../components/ui/typography/paragraph';
import SettingsService from '../../services/settings';
import { useSettingStore } from '../../stores/stores';
import { useThemeStore } from '../../stores/theme';
import { SIZE } from '../../utils/size';
import { AccentColorPicker } from './appearance';
import { AccentColorPicker, HomagePageSelector } from './appearance';
import { AutomaticBackupsSelector } from './backup-restore';
import { RouteParams, SettingSection } from './types';
const components: { [name: string]: ReactElement } = {
colorpicker: <AccentColorPicker wrap={true} />
colorpicker: <AccentColorPicker wrap={true} />,
homeselector: <HomagePageSelector />,
autobackups: <AutomaticBackupsSelector />
};
export const SectionItem = ({ item }: { item: SettingSection }) => {
const colors = useThemeStore(state => state.colors);
const settings = useSettingStore(state => state.settings);
const navigation = useNavigation<NavigationProp<RouteParams>>();
const current = item.useHook && item.useHook(item);
const isHidden = item.hidden && item.hidden(item.property || current);
const onChangeSettings = () => {
if (item.modifer) {
item.modifer(item.property || current);
return;
}
if (!item.property) return;
SettingsService.set(
item.modifer
? item.modifer(item.property)
: {
[item.property]: !settings[item.property]
}
);
SettingsService.set({
[item.property]: !settings[item.property]
});
};
return (
return isHidden ? null : (
<PressableButton
key={item.name}
disabled={item.type === 'component'}
@@ -46,18 +52,22 @@ export const SectionItem = ({ item }: { item: SettingSection }) => {
onPress={() => {
switch (item.type) {
case 'screen':
navigation.navigate('SettingsGroup', item);
navigation.dispatch(StackActions.push('SettingsGroup', item));
break;
case 'switch':
onChangeSettings();
break;
default:
item.modifer && item.modifer(current);
break;
}
}}
>
<View
style={{
flexDirection: 'row',
flexShrink: 1
flexShrink: 1,
alignItems: 'center'
}}
>
<View
@@ -71,7 +81,7 @@ export const SectionItem = ({ item }: { item: SettingSection }) => {
borderRadius: 100
}}
>
{!!item.icon && <Icon name={item.icon} size={30} />}
{!!item.icon && <Icon color={colors.icon} name={item.icon} size={30} />}
</View>
<View
@@ -83,15 +93,24 @@ export const SectionItem = ({ item }: { item: SettingSection }) => {
<Paragraph color={colors.heading} size={SIZE.md + 1}>
{item.name}
</Paragraph>
<Paragraph size={SIZE.sm}>{item.description}</Paragraph>
{!!item.description && (
<Paragraph size={SIZE.sm}>
{typeof item.description === 'function' ? item.description() : item.description}
</Paragraph>
)}
{!!item.component && components[item.component]}
{!!item.component && (
<>
<Seperator />
{components[item.component]}
</>
)}
</View>
</View>
{item.type === 'switch' && item.property && (
<ToggleSwitch
isOn={item.getter ? item.getter(item.property) : settings[item.property]}
isOn={item.getter ? item.getter(item.property || current) : settings[item.property]}
onColor={colors.accent}
offColor={colors.icon}
size="small"

View File

@@ -3,13 +3,15 @@ import { Settings } from '../../stores/interfaces';
export type SettingSection = {
type?: 'screen' | 'switch' | 'component';
name?: string;
description?: string;
description?: string | (() => string);
icon?: string;
property?: keyof Settings;
sections?: SettingSection[];
component?: string;
modifer?: (property: keyof Settings) => Partial<Settings>;
getter?: (property: keyof Settings) => any;
modifer?: (...args: any[]) => void;
getter?: (...args: any[]) => any;
useHook?: (...args: any[]) => unknown;
hidden?: (current: any) => boolean;
};
export type SettingsGroup = {

View File

@@ -1,6 +1,6 @@
import dayjs from 'dayjs';
import React from 'react';
import { Linking, Platform, View } from 'react-native';
import { Linking, Platform, Text, View } from 'react-native';
import * as RNIap from 'react-native-iap';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { ChangePassword } from '../../components/auth/change-password';
@@ -14,6 +14,8 @@ import PremiumService from '../../services/premium';
import Sync from '../../services/sync';
import { useUserStore } from '../../stores/stores';
import { useThemeStore } from '../../stores/theme';
import { SectionItem } from './section-item';
import {
SUBSCRIPTION_PROVIDER,
SUBSCRIPTION_STATUS,
@@ -31,6 +33,7 @@ import { sleep } from '../../utils/time';
import TwoFactorAuth from './2fa';
import { CustomButton } from './button';
import { verifyUser } from './functions';
import { TimeSince } from '../../components/ui/time-since';
const getTimeLeft = t2 => {
let daysRemaining = dayjs(t2).diff(dayjs(), 'days');
@@ -40,7 +43,7 @@ const getTimeLeft = t2 => {
};
};
const SettingsUserSection = () => {
const SettingsUserSection = ({ item }) => {
const colors = useThemeStore(state => state.colors);
const user = useUserStore(state => state.user);
@@ -50,6 +53,8 @@ const SettingsUserSection = () => {
const startDate = dayjs(user?.subscription?.start).format('MMMM D, YYYY');
const monthlyPlan = usePricing('monthly');
const lastSynced = useUserStore(state => state.lastSynced);
const manageSubscription = () => {
if (!user.isEmailConfirmed) {
PremiumService.showVerifyEmailDialog();
@@ -84,8 +89,7 @@ const SettingsUserSection = () => {
<View
style={{
paddingHorizontal: 12,
marginTop: 15,
marginBottom: 15
marginTop: 15
}}
>
<View
@@ -100,54 +104,73 @@ const SettingsUserSection = () => {
<View
style={{
justifyContent: 'space-between',
alignItems: 'center',
flexDirection: 'row',
paddingBottom: 4,
borderBottomWidth: 1,
borderColor: colors.accent
paddingBottom: 4
}}
>
<View
style={{
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center'
width: '100%',
justifyContent: 'space-between'
}}
>
<View
style={{
borderWidth: 1,
borderRadius: 100,
borderColor: colors.accent,
width: 20,
height: 20,
alignItems: 'center',
justifyContent: 'center'
flexDirection: 'row'
}}
>
<Icon size={SIZE.md} color={colors.accent} name="account-outline" />
<View
style={{
alignItems: 'center'
}}
>
<View
style={{
backgroundColor: colors.shade,
borderRadius: 100,
width: 50,
height: 50,
alignItems: 'center',
justifyContent: 'center'
}}
>
<Icon size={SIZE.xl} color={colors.accent} name="camera-outline" />
</View>
</View>
<View
style={{
marginLeft: 10
}}
>
<Heading color={colors.accent} size={SIZE.xs + 1}>
{SUBSCRIPTION_STATUS_STRINGS[user.subscription?.type].toUpperCase()}
</Heading>
<Paragraph color={colors.heading} size={SIZE.sm}>
{user?.email}
</Paragraph>
<Paragraph color={colors.icon} size={SIZE.xs}>
Last synced{' '}
<TimeSince
style={{ fontSize: SIZE.xs, color: colors.icon }}
time={lastSynced}
/>
</Paragraph>
</View>
</View>
<Paragraph
color={colors.heading}
size={SIZE.sm}
<Button
height={35}
style={{
marginLeft: 5
borderRadius: 100,
paddingHorizontal: 12
}}
>
{user?.email}
</Paragraph>
</View>
<View
style={{
borderRadius: 5,
padding: 5,
paddingVertical: 2.5
}}
>
<Heading color={colors.accent} size={SIZE.sm}>
{SUBSCRIPTION_STATUS_STRINGS[user.subscription?.type]}
</Heading>
fontSize={SIZE.xs}
type="accent"
title="GET PRO"
/>
</View>
</View>
<View>
@@ -195,17 +218,19 @@ const SettingsUserSection = () => {
</View>
) : null}
{user.subscription?.type !== SUBSCRIPTION_STATUS.PREMIUM &&
{/* {user.subscription?.type !== SUBSCRIPTION_STATUS.PREMIUM &&
user.subscription?.type !== SUBSCRIPTION_STATUS.BETA && (
<>
<Seperator />
<Button
onPress={manageSubscription}
width="100%"
style={{
paddingHorizontal: 0
paddingHorizontal: 24,
borderRadius: 100,
height: 40,
alignSelf: 'flex-end'
}}
fontSize={SIZE.md}
fontSize={SIZE.sm}
title={
!user.isEmailConfirmed
? 'Confirm your email'
@@ -216,14 +241,14 @@ const SettingsUserSection = () => {
Platform.OS === 'android'
? `Resubscribe from Google Playstore`
: user.subscription?.type === SUBSCRIPTION_STATUS.PREMIUM_EXPIRED
? `Resubscribe to Notesnook Pro (${monthlyPlan?.product?.localizedPrice} / mo)`
: `Subscribe to Notesnook Pro (${monthlyPlan?.product?.localizedPrice} / mo)`
? `Resubscribe to Pro (${monthlyPlan?.product?.localizedPrice} / mo)`
: `Get Pro (${monthlyPlan?.product?.localizedPrice} / mo)`
}
height={50}
type="accent"
/>
</>
)}
)} */}
</View>
{user?.subscription?.provider &&
@@ -255,84 +280,9 @@ const SettingsUserSection = () => {
</View>
</View>
{[
{
name: 'Save data recovery key',
func: async () => {
verifyUser(null, async () => {
await sleep(300);
eSendEvent(eOpenRecoveryKeyDialog);
});
},
desc: 'Recover your data using the recovery key if your password is lost.'
},
{
name: 'Manage attachments',
func: () => {
eSendEvent(eOpenAttachmentsDialog);
},
desc: 'Manage all attachments in one place.'
},
{
name: 'Change password',
func: async () => {
ChangePassword.present();
},
desc: 'Setup a new password for your account.'
},
{
name: 'Having problems with syncing?',
func: async () => {
Progress.present();
await Sync.run('global', true);
eSendEvent(eCloseProgressDialog);
},
desc: 'Try force sync to resolve issues with syncing.'
},
{
name: 'Subscription not activated?',
func: async () => {
if (Platform.OS === 'android') return;
presentSheet({
title: 'Loading subscriptions',
paragraph: `Please wait while we fetch your subscriptions.`
});
let subscriptions = await RNIap.getPurchaseHistory();
subscriptions.sort((a, b) => b.transactionDate - a.transactionDate);
let currentSubscription = subscriptions[0];
presentSheet({
title: 'Notesnook Pro',
paragraph: `You subscribed to Notesnook Pro on ${new Date(
currentSubscription.transactionDate
).toLocaleString()}. Verify this subscription?`,
action: async () => {
presentSheet({
title: 'Verifying subscription',
paragraph: `Please wait while we verify your subscription.`
});
await PremiumService.subscriptions.verify(currentSubscription);
eSendEvent(eCloseProgressDialog);
},
icon: 'information-outline',
actionText: 'Verify'
});
},
desc: 'Verify your subscription to Notesnook Pro'
}
].map(item =>
item.name === 'Subscription not activated?' &&
(Platform.OS !== 'ios' || PremiumService.get()) ? null : (
<CustomButton
key={item.name}
title={item.name}
onPress={item.func}
tagline={item.desc}
color={item.name === 'Logout' ? colors.errorText : colors.pri}
/>
)
)}
<TwoFactorAuth />
{item.sections.map(item => (
<SectionItem key={item.name} item={item} />
))}
</>
) : null}
</>

View File

@@ -68,7 +68,7 @@ async function getProducts() {
}
function get() {
// if (__DEV__) return true;
if (__DEV__) return true;
return SUBSCRIPTION_STATUS.BASIC !== premiumStatus;
}

View File

@@ -0,0 +1,42 @@
import React, { useCallback, useEffect } from 'react';
import BiometicService from '../../services/biometrics';
import { eSubscribeEvent, eUnSubscribeEvent } from '../../services/event-manager';
import { db } from '../database';
const VaultStatusCache = {
exists: false,
biometryEnrolled: false,
isBiometryAvailable: false
};
export const useVaultStatus = () => {
const [vaultStatus, setVaultStatus] = React.useState(VaultStatusCache);
const checkVaultStatus = useCallback(() => {
db.vault.exists().then(async exists => {
let available = await BiometicService.isBiometryAvailable();
let fingerprint = await BiometicService.hasInternetCredentials();
if (
VaultStatusCache.exists === exists &&
VaultStatusCache.biometryEnrolled === fingerprint &&
VaultStatusCache.isBiometryAvailable === available
)
return;
setVaultStatus({
exists: exists,
biometryEnrolled: fingerprint,
isBiometryAvailable: available ? true : false
});
});
}, []);
useEffect(() => {
checkVaultStatus();
eSubscribeEvent('vaultUpdated', () => checkVaultStatus());
return () => {
eUnSubscribeEvent('vaultUpdated', () => checkVaultStatus());
};
}, []);
return vaultStatus;
};