Files
notesnook/apps/mobile/app/components/paywall/index.tsx

1166 lines
36 KiB
TypeScript
Raw Normal View History

2025-07-01 12:13:03 +05:00
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
2025-08-21 10:34:00 +05:00
import { getFeaturesTable } from "@notesnook/common";
2025-09-13 12:13:36 +05:00
import {
EV,
EVENTS,
Plan,
SKUResponse,
SubscriptionPlan,
User
} from "@notesnook/core";
2025-08-21 10:34:00 +05:00
import { strings } from "@notesnook/intl";
2025-07-01 12:13:03 +05:00
import { useThemeColors } from "@notesnook/theme";
2025-09-29 10:01:59 +05:00
import React, { useEffect, useState } from "react";
2025-07-01 12:13:03 +05:00
import {
ActivityIndicator,
2025-09-29 10:01:59 +05:00
BackHandler,
2025-07-01 12:13:03 +05:00
Image,
2025-09-29 10:01:59 +05:00
NativeEventSubscription,
2025-07-01 12:13:03 +05:00
ScrollView,
Text,
TouchableOpacity,
2025-09-16 13:51:54 +05:00
useWindowDimensions,
2025-07-01 12:13:03 +05:00
View
} from "react-native";
2025-07-25 09:48:52 +05:00
import Config from "react-native-config";
2025-07-01 12:13:03 +05:00
import * as RNIap from "react-native-iap";
2025-09-29 10:01:59 +05:00
import { SafeAreaView } from "react-native-safe-area-context";
2025-07-01 12:13:03 +05:00
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
2025-10-23 21:39:20 +05:00
//@ts-ignore
2025-07-01 12:13:03 +05:00
import ToggleSwitch from "toggle-switch-react-native";
2025-09-03 10:54:01 +05:00
import {
ANDROID_POLICE_SVG,
APPLE_INSIDER_PNG,
ITS_FOSS_NEWS_PNG,
NESS_LABS_PNG,
PRIVACY_GUIDES_SVG,
TECHLORE_SVG,
XDA_SVG
} from "../../assets/images/assets";
2025-09-29 10:01:59 +05:00
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
2025-09-19 12:39:36 +05:00
import usePricingPlans, {
PlanOverView,
PricingPlan
} from "../../hooks/use-pricing-plans";
2025-09-29 10:01:59 +05:00
import Navigation, { NavigationProps } from "../../services/navigation";
import PremiumService from "../../services/premium";
2025-07-01 12:13:03 +05:00
import { getElevationStyle } from "../../utils/elevation";
import { openLinkInBrowser } from "../../utils/functions";
2025-09-24 10:23:00 +05:00
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
2025-09-29 10:01:59 +05:00
import { DefaultAppStyles } from "../../utils/styles";
2025-09-03 10:54:01 +05:00
import { AuthMode } from "../auth/common";
2025-09-29 10:01:59 +05:00
import { Header } from "../header";
2025-07-01 12:13:03 +05:00
import { BuyPlan } from "../sheets/buy-plan";
import { Toast } from "../toast";
2025-08-21 10:34:00 +05:00
import AppIcon from "../ui/AppIcon";
2025-07-01 12:13:03 +05:00
import { Button } from "../ui/button";
2025-08-21 10:34:00 +05:00
import { IconButton } from "../ui/icon-button";
2025-09-03 10:54:01 +05:00
import { SvgView } from "../ui/svg";
2025-07-01 12:13:03 +05:00
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
const Steps = {
select: 1,
buy: 2,
2025-07-25 09:48:52 +05:00
finish: 3,
buyWeb: 4
2025-07-01 12:13:03 +05:00
};
2025-09-29 10:01:59 +05:00
const PayWall = (props: NavigationProps<"PayWall">) => {
2025-07-25 09:48:52 +05:00
const isGithubRelease = Config.GITHUB_RELEASE === "true";
2025-09-29 10:01:59 +05:00
const routeParams = props.route.params;
2025-09-26 14:51:10 +05:00
const { width } = useWindowDimensions();
2025-09-16 13:51:54 +05:00
const isTablet = width > 600;
2025-07-01 12:13:03 +05:00
const { colors } = useThemeColors();
2025-07-25 09:48:52 +05:00
const pricingPlans = usePricingPlans({
planId: routeParams.state?.planId,
2025-09-26 14:51:10 +05:00
productId: routeParams.state?.productId,
onBuy: () => {
setStep(Steps.finish);
}
2025-07-25 09:48:52 +05:00
});
const [annualBilling, setAnnualBilling] = useState(
routeParams.state ? routeParams.state.billingType === "annual" : true
);
const [step, setStep] = useState(
2025-08-22 08:46:17 +05:00
routeParams.state ? Steps.buy : Steps.select
2025-07-25 09:48:52 +05:00
);
2025-09-29 10:01:59 +05:00
const isFocused = useNavigationFocus(props.navigation, {
onBlur: () => true,
onFocus: () => true
});
2025-09-26 14:51:10 +05:00
useEffect(() => {
if (routeParams.state) {
if (routeParams.state?.planId) {
pricingPlans.selectPlan(
routeParams.state?.planId,
routeParams.state?.productId
);
}
setStep(Steps.buy);
}
}, [routeParams.state]);
2025-09-25 08:41:06 +05:00
2025-09-29 10:01:59 +05:00
useEffect(() => {
let listener: NativeEventSubscription;
if (isFocused) {
listener = BackHandler.addEventListener("hardwareBackPress", () => {
if (routeParams.context === "signup" && step === Steps.select)
return true;
if (step === Steps.buy) {
setStep(Steps.select);
return true;
}
return false;
});
}
return () => {
2025-07-25 09:48:52 +05:00
listener?.remove();
2025-09-29 10:01:59 +05:00
};
}, [isFocused, step]);
2025-07-01 12:13:03 +05:00
2025-08-22 12:56:49 +05:00
useEffect(() => {
const sub = EV.subscribe(
EVENTS.userSubscriptionUpdated,
(sub: User["subscription"]) => {
if (sub.plan === SubscriptionPlan.FREE) return;
if (routeParams.context === "signup") {
Navigation.replace("FluidPanelsView", {});
} else {
Navigation.goBack();
}
}
);
return () => {
sub?.unsubscribe();
};
}, []);
2025-09-23 10:13:39 +05:00
const is5YearPlanSelected = (
isGithubRelease
? (pricingPlans.selectedProduct as Plan)?.period
: (pricingPlans.selectedProduct as RNIap.Product)?.productId
)?.includes("5");
2025-07-01 12:13:03 +05:00
return (
2025-09-29 10:01:59 +05:00
<SafeAreaView
style={{
backgroundColor: colors.primary.background,
flex: 1
}}
>
2025-08-21 10:34:00 +05:00
{step === Steps.finish ? null : routeParams.context === "signup" &&
step === Steps.select ? (
<View
style={{
height: 50,
justifyContent: "flex-end",
alignItems: "flex-end",
width: "100%",
flexDirection: "row",
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<IconButton
name="close"
onPress={() => {
Navigation.replace("FluidPanelsView", {});
}}
/>
</View>
) : (
2025-09-29 10:01:59 +05:00
<Header
canGoBack={true}
onLeftMenuButtonPress={() => {
if (step === Steps.buy) {
setStep(Steps.select);
return;
}
2025-08-21 10:34:00 +05:00
if (routeParams.context === "signup") {
Navigation.replace("FluidPanelsView", {});
} else {
Navigation.goBack();
}
2025-09-29 10:01:59 +05:00
}}
title={
2025-07-25 09:48:52 +05:00
step === Steps.buy || step === Steps.buyWeb
2025-09-29 10:01:59 +05:00
? pricingPlans.userCanRequestTrial
2025-08-22 12:10:26 +05:00
? strings.tryPlanForFree(
pricingPlans.currentPlan?.name as string
)
: strings.plan(pricingPlans.currentPlan?.name as string)
2025-09-29 10:01:59 +05:00
: ""
}
/>
)}
2025-08-21 10:34:00 +05:00
2025-07-01 12:13:03 +05:00
{step === Steps.select ? (
<>
<ScrollView
style={{
width: "100%"
}}
contentContainerStyle={{
2025-09-29 10:01:59 +05:00
gap: DefaultAppStyles.GAP_VERTICAL,
2025-07-01 12:13:03 +05:00
paddingBottom: 80
}}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
>
<View
style={{
paddingTop: 100,
borderBottomColor: colors.primary.border,
borderBottomWidth: 1,
2025-09-29 10:01:59 +05:00
paddingHorizontal: DefaultAppStyles.GAP,
2025-07-01 12:13:03 +05:00
paddingBottom: 25,
2025-09-29 10:01:59 +05:00
gap: DefaultAppStyles.GAP_VERTICAL
2025-07-01 12:13:03 +05:00
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 5
}}
>
<Heading
key="heading"
2025-09-29 10:01:59 +05:00
size={AppFontSize.xl}
2025-07-01 12:13:03 +05:00
style={{
alignSelf: "center"
}}
>
2025-09-24 10:23:00 +05:00
{pricingPlans.isSubscribed()
? strings.changePlan()
: strings.notesnookPlans[0]() + " "}
{pricingPlans.isSubscribed() ? null : (
<Heading
size={AppFontSize.xl}
color={colors.primary.accent}
>
{strings.notesnookPlans[1]()}
</Heading>
)}
2025-07-01 12:13:03 +05:00
</Heading>
</View>
2025-09-29 10:01:59 +05:00
<Paragraph key="description" size={AppFontSize.md}>
2025-08-22 12:10:26 +05:00
{strings.readyToTakeNextStep()}
2025-07-01 12:13:03 +05:00
</Paragraph>
</View>
<View
style={{
2025-09-29 10:01:59 +05:00
gap: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: DefaultAppStyles.GAP
2025-07-01 12:13:03 +05:00
}}
>
<TouchableOpacity
onPress={() => {
setAnnualBilling((state) => !state);
}}
style={{
flexDirection: "row",
alignItems: "center",
gap: 15,
width: "100%",
justifyContent: "center",
paddingVertical: 12
}}
activeOpacity={0.9}
>
2025-08-22 12:10:26 +05:00
<Paragraph>{strings.monthly()}</Paragraph>
2025-07-01 12:13:03 +05:00
<ToggleSwitch
isOn={annualBilling}
onColor={colors.primary.accent}
offColor={colors.secondary.accent}
size="small"
animationSpeed={150}
onToggle={() => {
setAnnualBilling((state) => !state);
}}
/>
2025-07-31 14:38:19 +05:00
<Paragraph>
2025-08-22 12:10:26 +05:00
{strings.yearly()}{" "}
<Paragraph color={colors.primary.accent}>
({strings.percentOff("15")})
</Paragraph>
2025-07-31 14:38:19 +05:00
</Paragraph>
2025-07-01 12:13:03 +05:00
</TouchableOpacity>
2025-09-16 13:51:54 +05:00
<View
style={{
flexDirection: isTablet ? "row" : "column",
2025-09-19 12:34:20 +05:00
gap: !isTablet ? DefaultAppStyles.GAP : 0
2025-09-16 13:51:54 +05:00
}}
>
{pricingPlans.pricingPlans.map((plan) =>
plan.id !== "free" ? (
<PricingPlanCard
key={plan.id}
plan={plan}
setStep={(step) => {
if (!pricingPlans.user) {
Navigation.navigate("Auth", {
mode: AuthMode.login,
state: {
planId: pricingPlans.currentPlan?.id,
productId:
(
pricingPlans.selectedProduct as RNIap.Subscription
)?.productId ||
2025-10-01 13:36:02 +05:00
(pricingPlans.selectedProduct as Plan)?.period,
2025-09-16 13:51:54 +05:00
billingType: annualBilling ? "annual" : "monthly"
}
});
return;
}
setStep(step);
}}
pricingPlans={pricingPlans}
annualBilling={annualBilling}
/>
) : null
)}
</View>
2025-07-01 12:13:03 +05:00
</View>
<View
style={{
2025-09-16 13:51:54 +05:00
width: "100%"
2025-07-01 12:13:03 +05:00
}}
>
2025-09-16 13:51:54 +05:00
<View
2025-07-01 12:13:03 +05:00
style={{
2025-09-16 13:51:54 +05:00
flexDirection: "row",
width: "100%",
justifyContent: "space-between",
flexWrap: "wrap",
2025-07-01 12:13:03 +05:00
alignItems: "center",
2025-09-16 13:51:54 +05:00
flexShrink: 1
2025-07-01 12:13:03 +05:00
}}
>
2025-09-16 13:51:54 +05:00
<TouchableOpacity
onPress={() => {
openLinkInBrowser(
"https://github.com/streetwriters/notesnook"
);
}}
activeOpacity={0.9}
2025-07-01 12:13:03 +05:00
style={{
2025-09-16 13:51:54 +05:00
padding: 16,
gap: 12,
2025-07-01 12:13:03 +05:00
alignItems: "center",
2025-09-16 13:51:54 +05:00
flexGrow: 1
2025-07-01 12:13:03 +05:00
}}
>
2025-09-16 13:51:54 +05:00
<View
style={{
justifyContent: "center",
alignItems: "center",
width: 50,
height: 50,
backgroundColor: "black",
borderRadius: 10
}}
>
<Icon
size={40}
name="open-source-initiative"
color={colors.static.white}
/>
</View>
<Paragraph
style={{
flexShrink: 1
}}
size={AppFontSize.md}
>
Open Source
</Paragraph>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
openLinkInBrowser(
"https://github.com/streetwriters/notesnook/stargazers"
);
}}
activeOpacity={0.9}
2025-07-01 12:13:03 +05:00
style={{
2025-09-16 13:51:54 +05:00
padding: 16,
gap: 12,
alignItems: "center",
justifyContent: "center",
flexGrow: 1
2025-07-01 12:13:03 +05:00
}}
>
2025-09-16 13:51:54 +05:00
<View
style={{
justifyContent: "center",
alignItems: "center",
width: 50,
height: 50,
backgroundColor: "black",
borderRadius: 10
}}
>
<Icon size={40} name="github" color={colors.static.white} />
</View>
<Paragraph
style={{
flexShrink: 1
}}
size={AppFontSize.md}
>
12.5K stars
</Paragraph>
</TouchableOpacity>
2025-07-01 12:13:03 +05:00
2025-09-16 13:51:54 +05:00
<TouchableOpacity
onPress={() => {
openLinkInBrowser(
"https://www.privacyguides.org/en/notebooks/#notesnook"
);
}}
activeOpacity={0.9}
2025-07-01 12:13:03 +05:00
style={{
justifyContent: "center",
2025-09-16 13:51:54 +05:00
padding: 16,
gap: 12,
2025-07-01 12:13:03 +05:00
alignItems: "center",
2025-09-16 13:51:54 +05:00
flexGrow: 1
2025-07-01 12:13:03 +05:00
}}
>
2025-09-16 13:51:54 +05:00
<SvgView width={60} height={60} src={PRIVACY_GUIDES_SVG} />
<Paragraph
style={{
flexShrink: 1,
maxWidth: 300,
textAlign: "center"
}}
size={AppFontSize.md}
>
{strings.recommendedByPrivacyGuides()}
</Paragraph>
</TouchableOpacity>
</View>
2025-07-01 12:13:03 +05:00
2025-09-16 13:51:54 +05:00
<Heading
style={{
marginBottom: 20,
alignSelf: "center"
2025-07-01 12:13:03 +05:00
}}
2025-09-16 13:51:54 +05:00
>
{strings.featuredOn()}
</Heading>
<View
2025-07-01 12:13:03 +05:00
style={{
2025-09-16 13:51:54 +05:00
width: "100%",
paddingHorizontal: 16,
2025-07-01 12:13:03 +05:00
alignItems: "center",
2025-09-16 13:51:54 +05:00
paddingBottom: 16,
flexDirection: "row",
gap: 20,
flexWrap: "wrap",
justifyContent: "center"
2025-07-01 12:13:03 +05:00
}}
>
2025-09-16 13:51:54 +05:00
<Image
source={{
uri: NESS_LABS_PNG
}}
2025-07-01 12:13:03 +05:00
style={{
2025-09-16 13:51:54 +05:00
width: 100,
height: 80
2025-07-01 12:13:03 +05:00
}}
2025-09-16 13:51:54 +05:00
/>
2025-07-01 12:13:03 +05:00
2025-09-16 13:51:54 +05:00
<Image
source={{
uri: ITS_FOSS_NEWS_PNG
}}
resizeMode="contain"
style={{
width: 150,
height: 100
}}
/>
2025-08-22 12:10:26 +05:00
2025-09-16 13:51:54 +05:00
<Image
source={{
uri: APPLE_INSIDER_PNG
}}
resizeMode="contain"
style={{
width: 368 * 0.5,
height: 100
}}
/>
2025-07-01 12:13:03 +05:00
2025-09-16 13:51:54 +05:00
<View
style={{
height: 100,
justifyContent: "center"
}}
>
<SvgView width={80} height={80} src={TECHLORE_SVG} />
</View>
2025-07-01 12:13:03 +05:00
2025-09-16 13:51:54 +05:00
<View
style={{
height: 100,
justifyContent: "center"
}}
>
<SvgView width={100} height={100} src={XDA_SVG} />
</View>
2025-08-22 12:10:26 +05:00
2025-09-16 13:51:54 +05:00
<View
style={{
height: 100,
justifyContent: "center"
}}
>
<SvgView width={100} height={100} src={ANDROID_POLICE_SVG} />
</View>
2025-08-22 12:10:26 +05:00
</View>
<View
style={{
2025-09-16 13:51:54 +05:00
padding: 16,
alignSelf: "center",
width: isTablet ? 500 : undefined
2025-08-22 12:10:26 +05:00
}}
>
2025-09-16 13:51:54 +05:00
<ReviewItem
user="Tagby on Discord"
link="https://discord.com/channels/796015620436787241/828701074465619990/1070172521846026271"
userImage="data:image/webp;base64,UklGRrAFAABXRUJQVlA4WAoAAAAQAAAATwAATwAAQUxQSAoAAAABB9D+iAhERP8DVlA4IIAFAAAQGACdASpQAFAAPm0skkYkIqGhL1K86IANiWdqN1k9SfbwDoK2qhJsXhBwv+m+3ujg5HTtBysrex6esj/qeU5883L4Rbay65pvw7npaa0yc/fmrdXlgwv8WNtJN9J5m8XMkAKD7lrd98bFKn2UcICVz6fIlJpW6gQDx3Y/lQzupEnm4fyWa/VI47NFSGLJjPe4K/NR5nIh1I+JaGqh5h6Zn/9771gyWEnxRaaHU7LdnMvNnAtVXhm3Ijxrv+9TKC7rtKa+oqC2ewAA/v17lyxCQMDU8AW7okR5UCCzmkNNZqVrX1NJ2Mu1LrM2qc+DCezUOb2Y8MzxK5xRgGrcR0/FAx88Y1KeuT5RUbhRcNNnTw1Un3MWtOlBIXpYjntwwdRskDymt47ZvkH+V5QANy6cAr+Lrol9EOVUxXc8G5bTJLcIHELmBulybx2w+O+btTXtlkgn0JSy0pAWMfZunaztVCHoyNnmWxhHAFMMUSycgx76ovzc8+Hwd/rRUY3/P+B4/y3dQZ9FB+vMiyYCEdci0l8arhH6f0IOVijOkAXiuS8KfnOh76NP5jbRF0qvz63DDZSUmpsXJGnI7phYFgJ5Em+3zIebcKqPcn4M5hznAH9mYIPsv8jeOCg7+DRYljkh4AbTxImyoRrPy9GUeUA1QC290cK+877ce6yyNzQK8jaLEAz53ccPZj/cHk17B2SCVknKQhV7bz6yg6fLGR5AT3qAPfdQsVeHrPLQdRZmw1ll62E7lCU7+WCuHk+xAg1JwiyZsdWlcSf1Oazs7qgdnon9lQNVKgzGSR+I811YKPAS5U3go9ANCZwqsmdQ07Y+ZzCuLukH46D0tWk/kBVgXBzMhOf8bAlayYfIGvIdEXjm6Z4j0SIz3juiY35mCjXXwez84ESBpc3z0c2gWgRFzWx/iyYBjPzJ5s3Mub6xzfsWtYqAmhAPzeoK5pJWgiB6tEyI2oZmLuj7sLVHARnaswrfzDlIUMPXRSZFoq+mmsIF1aKThJDQTNdgnkLjfz3rDkVaqVhk3sg8jZLIOMJUKsYvujEmGRbRlO3hwfL0JvoORLh8rOJ0prBA2Tnra+VA/RMfJtKC/AjVXRu/nH0tmGg2/Wkra7C/OWK9E2NqBVVjJvrgqvNN0V8gYZdScIl0jQSLRzA4emm2HfC+L8c4FCxheZFDMP+zGUH5JyXLkBX/hGWE+CWhcPjSJ8hAj8yBeXCyWpM07eiFPxxkqYF+Wka211fUU/HygHPX+Qj+CyoMjz9gstrjNPTX0zc3r55VrpiFUjxfgOoY32Eojwr9mWg/dcleov8wSmKuJZtzjxiXFzemVIP5kzrDIffxS6PR30mOq+d9k/ZgJfzhyuPr6W8kOmQXmLnMwbpcr1KSSgjOLmu6oBtjexL/ukI77ygGzDtdDfRPpj7klVHu34xcWAufMWOMlo70oenOeQGueYXGWPZW175I30dvCT/Ra2R+0MbxvxmdpmmoQmzC4d7l/MI2MhUHUW5N79x0vXB1PEGs+F3o+6dO+N1qqqXxJP/0N5BvADUF55dX2vpBSAXyN30UPO9IBqC0JkFPwXiOncsUir6r50C47HnS2WadAlu2QpHFL8YrTsSdhP19HAc6zWDM2/RaJZxFFQj/tqw0rYVYX//7iKKSiXWWQma12fG+HhJD2tBjEcp8+vU2nzjmd4tR6AurIEns1SsWg2cFcvrZzyzKFB0gdnvVpoCwouvl8UuSAyYc+rHMVGhWln+a+bhRgIERIb5XlrazMxJOqksed7mUs5ArKpOEuHWPYu+U+E2T1OkeusgAKL57F73Fm2xbIunW/IbW2wawYHv8u1AWGGSAB8XYJOW1XxpqIgq+V34pqd3WRpBGd1G4rGAA"
review={`I just want to say thank you so much.
2025-08-22 12:10:26 +05:00
2025-09-16 13:51:54 +05:00
After trying all the privacy security oriented note taking apps, for the price and the features afforded to your users, Notesnook is hands down the best.`}
/>
2025-08-22 12:10:26 +05:00
</View>
2025-07-01 12:13:03 +05:00
</View>
<View
style={{
2025-09-16 13:51:54 +05:00
alignItems: "center",
paddingVertical: 16
2025-07-01 12:13:03 +05:00
}}
>
2025-09-16 13:51:54 +05:00
<Heading>{strings.comparePlans()}</Heading>
2025-07-01 12:13:03 +05:00
</View>
2025-09-16 13:51:54 +05:00
2025-07-01 12:13:03 +05:00
<View
style={{
2025-09-24 10:23:00 +05:00
alignSelf: isTablet ? "center" : "flex-start",
2025-09-16 13:51:54 +05:00
flexShrink: 1
2025-07-01 12:13:03 +05:00
}}
>
2025-09-16 13:51:54 +05:00
<ComparePlans pricingPlans={pricingPlans} setStep={setStep} />
2025-07-01 12:13:03 +05:00
</View>
2025-09-16 13:51:54 +05:00
2025-07-01 12:13:03 +05:00
<View
style={{
alignItems: "center",
paddingVertical: 16
}}
>
2025-08-22 12:10:26 +05:00
<Heading>{strings.faqs()}</Heading>
2025-07-01 12:13:03 +05:00
</View>
<View
style={{
2025-09-16 13:51:54 +05:00
paddingHorizontal: 16
2025-07-01 12:13:03 +05:00
}}
>
2025-08-21 10:47:25 +05:00
{strings.checkoutFaqs.map((item) => (
2025-07-01 12:13:03 +05:00
<FAQItem
2025-08-21 10:47:25 +05:00
key={item.question()}
question={item.question()}
answer={item.answer()}
2025-07-01 12:13:03 +05:00
/>
))}
</View>
</ScrollView>
</>
) : step === Steps.buy ? (
<BuyPlan
planId={pricingPlans.currentPlan?.id as string}
canActivateTrial={pricingPlans.userCanRequestTrial}
2025-09-26 14:51:10 +05:00
pricingPlans={pricingPlans}
2025-07-01 12:13:03 +05:00
/>
2025-08-21 10:34:00 +05:00
) : step === Steps.finish ? (
<View
style={{
width: "100%",
height: "100%",
justifyContent: "center",
alignItems: "center",
gap: DefaultAppStyles.GAP_VERTICAL,
maxWidth: "80%",
alignSelf: "center"
}}
>
<View
style={{
width: 60,
height: 60,
borderRadius: 200,
backgroundColor: colors.primary.shade,
justifyContent: "center",
alignItems: "center"
}}
>
<AppIcon color={colors.primary.accent} name="check" size={30} />
</View>
2025-09-23 10:13:39 +05:00
<Heading>
{is5YearPlanSelected
? strings.thankYouForPurchase()
: strings.thankYouForSubscribing()}
</Heading>
2025-08-21 10:34:00 +05:00
<Paragraph
style={{
textAlign: "center"
}}
>
2025-08-22 12:10:26 +05:00
{strings.settingUpPlan()}
2025-08-21 10:34:00 +05:00
</Paragraph>
<Button
title={strings.continue()}
type="accent"
onPress={() => {
if (routeParams.context === "signup") {
Navigation.replace("FluidPanelsView", {});
} else {
Navigation.goBack();
}
}}
/>
</View>
) : null}
2025-07-01 12:13:03 +05:00
<Toast context="local" />
2025-09-29 10:01:59 +05:00
</SafeAreaView>
2025-07-01 12:13:03 +05:00
);
};
const FAQItem = (props: { question: string; answer: string }) => {
const [expanded, setExpanded] = useState(false);
const { colors } = useThemeColors();
return (
<TouchableOpacity
style={{
padding: 16,
backgroundColor: colors.secondary.background,
borderRadius: 10,
marginBottom: 10,
gap: 12
}}
activeOpacity={0.9}
onPress={() => {
setExpanded(!expanded);
}}
key={props.question}
>
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "space-between"
}}
>
<Heading
style={{
flexShrink: 1
}}
2025-09-29 10:01:59 +05:00
size={AppFontSize.md}
2025-07-01 12:13:03 +05:00
>
{props.question}
</Heading>
<Icon
name={expanded ? "chevron-up" : "chevron-down"}
color={colors.secondary.icon}
2025-09-29 10:01:59 +05:00
size={AppFontSize.xxl}
2025-07-01 12:13:03 +05:00
/>
</View>
2025-09-29 10:01:59 +05:00
{expanded ? (
<Paragraph size={AppFontSize.md}>{props.answer}</Paragraph>
) : null}
2025-07-01 12:13:03 +05:00
</TouchableOpacity>
);
};
const ComparePlans = React.memo(
2025-08-21 10:34:00 +05:00
(props: {
pricingPlans?: ReturnType<typeof usePricingPlans>;
setStep: (step: number) => void;
}) => {
2025-07-01 12:13:03 +05:00
const { colors } = useThemeColors();
2025-09-16 13:51:54 +05:00
const { width } = useWindowDimensions();
const isTablet = width > 600;
2025-07-01 12:13:03 +05:00
return (
<ScrollView
horizontal
style={{
2025-09-24 10:23:00 +05:00
width: isTablet ? "100%" : undefined
2025-07-01 12:13:03 +05:00
}}
contentContainerStyle={{
flexDirection: "column"
}}
>
2025-08-01 09:13:46 +05:00
<View
style={{
flexDirection: "row",
alignItems: "center",
width: "100%",
gap: 10
}}
>
{["Features", "Free", "Essential", "Pro", "Believer"].map(
(plan, index) => (
<View
style={{
width: index === 0 ? 150 : 120,
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor:
index === 0 ? colors.secondary.background : undefined,
borderBottomWidth: index === 0 ? 1 : undefined,
borderBottomColor: colors.primary.border
}}
>
<Heading size={AppFontSize.sm}>{plan}</Heading>
</View>
)
)}
</View>
{getFeaturesTable().map((item, keyIndex) => {
2025-07-01 12:13:03 +05:00
return (
<View
2025-08-01 09:13:46 +05:00
key={keyIndex + "feature-item"}
2025-07-01 12:13:03 +05:00
style={{
flexDirection: "row",
alignItems: "center",
width: "100%",
gap: 10
}}
>
2025-08-01 09:13:46 +05:00
{item.map((featureItem, index) => (
2025-07-01 12:13:03 +05:00
<View
style={{
width: index === 0 ? 150 : 120,
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor:
index === 0 ? colors.secondary.background : undefined,
borderBottomWidth: index === 0 ? 1 : undefined,
borderBottomColor: colors.primary.border
}}
2025-08-01 09:13:46 +05:00
key={item[0] + index}
2025-07-01 12:13:03 +05:00
>
2025-08-01 09:13:46 +05:00
{typeof featureItem === "string" ? (
<Heading size={AppFontSize.sm}>
{featureItem as string}
</Heading>
2025-07-01 12:13:03 +05:00
) : (
2025-08-01 09:13:46 +05:00
<>
{typeof featureItem.caption === "string" ||
typeof featureItem.caption === "number" ? (
<Paragraph>
{featureItem.caption === "infinity"
? "∞"
: featureItem.caption}
</Paragraph>
) : typeof featureItem.caption === "boolean" ? (
<>
{featureItem.caption === true ? (
<Icon
color={colors.primary.accent}
size={AppFontSize.sm}
name="check"
/>
) : (
<Icon
size={AppFontSize.sm}
color={colors.static.red}
name="close"
/>
)}
</>
) : null}
</>
2025-07-01 12:13:03 +05:00
)}
</View>
))}
</View>
);
})}
2025-08-21 10:34:00 +05:00
<View
style={{
flexDirection: "row",
alignItems: "center",
width: "100%",
gap: 10
}}
>
{["features", "free", "essential", "pro", "believer"].map(
(plan, index) => (
<View
style={{
width: index === 0 ? 150 : 120,
paddingHorizontal: 16,
paddingVertical: 8
}}
>
{plan !== "free" && plan !== "features" ? (
<Button
title={strings.select()}
type="accent"
fontSize={AppFontSize.xs}
onPress={() => {
props.pricingPlans?.selectPlan(plan);
props.setStep(Steps.buy);
}}
/>
) : null}
</View>
)
)}
</View>
2025-07-01 12:13:03 +05:00
</ScrollView>
);
},
() => true
);
ComparePlans.displayName = "ComparePlans";
const ReviewItem = (props: {
review: string;
user: string;
link: string;
userImage?: string;
}) => {
const { colors } = useThemeColors();
return (
<View
style={{
width: "100%",
padding: 16,
borderWidth: 1,
borderRadius: 10,
borderColor: colors.primary.border,
gap: 16
}}
>
<Paragraph
onPress={() => {
openLinkInBrowser(props.link);
}}
style={{
textAlign: "center"
}}
2025-09-29 10:01:59 +05:00
size={AppFontSize.md}
2025-07-01 12:13:03 +05:00
>
{props.review}
</Paragraph>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 10,
alignSelf: "center",
backgroundColor: colors.secondary.background,
borderRadius: 100,
padding: 6,
paddingHorizontal: 12
}}
>
{props.userImage ? (
<Image
source={{
uri: props.userImage
}}
style={{
width: 20,
height: 20,
borderRadius: 100
}}
/>
) : null}
2025-09-29 10:01:59 +05:00
<Paragraph size={AppFontSize.sm}>{props.user}</Paragraph>
2025-07-01 12:13:03 +05:00
</View>
</View>
);
};
const PricingPlanCard = ({
plan,
pricingPlans,
2025-08-21 10:34:00 +05:00
annualBilling,
setStep
2025-07-01 12:13:03 +05:00
}: {
plan: PricingPlan;
pricingPlans?: ReturnType<typeof usePricingPlans>;
annualBilling?: boolean;
2025-08-21 10:34:00 +05:00
setStep: (step: number) => void;
2025-07-01 12:13:03 +05:00
}) => {
const { colors } = useThemeColors();
2025-09-13 12:13:36 +05:00
const [regionalDiscount, setRegionaDiscount] = useState<SKUResponse>();
2025-09-26 14:51:10 +05:00
const { width } = useWindowDimensions();
2025-09-16 13:51:54 +05:00
const isTablet = width > 600;
2025-07-01 12:13:03 +05:00
const product =
plan.subscriptions?.[
2025-09-26 14:51:10 +05:00
regionalDiscount?.sku ||
`notesnook.${plan.id}.${annualBilling ? "yearly" : "monthly"}`
2025-07-01 12:13:03 +05:00
];
2025-08-22 08:46:17 +05:00
const WebPlan = pricingPlans?.getWebPlan(
plan.id,
annualBilling ? "yearly" : "monthly"
);
2025-07-25 09:48:52 +05:00
2025-10-17 10:56:18 +05:00
const price = pricingPlans?.getPrice(
pricingPlans.isGithubRelease && WebPlan
? WebPlan
: (product as RNIap.Subscription),
pricingPlans.hasTrialOffer(plan.id, product?.productId) ? 1 : 0,
annualBilling
);
2025-09-13 12:13:36 +05:00
useEffect(() => {
2025-10-17 10:56:18 +05:00
if (pricingPlans?.isGithubRelease || !annualBilling) return;
2025-09-13 12:13:36 +05:00
pricingPlans
?.getRegionalDiscount(
plan.id,
pricingPlans.isGithubRelease
? (WebPlan?.period as string)
2025-09-26 14:51:10 +05:00
: `notesnook.${plan.id}.${annualBilling ? "yearly" : "monthly"}`
2025-09-13 12:13:36 +05:00
)
.then((value) => {
setRegionaDiscount(value);
});
2025-10-17 10:56:18 +05:00
}, [annualBilling]);
useEffect(() => {
if (!annualBilling) {
setRegionaDiscount(undefined);
}
}, [annualBilling]);
2025-09-13 12:13:36 +05:00
2025-09-25 08:41:06 +05:00
const isSubscribed =
product?.productId &&
2025-09-26 14:51:10 +05:00
pricingPlans?.user?.subscription?.productId?.includes(plan.id) &&
2025-09-25 08:41:06 +05:00
pricingPlans.isSubscribed();
2025-10-01 13:36:02 +05:00
const isNotReady =
pricingPlans?.loadingPlans || (!price && !WebPlan?.price?.gross);
2025-10-01 13:36:02 +05:00
2025-07-01 12:13:03 +05:00
return (
<TouchableOpacity
2025-08-21 10:34:00 +05:00
activeOpacity={0.8}
2025-07-01 12:13:03 +05:00
onPress={() => {
2025-10-01 13:36:02 +05:00
if (isNotReady) return;
2025-09-26 14:51:10 +05:00
const currentPlanSubscribed =
2025-10-01 13:36:02 +05:00
PremiumService.get() &&
(pricingPlans?.user?.subscription?.productId ===
2025-09-26 14:51:10 +05:00
(product as RNIap.Subscription)?.productId ||
2025-10-01 13:36:02 +05:00
pricingPlans?.user?.subscription?.productId.startsWith(
(product as RNIap.Subscription)?.productId
));
2025-09-26 14:51:10 +05:00
pricingPlans?.selectPlan(
plan.id,
currentPlanSubscribed
? `notesnook.${plan.id}.${
!(product as RNIap.Subscription)?.productId.includes("yearly")
? "yearly"
: "monthly"
}`
: pricingPlans.isGithubRelease
2025-10-17 10:56:18 +05:00
? (WebPlan?.period as string)
: (product?.productId as string)
2025-09-26 14:51:10 +05:00
);
2025-08-21 10:34:00 +05:00
setStep(Steps.buy);
2025-07-01 12:13:03 +05:00
}}
style={{
...getElevationStyle(3),
backgroundColor: colors.primary.background,
borderWidth: 1,
2025-08-21 10:34:00 +05:00
borderColor:
plan.id === "pro" ? colors.primary.accent : colors.primary.border,
2025-07-01 12:13:03 +05:00
borderRadius: 10,
padding: 16,
2025-09-16 13:51:54 +05:00
width: isTablet ? undefined : "100%",
flexShrink: isTablet ? 1 : undefined,
2025-07-01 12:13:03 +05:00
flexDirection: "column",
justifyContent: "space-between",
gap: 6
}}
>
{regionalDiscount?.discount || WebPlan?.discount ? (
2025-07-01 12:13:03 +05:00
<View
style={{
backgroundColor: colors.static.red,
2025-09-24 10:23:00 +05:00
borderRadius: defaultBorderRadius,
2025-07-01 12:13:03 +05:00
paddingHorizontal: 6,
alignItems: "center",
justifyContent: "center",
height: 25,
alignSelf: "flex-start"
}}
>
2025-09-29 10:01:59 +05:00
<Heading color={colors.static.white} size={AppFontSize.xs}>
2025-09-26 14:51:10 +05:00
{strings.specialOffer()}{" "}
{strings.percentOff(
`${regionalDiscount?.discount || WebPlan?.discount?.amount}`
)}
2025-07-01 12:13:03 +05:00
</Heading>
</View>
2025-09-13 12:13:36 +05:00
) : null}
2025-07-01 12:13:03 +05:00
<View>
2025-09-29 10:01:59 +05:00
<Heading size={AppFontSize.md}>
2025-07-01 12:13:03 +05:00
{plan.name}{" "}
{plan.recommended ? (
<Text
style={{
color: colors.primary.accent,
fontSize: 12
}}
>
2025-08-22 12:10:26 +05:00
({strings.recommended()})
2025-07-01 12:13:03 +05:00
</Text>
) : null}
</Heading>
<Paragraph>{plan.description}</Paragraph>
2025-07-31 14:38:19 +05:00
<View
style={{
gap: 5,
marginVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "space-between"
}}
>
2025-08-22 12:10:26 +05:00
<Paragraph size={AppFontSize.xs}>{strings.storage()}</Paragraph>
2025-07-31 14:38:19 +05:00
<Paragraph size={AppFontSize.xs}>
{PlanOverView[plan.id as keyof typeof PlanOverView].storage}
</Paragraph>
</View>
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "space-between"
}}
>
2025-08-22 12:10:26 +05:00
<Paragraph size={AppFontSize.xs}>{strings.fileSize()}</Paragraph>
2025-07-31 14:38:19 +05:00
<Paragraph size={AppFontSize.xs}>
{PlanOverView[plan.id as keyof typeof PlanOverView].fileSize}
</Paragraph>
</View>
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "space-between"
}}
>
2025-08-22 12:10:26 +05:00
<Paragraph size={AppFontSize.xs}>{strings.hdImages()}</Paragraph>
2025-07-31 14:38:19 +05:00
<Paragraph size={AppFontSize.xs}>
{PlanOverView[plan.id as keyof typeof PlanOverView].hdImages
2025-08-22 12:10:26 +05:00
? strings.yes()
: strings.no()}
2025-07-31 14:38:19 +05:00
</Paragraph>
</View>
</View>
2025-07-01 12:13:03 +05:00
</View>
{pricingPlans?.loadingPlans || (!price && !WebPlan?.price?.gross) ? (
2025-07-01 12:13:03 +05:00
<ActivityIndicator size="small" color={colors.primary.accent} />
) : (
<View>
2025-09-29 10:01:59 +05:00
<Paragraph size={AppFontSize.lg}>
{price || `${WebPlan?.price?.currency} ${WebPlan?.price?.gross}`}{" "}
2025-08-22 12:10:26 +05:00
<Paragraph>/{strings.month()}</Paragraph>
2025-07-01 12:13:03 +05:00
</Paragraph>
{!product && !WebPlan ? null : (
2025-09-29 10:01:59 +05:00
<Paragraph color={colors.secondary.paragraph} size={AppFontSize.xs}>
2025-08-22 12:10:26 +05:00
{annualBilling
? strings.billedAnnually(
pricingPlans?.getStandardPrice(
2025-10-17 10:56:18 +05:00
(product || WebPlan) as any
2025-08-22 12:10:26 +05:00
) as string
)
: strings.billedMonthly(
pricingPlans?.getStandardPrice(
2025-10-17 10:56:18 +05:00
(product || WebPlan) as any
2025-08-22 12:10:26 +05:00
) as string
)}
2025-07-01 12:13:03 +05:00
</Paragraph>
)}
{isSubscribed ? (
<View
style={{
backgroundColor: colors.primary.accent,
borderRadius: defaultBorderRadius,
paddingHorizontal: 6,
alignItems: "center",
justifyContent: "center",
height: 25,
alignSelf: "flex-start",
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
>
<Heading color={colors.static.white} size={AppFontSize.xs}>
{strings.currentPlan()}
</Heading>
</View>
) : null}
2025-07-01 12:13:03 +05:00
</View>
)}
</TouchableOpacity>
);
};
2025-09-29 10:01:59 +05:00
export default PayWall;