Files
notesnook/apps/mobile/app/components/sheets/buy-plan/index.tsx

489 lines
15 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-09-26 14:51:10 +05:00
import { Plan, SKUResponse } from "@notesnook/core";
2025-08-22 12:10:26 +05:00
import { strings } from "@notesnook/intl";
2025-09-29 10:01:59 +05:00
import { useThemeColors } from "@notesnook/theme";
import dayjs from "dayjs";
2025-09-26 14:51:10 +05:00
import React, { useEffect, useState } from "react";
2025-10-13 11:33:48 +05:00
import {
Linking,
ScrollView,
Text,
TouchableOpacity,
View
} from "react-native";
2025-08-22 12:10:26 +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 Icon from "react-native-vector-icons/MaterialCommunityIcons";
2025-08-22 12:10:26 +05:00
import { WebView } from "react-native-webview";
import { db } from "../../../common/database";
2025-09-29 10:01:59 +05:00
import usePricingPlans from "../../../hooks/use-pricing-plans";
import { ToastManager } from "../../../services/event-manager";
2025-07-01 12:13:03 +05:00
import { openLinkInBrowser } from "../../../utils/functions";
2025-07-31 14:38:19 +05:00
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
2025-09-29 10:01:59 +05:00
import { Button } from "../../ui/button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
2025-08-22 08:46:17 +05:00
const isGithubRelease = Config.GITHUB_RELEASE === "true";
2025-07-01 12:13:03 +05:00
export const BuyPlan = (props: {
planId: string;
canActivateTrial?: boolean;
2025-09-26 14:51:10 +05:00
pricingPlans: ReturnType<typeof usePricingPlans>;
2025-07-01 12:13:03 +05:00
}) => {
const { colors } = useThemeColors();
2025-08-22 08:46:17 +05:00
const [checkoutUrl, setCheckoutUrl] = useState<string>();
2025-09-26 14:51:10 +05:00
const pricingPlans = props.pricingPlans;
2025-07-01 12:13:03 +05:00
const billingDuration = pricingPlans.getBillingDuration(
pricingPlans.selectedProduct as RNIap.Subscription,
0,
0,
true
);
2025-08-22 08:46:17 +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
2025-08-22 08:46:17 +05:00
return checkoutUrl ? (
<View
style={{
2025-10-13 11:33:48 +05:00
flex: 1,
justifyContent: "center",
alignItems: "center",
gap: DefaultAppStyles.GAP_VERTICAL
2025-08-22 08:46:17 +05:00
}}
>
2025-10-13 11:33:48 +05:00
<Paragraph>{strings.finishPurchaseInBrowser()}</Paragraph>
<Button
title={strings.goBack()}
onPress={() => {
setCheckoutUrl(undefined);
2025-08-22 08:46:17 +05:00
}}
/>
</View>
) : (
2025-07-01 12:13:03 +05:00
<ScrollView
contentContainerStyle={{
2025-07-23 12:32:37 +05:00
marginTop: DefaultAppStyles.GAP_VERTICAL
2025-07-01 12:13:03 +05:00
}}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
>
<View
style={{
2025-07-31 14:38:19 +05:00
paddingHorizontal: DefaultAppStyles.GAP,
gap: DefaultAppStyles.GAP_VERTICAL
2025-07-01 12:13:03 +05:00
}}
>
{[
2025-08-22 08:46:17 +05:00
Config.GITHUB_RELEASE === "true"
? "yearly"
: `notesnook.${props.planId}.yearly`,
Config.GITHUB_RELEASE === "true"
? "monthly"
: `notesnook.${props.planId}.monthly`,
2025-09-24 10:23:00 +05:00
...(props.planId === "essential" || pricingPlans.isSubscribed()
2025-07-01 12:13:03 +05:00
? []
2025-08-22 08:46:17 +05:00
: [
Config.GITHUB_RELEASE === "true"
? "5-year"
: `notesnook.${props.planId}.5year`
])
2025-07-01 12:13:03 +05:00
].map((item) => (
<ProductItem
key={item}
pricingPlans={pricingPlans}
productId={item}
/>
))}
<View
style={{
flexDirection: "row",
2025-07-23 12:32:37 +05:00
justifyContent: "space-between",
2025-07-31 14:38:19 +05:00
borderWidth: 1,
borderColor: colors.primary.border,
padding: DefaultAppStyles.GAP,
borderRadius: defaultBorderRadius
2025-07-01 12:13:03 +05:00
}}
>
2025-09-29 10:01:59 +05:00
<Heading color={colors.primary.paragraph} size={AppFontSize.sm}>
2025-08-22 12:10:26 +05:00
{strings.dueToday()}{" "}
2025-09-23 10:13:39 +05:00
{pricingPlans.hasTrialOffer(
props.planId,
(pricingPlans?.selectedProduct as RNIap.Product)?.productId ||
(pricingPlans?.selectedProduct as Plan)?.period
) ? (
2025-07-01 12:13:03 +05:00
<Text
style={{
color: colors.primary.accent
}}
>
2025-08-22 12:10:26 +05:00
({strings.daysFree(`${billingDuration?.duration || 0}`)})
2025-07-01 12:13:03 +05:00
</Text>
) : null}
</Heading>
<Paragraph color={colors.primary.paragraph}>
2025-09-23 10:13:39 +05:00
{pricingPlans.hasTrialOffer(
props.planId,
(pricingPlans?.selectedProduct as RNIap.Product)?.productId ||
(pricingPlans?.selectedProduct as Plan)?.period
)
2025-07-31 14:38:19 +05:00
? "FREE"
2025-07-01 12:13:03 +05:00
: pricingPlans.getStandardPrice(
pricingPlans.selectedProduct as RNIap.Subscription
)}
</Paragraph>
</View>
2025-09-10 11:20:46 +05:00
{pricingPlans.hasTrialOffer(
props.planId,
2025-09-12 14:52:39 +05:00
(pricingPlans?.selectedProduct as RNIap.Product)?.productId ||
(pricingPlans?.selectedProduct as Plan)?.period
2025-09-10 11:20:46 +05:00
) ? (
2025-07-01 12:13:03 +05:00
<View
style={{
flexDirection: "row",
2025-07-31 14:38:19 +05:00
justifyContent: "space-between",
borderWidth: 1,
borderColor: colors.primary.border,
padding: DefaultAppStyles.GAP,
borderRadius: defaultBorderRadius
2025-07-01 12:13:03 +05:00
}}
>
<Paragraph color={colors.secondary.paragraph}>
2025-08-22 12:10:26 +05:00
{strings.due(
dayjs()
.add(billingDuration?.duration || 0, "day")
.format("DD MMMM")
)}
2025-07-01 12:13:03 +05:00
</Paragraph>
<Paragraph color={colors.secondary.paragraph}>
{pricingPlans.getStandardPrice(
pricingPlans.selectedProduct as RNIap.Subscription
)}
</Paragraph>
</View>
) : null}
2025-09-10 11:20:46 +05:00
{pricingPlans.hasTrialOffer(
props.planId,
2025-09-12 14:52:39 +05:00
(pricingPlans.selectedProduct as RNIap.Product)?.productId ||
(pricingPlans.selectedProduct as Plan)?.period
) || is5YearPlanSelected ? (
2025-07-31 14:38:19 +05:00
<View
style={{
gap: DefaultAppStyles.GAP_VERTICAL,
borderWidth: 1,
borderColor: colors.primary.border,
padding: DefaultAppStyles.GAP,
borderRadius: defaultBorderRadius
}}
>
{(is5YearPlanSelected
2025-08-22 12:10:26 +05:00
? strings["5yearPlanConditions"]()
2025-07-31 14:38:19 +05:00
: [
2025-08-22 12:10:26 +05:00
strings.trialPlanConditions[0](
billingDuration?.duration as number
),
strings.trialPlanConditions[1](0)
2025-07-31 14:38:19 +05:00
]
).map((item) => (
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 10
}}
key={item}
>
<Icon
color={colors.primary.accent}
size={AppFontSize.lg}
name="check"
/>
<Paragraph>{item}</Paragraph>
</View>
))}
</View>
) : null}
2025-07-01 12:13:03 +05:00
<Button
width="100%"
type="accent"
loading={pricingPlans.loading}
title={
2025-08-22 08:46:17 +05:00
is5YearPlanSelected
2025-08-22 12:10:26 +05:00
? strings.purchase()
2025-07-01 12:13:03 +05:00
: pricingPlans?.userCanRequestTrial
2025-08-22 12:10:26 +05:00
? strings.subscribeAndStartTrial()
: strings.subscribe()
2025-07-01 12:13:03 +05:00
}
2025-08-22 08:46:17 +05:00
onPress={async () => {
if (isGithubRelease) {
2025-10-13 11:33:48 +05:00
const url = await db.subscriptions.checkoutUrl(
(pricingPlans.selectedProduct as Plan).plan,
(pricingPlans.selectedProduct as Plan).period
2025-08-22 08:46:17 +05:00
);
2025-10-13 11:33:48 +05:00
if (url) {
setCheckoutUrl(url);
Linking.openURL(url);
}
2025-08-22 08:46:17 +05:00
return;
}
2025-07-01 12:13:03 +05:00
const offerToken = pricingPlans.getOfferTokenAndroid(
pricingPlans.selectedProduct as RNIap.SubscriptionAndroid,
0
);
pricingPlans.subscribe(
pricingPlans.selectedProduct as RNIap.Subscription,
offerToken
);
}}
/>
<Paragraph
2025-09-29 10:01:59 +05:00
style={{
textAlign: "center"
}}
color={colors.secondary.paragraph}
size={AppFontSize.xs}
>
{is5YearPlanSelected
2025-08-22 12:10:26 +05:00
? strings.oneTimePurchase()
: strings.cancelAnytimeAlt()}
</Paragraph>
<Paragraph
2025-09-29 10:01:59 +05:00
style={{
textAlign: "center"
}}
color={colors.secondary.paragraph}
size={AppFontSize.xs}
>
2025-08-22 12:10:26 +05:00
{strings.subTerms[0]()}{" "}
2025-09-29 10:01:59 +05:00
<Text
2025-07-01 12:13:03 +05:00
style={{
2025-09-29 10:01:59 +05:00
textDecorationLine: "underline"
}}
onPress={() => {
openLinkInBrowser("https://notesnook.com/privacy");
2025-07-01 12:13:03 +05:00
}}
>
2025-08-22 12:10:26 +05:00
{strings.subTerms[1]()}
2025-09-29 10:01:59 +05:00
</Text>{" "}
2025-08-22 12:10:26 +05:00
{strings.subTerms[2]()}{" "}
2025-09-29 10:01:59 +05:00
<Text
2025-07-01 12:13:03 +05:00
style={{
2025-09-29 10:01:59 +05:00
textDecorationLine: "underline"
}}
onPress={() => {
openLinkInBrowser("https://notesnook.com/tos");
2025-07-01 12:13:03 +05:00
}}
>
2025-08-22 12:10:26 +05:00
{strings.subTerms[3]()}
2025-09-29 10:01:59 +05:00
</Text>
</Paragraph>
2025-07-01 12:13:03 +05:00
</View>
</ScrollView>
);
};
const ProductItem = (props: {
pricingPlans: ReturnType<typeof usePricingPlans>;
productId: string;
}) => {
const { colors } = useThemeColors();
2025-09-26 14:51:10 +05:00
const [regionalDiscount, setRegionaDiscount] = useState<SKUResponse>();
2025-07-01 12:13:03 +05:00
const product =
2025-09-26 14:51:10 +05:00
props.pricingPlans?.currentPlan?.subscriptions?.[
regionalDiscount?.sku || props.productId
] ||
2025-08-22 08:46:17 +05:00
props.pricingPlans?.currentPlan?.products?.[props.productId] ||
props.pricingPlans?.getWebPlan(
props.pricingPlans?.currentPlan?.id as string,
props.productId as "monthly" | "yearly"
);
const isAnnual = isGithubRelease
? (product as Plan)?.period === "yearly"
2025-09-26 14:51:10 +05:00
: (product as RNIap.Subscription)?.productId?.includes("yearly");
2025-08-22 08:46:17 +05:00
const isSelected = isGithubRelease
? (product as Plan)?.period ===
(props.pricingPlans.selectedProduct as Plan)?.period
: (product as RNIap.Subscription)?.productId ===
(props.pricingPlans.selectedProduct as RNIap.Subscription)?.productId;
const is5YearProduct = (
isGithubRelease
? (product as Plan)?.period
: (product as RNIap.Product)?.productId
)?.includes("5");
2025-07-01 12:13:03 +05:00
2025-09-24 10:23:00 +05:00
const isSubscribed =
props.pricingPlans.isSubscribed() &&
2025-09-26 14:51:10 +05:00
(props.pricingPlans.user?.subscription?.productId ===
(product as RNIap.Subscription)?.productId ||
props.pricingPlans.user?.subscription?.productId.startsWith(
(product as RNIap.Subscription)?.productId
) ||
props.pricingPlans.user?.subscription?.productId ===
(product as Plan).id);
useEffect(() => {
props.pricingPlans
?.getRegionalDiscount(
props.pricingPlans.currentPlan?.id as string,
props.pricingPlans.isGithubRelease
? ((product as Plan)?.period as string)
: props.productId
)
.then((value) => {
if (
value &&
value.sku?.startsWith(
(props.pricingPlans.selectedProduct as RNIap.Subscription)
?.productId
)
) {
props.pricingPlans.selectProduct(value?.sku as string);
}
setRegionaDiscount(value);
});
}, []);
2025-07-01 12:13:03 +05:00
return (
<TouchableOpacity
style={{
flexDirection: "row",
gap: 10,
opacity: isSubscribed ? 0.5 : 1
2025-07-01 12:13:03 +05:00
}}
activeOpacity={0.9}
onPress={() => {
if (isSubscribed) {
ToastManager.show({
message: strings.alreadySubscribed(),
type: "info"
});
return;
}
2025-07-31 14:38:19 +05:00
if (!product) return;
2025-08-22 08:46:17 +05:00
props.pricingPlans.selectProduct(
isGithubRelease
? (product as Plan)?.period
: (product as RNIap.Subscription)?.productId
);
2025-07-01 12:13:03 +05:00
}}
>
<Icon
name={isSelected ? "radiobox-marked" : "radiobox-blank"}
color={isSelected ? colors.primary.accent : colors.secondary.icon}
2025-09-29 10:01:59 +05:00
size={AppFontSize.lg}
2025-07-01 12:13:03 +05:00
/>
2025-07-23 12:32:37 +05:00
<View>
2025-07-01 12:13:03 +05:00
<View
style={{
flexDirection: "row",
2025-07-23 12:32:37 +05:00
gap: DefaultAppStyles.GAP_VERTICAL_SMALL
2025-07-01 12:13:03 +05:00
}}
>
2025-09-29 10:01:59 +05:00
<Heading size={AppFontSize.md}>
2025-07-01 12:13:03 +05:00
{isAnnual
2025-08-22 12:20:41 +05:00
? strings.yearly()
2025-08-22 08:46:17 +05:00
: is5YearProduct
2025-08-22 12:20:41 +05:00
? strings.fiveYearPlan()
: strings.monthly()}
2025-07-01 12:13:03 +05:00
</Heading>
2025-08-22 08:46:17 +05:00
{(isAnnual && !isGithubRelease) ||
(isGithubRelease && (product as Plan)?.discount?.amount) ? (
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"
}}
>
2025-09-29 10:01:59 +05:00
<Heading color={colors.static.white} size={AppFontSize.xs}>
2025-08-22 12:10:26 +05:00
{strings.bestValue()} -{" "}
{strings.percentOff(
2025-09-26 14:51:10 +05:00
(regionalDiscount
? regionalDiscount.discount
: isGithubRelease
2025-08-22 12:10:26 +05:00
? (product as Plan).discount?.amount
: props.pricingPlans.compareProductPrice(
props.pricingPlans.currentPlan?.id as string,
`notesnook.${props.pricingPlans.currentPlan?.id}.yearly`,
`notesnook.${props.pricingPlans.currentPlan?.id}.monthly`
)) as string
)}
2025-07-01 12:13:03 +05:00
</Heading>
</View>
) : null}
2025-09-24 10:23:00 +05:00
{isSubscribed ? (
<View
style={{
backgroundColor: colors.primary.accent,
borderRadius: defaultBorderRadius,
paddingHorizontal: 6,
alignItems: "center",
justifyContent: "center"
}}
>
<Heading color={colors.static.white} size={AppFontSize.xs}>
{strings.currentPlan()}
</Heading>
</View>
) : null}
2025-07-01 12:13:03 +05:00
</View>
2025-07-31 14:38:19 +05:00
<Paragraph size={AppFontSize.md}>
2025-08-22 08:46:17 +05:00
{isAnnual || is5YearProduct
2025-07-31 14:38:19 +05:00
? `${props.pricingPlans.getPrice(
2025-07-01 12:13:03 +05:00
product as RNIap.Subscription,
2025-08-22 08:46:17 +05:00
props.pricingPlans.hasTrialOffer(
undefined,
(product as RNIap.Subscription)?.productId
)
2025-07-31 14:38:19 +05:00
? 1
: 0,
2025-07-01 12:13:03 +05:00
isAnnual
2025-08-22 12:20:41 +05:00
)}/${strings.month()}`
2025-07-31 14:38:19 +05:00
: null}
2025-08-22 08:46:17 +05:00
{!isAnnual && !is5YearProduct
2025-07-31 14:38:19 +05:00
? `${props.pricingPlans.getStandardPrice(
product as RNIap.Subscription
2025-08-22 12:10:26 +05:00
)}/${strings.month()}`
2025-07-01 12:13:03 +05:00
: null}
</Paragraph>
</View>
</TouchableOpacity>
);
};