mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 19:57:52 +01:00
refactor and redesign premium dialog
This commit is contained in:
@@ -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/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
163
apps/mobile/src/components/Premium/component.js
Normal file
163
apps/mobile/src/components/Premium/component.js
Normal 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>
|
||||
);
|
||||
};
|
||||
50
apps/mobile/src/components/Premium/feature.js
Normal file
50
apps/mobile/src/components/Premium/feature.js
Normal 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>
|
||||
);
|
||||
};
|
||||
166
apps/mobile/src/components/Premium/features.js
Normal file
166
apps/mobile/src/components/Premium/features.js
Normal 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'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
60
apps/mobile/src/components/Premium/group.js
Normal file
60
apps/mobile/src/components/Premium/group.js
Normal 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>
|
||||
);
|
||||
};
|
||||
51
apps/mobile/src/components/Premium/index.js
Normal file
51
apps/mobile/src/components/Premium/index.js
Normal 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;
|
||||
23
apps/mobile/src/components/Premium/offer.js
Normal file
23
apps/mobile/src/components/Premium/offer.js
Normal 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>
|
||||
);
|
||||
};
|
||||
43
apps/mobile/src/components/Premium/pricingitem.js
Normal file
43
apps/mobile/src/components/Premium/pricingitem.js
Normal 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>
|
||||
);
|
||||
};
|
||||
382
apps/mobile/src/components/Premium/pricingplans.js
Normal file
382
apps/mobile/src/components/Premium/pricingplans.js
Normal 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>
|
||||
);
|
||||
};
|
||||
35
apps/mobile/src/components/Premium/protag.js
Normal file
35
apps/mobile/src/components/Premium/protag.js
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user