refactor and redesign premium dialog

This commit is contained in:
ammarahm-ed
2021-11-11 13:08:28 +05:00
parent 0303e72b17
commit 352356724d
16 changed files with 1008 additions and 853 deletions

View File

@@ -28,14 +28,13 @@ import { AddTopicDialog } from '../AddTopicDialog';
import { AttachmentDialog } from '../AttachmentDialog';
import { Dialog } from '../Dialog';
import ExportDialog from '../ExportDialog';
import GeneralSheet from '../GeneralSheet';
import ImagePreview from '../ImagePreview';
import LoginDialog from '../LoginDialog';
import MergeEditor from '../MergeEditor';
import MoveNoteDialog from '../MoveNoteDialog';
import PendingDialog from '../Premium/PendingDialog';
import PremiumDialog from '../Premium/PremiumDialog';
import PremiumStatusDialog from '../Premium/PremiumStatusDialog';
import GeneralSheet from '../GeneralSheet';
import PremiumDialog from '../Premium';
import { Expiring } from '../Premium/expiring';
import PublishNoteDialog from '../PublishNoteDialog';
import RateDialog from '../RateDialog';
import RecoveryKeyDialog from '../RecoveryKeyDialog';
@@ -289,8 +288,6 @@ export class DialogManager extends Component {
<MergeEditor />
<ExportDialog />
<RecoveryKeyDialog colors={colors} />
<PendingDialog colors={colors} />
<PremiumStatusDialog />
<GeneralSheet />
<RestoreDialog />
<ResultDialog />
@@ -303,6 +300,7 @@ export class DialogManager extends Component {
<PublishNoteDialog />
<TagsDialog />
<AttachmentDialog />
<Expiring/>
</>
);
}

View File

@@ -1,90 +0,0 @@
import React, {createRef} from 'react';
import {View} from 'react-native';
import {eSubscribeEvent, eUnSubscribeEvent} from '../../services/EventManager';
import {db} from '../../utils/database';
import {eClosePendingDialog, eOpenPendingDialog} from '../../utils/Events';
import {SIZE} from '../../utils/SizeUtils';
import ActionSheetWrapper from '../ActionSheetComponent/ActionSheetWrapper';
import Seperator from '../Seperator';
import Heading from '../Typography/Heading';
import Paragraph from '../Typography/Paragraph';
const actionSheet = createRef();
class PendingDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
user: null,
product: null
};
}
async open() {
actionSheet.current?.setModalVisible(true);
let u = await db.user.getUser();
this.setState({
user: u && u.Id ? u : null
});
}
close() {
actionSheet.current?.setModalVisible(false);
this.setState({
user: null
});
}
async componentDidMount() {
eSubscribeEvent(eOpenPendingDialog, this.open.bind(this));
eSubscribeEvent(eClosePendingDialog, this.close.bind(this));
}
componentWillUnmount() {
eUnSubscribeEvent(eOpenPendingDialog, this.open);
eUnSubscribeEvent(eClosePendingDialog, this.close);
}
render() {
const {colors} = this.props;
return (
<ActionSheetWrapper fwdRef={actionSheet}>
<View
style={{
width: '100%',
backgroundColor: colors.bg,
justifyContent: 'space-between',
paddingHorizontal: 12,
borderRadius: 10,
paddingTop: 10
}}>
<Heading
size={SIZE.xxxl}
color={colors.accent}
style={{
paddingBottom: 20,
paddingTop: 10,
alignSelf: 'center'
}}>
Thank you!
</Heading>
<Seperator />
<Paragraph
size={SIZE.md}
style={{
fontSize: SIZE.md,
width: '80%',
alignSelf: 'center',
textAlign: 'center'
}}>
We are processing your subscription. You account will be upgraded to
Notesnook Pro very soon.
</Paragraph>
<Seperator />
</View>
</ActionSheetWrapper>
);
}
}
export default PendingDialog;

View File

