add walkthroughs for pro/trial users

This commit is contained in:
ammarahm-ed
2022-02-18 14:45:38 +05:00
parent 80650f9d70
commit 80dea89173
16 changed files with 432 additions and 178 deletions

File diff suppressed because one or more lines are too long

View File

@@ -22,7 +22,7 @@ export const ChangePassword = () => {
const [loading, setLoading] = useState(false);
const changePassword = async () => {
if (error || !oldPassword || !password) {
if (error || !oldPassword.current || !password.current) {
ToastEvent.show({
heading: 'All fields required',
message: 'Fill all the fields and try again.',
@@ -34,7 +34,7 @@ export const ChangePassword = () => {
eSendEvent(eCloseProgressDialog);
setLoading(true);
try {
await db.user.changePassword(oldPassword, password);
await db.user.changePassword(oldPassword.current, password.current);
ToastEvent.show({
heading: `Account password updated`,
type: 'success',

View File

@@ -24,7 +24,7 @@ export const ForgotPassword = () => {
const [sent, setSent] = useState(false);
const sendRecoveryEmail = async () => {
if (!email || error) {
if (!email.current || error) {
ToastEvent.show({
heading: 'Account email is required.',
type: 'error',
@@ -38,7 +38,7 @@ export const ForgotPassword = () => {
if (lastRecoveryEmailTime && Date.now() - JSON.parse(lastRecoveryEmailTime) < 60000 * 3) {
throw new Error('Please wait before requesting another email');
}
await db.user.recoverAccount(email.toLowerCase());
await db.user.recoverAccount(email.current.toLowerCase());
await MMKV.setItem('lastRecoveryEmailTime', JSON.stringify(Date.now()));
ToastEvent.show({
heading: `Check your email to reset password`,

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useRef, useState } from 'react';
import { LayoutAnimation, Platform, View } from 'react-native';
import { Platform, View } from 'react-native';
import { SheetManager } from 'react-native-actions-sheet';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTracked } from '../../provider';
@@ -31,9 +31,7 @@ export const Login = ({ changeMode }) => {
const emailInputRef = useRef();
const passwordInputRef = useRef();
const password = useRef();
const [focused, setFocused] = useState(false);
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const insets = useSafeAreaInsets();

View File

@@ -8,7 +8,8 @@ import { eSendEvent, ToastEvent } from '../../services/EventManager';
import { clearMessage, setEmailVerifyMessage } from '../../services/Message';
import PremiumService from '../../services/PremiumService';
import { db } from '../../utils/database';
import { eCloseLoginDialog, eOpenResultDialog } from '../../utils/Events';
import { eCloseLoginDialog } from '../../utils/Events';
import { openLinkInBrowser } from '../../utils/functions';
import { SIZE } from '../../utils/SizeUtils';
import { sleep } from '../../utils/TimeUtils';
import umami from '../../utils/umami';
@@ -58,7 +59,7 @@ export const Signup = ({ changeMode, welcome, trial }) => {
if (!validateInfo() || error) return;
setLoading(true);
try {
await db.user.signup(email.toLowerCase(), password);
await db.user.signup(email.current.toLowerCase(), password.current);
let user = await db.user.getUser();
setUser(user);
setLastSynced(await db.lastSynced());
@@ -214,7 +215,36 @@ export const Signup = ({ changeMode, welcome, trial }) => {
marginBottom={5}
/>
<Paragraph size={SIZE.xs} color={colors.icon}>
By signing up, you agree to our terms of service and privacy policy.
By signing up, you agree to our{' '}
<Paragraph
size={SIZE.xs}
onPress={() => {
openLinkInBrowser('https://notesnook.com/tos', colors)
.catch(e => {})
.then(r => {});
}}
style={{
textDecorationLine: 'underline'
}}
color={colors.accent}
>
terms of service{' '}
</Paragraph>
and{' '}
<Paragraph
size={SIZE.xs}
onPress={() => {
openLinkInBrowser('https://notesnook.com/privacy', colors)
.catch(e => {})
.then(r => {});
}}
style={{
textDecorationLine: 'underline'
}}
color={colors.accent}
>
privacy policy.
</Paragraph>
</Paragraph>
<View

View File

@@ -53,7 +53,7 @@ export const Placeholder = ({ type, w, h, color }) => {
);
};
export const SvgToPngView = ({ width, height, src, color, img }) => {
export const SvgToPngView = ({ width = 250, height = 250, src }) => {
const [error, setError] = useState(false);
return (

View File

@@ -1,16 +1,15 @@
import React, { useEffect, useState } from 'react';
import { InteractionManager, View } from 'react-native';
import { View } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import ToggleSwitch from 'toggle-switch-react-native';
import { useTracked } from '../../provider';
import { eSubscribeEvent, eUnSubscribeEvent } from '../../services/EventManager';
import Navigation from '../../services/Navigation';
import { getElevation } from '../../utils';
import { normalize, SIZE } from '../../utils/SizeUtils';
import { Button } from '../Button';
import { PressableButton } from '../PressableButton';
import Heading from '../Typography/Heading';
import Paragraph from '../Typography/Paragraph';
import ToggleSwitch from 'toggle-switch-react-native';
export const MenuListItem = React.memo(
({ item, index, noTextMode, testID, rightBtn }) => {

View File

@@ -7,8 +7,15 @@ import { DDS } from '../../services/DeviceDetection';
import { eSendEvent, presentSheet } from '../../services/EventManager';
import PremiumService from '../../services/PremiumService';
import { getElevation } from '../../utils';
import { eOpenLoginDialog, eOpenResultDialog } from '../../utils/Events';
import { db } from '../../utils/database';
import {
eClosePremiumDialog,
eCloseProgressDialog,
eOpenLoginDialog,
eOpenResultDialog
} from '../../utils/Events';
import { SIZE } from '../../utils/SizeUtils';
import { sleep } from '../../utils/TimeUtils';
import umami from '../../utils/umami';
import { ActionIcon } from '../ActionIcon';
import { AuthMode } from '../Auth';
@@ -19,6 +26,7 @@ import Seperator from '../Seperator';
import { Toast } from '../Toast';
import Heading from '../Typography/Heading';
import Paragraph from '../Typography/Paragraph';
import { Walkthrough } from '../Walkthrough';
import { features } from './features';
import { Group } from './group';
import { PricingPlans } from './pricing-plans';
@@ -146,8 +154,14 @@ export const Component = ({ close, promo, getRef }) => {
{userCanRequestTrial ? (
<Button
key="calltoaction"
onPress={() => {
eSendEvent(eOpenResultDialog);
onPress={async () => {
try {
await db.user.activateTrial();
eSendEvent(eClosePremiumDialog);
eSendEvent(eCloseProgressDialog);
await sleep(300);
Walkthrough.present('trialstarted', false, true);
} catch (e) {}
}}
title="Try free for 14 days"
type="accent"

View File

@@ -22,6 +22,7 @@ import BaseDialog from '../Dialog/base-dialog';
import { presentDialog } from '../Dialog/functions';
import Heading from '../Typography/Heading';
import Paragraph from '../Typography/Paragraph';
import { Walkthrough } from '../Walkthrough';
import { PricingItem } from './pricing-item';
const promoCyclesMonthly = {
@@ -195,12 +196,14 @@ export const PricingPlans = ({
/>
<Button
onPress={() => {
eSendEvent(eClosePremiumDialog);
eSendEvent(eCloseProgressDialog);
setTimeout(() => {
eSendEvent(eOpenLoginDialog, 1);
}, 400);
onPress={async () => {
try {
await db.user.activateTrial();
eSendEvent(eClosePremiumDialog);
eSendEvent(eCloseProgressDialog);
await sleep(300);
Walkthrough.present('trialstarted', false, true);
} catch (e) {}
}}
title={`Try free for 14 days`}
type="grayAccent"

View File

@@ -11,7 +11,7 @@ import Heading from '../Typography/Heading';
import Paragraph from '../Typography/Paragraph';
import walkthroughs, { TStep } from './walkthroughs';
export const Walkthrough = ({ steps }: { steps: TStep[] }) => {
export const Walkthrough = ({ steps, canSkip = true }: { steps: TStep[]; canSkip: boolean }) => {
const [state] = useTracked();
const colors = state.colors;
const [step, setStep] = useState<TStep>(steps && steps[0]);
@@ -33,17 +33,36 @@ export const Walkthrough = ({ steps }: { steps: TStep[] }) => {
>
{step.walkthroughItem(colors)}
<Heading>{step.title}</Heading>
<Paragraph
style={{
textAlign: 'center',
alignSelf: 'center',
maxWidth: '80%'
}}
size={SIZE.md}
>
{step.text}
</Paragraph>
{step.title ? <Heading>{step.title}</Heading> : null}
{step.text ? (
<Paragraph
style={{
textAlign: 'center',
alignSelf: 'center',
maxWidth: '80%'
}}
size={SIZE.md}
>
{step.text}
</Paragraph>
) : null}
{step.actionButton && (
<Button
//@ts-ignore
style={{
height: 30,
marginTop: 10
}}
textStyle={{
textDecorationLine: 'underline'
}}
onPress={async () => {
step.actionButton?.action();
}}
type="transparent"
title={step.actionButton.text}
/>
)}
<Button
//@ts-ignore
@@ -69,43 +88,52 @@ export const Walkthrough = ({ steps }: { steps: TStep[] }) => {
type="accent"
/>
<Button
//@ts-ignore
style={{
height: 30,
marginTop: 10
}}
textStyle={{
textDecorationLine: 'underline'
}}
onPress={async () => {
eSendEvent(eCloseProgressDialog);
}}
type="gray"
title="Skip introduction"
/>
{canSkip ? (
<Button
//@ts-ignore
style={{
height: 30,
marginTop: 10
}}
textStyle={{
textDecorationLine: 'underline'
}}
onPress={async () => {
eSendEvent(eCloseProgressDialog);
}}
type="gray"
title="Skip introduction"
/>
) : null}
</View>
);
};
Walkthrough.present = async (id: 'notebooks') => {
let walkthroughState = await MMKV.getItem('walkthroughState');
if (walkthroughState) {
walkthroughState = JSON.parse(walkthroughState);
} else {
Walkthrough.present = async (
id: 'notebooks' | 'trialstarted' | 'emailconfirmed' | 'prouser',
canSkip = true,
nopersist?: boolean
) => {
if (!nopersist) {
let walkthroughState = await MMKV.getItem('walkthroughState');
if (walkthroughState) {
walkthroughState = JSON.parse(walkthroughState);
} else {
//@ts-ignore
walkthroughState = {};
}
//@ts-ignore
walkthroughState = {};
if (walkthroughState[id]) return;
//@ts-ignore
walkthroughState[id] = true;
MMKV.setItem('walkthroughState', JSON.stringify(walkthroughState));
}
//@ts-ignore
if (walkthroughState[id]) return;
//@ts-ignore
walkthroughState[id] = true;
MMKV.setItem('walkthroughState', JSON.stringify(walkthroughState));
//@ts-ignore
let walkthrough = walkthroughs[id];
if (!walkthrough) return;
presentSheet({
component: <Walkthrough steps={walkthrough.steps} />,
component: <Walkthrough canSkip={canSkip} steps={walkthrough.steps} />,
disableClosing: true
});
};

View File

@@ -1,25 +1,34 @@
import React from 'react';
import { View } from 'react-native';
import { Linking, View } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { COMMUNITY_SVG, LAUNCH_ROCKET, SUPPORT_SVG, WELCOME_SVG } from '../../assets/images/assets';
import { TrackedState, useTracked } from '../../provider';
import { eSendEvent } from '../../services/EventManager';
import { getElevation } from '../../utils';
import { eOpenAddNotebookDialog } from '../../utils/Events';
import { SIZE } from '../../utils/SizeUtils';
import useRotator from '../../utils/use-rotator';
import { AccentColorPicker } from '../../views/Settings/appearance';
import { Button } from '../Button';
import { SvgToPngView } from '../ListPlaceholders';
import { PinItem } from '../Menu/TagsSection';
import Seperator from '../Seperator';
import Heading from '../Typography/Heading';
import Paragraph from '../Typography/Paragraph';
export type TStep = {
text: string;
text?: string;
walkthroughItem: (colors: TrackedState['colors']) => React.ReactNode;
title: string;
title?: string;
button?: {
type: 'next' | 'done';
title: string;
action?: () => void;
};
actionButton?: {
text: string;
action: () => void;
};
};
const NotebookWelcome = () => {
@@ -237,4 +246,170 @@ const notebooks: { id: string; steps: TStep[] } = {
]
};
export default { notebooks };
const ChooseTheme = () => {
const [state] = useTracked();
const { colors } = state;
return (
<View
style={{
height: 100
}}
>
<AccentColorPicker settings={false} />
</View>
);
};
const trialstarted: { id: string; steps: TStep[] } = {
id: 'trialstarted',
steps: [
{
title: 'Your trial is activated',
text: 'You can use all permium features for free for the next 14 days',
walkthroughItem: colors => <SvgToPngView src={LAUNCH_ROCKET(colors.pri)} />,
button: {
type: 'next',
title: 'Next'
}
},
{
title: 'Make yourself at home',
text: 'Pick a theme of your choice',
walkthroughItem: () => <ChooseTheme />,
button: {
type: 'next',
title: 'Next'
}
},
{
title: 'Join the cause',
text: 'Meet other privacy-minded people and talk to us directly about your concerns, issues and suggestions.',
walkthroughItem: colors => <SvgToPngView src={COMMUNITY_SVG(colors.pri)} />,
button: {
type: 'done',
title: 'Continue'
},
actionButton: {
text: 'Join Discord Community',
action: () => {
Linking.openURL('https://discord.gg/zQBK97EE22').catch(console.log);
}
}
}
]
};
const emailconfirmed: { id: string; steps: TStep[] } = {
id: 'emailconfirmed',
steps: [
{
title: 'Email confirmed',
text: 'Your email was confirmed successfully. Thank you for choosing end-to-end encrypted note taking.',
walkthroughItem: colors => <SvgToPngView src={WELCOME_SVG(colors.pri)} />,
button: {
type: 'done',
title: 'Continue'
}
}
]
};
const Support = () => {
const [state] = useTracked();
const { colors } = state;
return (
<View
style={{
width: '100%',
alignItems: 'center'
}}
>
<SvgToPngView src={SUPPORT_SVG()} />
<Heading>Get Priority Support</Heading>
<Paragraph
style={{
textAlign: 'center'
}}
size={SIZE.md}
>
You can reach out to us via multiple channels if you face an issue or want to just talk.
</Paragraph>
<Seperator />
<Button
style={{
justifyContent: 'flex-start',
marginBottom: 10,
width: '90%'
}}
onPress={() => {
Linking.openURL('https://discord.gg/zQBK97EE22').catch(console.log);
}}
icon="discord"
type="grayBg"
title="Join our community on Discord"
/>
<Button
style={{
justifyContent: 'flex-start',
marginBottom: 10,
width: '90%'
}}
onPress={() => {
Linking.openURL('https://t.me/notesnook').catch(console.log);
}}
icon="telegram"
type="grayBg"
title="Join our Telegram group"
/>
<Button
style={{
justifyContent: 'flex-start',
marginBottom: 10,
width: '90%'
}}
icon="bug"
type="grayBg"
title="Submit an issue from Settings"
/>
<Button
style={{
justifyContent: 'flex-start',
marginBottom: 10,
width: '90%'
}}
icon="mail"
type="grayBg"
title="Email us at support@streetwriters.co"
/>
</View>
);
};
const prouser: { id: string; steps: TStep[] } = {
id: 'prouser',
steps: [
{
title: 'Welcome to Notesnook Pro',
text: 'Thank you for reaffirming our idea that privacy comes first',
walkthroughItem: colors => <SvgToPngView src={LAUNCH_ROCKET(colors.pri)} />,
button: {
type: 'next',
title: 'Next'
}
},
{
walkthroughItem: () => <Support />,
button: {
type: 'done',
title: 'Continue'
}
}
]
};
export default { notebooks, trialstarted, emailconfirmed, prouser };

View File

@@ -23,11 +23,11 @@ import {
} from '../utils/Events';
import { editorRef, tabBarRef } from '../utils/Refs';
import { sleep } from '../utils/TimeUtils';
import useTooltip, { hideAllTooltips } from '../utils/use-tooltip';
import { EditorWrapper } from '../views/Editor/EditorWrapper';
import { checkStatus, EditorWebView, getNote } from '../views/Editor/Functions';
import tiny from '../views/Editor/tiny/tiny';
import { NavigatorStack } from './NavigatorStack';
import useTooltip, { hideAllTooltips } from '../utils/use-tooltip';
let layoutTimer = null;

View File

@@ -70,13 +70,13 @@ export function setLoginMessage() {
const emailMessage = {
visible: true,
message: 'Email not confirmed',
actionText: 'Confirm now to get 7 more days of free trial',
actionText: 'Please confrim your email to sync notes.',
onPress: () => {
PremiumService.showVerifyEmailDialog();
},
data: {},
icon: 'email',
type: 'normal'
type: 'error'
};
export function setEmailVerifyMessage() {

View File

@@ -15,6 +15,7 @@ import * as RNIap from 'react-native-iap';
import { enabled } from 'react-native-privacy-snapshot';
import { doInBackground, editing } from '.';
import { ProFeatures } from '../components/ResultDialog/pro-features';
import { Walkthrough } from '../components/Walkthrough';
import {
clearAllStores,
initialize,
@@ -202,23 +203,7 @@ export const useAppEvents = () => {
if (!user) return;
MMKV.setItem('isUserEmailConfirmed', 'yes');
await PremiumService.setPremiumStatus();
let message = 'You have been rewarded 7 more days of free trial. Enjoy using Notesnook!';
presentSheet({
title: 'Email confirmed!',
paragraph: message,
component: (
<View
style={{
paddingHorizontal: 12,
paddingBottom: 15,
alignItems: 'center'
}}
>
<ProFeatures />
</View>
)
});
Walkthrough.present('emailconfirmed', false, true);
if (user?.isEmailConfirmed) {
clearMessage();
}
@@ -240,15 +225,7 @@ export const useAppEvents = () => {
const onAccountStatusChange = async userStatus => {
if (!PremiumService.get() && userStatus.type === 5) {
PremiumService.subscriptions.clear();
presentSheet({
title: 'Notesnook Pro',
paragraph: `Your Notesnook Pro subscription has been successfully activated.`,
action: async () => {
eSendEvent(eCloseProgressDialog);
},
icon: 'check',
actionText: 'Continue'
});
Walkthrough.present('prouser', false, true);
}
await PremiumService.setPremiumStatus();
};

View File

@@ -1,5 +1,6 @@
import React, { useRef, useState } from 'react';
import { Appearance, ScrollView, TouchableOpacity, View } from 'react-native';
import { Appearance, TouchableOpacity, View } from 'react-native';
import { ScrollView } from 'react-native-gesture-handler';
import Menu, { MenuItem } from 'react-native-reanimated-material-menu';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import ToggleSwitch from 'toggle-switch-react-native';
@@ -111,82 +112,8 @@ const SettingsAppearanceSection = () => {
Change the accent color of the app.
</Paragraph>
</View>
<ScrollView
horizontal={true}
showsHorizontalScrollIndicator={false}
onMoveShouldSetResponderCapture={() => {
tabBarRef.current?.setScrollEnabled(false);
}}
onMomentumScrollEnd={() => {
tabBarRef.current?.setScrollEnabled(true);
}}
style={{
borderRadius: 5,
padding: 5,
marginTop: 10,
marginBottom: pv + 5,
width: '100%',
paddingHorizontal: 12
}}
nestedScrollEnabled
contentContainerStyle={{
alignSelf: 'center',
flexDirection: 'row',
flexWrap: 'wrap'
}}
>
{[
'#FF5722',
'#FFA000',
'#1B5E20',
'#008837',
'#757575',
'#0560ff',
'#009688',
'#2196F3',
'#880E4F',
'#9C27B0',
'#FF1744',
'#B71C1C'
].map(item => (
<PressableButton
key={item}
customColor={
colors.accent === item
? RGB_Linear_Shade(!colors.night ? -0.2 : 0.2, hexToRGBA(item, 1))
: item
}
customSelectedColor={item}
alpha={!colors.night ? -0.1 : 0.1}
opacity={1}
onPress={async () => {
await PremiumService.verify(async () => {
changeAccentColor(item);
await MMKV.setStringAsync('accentColor', item);
});
}}
customStyle={{
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginRight: 10,
marginVertical: 5,
width: DDS.isLargeTablet() ? 40 : 50,
height: DDS.isLargeTablet() ? 40 : 50,
borderRadius: 100
}}
>
{colors.accent === item ? (
<Icon
size={DDS.isLargeTablet() ? SIZE.lg : SIZE.xxl}
color="white"
name="check"
/>
) : null}
</PressableButton>
))}
<View style={{ width: 50 }} />
</ScrollView>
<AccentColorPicker />
<CustomButton
title="Use system theme"
@@ -337,4 +264,99 @@ const SettingsAppearanceSection = () => {
);
};
export const AccentColorPicker = ({ settings = true }) => {
const [state, dispatch] = useTracked();
const { colors } = state;
function changeColorScheme(colors = COLOR_SCHEME, accent = ACCENT) {
let newColors = setColorScheme(colors, accent);
dispatch({ type: Actions.THEME, colors: newColors });
}
function changeAccentColor(accentColor) {
ACCENT.color = accentColor;
ACCENT.shade = accentColor + '12';
changeColorScheme();
}
return (
<ScrollView
horizontal={true}
showsHorizontalScrollIndicator={false}
onMoveShouldSetResponderCapture={() => {
if (!settings) return;
tabBarRef.current?.setScrollEnabled(false);
}}
onMomentumScrollEnd={() => {
if (!settings) return;
tabBarRef.current?.setScrollEnabled(true);
}}
style={{
borderRadius: 5,
padding: 5,
marginTop: 10,
marginBottom: pv + 5,
width: '100%',
paddingHorizontal: 12,
maxWidth: settings ? null : '100%'
}}
scrollEnabled={true}
nestedScrollEnabled={true}
contentContainerStyle={{
alignSelf: 'center',
flexDirection: 'row',
flexWrap: 'wrap'
}}
>
{[
'#FF5722',
'#FFA000',
'#1B5E20',
'#008837',
'#757575',
'#0560ff',
'#009688',
'#2196F3',
'#880E4F',
'#9C27B0',
'#FF1744',
'#B71C1C'
].map(item => (
<PressableButton
key={item}
customColor={
colors.accent === item
? RGB_Linear_Shade(!colors.night ? -0.2 : 0.2, hexToRGBA(item, 1))
: item
}
customSelectedColor={item}
alpha={!colors.night ? -0.1 : 0.1}
opacity={1}
onPress={async () => {
await PremiumService.verify(async () => {
changeAccentColor(item);
await MMKV.setStringAsync('accentColor', item);
});
}}
customStyle={{
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginRight: 10,
marginVertical: 5,
width: DDS.isLargeTablet() ? 40 : 50,
height: DDS.isLargeTablet() ? 40 : 50,
borderRadius: 100
}}
>
{colors.accent === item ? (
<Icon size={DDS.isLargeTablet() ? SIZE.lg : SIZE.xxl} color="white" name="check" />
) : null}
</PressableButton>
))}
<View style={{ width: 50 }} />
</ScrollView>
);
};
export default SettingsAppearanceSection;

View File

@@ -124,11 +124,17 @@ export const Settings = ({ navigation }) => {
component: <Issue />
});
},
desc: `Facing an issue? Click here to create a bug report`
desc: `Faced an issue or have a suggestion? Click here to create a bug report`
},
{
name: 'Join our Telegram group',
desc: "We are on telegram, let's talk",
func: () => {
Linking.openURL('https://t.me/notesnook').catch(console.log);
}
},
{
name: 'Join our Discord community',
func: async () => {
presentSheet({
title: 'Join our Discord Community',
@@ -144,7 +150,7 @@ export const Settings = ({ navigation }) => {
icon: 'discord',
action: async () => {
try {
await openLinkInBrowser('https://discord.gg/zQBK97EE22', colors);
Linking.openURL('https://discord.gg/zQBK97EE22').catch(console.log);
} catch (e) {}
},
actionText: 'Join Now'