mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-24 04:00:59 +01:00
mobile: pricing changes
This commit is contained in:
committed by
Abdullah Atta
parent
5fe29af51a
commit
24da82313e
@@ -19,9 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
@@ -31,9 +28,7 @@ import { eCloseLoginDialog, eOpenLoginDialog } from "../../utils/events";
|
||||
import { sleep } from "../../utils/time";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { Toast } from "../toast";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { hideAuth, initialAuthMode } from "./common";
|
||||
import { initialAuthMode } from "./common";
|
||||
import { Login } from "./login";
|
||||
import { Signup } from "./signup";
|
||||
import { strings } from "@notesnook/intl";
|
||||
@@ -51,7 +46,6 @@ const AuthModal = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [currentAuthMode, setCurrentAuthMode] = useState(AuthMode.login);
|
||||
const actionSheetRef = useRef();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenLoginDialog, open);
|
||||
@@ -99,81 +93,18 @@ const AuthModal = () => {
|
||||
centered={false}
|
||||
enableSheetKeyboardHandler
|
||||
>
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
enableAutomaticScroll={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{currentAuthMode !== AuthMode.login ? (
|
||||
<Signup
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
trial={AuthMode.trialSignup === currentAuthMode}
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
/>
|
||||
) : (
|
||||
<Login
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
/>
|
||||
)}
|
||||
</KeyboardAwareScrollView>
|
||||
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
paddingTop: Platform.OS === "android" ? 0 : insets.top,
|
||||
top: 0,
|
||||
zIndex: 999,
|
||||
backgroundColor: colors.secondary.background,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<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={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
/>
|
||||
)}
|
||||
|
||||
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
|
||||
<Button
|
||||
title={strings.skip()}
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
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)}
|
||||
trial={AuthMode.trialSignup === currentAuthMode}
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
/>
|
||||
) : (
|
||||
<Login
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Toast context="local" />
|
||||
</BaseDialog>
|
||||
|
||||
80
apps/mobile/app/components/auth/header.tsx
Normal file
80
apps/mobile/app/components/auth/header.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
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 { Platform, View } from "react-native";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { hideAuth } from "./common";
|
||||
|
||||
export const AuthHeader = (props: { welcome?: boolean }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingTop: Platform.OS === "android" ? 0 : insets.top,
|
||||
backgroundColor: colors.secondary.background,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
width: "100%",
|
||||
height: 50,
|
||||
justifyContent: !props.welcome ? "space-between" : "flex-end"
|
||||
}}
|
||||
>
|
||||
{props.welcome ? null : (
|
||||
<IconButton
|
||||
name="arrow-left"
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!props.welcome ? null : (
|
||||
<Button
|
||||
title="Skip"
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
iconSize={16}
|
||||
type="plain"
|
||||
iconPosition="right"
|
||||
icon="chevron-right"
|
||||
height={25}
|
||||
iconStyle={{
|
||||
marginTop: 2
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 6
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -302,4 +302,4 @@ export const Login = ({ changeMode }) => {
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
27
apps/mobile/app/components/auth/signup-context.ts
Normal file
27
apps/mobile/app/components/auth/signup-context.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
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, { useContext } from "react";
|
||||
|
||||
export const SignupContext = React.createContext<{
|
||||
signup?: () => Promise<boolean>;
|
||||
}>({
|
||||
signup: undefined
|
||||
});
|
||||
|
||||
export const useSignupContext = () => useContext(SignupContext);
|
||||
@@ -21,24 +21,32 @@ import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { clearMessage, setEmailVerifyMessage } from "../../services/message";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { AppFontSize, SIZE } from "../../utils/size";
|
||||
import { Loading } from "../loading";
|
||||
import { PaywallComponent } from "../premium/component";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { hideAuth } from "./common";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { AuthHeader } from "./header";
|
||||
import { SignupContext } from "./signup-context";
|
||||
|
||||
export const Signup = ({ changeMode, trial }) => {
|
||||
const SignupSteps = {
|
||||
signup: 0,
|
||||
selectPlan: 1,
|
||||
createAccount: 2
|
||||
};
|
||||
|
||||
export const Signup = ({ changeMode, welcome }) => {
|
||||
const [currentStep, setCurrentStep] = useState(SignupSteps.signup);
|
||||
const { colors } = useThemeColors();
|
||||
const email = useRef();
|
||||
const emailInputRef = useRef();
|
||||
@@ -52,6 +60,7 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
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({
|
||||
@@ -70,23 +79,20 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
const signup = async () => {
|
||||
if (!validateInfo() || error) return;
|
||||
if (loading) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
setCurrentStep(SignupSteps.createAccount);
|
||||
await db.user.signup(email.current.toLowerCase(), password.current);
|
||||
let user = await db.user.getUser();
|
||||
setUser(user);
|
||||
setLastSynced(await db.lastSynced());
|
||||
clearMessage();
|
||||
setEmailVerifyMessage();
|
||||
hideAuth();
|
||||
SettingsService.setProperty("encryptedBackup", true);
|
||||
await sleep(300);
|
||||
if (trial) {
|
||||
PremiumService.sheet(null, null, true);
|
||||
} else {
|
||||
PremiumService.showVerifyEmailDialog();
|
||||
}
|
||||
setCurrentStep(SignupSteps.selectPlan);
|
||||
return true;
|
||||
} catch (e) {
|
||||
setCurrentStep(SignupSteps.signup);
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: strings.signupFailed(),
|
||||
@@ -94,214 +100,265 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderBottomWidth: 0.8,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
borderBottomColor: colors.primary.border,
|
||||
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 : "70%",
|
||||
minHeight: height * 0.4
|
||||
}}
|
||||
>
|
||||
<View
|
||||
<SignupContext.Provider
|
||||
value={{
|
||||
signup: signup
|
||||
}}
|
||||
>
|
||||
{currentStep === SignupSteps.signup ? (
|
||||
<>
|
||||
<AuthHeader welcome={welcome} />
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
width: "100%"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minHeight: "90%"
|
||||
}}
|
||||
nestedScrollEnabled
|
||||
enableAutomaticScroll={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
height: "100%"
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
extraBold
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
size={AppFontSize.xxl}
|
||||
>
|
||||
{strings.createYourAccount()}
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
testID="input.email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage={strings.email()}
|
||||
placeholder={strings.email()}
|
||||
onSubmit={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
testID="input.password"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
validationType="password"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.password()}
|
||||
onSubmit={() => {
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={confirmPasswordInputRef}
|
||||
onChangeText={(value) => {
|
||||
confirmPassword.current = value;
|
||||
}}
|
||||
testID="input.confirmPassword"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
validationType="confirmPassword"
|
||||
customValidator={() => password.current}
|
||||
placeholder={strings.confirmPassword()}
|
||||
marginBottom={12}
|
||||
onSubmit={signup}
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: 25
|
||||
}}
|
||||
size={AppFontSize.xxs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{strings.signupAgreement[0]()}
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/tos", colors);
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{" "}
|
||||
{strings.signupAgreement[1]()}
|
||||
</Paragraph>{" "}
|
||||
{strings.signupAgreement[2]()}
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/privacy", colors);
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{" "}
|
||||
{strings.signupAgreement[3]()}
|
||||
</Paragraph>{" "}
|
||||
{strings.signupAgreement[4]()}
|
||||
</Paragraph>
|
||||
|
||||
<Button
|
||||
title={!loading ? strings.continue() : null}
|
||||
type="accent"
|
||||
loading={loading}
|
||||
onPress={signup}
|
||||
style={{
|
||||
width: 250
|
||||
}}
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(0);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
{strings.alreadyHaveAccount()}{" "}
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
style={{ color: colors.primary.accent }}
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 16,
|
||||
backgroundColor: colors.secondary.background,
|
||||
marginBottom: 20,
|
||||
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%",
|
||||
minHeight: height * 0.25
|
||||
}}
|
||||
>
|
||||
{strings.login()}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
extraBold
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: 10
|
||||
}}
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
{strings.createAccount()}
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
paddingHorizontal: 16,
|
||||
backgroundColor: colors.primary.background,
|
||||
flexGrow: 1
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
testID="input.email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage={strings.emailInvalid()}
|
||||
placeholder={strings.email()}
|
||||
blurOnSubmit={false}
|
||||
onSubmit={() => {
|
||||
if (!email.current) return;
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
testID="input.password"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
blurOnSubmit={false}
|
||||
validationType="password"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.password()}
|
||||
onSubmit={() => {
|
||||
if (!password.current) return;
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={confirmPasswordInputRef}
|
||||
onChangeText={(value) => {
|
||||
confirmPassword.current = value;
|
||||
}}
|
||||
testID="input.confirmPassword"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Signup"
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
blurOnSubmit={false}
|
||||
validationType="confirmPassword"
|
||||
customValidator={() => password.current}
|
||||
placeholder={strings.confirmPassword()}
|
||||
marginBottom={12}
|
||||
onSubmit={signup}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={!loading ? "Continue" : null}
|
||||
type="accent"
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
signup();
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
width="100%"
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(0);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{strings.alreadyHaveAccount()}{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
{strings.login()}
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: 25
|
||||
}}
|
||||
size={AppFontSize.xxs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{strings.signupAgreement[0]()}
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/tos", colors);
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{" "}
|
||||
{strings.signupAgreement[1]()}
|
||||
</Paragraph>{" "}
|
||||
{strings.signupAgreement[2]()}
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/privacy", colors);
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{" "}
|
||||
{strings.signupAgreement[3]()}
|
||||
</Paragraph>{" "}
|
||||
{strings.signupAgreement[4]()}
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,8 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { TextInput } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import { ToastManager, eSendEvent } from "../../services/event-manager";
|
||||
import { clearMessage } from "../../services/message";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
@@ -34,15 +35,18 @@ export const LoginSteps = {
|
||||
passwordAuth: 3
|
||||
};
|
||||
|
||||
export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
export const useLogin = (
|
||||
onFinishLogin?: () => void,
|
||||
sessionExpired = false
|
||||
) => {
|
||||
const [error, setError] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
const [step, setStep] = useState(LoginSteps.emailAuth);
|
||||
const email = useRef();
|
||||
const password = useRef();
|
||||
const emailInputRef = useRef();
|
||||
const passwordInputRef = useRef();
|
||||
const email = useRef<string>();
|
||||
const password = useRef<string>();
|
||||
const emailInputRef = useRef<TextInput>(null);
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
|
||||
const validateInfo = () => {
|
||||
if (
|
||||
@@ -69,11 +73,15 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
setLoading(true);
|
||||
switch (step) {
|
||||
case LoginSteps.emailAuth: {
|
||||
if (!email.current) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const mfaInfo = await db.user.authenticateEmail(email.current);
|
||||
|
||||
if (mfaInfo) {
|
||||
TwoFactorVerification.present(
|
||||
async (mfa, callback) => {
|
||||
async (mfa: any, callback: (success: boolean) => void) => {
|
||||
try {
|
||||
const success = await db.user.authenticateMultiFactorCode(
|
||||
mfa.code,
|
||||
@@ -111,10 +119,14 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
break;
|
||||
}
|
||||
case LoginSteps.passwordAuth: {
|
||||
if (!email.current || !password.current) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
await db.user.authenticatePassword(
|
||||
email.current,
|
||||
password.current,
|
||||
null,
|
||||
undefined,
|
||||
sessionExpired
|
||||
);
|
||||
finishLogin();
|
||||
@@ -123,11 +135,11 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
}
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
finishWithError(e);
|
||||
finishWithError(e as Error);
|
||||
}
|
||||
};
|
||||
|
||||
const finishWithError = async (e) => {
|
||||
const finishWithError = async (e: Error) => {
|
||||
if (e.message === "invalid_grant") setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
@@ -146,7 +158,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
clearMessage();
|
||||
ToastManager.show({
|
||||
heading: strings.loginSuccess(),
|
||||
message: strings.loginSuccessDesc(),
|
||||
message: strings.loginSuccessDesc(user.email),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
@@ -24,13 +24,13 @@ import { Linking, ScrollView, useWindowDimensions, View } from "react-native";
|
||||
import { SwiperFlatList } from "react-native-swiper-flatlist";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import Navigation from "../../services/navigation";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { AuthMode } from "../auth/common";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { AuthMode } from "../auth/common";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
const Intro = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -182,6 +182,17 @@ const Intro = () => {
|
||||
type="accent"
|
||||
title={strings.getStarted()}
|
||||
/>
|
||||
|
||||
<Button
|
||||
width="100%"
|
||||
title={"I already have an account"}
|
||||
type="secondary"
|
||||
onPress={() => {
|
||||
SettingsService.set({
|
||||
introCompleted: true
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
88
apps/mobile/app/components/loading/index.tsx
Normal file
88
apps/mobile/app/components/loading/index.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
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 { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { ProgressBarComponent } from "../ui/svg/lazy";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
export const Loading = (props: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16
|
||||
}}
|
||||
>
|
||||
{props.icon ? (
|
||||
<Icon name={props.icon} size={80} color={colors.primary.accent} />
|
||||
) : null}
|
||||
|
||||
{props.title ? (
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{props.title}
|
||||
</Heading>
|
||||
) : null}
|
||||
|
||||
{props.description ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{props.description}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: 100,
|
||||
marginTop: 15
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
height={5}
|
||||
width={100}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
indeterminate
|
||||
indeterminateAnimationDuration={2000}
|
||||
unfilledColor={colors.secondary.background}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -1,290 +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, { useState } from "react";
|
||||
import { ActivityIndicator, Platform, ScrollView, View } from "react-native";
|
||||
import { LAUNCH_ROCKET } from "../../assets/images/assets";
|
||||
import { db } from "../../common/database";
|
||||
import { usePricing } from "../../hooks/use-pricing";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { eClosePremiumDialog, eCloseSheet } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { AuthMode } from "../auth/common";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Toast } from "../toast";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Seperator from "../ui/seperator";
|
||||
import { SvgView } from "../ui/svg";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { Walkthrough } from "../walkthroughs";
|
||||
import { features } from "./features";
|
||||
import { Group } from "./group";
|
||||
import { PricingPlans } from "./pricing-plans";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
export const Component = ({ close, promo }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const user = useUserStore((state) => state.user);
|
||||
const userCanRequestTrial =
|
||||
user && (!user.subscription || !user.subscription.expiry) ? true : false;
|
||||
const [floatingButton, setFloatingButton] = useState(false);
|
||||
const pricing = usePricing("monthly");
|
||||
|
||||
const onPress = async () => {
|
||||
if (user) {
|
||||
presentSheet({
|
||||
context: "pricing_plans",
|
||||
component: (
|
||||
<PricingPlans showTrialOption={false} marginTop={1} promo={promo} />
|
||||
)
|
||||
});
|
||||
} else {
|
||||
close();
|
||||
Navigation.navigate("Auth", {
|
||||
mode: AuthMode.trialSignup
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onScroll = (event) => {
|
||||
let contentSize = event.nativeEvent.contentSize.height;
|
||||
contentSize = contentSize - event.nativeEvent.layoutMeasurement.height;
|
||||
let yOffset = event.nativeEvent.contentOffset.y;
|
||||
if (yOffset > 600 && yOffset < contentSize - 400) {
|
||||
setFloatingButton(true);
|
||||
} else {
|
||||
setFloatingButton(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
justifyContent: "space-between",
|
||||
borderRadius: 10,
|
||||
maxHeight: "100%"
|
||||
}}
|
||||
>
|
||||
<SheetProvider context="pricing_plans" />
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
close();
|
||||
}}
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: DDS.isTab ? 30 : 15,
|
||||
top: Platform.OS === "ios" ? 0 : 30,
|
||||
zIndex: 10,
|
||||
width: 50,
|
||||
height: 50
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
name="close"
|
||||
/>
|
||||
|
||||
<ScrollView
|
||||
style={{
|
||||
paddingHorizontal: DDS.isTab ? DDS.width / 5 : 0
|
||||
}}
|
||||
scrollEventThrottle={0}
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
onScroll={onScroll}
|
||||
>
|
||||
<View
|
||||
key="top-banner"
|
||||
style={{
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
height: 400,
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<SvgView
|
||||
width={350}
|
||||
height={350}
|
||||
src={LAUNCH_ROCKET(colors.primary.accent)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Heading
|
||||
key="heading"
|
||||
size={AppFontSize.lg}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
paddingTop: 20
|
||||
}}
|
||||
>
|
||||
Notesnook{" "}
|
||||
<Heading size={AppFontSize.lg} color={colors.primary.accent}>
|
||||
Pro
|
||||
</Heading>
|
||||
</Heading>
|
||||
|
||||
{!pricing ? (
|
||||
<ActivityIndicator
|
||||
style={{
|
||||
marginBottom: 20
|
||||
}}
|
||||
size={AppFontSize.md}
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
) : (
|
||||
<Paragraph
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginBottom: 20
|
||||
}}
|
||||
size={AppFontSize.md}
|
||||
>
|
||||
(
|
||||
{Platform.OS === "android"
|
||||
? pricing.product?.subscriptionOfferDetails[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0].formattedPrice
|
||||
: pricing.product?.localizedPrice}{" "}
|
||||
/ mo)
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
<Paragraph
|
||||
key="description"
|
||||
size={AppFontSize.md}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
textAlign: "center",
|
||||
alignSelf: "center",
|
||||
paddingBottom: 20,
|
||||
width: "90%"
|
||||
}}
|
||||
>
|
||||
Ready to take the next step on your private note taking journey?
|
||||
</Paragraph>
|
||||
|
||||
{userCanRequestTrial ? (
|
||||
<Button
|
||||
key="calltoaction"
|
||||
onPress={async () => {
|
||||
try {
|
||||
await db.user.activateTrial();
|
||||
eSendEvent(eClosePremiumDialog);
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
Walkthrough.present("trialstarted", false, true);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
title="Try free for 14 days"
|
||||
type="accent"
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginBottom: 15,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
key="calltoaction"
|
||||
onPress={onPress}
|
||||
title={
|
||||
promo ? promo.text : user ? "See all plans" : "Sign up for free"
|
||||
}
|
||||
type={userCanRequestTrial ? "secondaryAccented" : "accent"}
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginBottom: 15,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
|
||||
{!user || userCanRequestTrial ? (
|
||||
<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}
|
||||
|
||||
<Seperator key="seperator_1" />
|
||||
|
||||
{features.map((item, index) => (
|
||||
<Group key={item.title} item={item} index={index} />
|
||||
))}
|
||||
|
||||
<View
|
||||
key="plans"
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<PricingPlans showTrialOption={false} promo={promo} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{floatingButton ? (
|
||||
<Button
|
||||
onPress={onPress}
|
||||
title={
|
||||
promo ? promo.text : user ? "See all plans" : "Sign up for free"
|
||||
}
|
||||
type="accent"
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP * 2,
|
||||
position: "absolute",
|
||||
borderRadius: 100,
|
||||
bottom: 30,
|
||||
...getElevationStyle(10)
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Toast context="local" />
|
||||
<View
|
||||
style={{
|
||||
paddingBottom: 10
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
768
apps/mobile/app/components/premium/component.tsx
Normal file
768
apps/mobile/app/components/premium/component.tsx
Normal file
File diff suppressed because one or more lines are too long
60
apps/mobile/app/components/premium/features-list.tsx
Normal file
60
apps/mobile/app/components/premium/features-list.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
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 FeaturesList = {
|
||||
// Monthly: ["$0/mo", "$1.99/mo", "$5.99/mo", "$9.99/mo"],
|
||||
// Yearly: ["$0/yr", "$14.99/yr", "$59.99/yr", "$109.99/yr"],
|
||||
// "5 year": [null, null, "$249.99/5yr", "$449.99/5yr"],
|
||||
Plans: ["Free", "Essential", "Pro", "Believer"],
|
||||
Devices: [2, 5, "Unlimited", "Unlimited"],
|
||||
Storage: ["100MB/mo", "1GB/mo", "10GB/mo", "25GB/mo"],
|
||||
"File size": ["10MB", "100MB", "1GB", "5GB"],
|
||||
"File compression": [true, true, "Optional", "Optional"],
|
||||
"Check list": [true, true, true, true],
|
||||
"Bullet list": [true, true, true, true],
|
||||
Quote: [true, true, true, true],
|
||||
"Note link": [true, true, true, true],
|
||||
"Task list": [false, true, true, true],
|
||||
"Outline list": [false, true, true, true],
|
||||
Callouts: [false, true, true, true],
|
||||
Colors: [7, 20, "∞", "∞"],
|
||||
Tags: [50, 500, "∞", "∞"],
|
||||
Notebooks: [100, 1000, "∞", "∞"],
|
||||
"Active Reminders": [10, 50, "∞", "∞"],
|
||||
Shortcuts: [10, "∞", "∞", "∞"],
|
||||
"Note history": ["Local", "Local", "Synced", "Synced"],
|
||||
"Security key app lock": [false, false, true, true],
|
||||
"Pin note in notification": [false, false, true, true],
|
||||
"Create note from notification drawer": [false, false, true, true],
|
||||
"Default startup screen": [false, false, true, true],
|
||||
"Markdown shortcuts": [false, true, true, true],
|
||||
"Custom toolbar preset": [false, false, true, true],
|
||||
"Customizable navigation menu": [false, true, true, true],
|
||||
"Files in monograph": [
|
||||
"Images",
|
||||
"Images & files",
|
||||
"Images & files",
|
||||
"Images & files"
|
||||
],
|
||||
"Note history retention": ["1 month", "∞", "∞", "∞"],
|
||||
"App lock": ["immediate", "immediate", "any", "any"],
|
||||
"Password on monograph": ["Default", "Custom", "Custom", "Custom"],
|
||||
"Voice memos": ["5 minute", "1 hour", "∞", true],
|
||||
"Notebook publishing": [1, 2, 5, true],
|
||||
Vaults: [1, 3, "∞", "∞"]
|
||||
};
|
||||
@@ -24,7 +24,8 @@ import {
|
||||
} from "../../services/event-manager";
|
||||
import { eClosePremiumDialog, eOpenPremiumDialog } from "../../utils/events";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { Component } from "./component";
|
||||
import { PaywallComponent } from "./component";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
|
||||
class PremiumDialog extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -74,7 +75,7 @@ class PremiumDialog extends React.Component {
|
||||
background={this.props.colors.primary.background}
|
||||
onRequestClose={this.onClose}
|
||||
>
|
||||
<Component
|
||||
<PaywallComponent
|
||||
getRef={() => this.actionSheetRef}
|
||||
promo={this.state.promo}
|
||||
close={this.close}
|
||||
|
||||
@@ -17,40 +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, { useCallback, useEffect, useState } from "react";
|
||||
import React from "react";
|
||||
import { ActivityIndicator, Platform, Text, View } from "react-native";
|
||||
import * as RNIap from "react-native-iap";
|
||||
import { DatabaseLogger, db } from "../../common/database";
|
||||
import { usePricing } from "../../hooks/use-pricing";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
ToastManager
|
||||
} from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import PremiumService from "../../services/premium";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import usePricingPlans from "../../hooks/use-pricing-plans";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import {
|
||||
eClosePremiumDialog,
|
||||
eCloseSheet,
|
||||
eCloseSimpleDialog
|
||||
eCloseSimpleDialog,
|
||||
eOpenLoginDialog
|
||||
} from "../../utils/events";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { AuthMode } from "../auth/common";
|
||||
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 { Walkthrough } from "../walkthroughs";
|
||||
import { PricingItem } from "./pricing-item";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
|
||||
const UUID_PREFIX = "0bdaea";
|
||||
const UUID_VERSION = "4";
|
||||
@@ -95,173 +82,21 @@ export const PricingPlans = ({
|
||||
compact?: boolean;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const user = useUserStore((state) => state.user);
|
||||
const [product, setProduct] = useState<{
|
||||
type: string;
|
||||
offerType: "monthly" | "yearly";
|
||||
data: RNIap.Subscription;
|
||||
cycleText: string;
|
||||
info: string;
|
||||
}>();
|
||||
|
||||
const [buying, setBuying] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const userCanRequestTrial =
|
||||
user && (!user.subscription || !user.subscription.expiry) ? true : false;
|
||||
const [upgrade, setUpgrade] = useState(!userCanRequestTrial);
|
||||
const yearlyPlan = usePricing("yearly");
|
||||
const monthlyPlan = usePricing("monthly");
|
||||
|
||||
const getSkus = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
if (promo?.promoCode) {
|
||||
getPromo(promo?.promoCode);
|
||||
}
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [promo?.promoCode]);
|
||||
|
||||
const getPromo = async (code: string) => {
|
||||
try {
|
||||
let skuId: string;
|
||||
if (code.startsWith("com.streetwriters.notesnook")) {
|
||||
skuId = code;
|
||||
} else {
|
||||
skuId = await db.offers?.getCode(
|
||||
code.split(":")[0],
|
||||
Platform.OS as "ios" | "android"
|
||||
);
|
||||
}
|
||||
|
||||
const products = await PremiumService.getProducts();
|
||||
const product = products.find((p) => p.productId === skuId);
|
||||
if (!product) return false;
|
||||
const isMonthly = product.productId.indexOf(".mo") > -1;
|
||||
|
||||
const cycleText = isMonthly
|
||||
? promoCyclesMonthly[
|
||||
(Platform.OS === "android"
|
||||
? (product as RNIap.SubscriptionAndroid)
|
||||
.subscriptionOfferDetails[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0].billingCycleCount
|
||||
: parseInt(
|
||||
(product as RNIap.SubscriptionIOS)
|
||||
.introductoryPriceNumberOfPeriodsIOS as string
|
||||
)) as keyof typeof promoCyclesMonthly
|
||||
]
|
||||
: promoCyclesYearly[
|
||||
(Platform.OS === "android"
|
||||
? (product as RNIap.SubscriptionAndroid)
|
||||
.subscriptionOfferDetails[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0].billingCycleCount
|
||||
: parseInt(
|
||||
(product as RNIap.SubscriptionIOS)
|
||||
.introductoryPriceNumberOfPeriodsIOS as string
|
||||
)) as keyof typeof promoCyclesYearly
|
||||
];
|
||||
|
||||
setProduct({
|
||||
type: "promo",
|
||||
offerType: isMonthly ? "monthly" : "yearly",
|
||||
data: product,
|
||||
cycleText: cycleText,
|
||||
info: `Pay ${isMonthly ? "monthly" : "yearly"}, cancel anytime`
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getSkus();
|
||||
}, [getSkus]);
|
||||
|
||||
const buySubscription = async (product: RNIap.Subscription) => {
|
||||
if (buying || !product) return;
|
||||
setBuying(true);
|
||||
try {
|
||||
if (!user) {
|
||||
setBuying(false);
|
||||
return;
|
||||
}
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
|
||||
const androidOfferToken =
|
||||
Platform.OS === "android"
|
||||
? (product as RNIap.SubscriptionAndroid).subscriptionOfferDetails[0]
|
||||
.offerToken
|
||||
: null;
|
||||
|
||||
DatabaseLogger.info(
|
||||
`Subscription Requested initiated for user ${toUUID(user.id)}`
|
||||
);
|
||||
|
||||
await RNIap.requestSubscription({
|
||||
sku: product?.productId,
|
||||
obfuscatedAccountIdAndroid: user.id,
|
||||
obfuscatedProfileIdAndroid: user.id,
|
||||
appAccountToken: toUUID(user.id),
|
||||
andDangerouslyFinishTransactionAutomaticallyIOS: false,
|
||||
subscriptionOffers: androidOfferToken
|
||||
? [
|
||||
{
|
||||
offerToken: androidOfferToken,
|
||||
sku: product?.productId
|
||||
}
|
||||
]
|
||||
: undefined
|
||||
});
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(false);
|
||||
setBuying(false);
|
||||
eSendEvent(eCloseSheet);
|
||||
eSendEvent(eClosePremiumDialog);
|
||||
await sleep(500);
|
||||
presentSheet({
|
||||
title: "Thank you for subscribing!",
|
||||
paragraph:
|
||||
"Your Notesnook Pro subscription will be activated soon. If your account is not upgraded to Notesnook Pro, your money will be refunded to you. In case of any issues, please reach out to us at support@streetwriters.co",
|
||||
action: async () => {
|
||||
eSendEvent(eCloseSheet);
|
||||
},
|
||||
icon: "check",
|
||||
actionText: "Continue"
|
||||
});
|
||||
} catch (e) {
|
||||
setBuying(false);
|
||||
}
|
||||
};
|
||||
|
||||
function getStandardPrice() {
|
||||
if (!product) return;
|
||||
const productType = product.offerType;
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const pricingPhaseListItem = (product.data as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails[0]?.pricingPhases.pricingPhaseList?.[1];
|
||||
|
||||
if (!pricingPhaseListItem) {
|
||||
const product =
|
||||
productType === "monthly"
|
||||
? monthlyPlan?.product
|
||||
: yearlyPlan?.product;
|
||||
return (product as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails[0]?.pricingPhases.pricingPhaseList?.[0]
|
||||
?.formattedPrice;
|
||||
}
|
||||
|
||||
return pricingPhaseListItem?.formattedPrice;
|
||||
} else {
|
||||
const productDefault =
|
||||
productType === "monthly" ? monthlyPlan?.product : yearlyPlan?.product;
|
||||
return (
|
||||
(product.data as RNIap.SubscriptionIOS)?.localizedPrice ||
|
||||
(productDefault as RNIap.SubscriptionIOS)?.localizedPrice
|
||||
);
|
||||
}
|
||||
}
|
||||
const {
|
||||
buySubscription,
|
||||
buying,
|
||||
getStandardPrice,
|
||||
product,
|
||||
setBuying,
|
||||
loading,
|
||||
user,
|
||||
setProduct,
|
||||
monthlyPlan,
|
||||
yearlyPlan,
|
||||
getPromo
|
||||
} = usePricingPlans({
|
||||
promoOffer: promo
|
||||
});
|
||||
|
||||
return loading ? (
|
||||
<View
|
||||
@@ -281,354 +116,215 @@ export const PricingPlans = ({
|
||||
}}
|
||||
>
|
||||
{buying ? (
|
||||
<BaseDialog statusBarTranslucent centered>
|
||||
<BaseDialog visible statusBarTranslucent centered>
|
||||
<ActivityIndicator size={50} color="white" />
|
||||
</BaseDialog>
|
||||
) : null}
|
||||
|
||||
{!upgrade ? (
|
||||
{!user && !product ? (
|
||||
<>
|
||||
<Paragraph
|
||||
style={{
|
||||
alignSelf: "center"
|
||||
}}
|
||||
size={AppFontSize.lg}
|
||||
>
|
||||
{(Platform.OS === "android"
|
||||
? (monthlyPlan?.product as RNIap.SubscriptionAndroid | undefined)
|
||||
?.subscriptionOfferDetails[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0]?.formattedPrice
|
||||
: (monthlyPlan?.product as RNIap.SubscriptionIOS | undefined)
|
||||
?.localizedPrice) ||
|
||||
(PremiumService.getMontlySub() as any)?.localizedPrice}
|
||||
/ mo
|
||||
</Paragraph>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setUpgrade(true);
|
||||
}}
|
||||
title={"Upgrade now"}
|
||||
type="accent"
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginBottom: DefaultAppStyles.GAP,
|
||||
marginTop: DefaultAppStyles.GAP,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onPress={async () => {
|
||||
try {
|
||||
await db.user?.activateTrial();
|
||||
eSendEvent(eClosePremiumDialog);
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
Walkthrough.present("trialstarted", false, true);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
title={"Try free for 14 days"}
|
||||
type="secondaryAccented"
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginBottom: 15
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{product?.type === "promo" ? (
|
||||
<View
|
||||
style={{
|
||||
paddingVertical: 15,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{product?.offerType === "monthly" ? (
|
||||
<PricingItem
|
||||
product={{
|
||||
type: "monthly",
|
||||
data: monthlyPlan?.product,
|
||||
info: "Pay once a month, cancel anytime."
|
||||
{heading || (monthlyPlan?.info?.discount || 0) > 0 ? (
|
||||
<>
|
||||
{monthlyPlan && (monthlyPlan?.info?.discount || 0) > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: marginTop || 20,
|
||||
marginBottom: 20
|
||||
}}
|
||||
strikethrough={true}
|
||||
/>
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
Get {monthlyPlan?.info?.discount}% off in{" "}
|
||||
{monthlyPlan?.info?.country}
|
||||
</Heading>
|
||||
</View>
|
||||
) : (
|
||||
<PricingItem
|
||||
onPress={() => {
|
||||
if (!monthlyPlan?.product) return;
|
||||
buySubscription(monthlyPlan?.product);
|
||||
}}
|
||||
product={{
|
||||
type: "yearly",
|
||||
data: yearlyPlan?.product,
|
||||
info: "Pay once a year, cancel anytime."
|
||||
}}
|
||||
strikethrough={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Heading
|
||||
style={{
|
||||
paddingTop: 15,
|
||||
fontSize: AppFontSize.lg
|
||||
}}
|
||||
>
|
||||
Special offer for you
|
||||
</Heading>
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingVertical: 20,
|
||||
paddingBottom: 10
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
textAlign: "center"
|
||||
marginTop: marginTop || 20,
|
||||
marginBottom: 20
|
||||
}}
|
||||
size={AppFontSize.xxl}
|
||||
>
|
||||
{Platform.OS === "android"
|
||||
? (product.data as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0]?.formattedPrice
|
||||
: (product.data as RNIap.SubscriptionIOS)
|
||||
?.introductoryPrice ||
|
||||
(product.data as RNIap.SubscriptionIOS)
|
||||
?.localizedPrice}{" "}
|
||||
{product?.cycleText
|
||||
? `for ${product.cycleText}`
|
||||
: product?.offerType}
|
||||
Choose a plan
|
||||
</Heading>
|
||||
{product?.cycleText ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
color: colors.secondary.paragraph,
|
||||
alignSelf: "center",
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={AppFontSize.md}
|
||||
>
|
||||
then {getStandardPrice()} {product?.offerType}.
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
) : 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: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
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(strings.errorApplyingPromoCode());
|
||||
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);
|
||||
Navigation.navigate("Auth", {
|
||||
mode: AuthMode.login
|
||||
});
|
||||
}}
|
||||
title={"Sign up for free"}
|
||||
type="accent"
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
marginTop: product?.type === "promo" ? 0 : 30,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
/>
|
||||
{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"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
onPress={() => {
|
||||
setProduct(undefined);
|
||||
}}
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
}}
|
||||
height={30}
|
||||
fontSize={13}
|
||||
type="errorShade"
|
||||
title="Cancel promo code"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
<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 || !upgrade ? (
|
||||
{!user ? (
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={AppFontSize.xs}
|
||||
@@ -648,7 +344,7 @@ export const PricingPlans = ({
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
{user && upgrade ? (
|
||||
{user ? (
|
||||
<>
|
||||
{Platform.OS === "ios" ? (
|
||||
<Paragraph
|
||||
|
||||
@@ -49,7 +49,7 @@ export const ProTag = ({ width, size, background }) => {
|
||||
marginRight: 3
|
||||
}}
|
||||
size={size}
|
||||
color={colors.primary.accent}
|
||||
color={colors.static.orange}
|
||||
name="crown"
|
||||
/>
|
||||
<Paragraph size={size - 1.5} color={colors.primary.accent}>
|
||||
|
||||
371
apps/mobile/app/components/sheets/buy-plan/index.tsx
Normal file
371
apps/mobile/app/components/sheets/buy-plan/index.tsx
Normal file
@@ -0,0 +1,371 @@
|
||||
/*
|
||||
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,
|
||||
ScrollView,
|
||||
Text,
|
||||
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 useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
|
||||
|
||||
export const BuyPlan = (props: {
|
||||
planId: string;
|
||||
productId: string;
|
||||
canActivateTrial?: boolean;
|
||||
goBack: () => void;
|
||||
goNext: () => void;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const pricingPlans = usePricingPlans({
|
||||
planId: props.planId,
|
||||
productId: props.productId,
|
||||
onBuy: () => {
|
||||
props.goNext();
|
||||
}
|
||||
});
|
||||
|
||||
const billingDuration = pricingPlans.getBillingDuration(
|
||||
pricingPlans.selectedProduct as RNIap.Subscription,
|
||||
0,
|
||||
0,
|
||||
true
|
||||
);
|
||||
const is5YearPlanSelected =
|
||||
pricingPlans.selectedProduct?.productId.includes("5year");
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
gap: 16,
|
||||
paddingBottom: 80,
|
||||
paddingTop: Platform.OS === "android" ? insets.top : 0
|
||||
}}
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
stickyHeaderIndices={[0]}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background
|
||||
}}
|
||||
>
|
||||
<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={{
|
||||
gap: 10,
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
{(is5YearPlanSelected
|
||||
? [
|
||||
"One time purchase, no auto-renewal",
|
||||
"Pay once and use for 5 years"
|
||||
]
|
||||
: [
|
||||
`Free ${billingDuration.duration} day trial, cancel any time`,
|
||||
"Google will remind you before your trial ends"
|
||||
]
|
||||
).map((item) => (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10
|
||||
}}
|
||||
key={item}
|
||||
>
|
||||
<Icon
|
||||
color={colors.primary.accent}
|
||||
size={SIZE.lg}
|
||||
name="check"
|
||||
/>
|
||||
<Paragraph>{item}</Paragraph>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{[
|
||||
`notesnook.${props.planId}.yearly`,
|
||||
`notesnook.${props.planId}.monthly`,
|
||||
...(props.planId === "essential"
|
||||
? []
|
||||
: [`notesnook.${props.planId}.5year`])
|
||||
].map((item) => (
|
||||
<ProductItem
|
||||
key={item}
|
||||
pricingPlans={pricingPlans}
|
||||
productId={item}
|
||||
/>
|
||||
))}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
marginTop: 20
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.primary.paragraph} size={SIZE.sm}>
|
||||
Due today{" "}
|
||||
{pricingPlans.userCanRequestTrial ? (
|
||||
<Text
|
||||
style={{
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
>
|
||||
({billingDuration.duration} days free)
|
||||
</Text>
|
||||
) : null}
|
||||
</Heading>
|
||||
|
||||
<Paragraph color={colors.primary.paragraph}>
|
||||
{pricingPlans.userCanRequestTrial
|
||||
? "0.00"
|
||||
: pricingPlans.getStandardPrice(
|
||||
pricingPlans.selectedProduct as RNIap.Subscription
|
||||
)}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
{pricingPlans.userCanRequestTrial ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
Due{" "}
|
||||
{dayjs().add(billingDuration.duration, "day").format("DD MMMM")}
|
||||
</Paragraph>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
{pricingPlans.getStandardPrice(
|
||||
pricingPlans.selectedProduct as RNIap.Subscription
|
||||
)}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
width="100%"
|
||||
type="accent"
|
||||
loading={pricingPlans.loading}
|
||||
title={
|
||||
pricingPlans.selectedProduct?.productId.includes("5year")
|
||||
? "Purchase"
|
||||
: pricingPlans?.userCanRequestTrial
|
||||
? "Subscribe and start free trial"
|
||||
: "Subscribe"
|
||||
}
|
||||
onPress={() => {
|
||||
const offerToken = pricingPlans.getOfferTokenAndroid(
|
||||
pricingPlans.selectedProduct as RNIap.SubscriptionAndroid,
|
||||
0
|
||||
);
|
||||
pricingPlans.subscribe(
|
||||
pricingPlans.selectedProduct as RNIap.Subscription,
|
||||
offerToken
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<View>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
>
|
||||
{is5YearPlanSelected
|
||||
? `This is a one time purchase, no subscription.`
|
||||
: `Cancel anytime, subscription auto-renews.`}
|
||||
</Heading>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
>
|
||||
By joining you agree to our{" "}
|
||||
<Text
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/privacy");
|
||||
}}
|
||||
>
|
||||
privacy policy
|
||||
</Text>{" "}
|
||||
and{" "}
|
||||
<Text
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/tos");
|
||||
}}
|
||||
>
|
||||
terms of use
|
||||
</Text>
|
||||
.
|
||||
</Heading>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const ProductItem = (props: {
|
||||
pricingPlans: ReturnType<typeof usePricingPlans>;
|
||||
productId: string;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const product =
|
||||
props.pricingPlans?.currentPlan?.subscriptions?.[props.productId] ||
|
||||
props.pricingPlans?.currentPlan?.products?.[props.productId];
|
||||
const isAnnual = product?.productId.includes("yearly");
|
||||
const isSelected =
|
||||
product?.productId === props.pricingPlans.selectedProduct?.productId;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: 10
|
||||
}}
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
props.pricingPlans.selectProduct(product?.productId);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={isSelected ? "radiobox-marked" : "radiobox-blank"}
|
||||
color={isSelected ? colors.primary.accent : colors.secondary.icon}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.md}>
|
||||
{isAnnual
|
||||
? "Yearly"
|
||||
: product?.productId.includes("5year")
|
||||
? "5 year plan (One time purchase)"
|
||||
: "Monthly"}
|
||||
</Heading>
|
||||
|
||||
{isAnnual ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.static.red,
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 6,
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.static.white} size={SIZE.xs}>
|
||||
Best value -{" "}
|
||||
{props.pricingPlans.compareProductPrice(
|
||||
props.pricingPlans.currentPlan?.id as string,
|
||||
`notesnook.${props.pricingPlans.currentPlan?.id}.yearly`,
|
||||
`notesnook.${props.pricingPlans.currentPlan?.id}.monthly`
|
||||
)}
|
||||
% Off
|
||||
</Heading>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<Paragraph>
|
||||
{product?.productId.includes("5year")
|
||||
? (product as RNIap.Product).localizedPrice
|
||||
: props.pricingPlans.getStandardPrice(
|
||||
product as RNIap.Subscription
|
||||
)}{" "}
|
||||
{isAnnual || product?.productId.includes("5year")
|
||||
? `(${props.pricingPlans.getPrice(
|
||||
product as RNIap.Subscription,
|
||||
props.pricingPlans.hasTrialOffer() ? 1 : 0,
|
||||
isAnnual
|
||||
)}/month)`
|
||||
: null}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
817
apps/mobile/app/hooks/use-pricing-plans.ts
Normal file
817
apps/mobile/app/hooks/use-pricing-plans.ts
Normal file
@@ -0,0 +1,817 @@
|
||||
/*
|
||||
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 { useEffect, useState } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import * as RNIap from "react-native-iap";
|
||||
import { DatabaseLogger, db } from "../common/database";
|
||||
import { eSendEvent } from "../services/event-manager";
|
||||
import PremiumService from "../services/premium";
|
||||
import { useSettingStore } from "../stores/use-setting-store";
|
||||
import { useUserStore } from "../stores/use-user-store";
|
||||
import { eClosePremiumDialog, eCloseSheet } from "../utils/events";
|
||||
import { sleep } from "../utils/time";
|
||||
|
||||
function numberWithCommas(x: string) {
|
||||
const parts = x.toString().split(".");
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
return parts.join(".");
|
||||
}
|
||||
|
||||
export type PricingPlan = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
subscriptionSkuList: string[];
|
||||
subscriptions?: Record<string, RNIap.Subscription | undefined>;
|
||||
products?: Record<string, RNIap.Product | undefined>;
|
||||
trialSupported?: boolean;
|
||||
recommended?: boolean;
|
||||
productSkuList: string[];
|
||||
};
|
||||
|
||||
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 pricingPlans: PricingPlan[] = [
|
||||
{
|
||||
id: "free",
|
||||
name: "Free",
|
||||
description: "Basic features for personal use",
|
||||
subscriptionSkuList: [],
|
||||
productSkuList: []
|
||||
},
|
||||
{
|
||||
id: "essential",
|
||||
name: "Essential",
|
||||
description: "Unlocks essential features for personal use",
|
||||
subscriptionSkuList: [
|
||||
"notesnook.essential.monthly",
|
||||
"notesnook.essential.yearly",
|
||||
// no trial
|
||||
"notesnook.essential.monthly.nt",
|
||||
"notesnook.essential.yearly.nt"
|
||||
],
|
||||
trialSupported: true,
|
||||
productSkuList: []
|
||||
},
|
||||
{
|
||||
id: "pro",
|
||||
name: "Pro",
|
||||
description: "Unlocks all features for professional use",
|
||||
subscriptionSkuList: [
|
||||
"notesnook.pro.monthly",
|
||||
"notesnook.pro.yearly",
|
||||
"notesnook.pro.monthly.tier2",
|
||||
"notesnook.pro.yearly.tier2",
|
||||
"notesnook.pro.monthly.tier3",
|
||||
"notesnook.pro.yearly.tier3",
|
||||
// no trial
|
||||
"notesnook.pro.monthly.nt",
|
||||
"notesnook.pro.yearly.nt",
|
||||
"notesnook.pro.monthly.tier2.nt",
|
||||
"notesnook.pro.yearly.tier2.nt",
|
||||
"notesnook.pro.monthly.tier3.nt",
|
||||
"notesnook.pro.yearly.tier3.nt"
|
||||
],
|
||||
productSkuList: ["notesnook.pro.5year"],
|
||||
trialSupported: true,
|
||||
recommended: true
|
||||
},
|
||||
{
|
||||
id: "believer",
|
||||
name: "Believer",
|
||||
description: "Become a believer and support the project",
|
||||
subscriptionSkuList: [
|
||||
"notesnook.believer.monthly",
|
||||
"notesnook.believer.yearly",
|
||||
// no trial
|
||||
"notesnook.believer.monthly.nt",
|
||||
"notesnook.believer.yearly.nt"
|
||||
],
|
||||
productSkuList: ["notesnook.believer.5year"],
|
||||
trialSupported: true
|
||||
}
|
||||
];
|
||||
|
||||
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"
|
||||
};
|
||||
|
||||
type PricingPlansOptions = {
|
||||
promoOffer?: {
|
||||
promoCode: string;
|
||||
};
|
||||
planId?: string;
|
||||
productId?: string;
|
||||
onBuy?: () => void;
|
||||
};
|
||||
|
||||
const usePricingPlans = (options?: PricingPlansOptions) => {
|
||||
const user = useUserStore((state) => state.user);
|
||||
const [currentPlan, setCurrentPlan] = useState<string>(
|
||||
options?.planId || pricingPlans[0].id
|
||||
);
|
||||
const [plans, setPlans] = useState<PricingPlan[]>(pricingPlans);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingPlans, setLoadingPlans] = useState(true);
|
||||
const [selectedProductSku, setSelectedProductSku] = useState<
|
||||
string | undefined
|
||||
>(options?.productId || undefined);
|
||||
const [isPromoOffer, setIsPromoOffer] = useState(false);
|
||||
const [cancelPromo, setCancelPromo] = useState(false);
|
||||
|
||||
const getProduct = (planId: string, skuId: string) => {
|
||||
return (
|
||||
plans.find((p) => p.id === planId)?.subscriptions?.[skuId] ||
|
||||
plans.find((p) => p.id === planId)?.products?.[skuId]
|
||||
);
|
||||
};
|
||||
|
||||
const getProductAndroid = (planId: string, skuId: string) => {
|
||||
return getProduct(planId, skuId) as RNIap.SubscriptionAndroid;
|
||||
};
|
||||
|
||||
const getProductIOS = (planId: string, skuId: string) => {
|
||||
return getProduct(planId, skuId) as RNIap.SubscriptionIOS;
|
||||
};
|
||||
|
||||
const hasTrialOffer = () => {
|
||||
if (!selectedProductSku) return false;
|
||||
|
||||
return Platform.OS === "ios"
|
||||
? !!(getProduct(currentPlan, selectedProductSku) as RNIap.SubscriptionIOS)
|
||||
?.introductoryPrice
|
||||
: (
|
||||
getProduct(
|
||||
currentPlan,
|
||||
selectedProductSku
|
||||
) as RNIap.SubscriptionAndroid
|
||||
).subscriptionOfferDetails?.[0]?.pricingPhases?.pricingPhaseList
|
||||
?.length > 1;
|
||||
};
|
||||
|
||||
const userCanRequestTrial = hasTrialOffer();
|
||||
// user && (!user.subscription || !user.subscription.expiry) ? true : false;
|
||||
|
||||
useEffect(() => {
|
||||
const loadPlans = async () => {
|
||||
const items = await PremiumService.loadProductsAndSubs();
|
||||
pricingPlans.forEach((plan) => {
|
||||
plan.subscriptions = {};
|
||||
plan.products = {};
|
||||
plan.subscriptionSkuList.forEach((sku) => {
|
||||
if (!plan.subscriptions) plan.subscriptions = {};
|
||||
plan.subscriptions[sku] = items.subs.find((p) => p.productId === sku);
|
||||
});
|
||||
plan.productSkuList.forEach((sku) => {
|
||||
if (!plan.products) plan.products = {};
|
||||
plan.products[sku] = items.products.find((p) => p.productId === sku);
|
||||
});
|
||||
});
|
||||
setPlans([...pricingPlans]);
|
||||
setLoadingPlans(false);
|
||||
};
|
||||
|
||||
const loadPromoOffer = async () => {
|
||||
if (cancelPromo) {
|
||||
setIsPromoOffer(false);
|
||||
return;
|
||||
}
|
||||
if (options?.promoOffer?.promoCode) {
|
||||
const promoCode = options.promoOffer.promoCode;
|
||||
let skuId: string;
|
||||
if (promoCode.startsWith("com.streetwriters.notesnook")) {
|
||||
skuId = promoCode;
|
||||
} else {
|
||||
skuId = await db.offers?.getCode(
|
||||
promoCode.split(":")[0],
|
||||
Platform.OS as "ios" | "android" | "web"
|
||||
);
|
||||
}
|
||||
|
||||
const plan = pricingPlans.find((p) =>
|
||||
p.subscriptionSkuList.includes(skuId)
|
||||
);
|
||||
if (plan) {
|
||||
setCurrentPlan(plan.id);
|
||||
setSelectedProductSku(skuId);
|
||||
setIsPromoOffer(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadPlans().then(() => loadPromoOffer());
|
||||
}, [options?.promoOffer, cancelPromo]);
|
||||
|
||||
function getLocalizedPrice(product: RNIap.Subscription | RNIap.Product) {
|
||||
if (!product) return;
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const pricingPhaseListItem = (product as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails?.[0]?.pricingPhases?.pricingPhaseList?.[1];
|
||||
|
||||
return (
|
||||
pricingPhaseListItem?.formattedPrice ||
|
||||
(product as RNIap.ProductAndroid).oneTimePurchaseOfferDetails
|
||||
?.formattedPrice
|
||||
);
|
||||
} else {
|
||||
return (product as RNIap.SubscriptionIOS)?.localizedPrice;
|
||||
}
|
||||
}
|
||||
function getOfferTokenAndroid(
|
||||
product: RNIap.Subscription,
|
||||
offerIndex: number
|
||||
) {
|
||||
return (product as RNIap.SubscriptionAndroid).subscriptionOfferDetails?.[
|
||||
offerIndex
|
||||
].offerToken;
|
||||
}
|
||||
|
||||
async function subscribe(
|
||||
product: RNIap.Subscription,
|
||||
androidOfferToken?: string
|
||||
) {
|
||||
if (loading || !product) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
if (!user) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
|
||||
|
||||
if (product.productId.includes("5year")) {
|
||||
if (Platform.OS === "android") {
|
||||
androidOfferToken =
|
||||
(
|
||||
product as RNIap.SubscriptionAndroid
|
||||
).subscriptionOfferDetails.find(
|
||||
(offer) => offer.offerToken === androidOfferToken
|
||||
)?.offerToken ||
|
||||
(product as RNIap.SubscriptionAndroid).subscriptionOfferDetails?.[0]
|
||||
.offerToken;
|
||||
|
||||
if (!androidOfferToken) return;
|
||||
}
|
||||
|
||||
DatabaseLogger.info(
|
||||
`Subscription Requested initiated for user ${toUUID(user.id)}`
|
||||
);
|
||||
|
||||
await RNIap.requestSubscription({
|
||||
sku: product?.productId,
|
||||
obfuscatedAccountIdAndroid: user.id,
|
||||
obfuscatedProfileIdAndroid: user.id,
|
||||
/**
|
||||
* iOS
|
||||
*/
|
||||
appAccountToken: toUUID(user.id),
|
||||
andDangerouslyFinishTransactionAutomaticallyIOS: false,
|
||||
subscriptionOffers: androidOfferToken
|
||||
? [
|
||||
{
|
||||
offerToken: androidOfferToken,
|
||||
sku: product?.productId
|
||||
}
|
||||
]
|
||||
: undefined
|
||||
});
|
||||
} else {
|
||||
await RNIap.requestPurchase({
|
||||
andDangerouslyFinishTransactionAutomaticallyIOS: false,
|
||||
appAccountToken: toUUID(user.id),
|
||||
obfuscatedAccountIdAndroid: user.id,
|
||||
obfuscatedProfileIdAndroid: user.id,
|
||||
sku: product.productId,
|
||||
quantity: 1
|
||||
});
|
||||
}
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(false);
|
||||
setLoading(false);
|
||||
options?.onBuy?.();
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function getPromoCycleText(product: RNIap.Subscription) {
|
||||
if (!selectedProductSku) return;
|
||||
const isMonthly = selectedProductSku?.indexOf(".monthly") > -1;
|
||||
|
||||
const cycleText = isMonthly
|
||||
? promoCyclesMonthly[
|
||||
(Platform.OS === "android"
|
||||
? (product as RNIap.SubscriptionAndroid).subscriptionOfferDetails[0]
|
||||
?.pricingPhases.pricingPhaseList?.[0].billingCycleCount
|
||||
: parseInt(
|
||||
(product as RNIap.SubscriptionIOS)
|
||||
.introductoryPriceNumberOfPeriodsIOS as string
|
||||
)) as keyof typeof promoCyclesMonthly
|
||||
]
|
||||
: promoCyclesYearly[
|
||||
(Platform.OS === "android"
|
||||
? (product as RNIap.SubscriptionAndroid).subscriptionOfferDetails[0]
|
||||
?.pricingPhases.pricingPhaseList?.[0].billingCycleCount
|
||||
: parseInt(
|
||||
(product as RNIap.SubscriptionIOS)
|
||||
.introductoryPriceNumberOfPeriodsIOS as string
|
||||
)) as keyof typeof promoCyclesYearly
|
||||
];
|
||||
|
||||
return cycleText;
|
||||
}
|
||||
|
||||
const getBillingPeriod = (
|
||||
product: RNIap.Subscription,
|
||||
offerIndex: number
|
||||
) => {
|
||||
if (Platform.OS === "android") {
|
||||
const period = (product as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails?.[offerIndex]?.pricingPhases
|
||||
?.pricingPhaseList?.[0].billingPeriod;
|
||||
return period.endsWith("W")
|
||||
? "week"
|
||||
: period.endsWith("M")
|
||||
? "month"
|
||||
: "year";
|
||||
} else {
|
||||
const unit = (product as RNIap.SubscriptionIOS)
|
||||
?.subscriptionPeriodUnitIOS;
|
||||
return unit?.toLocaleLowerCase();
|
||||
}
|
||||
};
|
||||
|
||||
const getBillingDuration = (
|
||||
product: RNIap.Subscription,
|
||||
offerIndex: number,
|
||||
phaseIndex: number,
|
||||
trialDurationIos?: boolean
|
||||
) => {
|
||||
if (product.productId.includes("5year")) {
|
||||
return {
|
||||
type: "year",
|
||||
duration: 5
|
||||
};
|
||||
}
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const phase = (product as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails?.[offerIndex]?.pricingPhases
|
||||
?.pricingPhaseList?.[phaseIndex];
|
||||
|
||||
const duration = parseInt(phase.billingPeriod[1]);
|
||||
|
||||
return {
|
||||
duration: phase.billingPeriod.endsWith("W") ? duration * 7 : duration,
|
||||
type: phase.billingPeriod.endsWith("W")
|
||||
? "week"
|
||||
: phase.billingPeriod.endsWith("M")
|
||||
? "month"
|
||||
: "year"
|
||||
};
|
||||
} else {
|
||||
const productIos = product as RNIap.SubscriptionIOS;
|
||||
const unit = trialDurationIos
|
||||
? productIos.introductoryPriceSubscriptionPeriodIOS
|
||||
: productIos.subscriptionPeriodUnitIOS;
|
||||
|
||||
const duration = parseInt(
|
||||
(trialDurationIos
|
||||
? productIos.introductoryPriceNumberOfPeriodsIOS
|
||||
: productIos.subscriptionPeriodNumberIOS) || "1"
|
||||
);
|
||||
|
||||
return {
|
||||
duration: unit === "WEEK" ? duration * 7 : duration,
|
||||
type: unit?.toLocaleLowerCase()
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getTrialInfo = (product: RNIap.Subscription) => {
|
||||
if (Platform.OS === "android") {
|
||||
const ProductAndroid = (product as RNIap.SubscriptionAndroid)
|
||||
.subscriptionOfferDetails?.[0];
|
||||
if (ProductAndroid.pricingPhases.pricingPhaseList?.length === 1) return;
|
||||
return {
|
||||
period:
|
||||
ProductAndroid?.pricingPhases.pricingPhaseList?.[0].billingPeriod,
|
||||
cycles:
|
||||
ProductAndroid?.pricingPhases.pricingPhaseList?.[0].billingCycleCount
|
||||
};
|
||||
} else {
|
||||
const productIos = product as RNIap.SubscriptionIOS;
|
||||
if (!productIos.introductoryPrice) return;
|
||||
return {
|
||||
period: productIos.introductoryPriceSubscriptionPeriodIOS,
|
||||
cycles: productIos.introductoryPriceNumberOfPeriodsIOS
|
||||
? parseInt(productIos.introductoryPriceNumberOfPeriodsIOS as string)
|
||||
: 1
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const convertPrice = (
|
||||
amount: number,
|
||||
symbol: string,
|
||||
isAtLeft: boolean,
|
||||
splitBy = 12
|
||||
) => {
|
||||
const monthlyPrice = amount / splitBy;
|
||||
const formattedPrice = numberWithCommas(monthlyPrice.toFixed(2));
|
||||
|
||||
return isAtLeft
|
||||
? `${symbol} ${formattedPrice}`
|
||||
: `${formattedPrice} ${symbol}`;
|
||||
};
|
||||
|
||||
const getDiscountValue = (p1: string, p2: string, splitToMonth?: boolean) => {
|
||||
let price1 = Platform.OS === "ios" ? parseInt(p1) : parseInt(p1) / 1000000;
|
||||
const price2 =
|
||||
Platform.OS === "ios" ? parseInt(p2) : parseInt(p2) / 1000000;
|
||||
|
||||
price1 = splitToMonth ? price1 / 12 : price1;
|
||||
|
||||
return (((price2 - price1) / price2) * 100).toFixed(0);
|
||||
};
|
||||
|
||||
const compareProductPrice = (planId: string, sku1: string, sku2: string) => {
|
||||
const plan = pricingPlans.find((p) => p.id === planId);
|
||||
const p1 = plan?.subscriptions?.[sku1];
|
||||
const p2 = plan?.subscriptions?.[sku2];
|
||||
|
||||
if (!p1 || !p2) return 0;
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const androidPricingPhase1 = (p1 as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails?.[0].pricingPhases?.pricingPhaseList?.[1];
|
||||
const androidPricingPhase2 = (p2 as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails?.[0].pricingPhases?.pricingPhaseList?.[1];
|
||||
|
||||
return getDiscountValue(
|
||||
androidPricingPhase1.priceAmountMicros,
|
||||
androidPricingPhase2.priceAmountMicros,
|
||||
true
|
||||
);
|
||||
} else {
|
||||
return getDiscountValue(
|
||||
(p1 as RNIap.SubscriptionIOS).price,
|
||||
(p2 as RNIap.SubscriptionIOS).price,
|
||||
true
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getPriceParts = (price: number, localizedPrice: string) => {
|
||||
let priceValue: number;
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
priceValue = price;
|
||||
} else {
|
||||
priceValue = price / 1000000;
|
||||
}
|
||||
const priceSymbol = localizedPrice.replace(/[\s\d,.]+/, "");
|
||||
|
||||
return { priceValue, priceSymbol, localizedPrice };
|
||||
};
|
||||
|
||||
const getPrice = (
|
||||
product: RNIap.Subscription | RNIap.Product,
|
||||
phaseIndex: number,
|
||||
annualBilling?: boolean
|
||||
) => {
|
||||
if (!product) return null;
|
||||
|
||||
const androidPricingPhase = (product as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails?.[0].pricingPhases?.pricingPhaseList?.[
|
||||
phaseIndex
|
||||
];
|
||||
console.log(product as RNIap.Product);
|
||||
|
||||
const { localizedPrice, priceSymbol, priceValue } = getPriceParts(
|
||||
Platform.OS === "android"
|
||||
? parseInt(
|
||||
androidPricingPhase?.priceAmountMicros ||
|
||||
(product as RNIap.ProductAndroid)?.oneTimePurchaseOfferDetails
|
||||
?.priceAmountMicros ||
|
||||
"0"
|
||||
)
|
||||
: parseInt((product as RNIap.SubscriptionIOS).price),
|
||||
Platform.OS === "android"
|
||||
? androidPricingPhase?.formattedPrice ||
|
||||
(product as RNIap.ProductAndroid).oneTimePurchaseOfferDetails
|
||||
?.formattedPrice ||
|
||||
"0"
|
||||
: (product as RNIap.SubscriptionIOS).localizedPrice
|
||||
);
|
||||
|
||||
return !annualBilling && !product?.productId.includes("5year")
|
||||
? getLocalizedPrice(product as RNIap.Subscription)
|
||||
: convertPrice(
|
||||
priceValue,
|
||||
priceSymbol,
|
||||
localizedPrice.startsWith(priceSymbol),
|
||||
annualBilling ? 12 : 60
|
||||
);
|
||||
};
|
||||
|
||||
// const getPricingPhases = (product: RNIap.Subscription) => {
|
||||
// if (!product) return null;
|
||||
|
||||
// if (Platform.OS === "android") {
|
||||
// const offer = (product as RNIap.SubscriptionAndroid)
|
||||
// ?.subscriptionOfferDetails?.[0];
|
||||
|
||||
// return offer.pricingPhases?.pricingPhaseList?.map((phase, index) => {
|
||||
|
||||
// return {
|
||||
// localizedPrice: phase.formattedPrice,
|
||||
// price: parseInt(phase.priceAmountMicros) / 1000000,
|
||||
// monthlyFormattedPrice: getPrice(product, index, offer?.pricingPhases?.pricingPhaseList?.[1].billingPeriod.endsWith("Y"),
|
||||
// }
|
||||
// })
|
||||
// } else {
|
||||
// return (product as RNIap.SubscriptionIOS)?.pricingPhases;
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
return {
|
||||
currentPlan: pricingPlans.find((p) => p.id === currentPlan),
|
||||
pricingPlans: plans,
|
||||
getStandardPrice: getLocalizedPrice,
|
||||
loadingPlans,
|
||||
loading,
|
||||
selectPlan: (planId: string) => {
|
||||
setCurrentPlan(planId);
|
||||
setSelectedProductSku(
|
||||
plans.find((p) => p.id === planId)?.subscriptionSkuList?.[0]
|
||||
);
|
||||
setIsPromoOffer(false);
|
||||
},
|
||||
convertYearlyPriceToMonthly: convertPrice,
|
||||
getOfferTokenAndroid,
|
||||
subscribe,
|
||||
selectProduct: setSelectedProductSku,
|
||||
selectedProduct: selectedProductSku
|
||||
? getProduct(currentPlan, selectedProductSku)
|
||||
: undefined,
|
||||
isPromoOffer,
|
||||
getPromoCycleText,
|
||||
getProduct,
|
||||
getProductAndroid,
|
||||
getProductIOS,
|
||||
hasTrialOffer,
|
||||
userCanRequestTrial: userCanRequestTrial,
|
||||
cancelPromoOffer: () => setCancelPromo(true),
|
||||
getBillingDuration,
|
||||
getBillingPeriod,
|
||||
getTrialInfo,
|
||||
user,
|
||||
getPrice,
|
||||
compareProductPrice,
|
||||
get5YearPlanProduct: () => {
|
||||
if (currentPlan === "free" || currentPlan === "essential") return;
|
||||
return plans.find((p) => p.id === "pro")?.products?.[
|
||||
`notesnook.${currentPlan}.5year`
|
||||
];
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// const usePricingPlans = (options?: {
|
||||
// promo?: {
|
||||
// promoCode?: string;
|
||||
// };
|
||||
// }) => {
|
||||
// const user = useUserStore((state) => state.user);
|
||||
// const [product, setProduct] = useState<{
|
||||
// type: string;
|
||||
// offerType: "monthly" | "yearly";
|
||||
// data: RNIap.Subscription;
|
||||
// cycleText: string;
|
||||
// info: string;
|
||||
// }>();
|
||||
|
||||
// const [buying, setBuying] = useState(false);
|
||||
// const [loading, setLoading] = useState(false);
|
||||
// const userCanRequestTrial =
|
||||
// user && (!user.subscription || !user.subscription.expiry) ? true : false;
|
||||
// const yearlyPlan = usePricing("yearly");
|
||||
// const monthlyPlan = usePricing("monthly");
|
||||
// const promoCode = options?.promo?.promoCode;
|
||||
|
||||
// const getSkus = useCallback(async () => {
|
||||
// try {
|
||||
// setLoading(true);
|
||||
// if (promoCode) {
|
||||
// getPromo(promoCode);
|
||||
// }
|
||||
// setLoading(false);
|
||||
// } catch (e) {
|
||||
// setLoading(false);
|
||||
// console.log("error getting sku", e);
|
||||
// }
|
||||
// }, [promoCode]);
|
||||
|
||||
// const getPromo = async (code: string) => {
|
||||
// try {
|
||||
// let skuId: string;
|
||||
// if (code.startsWith("com.streetwriters.notesnook")) {
|
||||
// skuId = code;
|
||||
// } else {
|
||||
// skuId = await db.offers?.getCode(
|
||||
// code.split(":")[0],
|
||||
// Platform.OS as "ios" | "android" | "web"
|
||||
// );
|
||||
// }
|
||||
|
||||
// const products = await PremiumService.getProducts();
|
||||
// const product = products.find((p) => p.productId === skuId);
|
||||
// if (!product) return false;
|
||||
// const isMonthly = product.productId.indexOf(".mo") > -1;
|
||||
|
||||
// const cycleText = isMonthly
|
||||
// ? promoCyclesMonthly[
|
||||
// (Platform.OS === "android"
|
||||
// ? (product as RNIap.SubscriptionAndroid)
|
||||
// .subscriptionOfferDetails[0]?.pricingPhases
|
||||
// .pricingPhaseList?.[0].billingCycleCount
|
||||
// : parseInt(
|
||||
// (product as RNIap.SubscriptionIOS)
|
||||
// .introductoryPriceNumberOfPeriodsIOS as string
|
||||
// )) as keyof typeof promoCyclesMonthly
|
||||
// ]
|
||||
// : promoCyclesYearly[
|
||||
// (Platform.OS === "android"
|
||||
// ? (product as RNIap.SubscriptionAndroid)
|
||||
// .subscriptionOfferDetails[0]?.pricingPhases
|
||||
// .pricingPhaseList?.[0].billingCycleCount
|
||||
// : parseInt(
|
||||
// (product as RNIap.SubscriptionIOS)
|
||||
// .introductoryPriceNumberOfPeriodsIOS as string
|
||||
// )) as keyof typeof promoCyclesYearly
|
||||
// ];
|
||||
|
||||
// setProduct({
|
||||
// type: "promo",
|
||||
// offerType: isMonthly ? "monthly" : "yearly",
|
||||
// data: product,
|
||||
// cycleText: cycleText,
|
||||
// info: `Pay ${isMonthly ? "monthly" : "yearly"}, cancel anytime`
|
||||
// });
|
||||
// return true;
|
||||
// } catch (e) {
|
||||
// console.log("PROMOCODE ERROR:", code, e);
|
||||
// return false;
|
||||
// }
|
||||
// };
|
||||
|
||||
// useEffect(() => {
|
||||
// getSkus();
|
||||
// }, [getSkus]);
|
||||
|
||||
// const buySubscription = async (product: RNIap.Subscription) => {
|
||||
// if (buying || !product) return;
|
||||
// setBuying(true);
|
||||
// try {
|
||||
// if (!user) {
|
||||
// setBuying(false);
|
||||
// return;
|
||||
// }
|
||||
// useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
|
||||
// const androidOfferToken =
|
||||
// Platform.OS === "android"
|
||||
// ? (product as RNIap.SubscriptionAndroid).subscriptionOfferDetails[0]
|
||||
// .offerToken
|
||||
// : null;
|
||||
|
||||
// DatabaseLogger.info(
|
||||
// `Subscription Requested initiated for user ${toUUID(user.id)}`
|
||||
// );
|
||||
|
||||
// await RNIap.requestSubscription({
|
||||
// sku: product?.productId,
|
||||
// obfuscatedAccountIdAndroid: user.id,
|
||||
// obfuscatedProfileIdAndroid: user.id,
|
||||
// appAccountToken: toUUID(user.id),
|
||||
// andDangerouslyFinishTransactionAutomaticallyIOS: false,
|
||||
// subscriptionOffers: androidOfferToken
|
||||
// ? [
|
||||
// {
|
||||
// offerToken: androidOfferToken,
|
||||
// sku: product?.productId
|
||||
// }
|
||||
// ]
|
||||
// : undefined
|
||||
// });
|
||||
// useSettingStore.getState().setAppDidEnterBackgroundForAction(false);
|
||||
// setBuying(false);
|
||||
// eSendEvent(eCloseSheet);
|
||||
// eSendEvent(eClosePremiumDialog);
|
||||
// await sleep(500);
|
||||
// presentSheet({
|
||||
// title: "Thank you for subscribing!",
|
||||
// paragraph:
|
||||
// "Your Notesnook Pro subscription will be activated soon. If your account is not upgraded to Notesnook Pro, your money will be refunded to you. In case of any issues, please reach out to us at support@streetwriters.co",
|
||||
// action: async () => {
|
||||
// eSendEvent(eCloseSheet);
|
||||
// },
|
||||
// icon: "check",
|
||||
// actionText: "Continue"
|
||||
// });
|
||||
// } catch (e) {
|
||||
// setBuying(false);
|
||||
// console.log(e);
|
||||
// }
|
||||
// };
|
||||
|
||||
// function getStandardPrice(
|
||||
// type: "monthly" | "yearly",
|
||||
// product: RNIap.Subscription
|
||||
// ) {
|
||||
// if (!product) return;
|
||||
// const productType = type;
|
||||
|
||||
// if (Platform.OS === "android") {
|
||||
// const pricingPhaseListItem = (product as RNIap.SubscriptionAndroid)
|
||||
// ?.subscriptionOfferDetails?.[0]?.pricingPhases?.pricingPhaseList?.[1];
|
||||
|
||||
// if (!pricingPhaseListItem) {
|
||||
// const product =
|
||||
// productType === "monthly"
|
||||
// ? monthlyPlan?.product
|
||||
// : yearlyPlan?.product;
|
||||
// return (product as RNIap.SubscriptionAndroid)
|
||||
// ?.subscriptionOfferDetails?.[0]?.pricingPhases?.pricingPhaseList?.[0]
|
||||
// ?.formattedPrice;
|
||||
// }
|
||||
|
||||
// return pricingPhaseListItem?.formattedPrice;
|
||||
// } else {
|
||||
// const productDefault =
|
||||
// productType === "monthly" ? monthlyPlan?.product : yearlyPlan?.product;
|
||||
// return (
|
||||
// (product as RNIap.SubscriptionIOS)?.localizedPrice ||
|
||||
// (productDefault as RNIap.SubscriptionIOS)?.localizedPrice
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// return {
|
||||
// product,
|
||||
// buySubscription,
|
||||
// getStandardPrice,
|
||||
// loading,
|
||||
// userCanRequestTrial,
|
||||
// buying,
|
||||
// setBuying,
|
||||
// setLoading,
|
||||
// user,
|
||||
// setProduct,
|
||||
// monthlyPlan,
|
||||
// yearlyPlan,
|
||||
// getPromo
|
||||
// };
|
||||
// };
|
||||
|
||||
export default usePricingPlans;
|
||||
@@ -49,7 +49,9 @@ export const usePricing = (period: "monthly" | "yearly") => {
|
||||
));
|
||||
skuInfos[period] = skuInfo;
|
||||
|
||||
const products = (await PremiumService.getProducts()) as Subscription[];
|
||||
const products = (await (
|
||||
await PremiumService.loadProductsAndSubs()
|
||||
).subs) as Subscription[];
|
||||
let product = products.find((p) => p.productId === skuInfo?.sku);
|
||||
if (!product)
|
||||
product = products.find((p) => p.productId === getDefaultSku(period));
|
||||
|
||||
@@ -44,6 +44,10 @@ let premiumStatus = 0;
|
||||
/**
|
||||
* @type {RNIap.Subscription[]}
|
||||
*/
|
||||
let subs = [];
|
||||
/**
|
||||
* @type {RNIap.Product[]}
|
||||
*/
|
||||
let products = [];
|
||||
let user = null;
|
||||
|
||||
@@ -73,7 +77,7 @@ async function setPremiumStatus() {
|
||||
}
|
||||
try {
|
||||
await RNIap.initConnection();
|
||||
products = await RNIap.getSubscriptions({
|
||||
subs = await RNIap.getSubscriptions({
|
||||
skus: itemSkus
|
||||
});
|
||||
} catch (e) {}
|
||||
@@ -83,7 +87,7 @@ async function setPremiumStatus() {
|
||||
}
|
||||
|
||||
function getMontlySub() {
|
||||
let _product = products.find(
|
||||
let _product = subs.find(
|
||||
(p) => p.productId === "com.streetwriters.notesnook.sub.mo"
|
||||
);
|
||||
if (!_product) {
|
||||
@@ -95,11 +99,23 @@ function getMontlySub() {
|
||||
return _product;
|
||||
}
|
||||
|
||||
async function getProducts() {
|
||||
if (!products || products.length === 0) {
|
||||
products = await RNIap.getSubscriptions(itemSkus);
|
||||
async function loadProductsAndSubs() {
|
||||
if (!subs || subs.length === 0) {
|
||||
subs = await RNIap.getSubscriptions({
|
||||
skus: itemSkus
|
||||
});
|
||||
}
|
||||
return products;
|
||||
|
||||
if (!products || products.length === 0) {
|
||||
products = await RNIap.getProducts({
|
||||
skus: ["notesnook.pro.5year", "notesnook.believer.5year"]
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
subs,
|
||||
products
|
||||
};
|
||||
}
|
||||
|
||||
function get() {
|
||||
@@ -427,7 +443,7 @@ const PremiumService = {
|
||||
get,
|
||||
onUserStatusCheck,
|
||||
showVerifyEmailDialog,
|
||||
getProducts,
|
||||
loadProductsAndSubs: loadProductsAndSubs,
|
||||
getUser,
|
||||
subscriptions,
|
||||
getMontlySub,
|
||||
|
||||
@@ -31,7 +31,9 @@ import {
|
||||
import { ParamListBase } from "@react-navigation/core";
|
||||
import create, { State } from "zustand";
|
||||
|
||||
export type GenericRouteParam = undefined;
|
||||
export type GenericRouteParam = {
|
||||
canGoBack?: boolean;
|
||||
};
|
||||
|
||||
export type NotebookScreenParams = {
|
||||
item: Notebook;
|
||||
@@ -98,7 +100,8 @@ export interface RouteParams extends ParamListBase {
|
||||
reminder?: Reminder;
|
||||
reference?: Note;
|
||||
};
|
||||
}
|
||||
Intro: GenericRouteParam;
|
||||
};
|
||||
|
||||
export type RouteName = keyof RouteParams;
|
||||
|
||||
|
||||
@@ -50,19 +50,17 @@ export const SORT = {
|
||||
};
|
||||
|
||||
export const itemSkus = [
|
||||
"com.streetwriters.notesnook.sub.mo",
|
||||
"com.streetwriters.notesnook.sub.yr",
|
||||
"com.streetwriters.notesnook.sub.yr.15",
|
||||
"com.streetwriters.notesnook.sub.mo.15",
|
||||
"com.streetwriters.notesnook.sub.mo.ofr",
|
||||
"com.streetwriters.notesnook.sub.yr.trialoffer",
|
||||
"com.streetwriters.notesnook.sub.mo.trialoffer",
|
||||
"com.streetwriters.notesnook.sub.mo.tier1",
|
||||
"com.streetwriters.notesnook.sub.yr.tier1",
|
||||
"com.streetwriters.notesnook.sub.mo.tier2",
|
||||
"com.streetwriters.notesnook.sub.yr.tier2",
|
||||
"com.streetwriters.notesnook.sub.mo.tier3",
|
||||
"com.streetwriters.notesnook.sub.yr.tier3"
|
||||
"notesnook.essential.monthly",
|
||||
"notesnook.essential.yearly",
|
||||
"notesnook.pro.monthly",
|
||||
"notesnook.pro.yearly",
|
||||
"notesnook.pro.monthly.tier2",
|
||||
"notesnook.pro.yearly.tier2",
|
||||
"notesnook.pro.monthly.tier3",
|
||||
"notesnook.pro.yearly.tier3",
|
||||
"notesnook.believer.monthly",
|
||||
"notesnook.believer.yearly",
|
||||
"notesnook.believer.5year"
|
||||
];
|
||||
|
||||
export const SUBSCRIPTION_STATUS = {
|
||||
|
||||
5
apps/mobile/package-lock.json
generated
5
apps/mobile/package-lock.json
generated
@@ -44449,6 +44449,11 @@
|
||||
"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",
|
||||
|
||||
@@ -99,7 +99,9 @@ const EXTRA_ICON_NAMES = [
|
||||
"brain",
|
||||
"file-tree-outline",
|
||||
"format-list-bulleted",
|
||||
"file-tree"
|
||||
"file-tree",
|
||||
"github",
|
||||
"open-source-initiative"
|
||||
];
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
|
||||
Reference in New Issue
Block a user