@@ -1,546 +0,0 @@
import React, {useEffect, useRef, useState} from 'react';
import {Platform, View} from 'react-native';
import {FlatList} from 'react-native-gesture-handler';
import * as RNIap from 'react-native-iap';
import {useTracked} from '../../provider';
import {DDS} from '../../services/DeviceDetection';
import {eSendEvent, presentSheet, ToastEvent} from '../../services/EventManager';
import PremiumService from '../../services/PremiumService';
import {db} from '../../utils/database';
import {
eCloseProgressDialog,
eOpenLoginDialog,
eOpenProgressDialog
} from '../../utils/Events';
import {openLinkInBrowser} from '../../utils/functions';
import {normalize, SIZE} from '../../utils/SizeUtils';
import {sleep} from '../../utils/TimeUtils';
import {Button} from '../Button';
import {Dialog} from '../Dialog';
import {presentDialog} from '../Dialog/functions';
import {Toast} from '../Toast';
import Heading from '../Typography/Heading';
import Paragraph from '../Typography/Paragraph';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import {useUserStore} from '../../provider/stores';
const features = [
{
title: 'Unlimited file & image attachments'
},
{
title: 'Unlimited storage'
},
{
title: 'Unlimited notebooks and tags'
},
{
title: 'Automatic syncing'
},
{
title: 'Secure private vault for notes'
},
{
title: 'Full rich text editor with markdown'
},
{
title: 'Export notes in PDF, Markdown and HTML'
},
{
title: 'Automatic encrypted backups'
},
{
title: 'Change app theme & accent colors'
},
{
title: 'Pro badge on our Discord server'
}
];
const promoCyclesMonthly = {
1: 'first month',
2: 'first 2 months',
3: 'first 3 months'
};
const promoCyclesYearly = {
1: 'first year',
2: 'first 2 years',
3: 'first 3 years'
};
export const PremiumComponent = ({close, promo, getRef}) => {
const [state, dispatch] = useTracked();
const colors = state.colors;
const user = useUserStore(state => state.user);
const [product, setProduct] = useState(null);
const [products, setProducts] = useState([]);
const [buying, setBuying] = useState(false);
const scrollViewRef = useRef();
const [offers, setOffers] = useState(null);
const getSkus = async () => {
try {
let products = PremiumService.getProducts();
if (products.length > 0) {
let offers = {
monthly: products.find(
p => p.productId === 'com.streetwriters.notesnook.sub.mo'
),
yearly: products.find(
p => p.productId === 'com.streetwriters.notesnook.sub.yr'
)
};
setOffers(offers);
if (promo?.promoCode) {
getPromo(promo.promoCode);
} else {
setProduct({
type: 'monthly',
data: offers.monthly
});
}
setProducts(products);
}
} catch (e) {
console.log('error getting sku', e);
}
};
const getPromo = productId => {
let product = products.find(p => p.productId === productId);
let isMonthly = product.productId.indexOf('.mo') > -1;
let cycleText = isMonthly
? promoCyclesMonthly[
product.introductoryPriceCyclesAndroid ||
product.introductoryPriceNumberOfPeriodsIOS
]
: promoCyclesYearly[
product.introductoryPriceCyclesAndroid ||
product.introductoryPriceNumberOfPeriodsIOS
];
setProduct({
type: 'promo',
offerType: isMonthly ? 'monthly' : 'yearly',
data: product,
cycleText: cycleText
});
};
useEffect(() => {
getSkus();
}, []);
const buySubscription = async () => {
if (buying) return;
if (!user) {
close();
setTimeout(() => {
eSendEvent(eOpenLoginDialog, 1);
}, 400);
} else {
setBuying(true);
RNIap.requestSubscription(
product?.data.productId,
false,
null,
null,
null,
user.id
)
.then(async r => {
setBuying(false);
close();
await sleep(1000);
presentSheet({
title: 'Thank you for subscribing!',
paragraph: `Your Notesnook Pro subscription will be activated within a few hours. If your account is not upgraded to Notesnook Pro, your money will be refunded to you. In case of any issues, please reach out to us at support@streetwriters.co`,
action: async () => {
eSendEvent(eCloseProgressDialog);
},
icon: 'check',
actionText: 'Continue',
noProgress: true
});
})
.catch(e => {
setBuying(false);
console.log(e);
});
}
};
return (
<View
style={{
width: '100%',
backgroundColor: colors.bg,
justifyContent: 'space-between',
borderRadius: 10,
maxHeight: DDS.isTab ? '90%' : '100%'
}}>
<Dialog context="local" />
<Heading
size={SIZE.xxxl}
style={{
alignSelf: 'center'
}}>
Notesnook{' '}
<Heading size={SIZE.xxxl} color={colors.accent}>
Pro
</Heading>
</Heading>
<Paragraph
size={SIZE.md}
style={{
paddingHorizontal: 12,
textAlign: 'center',
alignSelf: 'center',
paddingBottom: 20,
width: '90%'
}}>
Ready to take the next step on your private note taking journey?
</Paragraph>
<FlatList
keyExtractor={item => item.title}
data={features}
ref={scrollViewRef}
nestedScrollEnabled
style={{
borderBottomWidth: 1,
borderBottomColor: colors.nav,
paddingVertical: 10,
borderTopWidth: 1,
borderTopColor: colors.nav
}}
onMomentumScrollEnd={() => {
getRef().current?.handleChildScrollEnd();
}}
renderItem={({item, index}) => <RenderItem item={item} index={index} />}
/>
<View
style={{
borderRadius: 10,
paddingHorizontal: 12
}}>
{product?.type !== 'promo' ? (
user ? (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 10
}}>
<Paragraph
onPress={() => {
setProduct({
type: 'monthly',
data: offers?.monthly
});
}}
style={{
color:
product?.type == 'monthly' ? colors.accent : colors.pri,
fontWeight: product?.type == 'monthly' ? 'bold' : 'normal',
paddingVertical: 15,
minWidth: 100,
textAlign: 'right'
}}>
Monthly
</Paragraph>
<Paragraph size={20} style={{paddingHorizontal: 12}}>
{' | '}
</Paragraph>
<Paragraph
onPress={() => {
setProduct({
type: 'yearly',
data: offers?.yearly
});
}}
style={{
color: product?.type == 'yearly' ? colors.accent : colors.pri,
fontWeight: product?.type == 'yearly' ? 'bold' : 'normal',
paddingVertical: 15,
minWidth: 100,
textAlign: 'left'
}}>
Yearly
</Paragraph>
</View>
) : null
) : (
<Heading
style={{
paddingVertical: 15,
alignSelf: 'center',
textAlign: 'center'
}}
size={SIZE.lg - 4}>
{product.data.introductoryPrice}
<Paragraph
style={{
textDecorationLine: 'line-through',
color: colors.icon
}}
size={SIZE.sm}>
({product.data.localizedPrice})
</Paragraph>{' '}
for {product.cycleText}
</Heading>
)}
{product?.data ? (
<>
<Button
onPress={buySubscription}
fontSize={SIZE.lg}
loading={buying}
title={
promo
? promo.text
: user
? `Subscribe for ${product?.data?.localizedPrice} / ${
product.type === 'yearly' ||
product.offerType === 'yearly'
? 'yr'
: 'mo'
}`
: 'Try free for 14 days'
}
type="accent"
height={normalize(60)}
width="100%"
/>
{user ? (
<Button
height={35}
style={{
marginTop: 10
}}
onPress={() => {
presentDialog({
context: 'local',
input: true,
inputPlaceholder: 'Enter code',
positiveText: 'Apply',
positivePress: async value => {
if (!value) return;
console.log(value);
try {
let productId = await db.offers.getCode(
value,
Platform.OS
);
if (productId) {
getPromo(productId);
ToastEvent.show({
heading: 'Discount applied!',
type: 'success',
context: 'local'
});
} else {
ToastEvent.show({
heading: 'Promo code invalid or expired',
type: 'error',
context: 'local'
});
}
} catch (e) {
ToastEvent.show({
heading: 'Promo code invalid or expired',
message: e.message,
type: 'error',
context: 'local'
});
}
},
title: 'Have a promo code?',
paragraph:
'Enter your promo code to get a special discount.'
});
}}
title="I have a promo code"
/>
) : null}
</>
) : (
<Paragraph
color={colors.icon}
style={{
alignSelf: 'center',
height: 50
}}>
This subscription is unavailable at the moment
</Paragraph>
)}
{!user ? (
<Paragraph
color={colors.icon}
size={SIZE.xs + 1}
style={{
alignSelf: 'center',
textAlign: 'center',
marginTop: 10
}}>
Upon signing up, your 14 day free trial of Notesnook Pro will be
activated automatically.{' '}
<Paragraph size={SIZE.xs + 1} style={{fontWeight: 'bold'}}>
No credit card information is required.
</Paragraph>{' '}
Once the free trial period ends, your account will be downgraded to
basic free account.{' '}
<Paragraph
size={SIZE.xs + 1}
onPress={() => {
openLinkInBrowser('https://notesnook.com/#pricing', colors)
.catch(e => {})
.then(r => {
console.log('closed');
});
}}
color={colors.accent}
style={{fontWeight: 'bold', textDecorationLine: 'underline'}}>
Visit our website to learn what is included in the basic free
account.
</Paragraph>
</Paragraph>
) : null}
{user ? (
<>
{Platform.OS === 'ios' ? (
<Paragraph
textBreakStrategy="balanced"
size={SIZE.xs + 1}
color={colors.icon}
style={{
alignSelf: 'center',
marginTop: 10,
textAlign: 'center'
}}>
By tapping Subscribe,
<Paragraph size={SIZE.xs + 1} color={colors.accent}>
{product?.data?.localizedPrice}
</Paragraph>{' '}
will be charged to your iTunes Account for 1-
{product?.type === 'yearly' ? 'year' : 'month'} subscription of
Notesnook Pro.{'\n\n'}
Subscriptions will automatically renew unless cancelled within
24-hours before the end of the current period. You can cancel
anytime with your iTunes Account settings.
</Paragraph>
) : (
<Paragraph
textBreakStrategy="balanced"
size={SIZE.xs + 1}
color={colors.icon}
style={{
alignSelf: 'center',
marginTop: 10,
textAlign: 'center'
}}>
By tapping Subscribe, your payment will be charged on your
Google Account, and your subscription will automatically renew
for the same package length at the same price until you cancel
in settings in the Android Play Store prior to the end of the
then current period.
</Paragraph>
)}
<View
style={{
backgroundColor: colors.nav,
width: '100%',
paddingVertical: 10,
marginTop: 5,
borderRadius: 5,
paddingHorizontal: 12
}}>
<Paragraph
size={SIZE.xs + 1}
color={colors.icon}
style={{
maxWidth: '100%',
textAlign: 'center'
}}>
By subscribing, you agree to our{' '}
<Paragraph
size={SIZE.xs + 1}
onPress={() => {
openLinkInBrowser('https://notesnook.com/tos', colors)
.catch(e => {})
.then(r => {
console.log('closed');
});
}}
style={{
textDecorationLine: 'underline'
}}
color={colors.accent}>
Terms of Service{' '}
</Paragraph>
and{' '}
<Paragraph
size={SIZE.xs + 1}
onPress={() => {
openLinkInBrowser('https://notesnook.com/privacy', colors)
.catch(e => {})
.then(r => {
console.log('closed');
});
}}
style={{
textDecorationLine: 'underline'
}}
color={colors.accent}>
Privacy Policy.
</Paragraph>
</Paragraph>
</View>
</>
) : null}
</View>
<Toast context="local" />
<View
style={{
paddingBottom: 10
}}
/>
</View>
);
};
const RenderItem = React.memo(
({item, index}) => {
const [state] = useTracked();
const colors = state.colors;
return (
<View
style={{
paddingHorizontal: 24,
paddingVertical: 8,
flexDirection: 'row',
alignItems: 'center'
}}>
<Icon size={SIZE.lg} color={colors.accent} name="check" />
<Paragraph
style={{
marginLeft: 10
}}
size={SIZE.md}>
{item.title}
</Paragraph>
</View>
);
},
() => true
);

