mobile: initial pricing changes

This commit is contained in:
Abdullah Atta
2025-09-29 10:01:59 +05:00
parent 24da82313e
commit bcbcd516f6
40 changed files with 404 additions and 3571 deletions

View File

@@ -25,7 +25,6 @@ import { eSendEvent, presentSheet } from "../../services/event-manager";
import { eCloseAnnouncementDialog } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { sleep } from "../../utils/time";
import { PricingPlans } from "../premium/pricing-plans";
import SheetProvider from "../sheet-provider";
import { Button } from "../ui/button";
import { allowedOnPlatform, getStyle } from "./functions";
@@ -45,18 +44,6 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
Linking.openURL(item.data).catch(() => {
/* empty */
});
} else if (item.type === "promo") {
presentSheet({
component: (
<PricingPlans
marginTop={1}
promo={{
promoCode: item.data,
text: item.title
}}
/>
)
});
}
};
return (

View File

@@ -31,7 +31,6 @@ export const AuthHeader = (props: { welcome?: boolean }) => {
<View
style={{
paddingTop: Platform.OS === "android" ? 0 : insets.top,
backgroundColor: colors.secondary.background,
width: "100%"
}}
>

View File

@@ -17,101 +17,27 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import { View } from "react-native";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import Navigation from "../../services/navigation";
import { SafeAreaView } from "react-native-safe-area-context";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { Toast } from "../toast";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import { AuthMode, initialAuthMode } from "./common";
import { Login } from "./login";
import { Signup } from "./signup";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { DefaultAppStyles } from "../../utils/styles";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
const Auth = ({ navigation, route }) => {
const [currentAuthMode, setCurrentAuthMode] = useState(
route?.params?.mode || AuthMode.login
);
const deviceMode = useSettingStore((state) => state.deviceMode);
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
initialAuthMode.current = route?.params.mode || AuthMode.login;
useNavigationFocus(navigation, {});
return (
<View style={{ flex: 1 }}>
<View
style={{
position: "absolute",
paddingTop: insets.top,
top: 0,
zIndex: 999,
backgroundColor:
deviceMode === "mobile" ? colors.secondary.background : null,
width: "100%"
}}
<SafeAreaView
style={{ flex: 1, backgroundColor: colors.primary.background }}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
paddingHorizontal: DefaultAppStyles.GAP,
width: "100%",
height: 50,
justifyContent:
initialAuthMode.current !== AuthMode.welcomeSignup
? "space-between"
: "flex-end"
}}
>
{initialAuthMode.current === AuthMode.welcomeSignup ? null : (
<IconButton
name="arrow-left"
onPress={() => {
if (initialAuthMode.current === 2) {
Navigation.replace("FluidPanelsView");
} else {
Navigation.goBack();
}
}}
color={colors.primary.paragraph}
/>
)}
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
<Button
title={strings.skip()}
onPress={() => {
if (initialAuthMode.current === 2) {
Navigation.replace("FluidPanelsView");
setTimeout(() => {
Navigation.resetRootState();
}, 1000);
} else {
Navigation.goBack();
}
}}
iconSize={16}
type="plain"
iconPosition="right"
icon="chevron-right"
height={25}
iconStyle={{
marginTop: 2
}}
style={{
paddingHorizontal: DefaultAppStyles.GAP_SMALL
}}
/>
)}
</View>
</View>
{currentAuthMode !== AuthMode.login ? (
<Signup
changeMode={(mode) => setCurrentAuthMode(mode)}
@@ -126,7 +52,7 @@ const Auth = ({ navigation, route }) => {
)}
<Toast context="local" />
</View>
</SafeAreaView>
);
};

View File