View File

@@ -1,47 +0,0 @@
import React, {createRef} from 'react';
import ActionSheetWrapper from '../ActionSheetComponent/ActionSheetWrapper';
import {PremiumComponent} from './PremiumComponent';
class PremiumDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
promo:null
};
this.actionSheetRef = createRef();
}
open(promoInfo) {
console.log(promoInfo)
this.setState(
{
visible: true,
promo:promoInfo
},
() => {
this.actionSheetRef.current?.setModalVisible(true);
},
);
}
close = () => {
this.actionSheetRef.current?.setModalVisible(false);
};
onClose = () => {
this.setState({
visible: false,
});
};
render() {
return !this.state.visible ? null : (
<ActionSheetWrapper onClose={this.onClose} fwdRef={this.actionSheetRef}>
<PremiumComponent getRef={() => this.actionSheetRef} promo={this.state.promo} close={this.close} />
</ActionSheetWrapper>
);
}
}
export default PremiumDialog;

View File

@@ -1,132 +0,0 @@
import React, { useEffect, useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { useTracked } from '../../provider';
import { getElevation } from '../../utils';
import { ph, pv, SIZE } from '../../utils/SizeUtils';
import BaseDialog from '../Dialog/base-dialog';
import DialogContainer from '../Dialog/dialog-container';
import Seperator from '../Seperator';
import Heading from '../Typography/Heading';
import Paragraph from '../Typography/Paragraph';
const {
eSubscribeEvent,
eUnSubscribeEvent,
} = require('../../services/EventManager');
const {
eOpenPremiumStatusDialog,
eClosePremiumStatusDialog,
} = require('../../utils/Events');
const PremiumStatusDialog = () => {
const [state] = useTracked();
const {colors} = state;
const [visible, setVisible] = useState(false);
useEffect(() => {
eSubscribeEvent(eOpenPremiumStatusDialog, open);
eSubscribeEvent(eClosePremiumStatusDialog, close);
return () => {
eUnSubscribeEvent(eOpenPremiumStatusDialog, open);
eUnSubscribeEvent(eClosePremiumStatusDialog, close);
};
}, []);
const open = () => {
setVisible(true);
};
const close = () => {
setVisible(false);
};
return (
visible && (
<BaseDialog visible={true} onRequestClose={close}>
<DialogContainer>
<View
style={[
{
justifyContent: 'center',
alignItems: 'center',
},
]}>
<View style={styles.headingContainer}>
<Heading color={colors.accent} style={styles.heading}>
Notesnook Pro
</Heading>
</View>
<Seperator />
<Paragraph
style={{
textAlign: 'center',
width: '90%',
alignSelf: 'center',
}}>
Your account has been upgraded to Notesnook Pro successfully. Now
you can enjoy all premium features!
</Paragraph>
<Seperator />
</View>
</DialogContainer>
</BaseDialog>
)
);
};
const styles = StyleSheet.create({
wrapper: {
width: '100%',
height: '100%',
backgroundColor: 'rgba(0,0,0,0.3)',
justifyContent: 'center',
alignItems: 'center',
},
container: {
...getElevation(5),
maxHeight: 350,
borderRadius: 5,
paddingHorizontal: ph,
paddingVertical: pv,
},
headingContainer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
buttonContainer: {
justifyContent: 'space-between',
alignItems: 'center',
},
heading: {
//fontFamily: "sans-serif",
fontWeight:'bold',
marginLeft: 5,
fontSize: SIZE.xxxl,
},
button: {
paddingVertical: pv,
paddingHorizontal: ph,
marginTop: 10,
borderRadius: 5,
width: '100%',
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
flexDirection: 'row',
},
buttonText: {
fontFamily: "sans-serif",
color: 'white',
fontSize: SIZE.sm,
marginLeft: 5,
},
overlay: {
width: '100%',
height: '100%',
position: 'absolute',
},
});
export default PremiumStatusDialog;

View File

@@ -1,31 +0,0 @@
import React from 'react';
import { View } from 'react-native';
import { useTracked } from '../../provider';
import { SIZE } from '../../utils/SizeUtils';
import Paragraph from '../Typography/Paragraph';
export const PremiumTag = ({pro}) => {
const [state, dispatch] = useTracked();
const {colors} = state;
return !pro ? (
<View
style={{
backgroundColor: colors.accent,
paddingVertical: 5,
alignItems: 'center',
borderRadius: 5,
marginRight: 20,
elevation: 1,
}}>
<Paragraph
size={SIZE.xs}
style={{
color: 'white',
paddingHorizontal: 4,
}}>
PRO
</Paragraph>
</View>
) : null;
};

View File

@@ -0,0 +1,163 @@
import React, { useState } from 'react';
import { Image, ScrollView, View } from 'react-native';
import { useTracked } from '../../provider';
import { DDS } from '../../services/DeviceDetection';
import { eSendEvent, presentSheet } from '../../services/EventManager';
import { getElevation } from '../../utils';
import { eOpenLoginDialog } from '../../utils/Events';
import { SIZE } from '../../utils/SizeUtils';
import { ActionIcon } from '../ActionIcon';
import { Button } from '../Button';
import { Dialog } from '../Dialog';
import GeneralSheet from '../GeneralSheet';
import Seperator from '../Seperator';
import { Toast } from '../Toast';
import Heading from '../Typography/Heading';
import Paragraph from '../Typography/Paragraph';
import { features } from './features';
import { Group } from './group';
import { PricingPlans } from './pricing-plans';
export const Component = ({close, promo, getRef}) => {
const [state, dispatch] = useTracked();
const colors = state.colors;
const user = {}; //useUserStore(state => state.user);
const [floatingButton, setFloatingButton] = useState(false);
const onPress = async () => {
if (user) {
presentSheet({
context: 'pricing_plans',
component: <PricingPlans marginTop={1} promo={promo} />,
noIcon: true,
noProgress: true
});
} else {
close();
setTimeout(() => {
eSendEvent(eOpenLoginDialog, 1);
}, 400);
}
};
const onScroll = event => {
let contentSize = event.nativeEvent.contentSize.height;
contentSize = contentSize - event.nativeEvent.layoutMeasurement.height;
let yOffset = event.nativeEvent.contentOffset.y;
if (yOffset > 600 && yOffset < contentSize - 400) {
setFloatingButton(true);
} else {
setFloatingButton(false);
}
};
return (
<View
style={{
width: '100%',
backgroundColor: colors.bg,
justifyContent: 'space-between',
borderRadius: 10,
maxHeight: DDS.isTab ? '90%' : '100%'
}}>
<GeneralSheet context="pricing_plans" />
<ActionIcon
onPress={() => {
close();
}}
customStyle={{
position: 'absolute',
right: 15,
top: 30,
zIndex: 10,
width: 50,
height: 50
}}
color={colors.pri}
name="close"
/>
<ScrollView onScroll={onScroll}>
<Image
source={require('../../assets/images/to_the_stars.png')}
style={{
width: '100%',
height: 400
}}
/>
<Heading
size={SIZE.lg}
style={{
alignSelf: 'center',
paddingTop: 20
}}>
Notesnook{' '}
<Heading size={SIZE.lg} color={colors.accent}>
Pro
</Heading>
</Heading>
<Paragraph
size={SIZE.md}
style={{
paddingHorizontal: 12,
textAlign: 'center',
alignSelf: 'center',
paddingBottom: 20,
width: '90%'
}}>
Ready to take the next step on your private note taking journey?
</Paragraph>
<Button
onPress={onPress}
title={
promo ? promo.text : user ? `See all plans` : 'Try free for 14 days'
}
type="accent"
style={{
paddingHorizontal: 24,
marginBottom: 10
}}
/>
<Seperator />
{features.map((item, index) => (
<Group item={item} index={index} />
))}
<View
style={{
paddingHorizontal: 12
}}>
<PricingPlans promo={promo} />
</View>
</ScrollView>
{floatingButton ? (
<Button
onPress={onPress}
title={
promo ? promo.text : user ? `See all plans` : 'Try free for 14 days'
}
type="accent"
style={{
paddingHorizontal: 24,
position: 'absolute',
bottom: 30,
...getElevation(10)
}}
/>
) : null}
<Toast context="local" />
<View
style={{
paddingBottom: 10
}}
/>
</View>
);
};

View File

@@ -0,0 +1,50 @@
import React from 'react';
import {Text, View} from 'react-native';
import {color} from 'react-native-reanimated';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import {useTracked} from '../../provider';
import {SIZE} from '../../utils/SizeUtils';
import Seperator from '../Seperator';
import Paragraph from '../Typography/Paragraph';
import {ProTag} from './pro-tag';
export const FeatureBlock = ({highlight, content, icon, pro, proTagBg}) => {
const [state, dispatch] = useTracked();
const {colors} = state;
return (
<View
style={{
height: 100,
justifyContent: 'center',
padding: 10,
marginRight: 10,
borderRadius: 5,
minWidth: 100
}}>
<Icon color={colors.icon} name={icon} size={SIZE.xl} />
<Paragraph size={SIZE.md}>
<Text style={{color: colors.accent}}>{highlight}</Text>
{'\n'}
{content}
</Paragraph>
{pro ? (
<>
<View style={{height: 5}} />
<ProTag width={50} size={SIZE.xs} background={proTagBg} />
</>
) : (
<View
style={{
width: 30,
height: 3,
marginTop: 10,
borderRadius: 100,
backgroundColor: colors.accent
}}
/>
)}
</View>
);
};

View File

@@ -0,0 +1,166 @@
export const features = [
{
title: 'Focused on privacy',
detail:
'Everything you do in Notesnook stays private. We use XChaCha20-Poly1305-IETF and Argon2 to encrypt your notes.',
features: [
{
highlight: 'Zero ads',
content: '& zero trackers',
icon: 'billboard'
},
{
highlight: 'On device',
content: 'encryption',
icon: 'cellphone'
},
{
highlight: 'Secure app',
content: 'lock for all',
icon: 'cellphone-lock'
},
{
highlight: '100% end-to-end ',
content: 'encrypted',
icon: 'lock'
},
{
highlight: 'Password protected',
content: 'notes sharing',
icon: 'file-lock'
},
{
highlight: 'Private vault',
content: 'for notes',
icon: 'shield-lock'
}
]
},
{
title: 'Keep your files where they belong',
detail:
'Organize and simplify your work. Securely access your files from any device, anywhere without compromising on privacy.',
pro: true,
features: [
{
highlight: 'Bullet proof',
content: 'encryption',
icon: 'lock'
},
{
highlight: 'High quality',
content: '4k images',
icon: 'image-multiple'
},
{
highlight: 'No monthly',
content: 'storage limit',
icon: 'harddisk'
},
{
highlight: 'Generous 500 MB',
content: 'max file size',
icon: 'file-cabinet'
},
{
highlight: 'No restriction',
content: 'on file type',
icon: 'file'
}
]
},
{
title: 'No limit on notes for free users',
detail:
"Basic or Pro, you can create unlimited number of notes. You won't be running out of space or blocks ever."
},
{
title: 'Find what you need, when you need to',
detail:
'You are not limited on how you want to organize your notes. Use tags, notebooks, topics, colors.',
features: [
{
highlight: 'Unlimited',
content: 'notebooks & tags*',
icon: 'emoticon',
pro: true
},
{
highlight: 'Organize',
content: 'with colors',
icon: 'palette',
pro: true
},
{
highlight: 'Side menu',
content: 'shortcuts',
icon: 'link-variant'
}
],
info: '* Free users are limited to keeping 3 notebooks (no limit on topics) and 5 tags.'
},
{
title: 'Instant syncing',
detail:
'Seemlessly work from anywhere. Every change is synced instantly everywhere.',
pro: true
},
{
title: 'Write better, faster and smarter',
detail:
'With our powerful rich text editor, edit your notes with advanced features like tables, checklists, images and videos.',
features: [
{
highlight: 'Basic formating',
content: 'and lists',
icon: 'format-bold'
},
{
highlight: 'Advanced editing',
content: 'features',
icon: 'table',
pro: true
},
{
highlight: 'Markdown',
content: 'support',
icon: 'language-markdown',
pro: true
}
]
},
{
title: 'You own your data, take it anywhere',
pro: true,
detail:
'Export notes in the most commonly used formats such as PDF, Markdown and HTML.',
info: '* Free users can export notes in well formatted plain text.'
},
{
title: 'Automatic backups',
detail:
'Do not worry about losing your data. Turn on automatic backups on weekly or daily basis.',
features: [
{
highlight: 'Backup',
content: 'encryption',
icon: 'backup-restore'
}
],
pro: true
},
{
title: 'Personalize',
detail:
'Change app accent color from default to match your mood and use automatic dark mode.',
pro: true,
features: [
{
highlight: 'Automatic',
content: 'dark mode',
icon: 'theme-light-dark'
}
]
}
];

View File

@@ -0,0 +1,60 @@
import React from 'react';
import {ScrollView, View} from 'react-native';
import {useTracked} from '../../provider';
import {SIZE} from '../../utils/SizeUtils';
import Heading from '../Typography/Heading';
import Paragraph from '../Typography/Paragraph';
import {FeatureBlock} from './feature';
import {ProTag} from './pro-tag';
export const Group = ({item, index}) => {
const [state] = useTracked();
const colors = state.colors;
return (
<View
style={{
paddingHorizontal: 24,
paddingVertical: 8,
backgroundColor: index % 2 !== 0 ? colors.bg : colors.nav,
paddingVertical: 40
}}>
{item?.pro && (
<ProTag
size={SIZE.sm}
background={index % 2 === 0 ? colors.bg : colors.nav}
/>
)}
<Heading>{item.title}</Heading>
<Paragraph size={SIZE.md}>{item.detail}</Paragraph>
{item.features && (
<ScrollView
style={{
marginTop: 20
}}
horizontal
showsHorizontalScrollIndicator={false}>
{item.features?.map(item => (
<FeatureBlock
{...item}
detail={item.detail}
pro={item.pro}
proTagBg={index % 2 === 0 ? colors.bg : colors.nav}
/>
))}
</ScrollView>
)}
{item.info ? (
<Paragraph
style={{
marginTop: 10
}}
size={SIZE.xs + 1}
color={colors.icon}>
{item.info}
</Paragraph>
) : null}
</View>
);
};

View File

@@ -0,0 +1,51 @@
import React, {createRef} from 'react';
import BaseDialog from '../Dialog/base-dialog';
import {Component} from './component';
class PremiumDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
promo: null
};
this.actionSheetRef = createRef();
}
open(promoInfo) {
console.log(promoInfo);
this.setState({
visible: true,
promo: promoInfo
});
}
close = () => {
this.setState({
visible: false,
promo: null
});
};
onClose = () => {
this.setState({
visible: false
});
};
render() {
return !this.state.visible ? null : (
<BaseDialog
background={this.props.colors.bg}
onRequestClose={this.onClose}>
<Component
getRef={() => this.actionSheetRef}
promo={this.state.promo}
close={this.close}
/>
</BaseDialog>
);
}
}
export default PremiumDialog;

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { Text } from 'react-native';
import { useTracked } from '../../provider';
import { SIZE } from '../../utils/SizeUtils';
import Paragraph from '../Typography/Paragraph';
export const Offer = ({off = '30', text = 'on both yearly & monthly plans',padding=0}) => {
const [state, dispatch] = useTracked();
const {colors} = state;
return (
<Paragraph
style={{
textAlign: 'center',
paddingVertical:padding
}}
size={SIZE.xxxl}>
GET {off}
<Text style={{color: colors.accent}}>%</Text> OFF!{'\n'}
<Paragraph color={colors.icon}>{text}</Paragraph>
</Paragraph>
);
};