@@ -24,12 +24,15 @@ import { TouchableOpacity, View, useWindowDimensions } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { DDS } from "../../services/device-detection";
import { eSendEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import PremiumService from "../../services/premium";
import Sync from "../../services/sync";
import { useUserStore } from "../../stores/use-user-store";
import { eUserLoggedIn } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { sleep } from "../../utils/time";
import SheetProvider from "../sheet-provider";
import { Dialog } from "../dialog";
import { Progress } from "../sheets/progress";
import { Button } from "../ui/button";
import Input from "../ui/input";
@@ -37,9 +40,8 @@ import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { hideAuth } from "./common";
import { ForgotPassword } from "./forgot-password";
import { AuthHeader } from "./header";
import { useLogin } from "./use-login";
import { DefaultAppStyles } from "../../utils/styles";
import { Dialog } from "../dialog";
const LoginSteps = {
emailAuth: 1,
@@ -62,15 +64,21 @@ export const Login = ({ changeMode }) => {
setError,
login
} = useLogin(async () => {
hideAuth();
eSendEvent(eUserLoggedIn, true);
await sleep(500);
Progress.present();
hideAuth();
setTimeout(() => {
if (!useUserStore.getState().syncing) {
Sync.run("global", false, "full");
}
}, 5000);
if (!PremiumService.get()) {
Navigation.navigate("PayWall", {
context: "logged-in"
});
} else {
Progress.present();
}
});
const { width, height } = useWindowDimensions();
const isTablet = width > 600;
@@ -89,6 +97,7 @@ export const Login = ({ changeMode }) => {
return (
<>
<AuthHeader />
<ForgotPassword />
<Dialog context="two_factor_verify" />
<View
@@ -105,7 +114,6 @@ export const Login = ({ changeMode }) => {
style={{
justifyContent: "flex-end",
paddingHorizontal: DefaultAppStyles.GAP,
backgroundColor: colors.secondary.background,
borderBottomWidth: 0.8,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
borderBottomColor: colors.primary.border,
@@ -165,7 +173,8 @@ export const Login = ({ changeMode }) => {
: "99.9%",
backgroundColor: colors.primary.background,
alignSelf: "center",
paddingHorizontal: DefaultAppStyles.GAP
paddingHorizontal: DefaultAppStyles.GAP,
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<Input
@@ -179,6 +188,7 @@ export const Login = ({ changeMode }) => {
returnKeyType="next"
autoComplete="email"
validationType="email"
marginBottom={0}
autoCorrect={false}
autoCapitalize="none"
errorMessage={strings.emailInvalid()}
@@ -234,11 +244,7 @@ export const Login = ({ changeMode }) => {
</>
)}
<View
style={{
marginTop: 25
}}
>
<View>
<Button
loading={loading}
onPress={() => {
@@ -246,10 +252,11 @@ export const Login = ({ changeMode }) => {
login();
}}
style={{
width: 250
width: "100%"
}}
type="accent"
title={!loading ? strings.continue() : null}
fontSize={AppFontSize.md}
/>
{step === LoginSteps.passwordAuth && (

View File

@@ -28,9 +28,10 @@ import { ToastManager } from "../../services/event-manager";
import { clearMessage, setEmailVerifyMessage } from "../../services/message";
import { useUserStore } from "../../stores/use-user-store";
import { openLinkInBrowser } from "../../utils/functions";
import { AppFontSize, SIZE } from "../../utils/size";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Loading } from "../loading";
import { PaywallComponent } from "../premium/component";
import { PaywallComponent } from "../premium/paywall";
import { Button } from "../ui/button";
import Input from "../ui/input";
import Heading from "../ui/typography/heading";
@@ -38,6 +39,7 @@ import Paragraph from "../ui/typography/paragraph";
import { hideAuth } from "./common";
import { AuthHeader } from "./header";
import { SignupContext } from "./signup-context";
import Navigation from "../../services/navigation";
const SignupSteps = {
signup: 0,
@@ -60,7 +62,6 @@ export const Signup = ({ changeMode, welcome }) => {
const setLastSynced = useUserStore((state) => state.setLastSynced);
const { width, height } = useWindowDimensions();
const isTablet = width > 600;
const deviceMode = useSettingStore((state) => state.deviceMode);
const validateInfo = () => {
if (!password.current || !email.current || !confirmPassword.current) {
ToastManager.show({
@@ -89,7 +90,9 @@ export const Signup = ({ changeMode, welcome }) => {
setLastSynced(await db.lastSynced());
clearMessage();
setEmailVerifyMessage();
setCurrentStep(SignupSteps.selectPlan);
Navigation.navigate("PayWall", {
canGoBack: false
});
return true;
} catch (e) {
setCurrentStep(SignupSteps.signup);
@@ -138,17 +141,15 @@ export const Signup = ({ changeMode, welcome }) => {
style={{
justifyContent: "flex-end",
paddingHorizontal: 16,
backgroundColor: colors.secondary.background,
marginBottom: 20,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
borderBottomWidth: 0.8,
borderBottomColor: colors.primary.border,
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
borderWidth: deviceMode !== "mobile" ? 1 : null,
borderColor:
deviceMode !== "mobile" ? colors.primary.border : null,
borderRadius: deviceMode !== "mobile" ? 20 : null,
marginTop: deviceMode !== "mobile" ? 50 : null,
width: deviceMode === "mobile" ? null : "50%",
alignSelf: isTablet ? "center" : undefined,
borderWidth: isTablet ? 1 : null,
borderColor: isTablet ? colors.primary.border : null,
borderRadius: isTablet ? 20 : null,
marginTop: isTablet ? 50 : null,
width: !isTablet ? null : "50%",
minHeight: height * 0.25
}}
>
@@ -182,7 +183,7 @@ export const Signup = ({ changeMode, welcome }) => {
marginBottom: 25,
marginTop: 10
}}
size={SIZE.xxl}
size={AppFontSize.xxl}
>
{strings.createAccount()}
</Heading>
@@ -268,7 +269,7 @@ export const Signup = ({ changeMode, welcome }) => {
onPress={() => {
signup();
}}
fontSize={SIZE.md}
fontSize={AppFontSize.md}
width="100%"
/>
@@ -285,12 +286,12 @@ export const Signup = ({ changeMode, welcome }) => {
}}
>
<Paragraph
size={SIZE.xs + 1}
size={AppFontSize.xs + 1}
color={colors.secondary.paragraph}
>
{strings.alreadyHaveAccount()}{" "}
<Paragraph
size={SIZE.xs + 1}
size={AppFontSize.xs + 1}
style={{ color: colors.primary.accent }}
>
{strings.login()}
@@ -299,9 +300,15 @@ export const Signup = ({ changeMode, welcome }) => {
</TouchableOpacity>
</View>
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<Paragraph
style={{
marginBottom: 25
marginBottom: 25,
textAlign: "center"
}}
size={AppFontSize.xxs}
color={colors.secondary.paragraph}
@@ -324,7 +331,10 @@ export const Signup = ({ changeMode, welcome }) => {
<Paragraph
size={AppFontSize.xxs}
onPress={() => {
openLinkInBrowser("https://notesnook.com/privacy", colors);
openLinkInBrowser(
"https://notesnook.com/privacy",
colors
);
}}
style={{
textDecorationLine: "underline"
@@ -337,27 +347,16 @@ export const Signup = ({ changeMode, welcome }) => {
{strings.signupAgreement[4]()}
</Paragraph>
</View>
</View>
</KeyboardAwareScrollView>
</>
) : currentStep === SignupSteps.createAccount ? (
) : (
<>
<Loading
title={"Setting up your account..."}
description="Your account is almost ready, please wait..."
/>
</>
) : (
<>
<PaywallComponent
close={() => {
hideAuth();
}}
setupAccount={() => {
setCurrentStep(SignupSteps.createAccount);
}}
isModal={false}
/>
</>
)}
</SignupContext.Provider>
);

View File

@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { strings } from "@notesnook/intl";
import { useRef, useState } from "react";
import { TextInput } from "react-native";
import { db } from "../../common/database";
@@ -27,7 +28,6 @@ import SettingsService from "../../services/settings";
import { useUserStore } from "../../stores/use-user-store";
import { eCloseSimpleDialog } from "../../utils/events";
import TwoFactorVerification from "./two-factor";
import { strings } from "@notesnook/intl";
export const LoginSteps = {
emailAuth: 1,

View File

@@ -31,8 +31,6 @@ import ResultDialog from "../dialogs/result";
import { VaultDialog } from "../dialogs/vault";
import ImagePreview from "../image-preview";
import MergeConflicts from "../merge-conflicts";
import PremiumDialog from "../premium";
import { Expiring } from "../premium/expiring";
import SheetProvider from "../sheet-provider";
import RateAppSheet from "../sheets/rate-app";
import RecoveryKeySheet from "../sheets/recovery-key";
@@ -46,7 +44,6 @@ const DialogProvider = () => {
<AppLockPassword />
<LoadingDialog />
<Dialog context="global" />
<PremiumDialog colors={colors} />
<AuthModal colors={colors} />
<MergeConflicts />
<RecoveryKeySheet colors={colors} />
@@ -56,7 +53,6 @@ const DialogProvider = () => {
<VaultDialog colors={colors} />
<RateAppSheet />
<ImagePreview />
<Expiring />
<AnnouncementDialog />
<SessionExpired />
<PDFPreview />

View File

@@ -115,23 +115,28 @@ const Intro = () => {
);
return (
<ScrollView
<View
style={{
flex: 1,
height: "100%",
backgroundColor: colors.primary.background
}}
>
<View
testID="notesnook.splashscreen"
style={{
width: "100%",
backgroundColor: colors.primary.background
flex: 1
}}
>
<View
style={[
{
width: "100%",
backgroundColor: colors.secondary.background,
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
paddingTop: insets.top + 10,
paddingBottom: insets.top + 10,
minHeight: height * 0.7 - (insets.top + insets.bottom)
flexGrow: 1
},
isTablet && {
width: width / 2,
@@ -162,18 +167,23 @@ const Intro = () => {
renderItem={renderItem}
/>
</View>
</View>
<View
style={{
width: "100%",
justifyContent: "center",
minHeight: height * 0.3
gap: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
flexShrink: 1
}}
>
<Button
width={250}
style={{
width: "100%"
}}
onPress={async () => {
SettingsService.set({ introCompleted: true });
// SettingsService.set({ introCompleted: true });
Navigation.push("Auth", {
mode: AuthMode.welcomeSignup
});
@@ -184,17 +194,23 @@ const Intro = () => {
/>
<Button
width="100%"
title={"I already have an account"}
style={{
width: "100%"
}}
title={strings.iAlreadyHaveAnAccount()}
type="secondary"
fontSize={AppFontSize.md}
onPress={() => {
SettingsService.set({
introCompleted: true
// SettingsService.set({
// introCompleted: true
// });
Navigation.push("Auth", {
mode: AuthMode.login
});
}}
/>
</View>
</ScrollView>
</View>
);
};

View File

@@ -1,96 +0,0 @@
/*
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/>.
*/
import React from "react";
import { FeatureBlock } from "./feature";
import { ScrollView } from "react-native-actions-sheet";
import { DefaultAppStyles } from "../../utils/styles";
export const CompactFeatures = ({
vertical,
features = [],
maxHeight = 600,
scrollRef
}) => {
let data = vertical
? features
: [
{
highlight: "Everything",
content: "in basic",
icon: "emoticon-wink"
},
{
highlight: "Unlimited",
content: "notebooks",
icon: "notebook"
},
{
highlight: "File & image",
content: "attachments",
icon: "attachment"
},
{
highlight: "Instant",
content: "syncing",
icon: "sync"
},
{
highlight: "Private",
content: "vault",
icon: "shield"
},
{
highlight: "Daily, weekly & monthly",
content: "recurring reminders",
icon: "bell"
},
{
highlight: "Rich text",
content: "editing",
icon: "square-edit-outline"
},
{
highlight: "PDF & markdown",
content: "exports",
icon: "file"
},
{
highlight: "Encrypted",
content: "backups",
icon: "backup-restore"
}
];
return (
<ScrollView
horizontal={!vertical}
showsHorizontalScrollIndicator={false}
style={{
width: "100%",
maxHeight: maxHeight,
paddingHorizontal: DefaultAppStyles.GAP
}}
>
{data.map((item) => (
<FeatureBlock key={item.highlight} vertical={vertical} {...item} />
))}
</ScrollView>
);
};

View File

@@ -1,218 +0,0 @@
/*
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/>.
*/
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { usePricing } from "../../hooks/use-pricing";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import PremiumService from "../../services/premium";
import { useThemeColors } from "@notesnook/theme";
import {
eOpenPremiumDialog,
eOpenResultDialog,
eOpenTrialEndingDialog
} from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { sleep } from "../../utils/time";
import BaseDialog from "../dialog/base-dialog";
import DialogContainer from "../dialog/dialog-container";
import { Button } from "../ui/button";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { CompactFeatures } from "./compact-features";
import { Offer } from "./offer";
import { DefaultAppStyles } from "../../utils/styles";
export const Expiring = () => {
const { colors } = useThemeColors();
const [visible, setVisible] = useState(false);
const [status, setStatus] = useState({
title: "Your trial is ending soon",
offer: "Get 30% off",
extend: true
});
const pricing = usePricing("yearly");
const promo =
status.offer && pricing?.info
? {
promoCode:
pricing?.info?.discount > 30
? pricing.info.sku
: "com.streetwriters.notesnook.sub.yr.trialoffer",
text: `GET ${
pricing?.info?.discount > 30 ? pricing?.info?.discount : 30
}% OFF on yearly`,
discount: pricing?.info?.discount > 30 ? pricing?.info?.discount : 30
}
: null;
useEffect(() => {
eSubscribeEvent(eOpenTrialEndingDialog, open);
return () => {
eUnSubscribeEvent(eOpenTrialEndingDialog, open);
};
}, []);
const open = (status) => {
setStatus(status);
setVisible(true);
};
return (
visible && (
<BaseDialog
onRequestClose={() => {
setVisible(false);
}}
>
<DialogContainer>
<View
style={{
width: "100%",
alignItems: "center"
}}
>
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
width: "100%"
}}
>
<Heading
textBreakStrategy="balanced"
style={{
textAlign: "center",
paddingTop: 18
}}
>
{status.title}
</Heading>
<Seperator />
<View
style={{
width: "100%",
alignItems: "center"
}}
>
{status.offer ? (
<>
<Offer padding={20} off={promo?.discount || 30} />
</>
) : (
<>
<Paragraph
textBreakStrategy="balanced"
style={{
textAlign: "center",
paddingTop: 0,
paddingBottom: 20
}}
size={AppFontSize.md + 2}
>
Upgrade now to continue using all the pro features after
your trial ends
</Paragraph>
</>
)}
<CompactFeatures />
<Paragraph
onPress={async () => {
setVisible(false);
await sleep(300);
eSendEvent(eOpenPremiumDialog, promo);
}}
size={AppFontSize.xs}
style={{
textDecorationLine: "underline",
color: colors.secondary.paragraph,
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
>
{"See what's included in Basic & Pro plans"}
</Paragraph>
<Seperator />
</View>
</View>
<View
style={{
backgroundColor: colors.secondary.background,
width: "100%",
borderBottomRightRadius: 10,
borderBottomLeftRadius: 10
}}
>
<Button
type="transparent"
title="Subscribe now"
onPress={async () => {
setVisible(false);
await sleep(300);
PremiumService.sheet(
null,
promo?.discount > 30 ? null : promo
);
}}
fontSize={AppFontSize.md + 2}
style={{
marginBottom: status.extend ? 0 : 10,
marginTop: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: DefaultAppStyles.GAP * 2
}}
/>
{status.extend && (
<Button
type="plain"
title="Not sure yet? Extend trial for 7 days"
textStyle={{
textDecorationLine: "underline"
}}
onPress={async () => {
setVisible(false);
await sleep(300);
eSendEvent(eOpenResultDialog, {
title: "Your trial has been extended",
paragraph:
"Try out all features of Notesnook free for 7 more days. No limitations. No commitments.",
button: "Continue"
});
}}
fontSize={AppFontSize.xs}
height={30}
style={{
marginBottom: DefaultAppStyles.GAP_VERTICAL
}}
/>
)}
</View>
</View>
</DialogContainer>
</BaseDialog>
)
);
};

View File

@@ -1,97 +0,0 @@
/*
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/>.
*/
import React from "react";
import { Text, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useThemeColors } from "@notesnook/theme";
import { defaultBorderRadius, AppFontSize } from "../../utils/size";
import Paragraph from "../ui/typography/paragraph";
import { ProTag } from "./pro-tag";
import { DefaultAppStyles } from "../../utils/styles";
export const FeatureBlock = ({
vertical,
highlight,
content,
icon,
pro,
proTagBg
}) => {
const { colors } = useThemeColors();
return vertical ? (
<View
style={{
flexDirection: "row",
alignItems: "center",
paddingHorizontal: DefaultAppStyles.GAP,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
backgroundColor: colors.secondary.background,
borderRadius: 10,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
<Paragraph
style={{
flexWrap: "wrap",
marginLeft: 5,
flexShrink: 1
}}
size={AppFontSize.sm}
>
{content}
</Paragraph>
</View>
) : (
<View
style={{
height: 100,
justifyContent: "center",
padding: DefaultAppStyles.GAP_SMALL,
marginRight: 10,
borderRadius: defaultBorderRadius,
minWidth: 100
}}
>
<Icon color={colors.primary.icon} name={icon} size={AppFontSize.xl} />
<Paragraph size={AppFontSize.md}>
<Text style={{ color: colors.primary.accent }}>{highlight}</Text>
{content ? "\n" + content : null}
</Paragraph>
{pro ? (
<>
<View style={{ height: 5 }} />
<ProTag width={50} size={AppFontSize.xs} background={proTagBg} />
</>
) : (
<View
style={{
width: 30,
height: 3,
marginTop: DefaultAppStyles.GAP_VERTICAL,
borderRadius: 100,
backgroundColor: colors.primary.accent
}}
/>
)}
</View>
);
};

View File

@@ -1,318 +0,0 @@
/*
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/>.
*/
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"
}
]
},
{
title: "No limit on notes or devices",
detail:
"Basic or Pro, you can create unlimited number of notes and access them on all your devices. You won't be running out of space or blocks ever."
},
{
title: "Attach files & images",
detail:
"Add your documents, PDFs, images and videos, and keep them safe and organized.",
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: "Cross platform Reminders",
detail: "Stay updated on all your upcoming tasks with reminders.",
features: [
{
highlight: "One-time",
content: "reminders",
icon: "bell"
},
{
highlight: "Daily, weekly & monthly",
content: "reminders",
icon: "refresh",
pro: true
}
]
},
{
title: "Two-factor authentication",
detail:
"Improve account security & prevent intruders from accessing your notes",
info: "* 2FA via email is enabled by default for all users.",
features: [
{
highlight: "Email *",
icon: "bell"
},
{
highlight: "Authentication",
content: "app",
icon: "refresh"
},
{
highlight: "SMS",
icon: "refresh",
pro: true
}
]
},
{
title: "Keep secrets always locked with private vault",
detail:
"An extra layer of security for any important data. Notes in the vault always stay encrypted and require a password to be accessed or edited everytime.",
pro: true
},
{
title: "Organize yourself in the best way",
detail:
"We offer multiple ways to keep you organized. The only limit is your imagination.",
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"
},
{
highlight: "Pin note in",
content: "notifications",
icon: "pin",
platform: "android"
}
],
info: "* Free users are limited to keeping 3 notebooks and 5 tags."
},
{
title: "Instant sync",
detail:
"Seamlessly work from anywhere on any device. Every change is synced instantly to all your devices.",
info: "* Disable sync completely, turn off auto sync or disable editor realtime sync.",
features: [
{
highlight: "Sync to unlimited",
content: "devices",
icon: "cellphone"
},
{
highlight: "Realtime",
content: "editor sync",
icon: "sync"
},
{
highlight: "Granular sync",
content: "controls *",
icon: "sync-off"
}
]
},
{
title: "Rich tools for rich editing",
detail:
"Having the right tool at the right time is crucial for note taking. Lists, tables, codeblocks — you name it, we have it.",
features: [
{
highlight: "Basic formatting",
content: "and lists",
icon: "format-bold"
},
{
highlight: "Checklists",
content: "& tables",
icon: "table",
pro: true
},
{
highlight: "Markdown",
content: "support",
icon: "language-markdown",
pro: true
},
{
highlight: "Personalized",
content: "editor toolbar",
icon: "gesture-tap-button",
pro: true
},
{
highlight: "Write notes from",
content: "notifications",
icon: "bell",
platform: "android"
}
]
},
{
title: "Safe publishing to the Internet",
detail:
"Publishing is nothing new but we offer fully encrypted, anonymous publishing. Take any note & share it with the world.",
features: [
{
highlight: "Password protected",
content: "sharing",
icon: "send-lock"
},
{
highlight: "Self destruct",
content: "monographs",
icon: "bomb"
}
]
},
{
title: "Export and take your notes anywhere",
pro: true,
detail:
"You own your notes, not us. No proprietary formats. No vendor lock in. No waiting for hours to download your notes.",
info: "* Free users can export notes in well formatted plain text.",
features: [
{
highlight: "Export as ",
content: "Markdown",
icon: "language-markdown",
pro: true
},
{
highlight: "Export as",
content: "PDF",
icon: "file-pdf-box",
pro: true
},
{
highlight: "Export as",
content: "HTML",
icon: "language-html5",
pro: true
},
{
highlight: "Export as",
content: "text",
icon: "clipboard-text-outline"
}
]
},
{
title: "Backup & keep your notes safe",
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 & make Notesnook your own",
detail:
"Change app themes to match your style. Custom themes are coming soon.",
features: [
{
highlight: "Automatic",
content: "dark mode",
icon: "theme-light-dark",
pro: false
},
{
highlight: "Change accent",
content: "color",
icon: "invert-colors",
pro: true
}
]
}
];
/**
*
{
highlight: 'Private vault',
content: 'for notes',
icon: 'shield-lock'
}
*/

View File

@@ -1,93 +0,0 @@
/*
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/>.
*/
import React from "react";
import { ScrollView, View } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import { AppFontSize } from "../../utils/size";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { FeatureBlock } from "./feature";
import { ProTag } from "./pro-tag";
import { DefaultAppStyles } from "../../utils/styles";
export const Group = ({ item, index }) => {
const { colors } = useThemeColors();
return (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
backgroundColor:
index % 2 !== 0
? colors.primary.background
: colors.secondary.background,
paddingVertical: 40
}}
>
{item?.pro ? (
<ProTag
size={AppFontSize.sm}
background={
index % 2 === 0
? colors.primary.background
: colors.secondary.background
}
/>
) : null}
<Heading>{item.title}</Heading>
<Paragraph size={AppFontSize.md}>{item.detail}</Paragraph>
{item.features && (
<ScrollView
style={{
marginTop: DefaultAppStyles.GAP
}}
horizontal
showsHorizontalScrollIndicator={false}
>
{item.features?.map((item) => (
<FeatureBlock
key={item.detail}
{...item}
detail={item.detail}
pro={item.pro}
proTagBg={
index % 2 === 0
? colors.primary.background
: colors.secondary.background
}
/>
))}
</ScrollView>
)}
{item.info ? (
<Paragraph
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
size={AppFontSize.xs}
color={colors.secondary.paragraph}
>
{item.info}
</Paragraph>
) : null}
</View>
);
};

View File

@@ -1,88 +0,0 @@
/*
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/>.
*/
import React, { createRef } from "react";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import { eClosePremiumDialog, eOpenPremiumDialog } from "../../utils/events";
import BaseDialog from "../dialog/base-dialog";
import { PaywallComponent } from "./component";
import { IconButton } from "../ui/icon-button";
class PremiumDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
promo: null
};
this.actionSheetRef = createRef();
}
componentDidMount() {
eSubscribeEvent(eOpenPremiumDialog, this.open);
eSubscribeEvent(eClosePremiumDialog, this.close);
}
componentWillUnmount() {
eUnSubscribeEvent(eOpenPremiumDialog, this.open);
eUnSubscribeEvent(eClosePremiumDialog, this.close);
}
open = (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
animation="slide"
bounce={false}
background={this.props.colors.primary.background}
onRequestClose={this.onClose}
>
<PaywallComponent
getRef={() => this.actionSheetRef}
promo={this.state.promo}
close={this.close}
/>
</BaseDialog>
);
}
}
export default PremiumDialog;

View File

@@ -1,46 +0,0 @@
/*
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/>.
*/
import React from "react";
import { Text } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import { AppFontSize } from "../../utils/size";
import Paragraph from "../ui/typography/paragraph";
export const Offer = ({
off = "30",
text = "on yearly plan, offer ends soon",
padding = 0
}) => {
const { colors } = useThemeColors();
return (
<Paragraph
style={{
textAlign: "center",
paddingVertical: padding
}}
size={AppFontSize.xxxl}
>
GET {off}
<Text style={{ color: colors.primary.accent }}>%</Text> OFF!{"\n"}
<Paragraph color={colors.secondary.paragraph}>{text}</Paragraph>
</Paragraph>
);
};

View File

@@ -18,37 +18,35 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import {
ActivityIndicator,
BackHandler,
Image,
NativeEventSubscription,
ScrollView,
Text,
TouchableOpacity,
View
} from "react-native";
import * as RNIap from "react-native-iap";
import { SafeAreaView } from "react-native-safe-area-context";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import ToggleSwitch from "toggle-switch-react-native";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import usePricingPlans, { PricingPlan } from "../../hooks/use-pricing-plans";
import Navigation, { NavigationProps } from "../../services/navigation";
import { getElevationStyle } from "../../utils/elevation";
import { openLinkInBrowser } from "../../utils/functions";
import { SIZE } from "../../utils/size";
import SheetProvider from "../sheet-provider";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Header } from "../header";
import { BuyPlan } from "../sheets/buy-plan";
import { Toast } from "../toast";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { FeaturesList } from "./features-list";
import { IconButton } from "../ui/icon-button";
type PaywallComponentProps = {
isModal?: boolean;
close?: () => void;
promo?: {
promoCode: string;
};
};
const Steps = {
select: 1,
@@ -56,20 +54,61 @@ const Steps = {
finish: 3
};
export const PaywallComponent = ({
close,
promo,
isModal
}: PaywallComponentProps) => {
const PayWall = (props: NavigationProps<"PayWall">) => {
const routeParams = props.route.params;
const { colors } = useThemeColors();
const pricingPlans = usePricingPlans({
promoOffer: promo
});
const pricingPlans = usePricingPlans();
const [annualBilling, setAnnualBilling] = useState(true);
const [step, setStep] = useState(Steps.select);
const isFocused = useNavigationFocus(props.navigation, {
onBlur: () => true,
onFocus: () => true
});
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 () => {
listener.remove();
};
}, [isFocused, step]);
return (
<>
<SafeAreaView
style={{
backgroundColor: colors.primary.background,
flex: 1
}}
>
{routeParams.context === "signup" && step === Steps.select ? null : (
<Header
canGoBack={true}
onLeftMenuButtonPress={() => {
if (step === Steps.buy) {
setStep(Steps.select);
return;
}
props.navigation.goBack();
}}
title={
step === Steps.buy
? pricingPlans.userCanRequestTrial
? `Try ${pricingPlans.currentPlan?.name} plan for free`
: `${pricingPlans.currentPlan?.name} plan`
: ""
}
/>
)}
{step === Steps.select ? (
<>
<ScrollView
@@ -77,40 +116,20 @@ export const PaywallComponent = ({
width: "100%"
}}
contentContainerStyle={{
gap: 12,
gap: DefaultAppStyles.GAP_VERTICAL,
paddingBottom: 80
}}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
stickyHeaderIndices={[0]}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
width: "100%",
justifyContent: "flex-start",
marginBottom: -12,
backgroundColor: colors.primary.background
}}
>
<IconButton
style={{
alignSelf: "flex-start"
}}
onPress={() => close?.()}
name="chevron-left"
/>
</View>
<View
style={{
paddingTop: 100,
borderBottomColor: colors.primary.border,
borderBottomWidth: 1,
paddingHorizontal: 16,
paddingHorizontal: DefaultAppStyles.GAP,
paddingBottom: 25,
gap: 12
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<View
@@ -123,27 +142,27 @@ export const PaywallComponent = ({
<Icon name="crown" size={30} color={colors.static.orange} />
<Heading
key="heading"
size={SIZE.xl}
size={AppFontSize.xl}
style={{
alignSelf: "center"
}}
>
Notesnook{" "}
<Heading size={SIZE.xl} color={colors.primary.accent}>
Pro
<Heading size={AppFontSize.xl} color={colors.primary.accent}>
Plans
</Heading>
</Heading>
</View>
<Paragraph key="description" size={SIZE.md}>
<Paragraph key="description" size={AppFontSize.md}>
Ready to take the next step on your private note taking journey?
</Paragraph>
</View>
<View
style={{
gap: 16,
paddingHorizontal: 16
gap: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<TouchableOpacity
@@ -231,7 +250,7 @@ export const PaywallComponent = ({
style={{
flexShrink: 1
}}
size={SIZE.lg}
size={AppFontSize.lg}
>
Open Source
</Paragraph>
@@ -269,7 +288,7 @@ export const PaywallComponent = ({
style={{
flexShrink: 1
}}
size={SIZE.lg}
size={AppFontSize.lg}
>
8.7K stars
</Paragraph>
@@ -307,7 +326,7 @@ export const PaywallComponent = ({
maxWidth: 300,
textAlign: "center"
}}
size={SIZE.lg}
size={AppFontSize.lg}
>
Recommended by Privacy Guides
</Paragraph>
@@ -436,6 +455,7 @@ After trying all the privacy security oriented note taking apps, for the price a
borderTopWidth: 1,
borderTopColor: colors.primary.border,
position: "absolute",
paddingBottom: DefaultAppStyles.GAP_VERTICAL,
bottom: 0
}}
>
@@ -451,7 +471,11 @@ After trying all the privacy security oriented note taking apps, for the price a
}
onPress={() => {
if (pricingPlans.currentPlan?.id === "free") {
close?.();
if (routeParams.context === "signup") {
Navigation.replace("FluidPanelsView", {});
} else {
Navigation.goBack();
}
}
if (
!pricingPlans.currentPlan?.id ||
@@ -484,7 +508,7 @@ After trying all the privacy security oriented note taking apps, for the price a
)}
<Toast context="local" />
</>
</SafeAreaView>
);
};
@@ -517,17 +541,19 @@ const FAQItem = (props: { question: string; answer: string }) => {
style={{
flexShrink: 1
}}
size={SIZE.md}
size={AppFontSize.md}
>
{props.question}
</Heading>
<Icon
name={expanded ? "chevron-up" : "chevron-down"}
color={colors.secondary.icon}
size={SIZE.xxl}
size={AppFontSize.xxl}
/>
</View>
{expanded ? <Paragraph size={SIZE.md}>{props.answer}</Paragraph> : null}
{expanded ? (
<Paragraph size={AppFontSize.md}>{props.answer}</Paragraph>
) : null}
</TouchableOpacity>
);
};
@@ -575,17 +601,17 @@ const ComparePlans = React.memo(
{item === true ? (
<Icon
color={colors.primary.accent}
size={SIZE.sm}
size={AppFontSize.sm}
name="check"
/>
) : item === false ? (
<Icon
size={SIZE.sm}
size={AppFontSize.sm}
color={colors.static.red}
name="close"
/>
) : keyIndex === 0 ? (
<Heading size={SIZE.sm}>{item}</Heading>
<Heading size={AppFontSize.sm}>{item}</Heading>
) : (
<Paragraph>{item}</Paragraph>
)}
@@ -626,7 +652,7 @@ const ReviewItem = (props: {
style={{
textAlign: "center"
}}
size={SIZE.md}
size={AppFontSize.md}
>
{props.review}
</Paragraph>
@@ -655,7 +681,7 @@ const ReviewItem = (props: {
}}
/>
) : null}
<Paragraph size={SIZE.sm}>{props.user}</Paragraph>
<Paragraph size={AppFontSize.sm}>{props.user}</Paragraph>
</View>
</View>
);
@@ -724,14 +750,14 @@ const PricingPlanCard = ({
alignSelf: "flex-start"
}}
>
<Heading color={colors.static.white} size={SIZE.xs}>
<Heading color={colors.static.white} size={AppFontSize.xs}>
{discountPercentage}% Off
</Heading>
</View>
) : null}
<View>
<Heading size={SIZE.md}>
<Heading size={AppFontSize.md}>
{plan.name}{" "}
{plan.recommended ? (
<Text
@@ -751,12 +777,12 @@ const PricingPlanCard = ({
<ActivityIndicator size="small" color={colors.primary.accent} />
) : (
<View>
<Paragraph size={SIZE.lg}>
<Paragraph size={AppFontSize.lg}>
{isFreePlan ? "0.00" : price} <Paragraph>/month</Paragraph>
</Paragraph>
{isFreePlan ? null : (
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
<Paragraph color={colors.secondary.paragraph} size={AppFontSize.xs}>
billed {annualBilling ? "annually" : "monthly"} at{" "}
{pricingPlans?.getStandardPrice(product as RNIap.Subscription)}{" "}
</Paragraph>
@@ -766,3 +792,5 @@ const PricingPlanCard = ({
</TouchableOpacity>
);
};
export default PayWall;

View File

@@ -1,144 +0,0 @@
/*
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/>.
*/
import React, { useCallback, useEffect, useRef, useState } from "react";
import { View } from "react-native";
import Animated, { FadeInUp, FadeOutUp } from "react-native-reanimated";
import useKeyboard from "../../hooks/use-keyboard";
import { DDS } from "../../services/device-detection";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { getElevationStyle } from "../../utils/elevation";
import {
eCloseActionSheet,
eCloseSheet,
eOpenPremiumDialog,
eShowGetPremium
} from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DefaultAppStyles } from "../../utils/styles";
export const PremiumToast = ({ context = "global", offset = 0 }) => {
const { colors } = useThemeColors();
const [msg, setMsg] = useState(null);
const timer = useRef();
const keyboard = useKeyboard();
const open = useCallback(
(event) => {
if (!event) {
clearTimeout(timer);
timer.current = null;
setMsg(null);
return;
}
if (event.context === context && msg?.desc !== event.desc) {
if (timer.current !== null) {
clearTimeout(timer.current);
timer.current = null;
}
setMsg(event);
timer.current = setTimeout(async () => {
setMsg(null);
}, 3000);
}
},
[context, msg?.desc]
);
useEffect(() => {
eSubscribeEvent(eShowGetPremium, open);
return () => {
eUnSubscribeEvent(eShowGetPremium, open);
};
}, [open]);
const onPress = async () => {
open(null);
eSendEvent(eCloseActionSheet);
eSendEvent(eCloseSheet);
await sleep(300);
eSendEvent(eOpenPremiumDialog);
};
return (
!!msg && (
<Animated.View
entering={FadeInUp}
exiting={FadeOutUp}
style={{
position: "absolute",
backgroundColor: colors.secondary.background,
zIndex: 999,
...getElevationStyle(20),
padding: DefaultAppStyles.GAP,
borderRadius: 10,
flexDirection: "row",
alignSelf: "center",
justifyContent: "space-between",
top: offset + keyboard.keyboardHeight,
maxWidth: DDS.isLargeTablet() ? 400 : "98%"
}}
onTouchEnd={() => {
setMsg(null);
clearTimeout(timer.current);
}}
>
<View
style={{
flexShrink: 1,
flexGrow: 1,
paddingRight: 6
}}
>
<Heading
style={{
flexWrap: "wrap"
}}
color={colors.primary.accent}
size={AppFontSize.md}
>
{msg.title}
</Heading>
<Paragraph
style={{
flexWrap: "wrap"
}}
size={AppFontSize.sm}
color={colors.primary.paragraph}
>
{msg.desc}
</Paragraph>
</View>
<Button onPress={onPress} title="Get Now" type="accent" />
</Animated.View>
)
);
};

View File

@@ -1,101 +0,0 @@
/*
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/>.
*/
import React from "react";
import { Platform, View } from "react-native";
import { AppFontSize } from "../../utils/size";
import { Pressable } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import RNIap from "react-native-iap";
import { DefaultAppStyles } from "../../utils/styles";
export const PricingItem = ({
product,
onPress,
compact,
strikethrough
}: {
product: {
type: "yearly" | "monthly";
data?: RNIap.Subscription;
info: string;
offerType?: "yearly" | "monthly";
};
strikethrough?: boolean;
onPress?: () => void;
compact?: boolean;
}) => {
return (
<Pressable
onPress={onPress}
type="secondary"
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: compact ? 15 : 10,
width: compact ? null : "100%",
minWidth: 150,
opacity: strikethrough ? 0.7 : 1
}}
disabled={strikethrough}
>
{!compact && (
<View>
<Heading size={AppFontSize.lg - 2}>
{product?.type === "yearly" || product?.offerType === "yearly"
? "Yearly"
: "Monthly"}
</Heading>
{product?.info && (
<Paragraph size={AppFontSize.xs}>{product.info}</Paragraph>
)}
</View>
)}
<View>
<Paragraph
style={{
textDecorationLine: strikethrough ? "line-through" : undefined
}}
size={AppFontSize.sm}
>
<Heading
style={{
textDecorationLine: strikethrough ? "line-through" : undefined
}}
size={AppFontSize.lg - 2}
>
{Platform.OS === "android"
? (product.data as RNIap.SubscriptionAndroid | undefined)
?.subscriptionOfferDetails?.[0]?.pricingPhases
.pricingPhaseList?.[0]?.formattedPrice
: (product.data as RNIap.SubscriptionIOS | undefined)
?.localizedPrice}
</Heading>
{product?.type === "yearly" || product?.offerType === "yearly"
? "/year"
: "/month"}
</Paragraph>
</View>
</Pressable>
);
};

View File

@@ -1,431 +0,0 @@
/*
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/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { ActivityIndicator, Platform, Text, View } from "react-native";
import usePricingPlans from "../../hooks/use-pricing-plans";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import {
eClosePremiumDialog,
eCloseSheet,
eCloseSimpleDialog,
eOpenLoginDialog
} from "../../utils/events";
import { openLinkInBrowser } from "../../utils/functions";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Dialog } from "../dialog";
import BaseDialog from "../dialog/base-dialog";
import { presentDialog } from "../dialog/functions";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { PricingItem } from "./pricing-item";
const UUID_PREFIX = "0bdaea";
const UUID_VERSION = "4";
const UUID_VARIANT = "a";
function toUUID(str: string) {
return [
UUID_PREFIX + str.substring(0, 2), // 6 digit prefix + first 2 oid digits
str.substring(2, 6), // # next 4 oid digits
UUID_VERSION + str.substring(6, 9), // # 1 digit version(0x4) + next 3 oid digits
UUID_VARIANT + str.substring(9, 12), // # 1 digit variant(0b101) + 1 zero bit + next 3 oid digits
str.substring(12)
].join("-");
}
const promoCyclesMonthly = {
1: "first month",
2: "first 2 months",
3: "first 3 months",
4: "first 4 months",
5: "first 5 months",
6: "first 3 months"
};
const promoCyclesYearly = {
1: "first year",
2: "first 2 years",
3: "first 3 years"
};
export const PricingPlans = ({
promo,
marginTop,
heading = true,
compact = false
}: {
promo?: {
promoCode: string;
};
marginTop?: any;
heading?: boolean;
compact?: boolean;
}) => {
const { colors } = useThemeColors();
const {
buySubscription,
buying,
getStandardPrice,
product,
setBuying,
loading,
user,
setProduct,
monthlyPlan,
yearlyPlan,
getPromo
} = usePricingPlans({
promoOffer: promo
});
return loading ? (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
justifyContent: "center",
alignItems: "center",
height: 100
}}
>
<ActivityIndicator color={colors.primary.accent} size={25} />
</View>
) : (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
{buying ? (
<BaseDialog visible statusBarTranslucent centered>
<ActivityIndicator size={50} color="white" />
</BaseDialog>
) : null}
{!user && !product ? (
<>
{heading || (monthlyPlan?.info?.discount || 0) > 0 ? (
<>
{monthlyPlan && (monthlyPlan?.info?.discount || 0) > 0 ? (
<View
style={{
alignSelf: "center",
marginTop: marginTop || 20,
marginBottom: 20
}}
>
<Heading
style={{
textAlign: "center"
}}
color={colors.primary.accent}
>
Get {monthlyPlan?.info?.discount}% off in{" "}
{monthlyPlan?.info?.country}
</Heading>
</View>
) : (
<Heading
style={{
alignSelf: "center",
marginTop: marginTop || 20,
marginBottom: 20
}}
>
Choose a plan
</Heading>
)}
</>
) : null}
<View
style={{
flexDirection: !compact ? "column" : "row",
flexWrap: "wrap",
justifyContent: "space-around"
}}
>
<PricingItem
onPress={() => {
if (!monthlyPlan?.product) return;
buySubscription(monthlyPlan?.product);
}}
compact={compact}
product={{
type: "monthly",
data: monthlyPlan?.product,
info: "Pay once a month, cancel anytime."
}}
/>
{!compact && (
<View
style={{
height: 1,
marginVertical: 5
}}
/>
)}
<PricingItem
onPress={() => {
if (!yearlyPlan?.product) return;
buySubscription(yearlyPlan?.product);
}}
compact={compact}
product={{
type: "yearly",
data: yearlyPlan?.product,
info: "Pay once a year, cancel anytime."
}}
/>
</View>
{Platform.OS !== "ios" ? (
<Button
height={35}
style={{
marginTop: 10
}}
onPress={() => {
presentDialog({
context: "local",
input: true,
inputPlaceholder: "Enter code",
positiveText: "Apply",
positivePress: async (value) => {
if (!value) return;
eSendEvent(eCloseSimpleDialog);
setBuying(true);
try {
if (!(await getPromo(value as string)))
throw new Error("Error applying promo code");
ToastManager.show({
heading: "Discount applied!",
type: "success",
context: "local"
});
setBuying(false);
} catch (e) {
setBuying(false);
ToastManager.show({
heading: "Promo code invalid or expired",
message: (e as Error).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
style={{
height: 15
}}
/>
)}
</>
) : (
<View>
{!user ? (
<>
<Button
onPress={() => {
eSendEvent(eClosePremiumDialog);
eSendEvent(eCloseSheet);
setTimeout(() => {
eSendEvent(eOpenLoginDialog, 1);
}, 400);
}}
title={"Sign up for free"}
type="accent"
width={250}
style={{
paddingHorizontal: 12,
marginTop: product?.type === "promo" ? 0 : 30,
marginBottom: 10
}}
/>
{Platform.OS !== "ios" &&
promo &&
!promo.promoCode.startsWith("com.streetwriters.notesnook") ? (
<Paragraph
size={AppFontSize.md}
textBreakStrategy="balanced"
style={{
alignSelf: "center",
justifyContent: "center",
textAlign: "center"
}}
>
Use promo code{" "}
<Text
style={{
fontFamily: "OpenSans-SemiBold"
}}
>
{promo.promoCode}
</Text>{" "}
at checkout
</Paragraph>
) : null}
</>
) : (
<>
<Button
onPress={() => {
if (!product?.data) return;
buySubscription(product.data);
}}
height={40}
width="50%"
type="accent"
title="Subscribe now"
/>
<Button
onPress={() => {
setProduct(undefined);
}}
style={{
marginTop: 5
}}
height={30}
fontSize={13}
type="errorShade"
title="Cancel promo code"
/>
</>
)}
</View>
)}
{!user ? (
<Paragraph
color={colors.secondary.paragraph}
size={AppFontSize.xs}
style={{
alignSelf: "center",
textAlign: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL,
maxWidth: "80%"
}}
>
{user
? 'On clicking "Try free for 14 days", your free trial will be activated.'
: "After sign up you will be asked to activate your free trial."}{" "}
<Paragraph size={AppFontSize.xs} style={{ fontWeight: "bold" }}>
No credit card is required.
</Paragraph>
</Paragraph>
) : null}
{user ? (
<>
{Platform.OS === "ios" ? (
<Paragraph
textBreakStrategy="balanced"
size={AppFontSize.xs}
color={colors.secondary.paragraph}
style={{
alignSelf: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL,
textAlign: "center"
}}
>
By subscribing, you will be charged to your iTunes Account for the
selected plan. Subscriptions will automatically renew unless
cancelled within 24-hours before the end of the current period.
</Paragraph>
) : (
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
style={{
alignSelf: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL,
textAlign: "center"
}}
>
By subscribing, you will be charged on your Google Account, and
your subscription will automatically renew until you cancel prior
to the end of the then current period.
</Paragraph>
)}
<View
style={{
width: "100%"
}}
>
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
style={{
maxWidth: "100%",
textAlign: "center"
}}
>
By subscribing, you agree to our{" "}
<Paragraph
size={AppFontSize.xs}
onPress={() => {
openLinkInBrowser("https://notesnook.com/tos")
.catch(() => {})
.then(() => {});
}}
style={{
textDecorationLine: "underline"
}}
color={colors.primary.accent}
>
Terms of Service{" "}
</Paragraph>
and{" "}
<Paragraph
size={AppFontSize.xs}
onPress={() => {
openLinkInBrowser("https://notesnook.com/privacy")
.catch(() => {})
.then(() => {});
}}
style={{
textDecorationLine: "underline"
}}
color={colors.primary.accent}
>
Privacy Policy.
</Paragraph>
</Paragraph>
</View>
</>
) : null}
<Dialog context="local" />
</View>
);
};

View File

@@ -1,60 +0,0 @@
/*
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/>.
*/
import React from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useThemeColors } from "@notesnook/theme";
import Paragraph from "../ui/typography/paragraph";
import { DefaultAppStyles } from "../../utils/styles";
/**
*
* @param {any} param0
* @returns
*/
export const ProTag = ({ width, size, background }) => {
const { colors } = useThemeColors();
return (
<View
style={{
backgroundColor: background || colors.primary.background,
borderRadius: 100,
width: width || 60,
justifyContent: "center",
alignItems: "center",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2,
flexDirection: "row"
}}
>
<Icon
style={{
marginRight: 3
}}
size={size}
color={colors.static.orange}
name="crown"
/>
<Paragraph size={size - 1.5} color={colors.primary.accent}>
PRO
</Paragraph>
</View>
);
};

View File

@@ -16,6 +16,8 @@ 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/>.
*/
import { useThemeColors } from "@notesnook/theme";
import dayjs from "dayjs";
import React from "react";
import {
Platform,
@@ -24,19 +26,17 @@ import {
TouchableOpacity,
View
} from "react-native";
import usePricingPlans from "../../../hooks/use-pricing-plans";
import { presentSheet } from "../../../services/event-manager";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { SIZE } from "../../../utils/size";
import Paragraph from "../../ui/typography/paragraph";
import Heading from "../../ui/typography/heading";
import { useThemeColors } from "@notesnook/theme";
import * as RNIap from "react-native-iap";
import { Button } from "../../ui/button";
import dayjs from "dayjs";
import { openLinkInBrowser } from "../../../utils/functions";
import { IconButton } from "../../ui/icon-button";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
import usePricingPlans from "../../../hooks/use-pricing-plans";
import { openLinkInBrowser } from "../../../utils/functions";
import { AppFontSize } from "../../../utils/size";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { DefaultAppStyles } from "../../../utils/styles";
export const BuyPlan = (props: {
planId: string;
@@ -67,9 +67,8 @@ export const BuyPlan = (props: {
return (
<ScrollView
contentContainerStyle={{
gap: 16,
paddingBottom: 80,
paddingTop: Platform.OS === "android" ? insets.top : 0
marginTop: DefaultAppStyles.GAP
}}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
@@ -77,40 +76,9 @@ export const BuyPlan = (props: {
>
<View
style={{
flexDirection: "row",
alignItems: "center",
width: "100%",
backgroundColor: colors.primary.background
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<IconButton
name="chevron-left"
onPress={() => {
props.goBack();
}}
style={{
alignSelf: "flex-start"
}}
/>
</View>
<View
style={{
paddingHorizontal: 16,
gap: 16
}}
>
<Heading
style={{
alignSelf: "center",
marginBottom: 10
}}
>
{props.canActivateTrial
? `Try ${pricingPlans.currentPlan?.name} plan for free`
: `${pricingPlans.currentPlan?.name} plan`}{" "}
</Heading>
{props.canActivateTrial ? (
<View
style={{
@@ -138,7 +106,7 @@ export const BuyPlan = (props: {
>
<Icon
color={colors.primary.accent}
size={SIZE.lg}
size={AppFontSize.lg}
name="check"
/>
<Paragraph>{item}</Paragraph>
@@ -164,11 +132,10 @@ export const BuyPlan = (props: {
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
marginTop: 20
justifyContent: "space-between"
}}
>
<Heading color={colors.primary.paragraph} size={SIZE.sm}>
<Heading color={colors.primary.paragraph} size={AppFontSize.sm}>
Due today{" "}
{pricingPlans.userCanRequestTrial ? (
<Text
@@ -220,6 +187,10 @@ export const BuyPlan = (props: {
? "Subscribe and start free trial"
: "Subscribe"
}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL,
marginBottom: DefaultAppStyles.GAP_VERTICAL
}}
onPress={() => {
const offerToken = pricingPlans.getOfferTokenAndroid(
pricingPlans.selectedProduct as RNIap.SubscriptionAndroid,
@@ -232,13 +203,12 @@ export const BuyPlan = (props: {
}}
/>
<View>
<Heading
style={{
textAlign: "center"
}}
color={colors.secondary.paragraph}
size={SIZE.xs}
size={AppFontSize.xs}
>
{is5YearPlanSelected
? `This is a one time purchase, no subscription.`
@@ -249,7 +219,7 @@ export const BuyPlan = (props: {
textAlign: "center"
}}
color={colors.secondary.paragraph}
size={SIZE.xs}
size={AppFontSize.xs}
>
By joining you agree to our{" "}
<Text
@@ -276,7 +246,6 @@ export const BuyPlan = (props: {
.
</Heading>
</View>
</View>
</ScrollView>
);
};
@@ -307,20 +276,20 @@ const ProductItem = (props: {
<Icon
name={isSelected ? "radiobox-marked" : "radiobox-blank"}
color={isSelected ? colors.primary.accent : colors.secondary.icon}
size={SIZE.lg}
size={AppFontSize.lg}
/>
<View
style={{
gap: 10
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<View
style={{
flexDirection: "row",
gap: 10
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<Heading size={SIZE.md}>
<Heading size={AppFontSize.md}>
{isAnnual
? "Yearly"
: product?.productId.includes("5year")
@@ -338,7 +307,7 @@ const ProductItem = (props: {
justifyContent: "center"
}}
>
<Heading color={colors.static.white} size={SIZE.xs}>
<Heading color={colors.static.white} size={AppFontSize.xs}>
Best value -{" "}
{props.pricingPlans.compareProductPrice(
props.pricingPlans.currentPlan?.id as string,

View File

@@ -48,7 +48,6 @@ import { DefaultAppStyles } from "../../../utils/styles";
import { sleep } from "../../../utils/time";
import { Dialog } from "../../dialog";
import DialogHeader from "../../dialog/dialog-header";
import { ProTag } from "../../premium/pro-tag";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import { Pressable } from "../../ui/pressable";
@@ -227,7 +226,6 @@ const ExportNotesSheet = ({
flexShrink: 1
}}
>
{!item.pro ? <ProTag size={12} /> : null}
<Heading style={{ marginLeft: 10 }} size={AppFontSize.md}>
{item.title}
</Heading>

View File

@@ -24,19 +24,17 @@ import {
ColorValue,
DimensionValue,
TextStyle,
View,
ViewStyle,
useWindowDimensions
} from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useUserStore } from "../../../stores/use-user-store";
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import NativeTooltip from "../../../utils/tooltip";
import { ProTag } from "../../premium/pro-tag";
import { Pressable, PressableProps, useButton } from "../pressable";
import Heading from "../typography/heading";
import Paragraph from "../typography/paragraph";
import { DefaultAppStyles } from "../../../utils/styles";
export interface ButtonProps extends PressableProps {
height?: number;
icon?: string;
@@ -167,15 +165,6 @@ export const Button = ({
{title}
</Component>
)}
{proTag && !premium ? (
<View
style={{
marginLeft: 10
}}
>
<ProTag size={10} width={40} background={colors.primary.shade} />
</View>
) : null}
{icon && !loading && iconPosition === "right" ? (
<Icon

View File

@@ -25,9 +25,9 @@ import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets"
import { useSettingStore } from "../../../stores/use-setting-store";
import { useUserStore } from "../../../stores/use-user-store";
import { getContainerBorder } from "../../../utils/colors";
import { PremiumToast } from "../../premium/premium-toast";
import { Toast } from "../../toast";
import { NotesnookModule } from "../../../utils/notesnook-module";
import { Toast } from "../../toast";
/**
*
* @param {any} param0
@@ -155,11 +155,6 @@ const SheetWrapper = ({
ExtraOverlayComponent={
<>
{overlay}
<PremiumToast
context="sheet"
close={() => fwdRef?.current?.hide()}
offset={50}
/>
<Toast context="local" />
</>
}

View File

@@ -329,7 +329,6 @@ const doAppLoadActions = async () => {
if (NewFeature.present()) return;
if (await checkAppUpdateAvailable()) return;
if (await checkForRateAppRequest()) return;
if (await PremiumService.getRemainingTrialDaysStatus()) return;
if (SettingsService.get().introCompleted) {
useMessageStore.subscribe((state) => {
const dialogs = state.dialogs;

View File

@@ -250,7 +250,7 @@ let MoveNotes: any = null;
let Settings: any = null;
let ManageTags: any = null;
let AddReminder: any = null;
let PayWall: any = null;
export const RootNavigation = () => {
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
@@ -353,6 +353,14 @@ export const RootNavigation = () => {
return AddReminder;
}}
/>
<RootStack.Screen
name="PayWall"
getComponent={() => {
PayWall =
PayWall || require("../components/premium/paywall").default;
return PayWall;
}}
/>
</RootStack.Navigator>
</NavigationContainer>
);

View File

@@ -28,7 +28,6 @@ import {
View
} from "react-native";
import Editor from ".";
import { PremiumToast } from "../../components/premium/premium-toast";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
import useKeyboard from "../../hooks/use-keyboard";
@@ -112,7 +111,6 @@ export const EditorWrapper = ({ widths }: { widths: any }) => {
enabled={!floating}
keyboardVerticalOffset={0}
>
<PremiumToast key="toast" context="editor" offset={50 + insets.top} />
<TextInput
key="input"
ref={textInput}

View File

@@ -70,7 +70,8 @@ const routeNames = {
MoveNotes: "MoveNotes",
Archive: "Archive",
ManageTags: "ManageTags",
AddReminder: "AddReminder"
AddReminder: "AddReminder",
PayWall: "PayWall"
};
export type NavigationProps<T extends RouteName> = NativeStackScreenProps<

View File

@@ -18,27 +18,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { CHECK_IDS } from "@notesnook/core";
import React from "react";
import { Platform } from "react-native";
import Config from "react-native-config";
import * as RNIap from "react-native-iap";
import { db } from "../common/database";
import { MMKV } from "../common/database/mmkv";
import DialogHeader from "../components/dialog/dialog-header";
import { CompactFeatures } from "../components/premium/compact-features";
import { PricingPlans } from "../components/premium/pricing-plans";
import Seperator from "../components/ui/seperator";
import { useUserStore } from "../stores/use-user-store";
import { itemSkus, SUBSCRIPTION_STATUS } from "../utils/constants";
import {
eOpenPremiumDialog,
eOpenTrialEndingDialog,
eShowGetPremium
} from "../utils/events";
import { eOpenPremiumDialog, eShowGetPremium } from "../utils/events";
import { eSendEvent, presentSheet, ToastManager } from "./event-manager";
import SettingsService from "./settings";
import { strings } from "@notesnook/intl";
import SettingsService from "./settings";
let premiumStatus = 0;
/**
@@ -100,10 +91,12 @@ function getMontlySub() {
}
async function loadProductsAndSubs() {
try {
if (!subs || subs.length === 0) {
subs = await RNIap.getSubscriptions({
skus: itemSkus
});
console.log("SUBS", subs);
}
if (!products || products.length === 0) {
@@ -116,6 +109,13 @@ async function loadProductsAndSubs() {
subs,
products
};
} catch (e) {
console.error("Failed to load products and subscriptions", e);
return {
subs: [],
products: []
};
}
}
function get() {
@@ -355,87 +355,7 @@ const subscriptions = {
}
};
async function getRemainingTrialDaysStatus() {
let user = await db.user.getUser();
if (!user || !user.subscription || user.subscription?.expiry === 0) return;
let premium = user.subscription.type !== SUBSCRIPTION_STATUS.BASIC;
let isTrial = user.subscription.type === SUBSCRIPTION_STATUS.TRIAL;
let total = user.subscription.expiry - user.subscription.start;
let current = Date.now() - user.subscription.start;
current = (current / total) * 100;
let lastTrialDialogShownAt = MMKV.getString("lastTrialDialogShownAt");
if (current > 75 && isTrial && lastTrialDialogShownAt !== "ending") {
eSendEvent(eOpenTrialEndingDialog, {
title: strings.trialEndingSoon(),
offer: null,
extend: false
});
MMKV.setItem("lastTrialDialogShownAt", "ending");
return true;
}
if (!premium && lastTrialDialogShownAt !== "expired") {
eSendEvent(eOpenTrialEndingDialog, {
title: strings.trialExpired(),
offer: 30,
extend: false
});
MMKV.setItem("lastTrialDialogShownAt", "expired");
return true;
}
return false;
}
const features_list = [
{
content: "Unlock unlimited notebooks, tags, colors. Organize like a pro"
},
{
content: "Attach files upto 500MB, upload 4K images with unlimited storage"
},
{
content: "Instantly sync to unlimited devices"
},
{
content: "A private vault to keep everything important always locked"
},
{
content:
"Rich note editing experience with markdown, tables, checklists and more"
},
{
content: "Export your notes in PDF, markdown and html formats"
}
];
const sheet = (context, promo, trial) => {
presentSheet({
context: context,
component: (ref) => (
<>
<DialogHeader
centered
title="Upgrade to Notesnook"
titlePart="Pro"
paragraph="Manage your work on another level, enjoy seamless sync and keep all notes in one place."
padding={12}
/>
<Seperator />
<CompactFeatures
scrollRef={ref}
maxHeight={400}
features={features_list}
vertical
/>
<Seperator half />
<PricingPlans trial={trial} compact heading={false} promo={promo} />
</>
)
});
};
const sheet = (context, promo, trial) => {};
const PremiumService = {
verify,
@@ -447,7 +367,6 @@ const PremiumService = {
getUser,
subscriptions,
getMontlySub,
getRemainingTrialDaysStatus,
sheet
};

View File

@@ -101,7 +101,11 @@ export interface RouteParams extends ParamListBase {
reference?: Note;
};
Intro: GenericRouteParam;
};
PayWall: {
canGoBack?: boolean;
context: "signup" | "logged-in" | "logged-out";
};
}
export type RouteName = keyof RouteParams;

View File

@@ -22,7 +22,7 @@ import {
import Config from "react-native-config";
i18n.load({
en: __DEV__ && Config.isTesting !== "true" ? $pseudo : $en
en: !__DEV__ && Config.isTesting !== "true" ? $pseudo : $en
});
setI18nGlobal(i18n);
i18n.activate("en");

View File

@@ -1,7 +1,7 @@
const isGithubRelease = false;
const config = {
commands: require('@callstack/repack/commands/rspack'),
// commands: require('@callstack/repack/commands/rspack'),
project: {
android: {
sourceDir: './android'

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/mobile",
"version": "3.2.10",
"version": "3.2.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "3.2.10",
"version": "3.2.5",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"workspaces": [
@@ -44449,11 +44449,6 @@
"resolved": "https://registry.npmjs.org/react-native-format-currency/-/react-native-format-currency-0.0.5.tgz",
"integrity": "sha512-iarFkCig987Zw8GFPv0+ltEdAsksf0gfZGWL3+tC+60VHtnfbtFBFWP4523ltuXwplDxKu4Veem1ThEr+pZlvQ=="
},
"node_modules/react-native-format-currency": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/react-native-format-currency/-/react-native-format-currency-0.0.5.tgz",
"integrity": "sha512-iarFkCig987Zw8GFPv0+ltEdAsksf0gfZGWL3+tC+60VHtnfbtFBFWP4523ltuXwplDxKu4Veem1ThEr+pZlvQ=="
},
"node_modules/react-native-gesture-handler": {
"version": "2.25.0",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.25.0.tgz",

View File

@@ -70,6 +70,7 @@
"dependencies": {
"@leeoniya/ufuzzy": "^1.0.10",
"@microsoft/signalr": "^8.0.0",
"@notesnook/intl": "file:../intl",
"@notesnook/logger": "file:../logger",
"@readme/data-urls": "^3.0.0",
"@streetwriters/kysely": "^0.27.4",
@@ -83,7 +84,7 @@
"entities": "5.0.0",
"fuzzyjs": "^5.0.1",
"html-to-text": "^9.0.5",
"htmlparser2": "^8.0.1",
"htmlparser2": "^10.0.0",
"katex": "0.16.11",
"linkedom": "^0.14.17",
"liqe": "^1.13.0",
@@ -91,8 +92,7 @@
"prismjs": "^1.29.0",
"qclone": "^1.2.0",
"rfdc": "^1.3.0",
"spark-md5": "^3.0.2",
"sqlite-better-trigram": "0.0.2"
"spark-md5": "^3.0.2"
},
"devDependencies": {
"@notesnook/crypto": "file:../crypto",
@@ -118,6 +118,9 @@
"nanoid": "5.0.7",
"otplib": "^12.0.1",
"refractor": "^4.8.1",
"sqlite-better-trigram": "^0.0.3",
"sqlite-regex": "^0.2.4-alpha.1",
"sqlite3-fts5-html": "^0.0.4",
"vitest": "2.1.8",
"vitest-fetch-mock": "^0.2.2",
"ws": "^8.13.0"
@@ -138,7 +141,7 @@
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
"@notesnook-importer/core": "^2.1.1",
"@notesnook-importer/core": "^2.2.2",
"@notesnook/common": "file:../common",
"@notesnook/intl": "file:../intl",
"@notesnook/theme": "file:../theme",

View File

@@ -987,7 +987,7 @@
},
"../web": {
"name": "@notesnook/web",
"version": "3.0.30",
"version": "3.2.3",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -1002,7 +1002,7 @@
"@lingui/react": "5.1.2",
"@mdi/js": "7.4.47",
"@mdi/react": "1.6.1",
"@notesnook-importer/core": "^2.1.1",
"@notesnook-importer/core": "^2.2.2",
"@notesnook/common": "file:../../packages/common",
"@notesnook/core": "file:../../packages/core",
"@notesnook/crypto": "file:../../packages/crypto",
@@ -1015,6 +1015,7 @@
"@notesnook/themes-server": "file:../../servers/themes",
"@notesnook/ui": "file:../../packages/ui",
"@notesnook/web-clipper": "file:../../extensions/web-clipper",
"@paddle/paddle-js": "^1.4.2",
"@react-pdf-viewer/core": "^3.12.0",
"@react-pdf-viewer/toolbar": "^3.12.0",
"@rehookify/datepicker": "^6.6.7",
@@ -1026,7 +1027,7 @@
"@theme-ui/core": "0.16.1",
"@trpc/client": "10.45.2",
"@trpc/react-query": "10.45.2",
"@zip.js/zip.js": "^2.7.54",
"@zip.js/zip.js": "^2.7.62",
"async-mutex": "0.5.0",
"axios": "^1.7.9",
"clipboard-polyfill": "4.1.0",
@@ -1034,7 +1035,7 @@
"cronosjs": "^1.7.1",
"dayjs": "1.11.13",
"diffblazer": "^1.0.1",
"electron-trpc": "0.6.1",
"electron-trpc": "0.7.1",
"event-source-polyfill": "1.0.31",
"fflate": "^0.8.0",
"file-saver": "^2.0.5",
@@ -1058,7 +1059,7 @@
"react-modal": "3.16.3",
"react-qrcode-logo": "^2.9.0",
"react-scroll-sync": "^0.11.2",
"react-virtuoso": "^4.6.2",
"react-virtuoso": "^4.12.3",
"snarkdown": "^2.0.0",
"timeago.js": "4.0.2",
"w3c-keyname": "^2.2.6",

File diff suppressed because it is too large Load Diff

View File

@@ -3056,6 +3056,10 @@ msgstr "How to fix it?"
msgid "hr"
msgstr "hr"
#: src/strings.ts:2489
msgid "I already have an account"
msgstr "I already have an account"
#: src/strings.ts:125
msgid "I don't have access to authenticator app"
msgstr "I don't have access to authenticator app"

View File

@@ -3038,6 +3038,10 @@ msgstr ""
msgid "hr"
msgstr ""
#: src/strings.ts:2489
msgid "I already have an account"
msgstr ""
#: src/strings.ts:125
msgid "I don't have access to authenticator app"
msgstr ""

View File

@@ -2493,5 +2493,6 @@ Use this if changes from other devices are not appearing on this device. This wi
scrollToBottom: () => t`Scroll to bottom`,
emailConfirmedDesc: () =>
t`Your email has been confirmed. You can now securely sync your encrypted notes across all devices.`,
charactersCount: (count: number) => t`${count} characters`
charactersCount: (count: number) => t`${count} characters`,
iAlreadyHaveAnAccount: () => t`I already have an account`
};