View File

@@ -0,0 +1,43 @@
import React from 'react';
import {View} from 'react-native';
import {useTracked} from '../../provider';
import {SIZE} from '../../utils/SizeUtils';
import {PressableButton} from '../PressableButton';
import Heading from '../Typography/Heading';
import Paragraph from '../Typography/Paragraph';
export const PricingItem = ({product, onPress}) => {
const [state, dispatch] = useTracked();
const colors = state.colors;
return (
<PressableButton
onPress={onPress}
customStyle={{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
paddingHorizontal: 12,
paddingVertical: 10
}}>
<View>
<Heading size={SIZE.lg - 2}>
{product.type === 'yearly' || product.offerType === 'yearly'
? 'Yearly'
: 'Monthly'}
</Heading>
{product.info && (
<Paragraph size={SIZE.xs + 1}>{product.info}</Paragraph>
)}
</View>
<View>
<Paragraph size={SIZE.sm}>
<Heading size={SIZE.lg - 2}>{product?.data?.localizedPrice}/</Heading>
{product.type === 'yearly' || product.offerType === 'yearly'
? '/year'
: '/month'}
</Paragraph>
</View>
</PressableButton>
);
};

View File

@@ -0,0 +1,382 @@
import React, {useEffect, useState} from 'react';
import {Platform, View} from 'react-native';
import * as RNIap from 'react-native-iap';
import {useTracked} from '../../provider';
import {
eSendEvent,
presentSheet,
ToastEvent
} from '../../services/EventManager';
import PremiumService from '../../services/PremiumService';
import {db} from '../../utils/database';
import {eCloseProgressDialog, eOpenLoginDialog} from '../../utils/Events';
import {openLinkInBrowser} from '../../utils/functions';
import {SIZE} from '../../utils/SizeUtils';
import {sleep} from '../../utils/TimeUtils';
import {Button} from '../Button';
import {Dialog} from '../Dialog';
import {presentDialog} from '../Dialog/functions';
import Heading from '../Typography/Heading';
import Paragraph from '../Typography/Paragraph';
import {PricingItem} from './pricing-item';
const promoCyclesMonthly = {
1: 'first month',
2: 'first 2 months',
3: 'first 3 months'
};
const promoCyclesYearly = {
1: 'first year',
2: 'first 2 years',
3: 'first 3 years'
};
export const PricingPlans = ({promo, marginTop}) => {
const [state, dispatch] = useTracked();
const colors = state.colors;
const user = {}; //useUserStore(state => state.user);
const [product, setProduct] = useState(null);
const [products, setProducts] = useState([]);
const [offers, setOffers] = useState(null);
const [buying, setBuying] = useState(false);
const getSkus = async () => {
try {
let products = PremiumService.getProducts();
if (products.length > 0) {
let offers = {
monthly: products.find(
p => p.productId === 'com.streetwriters.notesnook.sub.mo'
),
yearly: products.find(
p => p.productId === 'com.streetwriters.notesnook.sub.yr'
)
};
setOffers(offers);
if (promo?.promoCode) {
getPromo(promo?.promoCode);
}
setProducts(products);
}
} catch (e) {
console.log('error getting sku', e);
}
};
const getPromo = productId => {
let product = products.find(p => p.productId === productId);
let isMonthly = product.productId.indexOf('.mo') > -1;
let cycleText = isMonthly
? promoCyclesMonthly[
product.introductoryPriceCyclesAndroid ||
product.introductoryPriceNumberOfPeriodsIOS
]
: promoCyclesYearly[
product.introductoryPriceCyclesAndroid ||
product.introductoryPriceNumberOfPeriodsIOS
];
setProduct({
type: 'promo',
offerType: isMonthly ? 'monthly' : 'yearly',
data: product,
cycleText: cycleText,
info: 'Pay monthly, cancel anytime'
});
};
useEffect(() => {
getSkus();
}, []);
const buySubscription = async product => {
if (buying) return;
setBuying(true);
try {
await RNIap.requestSubscription(
product?.productId,
false,
null,
null,
null,
user.id
);
setBuying(false);
close();
await sleep(500);
presentSheet({
title: 'Thank you for subscribing!',
paragraph: `Your Notesnook Pro subscription will be activated within a few hours. If your account is not upgraded to Notesnook Pro, your money will be refunded to you. In case of any issues, please reach out to us at support@streetwriters.co`,
action: async () => {
eSendEvent(eCloseProgressDialog);
},
icon: 'check',
actionText: 'Continue',
noProgress: true
});
} catch (e) {
setBuying(false);
console.log(e);
}
};
return (
<View
style={{
paddingHorizontal: 12
}}>
{product?.type === 'promo' ? (
<Heading
style={{
paddingVertical: 15,
alignSelf: 'center',
textAlign: 'center'
}}
size={SIZE.lg - 4}>
{product.data.introductoryPrice}
<Paragraph
style={{
textDecorationLine: 'line-through',
color: colors.icon
}}
size={SIZE.sm}>
({product.data.localizedPrice})
</Paragraph>{' '}
for {product.cycleText}
</Heading>
) : null}
{user && !product ? (
<>
<Heading
style={{
alignSelf: 'center',
marginTop: marginTop || 20,
marginBottom: 20
}}>
Choose a plan
</Heading>
<PricingItem
onPress={() => buySubscription(offers?.monthly)}
product={{
type: 'monthly',
data: offers?.monthly,
info: 'Pay monthly, cancel anytime.'
}}
/>
<View
style={{
height: 1,
backgroundColor: colors.nav,
marginVertical: 10
}}
/>
<PricingItem
onPress={() => buySubscription(offers?.yearly)}
product={{
type: 'yearly',
data: offers?.yearly,
info: 'Pay yearly'
}}
/>
<Button
height={35}
style={{
marginTop: 10
}}
onPress={() => {
presentDialog({
context: 'local',
input: true,
inputPlaceholder: 'Enter code',
positiveText: 'Apply',
positivePress: async value => {
if (!value) return;
console.log(value);
try {
let productId = await db.offers.getCode(value, Platform.OS);
if (productId) {
getPromo(productId);
ToastEvent.show({
heading: 'Discount applied!',
type: 'success',
context: 'local'
});
} else {
ToastEvent.show({
heading: 'Promo code invalid or expired',
type: 'error',
context: 'local'
});
}
} catch (e) {
ToastEvent.show({
heading: 'Promo code invalid or expired',
message: e.message,
type: 'error',
context: 'local'
});
}
},
title: 'Have a promo code?',
paragraph: 'Enter your promo code to get a special discount.'
});
}}
title="I have a promo code"
/>
</>
) : (
<View>
<PricingItem
product={product}
onPress={() => buySubscription(product)}
/>
<Button
onPress={() => {
setProduct(null);
}}
height={30}
type="errorShade"
title="Cancel promo code"
/>
</View>
)}
{!user ? (
<Paragraph
color={colors.icon}
size={SIZE.xs + 1}
style={{
alignSelf: 'center',
textAlign: 'center',
marginTop: 10
}}>
Upon signing up, your 14 day free trial of Notesnook Pro will be
activated automatically.{' '}
<Paragraph size={SIZE.xs + 1} style={{fontWeight: 'bold'}}>
No credit card information is required.
</Paragraph>{' '}
Once the free trial period ends, your account will be downgraded to
basic free account.{' '}
<Paragraph
size={SIZE.xs + 1}
onPress={() => {
openLinkInBrowser('https://notesnook.com/#pricing', colors)
.catch(e => {})
.then(r => {
console.log('closed');
});
}}
color={colors.accent}
style={{fontWeight: 'bold', textDecorationLine: 'underline'}}>
Visit our website to learn what is included in the basic free
account.
</Paragraph>
</Paragraph>
) : null}
{user ? (
<>
{Platform.OS === 'ios' ? (
<Paragraph
textBreakStrategy="balanced"
size={SIZE.xs + 1}
color={colors.icon}
style={{
alignSelf: 'center',
marginTop: 10,
textAlign: 'center'
}}>
By tapping Subscribe,
<Paragraph size={SIZE.xs + 1} color={colors.accent}>
{product?.data?.localizedPrice}
</Paragraph>{' '}
will be charged to your iTunes Account for 1-
{product?.type === 'yearly' ? 'year' : 'month'} subscription of
Notesnook Pro.{'\n\n'}
Subscriptions will automatically renew unless cancelled within
24-hours before the end of the current period. You can cancel
anytime with your iTunes Account settings.
</Paragraph>
) : (
<Paragraph
textBreakStrategy="balanced"
size={SIZE.xs + 1}
color={colors.icon}
style={{
alignSelf: 'center',
marginTop: 10,
textAlign: 'center'
}}>
By tapping Subscribe, your payment will be charged on your Google
Account, and your subscription will automatically renew for the
same package length at the same price until you cancel in settings
in the Android Play Store prior to the end of the then current
period.
</Paragraph>
)}
<View
style={{
backgroundColor: colors.nav,
width: '100%',
paddingVertical: 10,
marginTop: 5,
borderRadius: 5,
paddingHorizontal: 12
}}>
<Paragraph
size={SIZE.xs + 1}
color={colors.icon}
style={{
maxWidth: '100%',
textAlign: 'center'
}}>
By subscribing, you agree to our{' '}
<Paragraph
size={SIZE.xs + 1}
onPress={() => {
openLinkInBrowser('https://notesnook.com/tos', colors)
.catch(e => {})
.then(r => {
console.log('closed');
});
}}
style={{
textDecorationLine: 'underline'
}}
color={colors.accent}>
Terms of Service{' '}
</Paragraph>
and{' '}
<Paragraph
size={SIZE.xs + 1}
onPress={() => {
openLinkInBrowser('https://notesnook.com/privacy', colors)
.catch(e => {})
.then(r => {
console.log('closed');
});
}}
style={{
textDecorationLine: 'underline'
}}
color={colors.accent}>
Privacy Policy.
</Paragraph>
</Paragraph>
</View>
</>
) : null}
<Dialog context="local" />
</View>
);
};

View File

@@ -0,0 +1,35 @@
import React from 'react';
import {View} from 'react-native';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import {useTracked} from '../../provider';
import Paragraph from '../Typography/Paragraph';
export const ProTag = ({width, size, background}) => {
const [state] = useTracked();
const colors = state.colors;
return (
<View
style={{
backgroundColor: background || colors.bg,
borderRadius: 100,
width:width || 60,
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 2.5,
flexDirection: 'row'
}}>
<Icon
style={{
marginRight: 3
}}
size={size}
color={colors.accent}
name="crown"
/>
<Paragraph size={size - 1.5} color={colors.accent}>
PRO
</Paragraph>
</View>
);
};

View File

@@ -7,6 +7,7 @@ import {db} from '../utils/database';
import {
eOpenPremiumDialog,
eOpenProgressDialog,
eOpenTrialEndingDialog,
eShowGetPremium
} from '../utils/Events';
import {MMKV} from '../utils/mmkv';
@@ -273,6 +274,34 @@ const subscriptions = {
}
};
async function getRemainingTrialDaysStatus() {
let user = await db.user.getUser();
if (!user) return;
let total = user.subscription.expiry - user.subscription.start;
let current = Date.now() - user.subscription.start;
current = ((current / total) * 100).toFixed(0);
let lastTrialDialogShownAt = await MMKV.getItem('lastTrialDialogShownAt');
if (current > 75 && lastTrialDialogShownAt !== 'ending') {
eSendEvent(eOpenTrialEndingDialog, {
title: 'Your trial is ending soon',
offer: null,
extend: false
});
MMKV.setItem('lastTrialDialogShownAt', 'ending');
} else if (!get() && lastTrialDialogShownAt !== 'expired') {
eSendEvent(eOpenTrialEndingDialog, {
title: 'Your trial has expired',
offer: 30,
extend: true
});
MMKV.setItem('lastTrialDialogShownAt', 'expired');
} else {
return;
}
}
export default {
verify,
setPremiumStatus,
@@ -282,5 +311,6 @@ export default {
getProducts,
getUser,
subscriptions,
getMontlySub
getMontlySub,
getRemainingTrialDaysStatus
};