mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-21 22:19:41 +01:00
web: fix subscription settings for legacy pro users
This commit is contained in:
@@ -34,10 +34,6 @@ import { db } from "./common/db";
|
|||||||
import { EV, EVENTS } from "@notesnook/core";
|
import { EV, EVENTS } from "@notesnook/core";
|
||||||
import { registerKeyMap } from "./common/key-map";
|
import { registerKeyMap } from "./common/key-map";
|
||||||
import { updateStatus, removeStatus, getStatus } from "./hooks/use-status";
|
import { updateStatus, removeStatus, getStatus } from "./hooks/use-status";
|
||||||
import {
|
|
||||||
interruptedOnboarding,
|
|
||||||
OnboardingDialog
|
|
||||||
} from "./dialogs/onboarding-dialog";
|
|
||||||
import { hashNavigate } from "./navigation";
|
import { hashNavigate } from "./navigation";
|
||||||
import { desktop } from "./common/desktop-bridge";
|
import { desktop } from "./common/desktop-bridge";
|
||||||
import { FeatureDialog } from "./dialogs/feature-dialog";
|
import { FeatureDialog } from "./dialogs/feature-dialog";
|
||||||
@@ -69,8 +65,6 @@ export default function AppEffects() {
|
|||||||
await resetNotices();
|
await resetNotices();
|
||||||
setIsVaultCreated(await db.vault.exists());
|
setIsVaultCreated(await db.vault.exists());
|
||||||
|
|
||||||
const onboardingKey = interruptedOnboarding();
|
|
||||||
if (onboardingKey) await OnboardingDialog.show({ type: onboardingKey });
|
|
||||||
await FeatureDialog.show({ featureName: "highlights" });
|
await FeatureDialog.show({ featureName: "highlights" });
|
||||||
await scheduleBackups();
|
await scheduleBackups();
|
||||||
await scheduleFullBackups();
|
await scheduleFullBackups();
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ import {
|
|||||||
} from "./plan-list";
|
} from "./plan-list";
|
||||||
import { useCheckoutStore } from "./store";
|
import { useCheckoutStore } from "./store";
|
||||||
import { getCurrencySymbol, toPricingInfo } from "./helpers";
|
import { getCurrencySymbol, toPricingInfo } from "./helpers";
|
||||||
import { isMacStoreApp } from "../../utils/platform";
|
|
||||||
import { isUserSubscribed } from "../../hooks/use-is-user-premium";
|
import { isUserSubscribed } from "../../hooks/use-is-user-premium";
|
||||||
import { SUBSCRIPTION_STATUS } from "../../common/constants";
|
import { SUBSCRIPTION_STATUS } from "../../common/constants";
|
||||||
import BaseDialog from "../../components/dialog";
|
import BaseDialog from "../../components/dialog";
|
||||||
@@ -126,7 +125,7 @@ export const BuyDialog = DialogManager.register(function BuyDialog(
|
|||||||
/>
|
/>
|
||||||
</ScopedThemeProvider>
|
</ScopedThemeProvider>
|
||||||
<CheckoutDetails
|
<CheckoutDetails
|
||||||
onComplete={onCheckoutComplete ?? (() => {})}
|
onComplete={onCheckoutComplete ?? (() => onClose(false))}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { ConfirmDialog } from "./confirm";
|
|||||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||||
import { strings } from "@notesnook/intl";
|
import { strings } from "@notesnook/intl";
|
||||||
import { getDeviceInfo } from "../utils/platform";
|
import { getDeviceInfo } from "../utils/platform";
|
||||||
|
import { getSubscriptionInfo } from "./settings/components/user-profile";
|
||||||
|
|
||||||
const PLACEHOLDERS = {
|
const PLACEHOLDERS = {
|
||||||
title: strings.issueTitlePlaceholder(),
|
title: strings.issueTitlePlaceholder(),
|
||||||
@@ -141,7 +142,7 @@ export const IssueDialog = DialogManager.register(function IssueDialog(
|
|||||||
/
|
/
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant="subBody" mt={1}>
|
<Text variant="subBody" mt={1}>
|
||||||
{getDeviceInfo([`Pro: ${isUserPremium()}`])
|
{getDeviceInfo([`Plan: ${getSubscriptionInfo().title}`])
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map((t) => (
|
.map((t) => (
|
||||||
<>
|
<>
|
||||||
@@ -168,7 +169,7 @@ function showIssueReportedDialog({ url }: { url: string }) {
|
|||||||
|
|
||||||
const BODY_TEMPLATE = (body: string) => {
|
const BODY_TEMPLATE = (body: string) => {
|
||||||
const info = `**Device information:**\n${getDeviceInfo([
|
const info = `**Device information:**\n${getDeviceInfo([
|
||||||
`Pro: ${isUserPremium()}`
|
`Plan: ${getSubscriptionInfo().title}`
|
||||||
])}`;
|
])}`;
|
||||||
if (!body) return info;
|
if (!body) return info;
|
||||||
return `${body}\n\n${info}`;
|
return `${body}\n\n${info}`;
|
||||||
|
|||||||
@@ -17,184 +17,21 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Text, Flex, Button, Image, Box, Link } from "@theme-ui/components";
|
import { Text, Flex, Button } from "@theme-ui/components";
|
||||||
import Dialog from "../components/dialog";
|
import Dialog from "../components/dialog";
|
||||||
import {
|
|
||||||
Pro,
|
|
||||||
Email,
|
|
||||||
Discord,
|
|
||||||
Twitter,
|
|
||||||
Github,
|
|
||||||
Loading
|
|
||||||
} from "../components/icons";
|
|
||||||
import E2E from "../assets/e2e.svg?url";
|
|
||||||
import Note from "../assets/note2.svg?url";
|
|
||||||
import Nomad from "../assets/nomad.svg?url";
|
|
||||||
import WorkAnywhere from "../assets/workanywhere.svg?url";
|
|
||||||
import Friends from "../assets/cause.svg?url";
|
|
||||||
import LightUI from "../assets/light1.png?url";
|
|
||||||
import DarkUI from "../assets/dark1.png?url";
|
|
||||||
import GooglePlay from "../assets/play.png?url";
|
|
||||||
import AppleStore from "../assets/apple.png?url";
|
|
||||||
import { useStore as useThemeStore } from "../stores/theme-store";
|
|
||||||
import { Features } from "../components/announcements/body";
|
|
||||||
import { TaskManager } from "../common/task-manager";
|
|
||||||
import { db } from "../common/db";
|
|
||||||
import { usePersistentState } from "../hooks/use-persistent-state";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import Config from "../utils/config";
|
|
||||||
import { isMacStoreApp } from "../utils/platform";
|
|
||||||
import { ErrorText } from "../components/error-text";
|
|
||||||
import { BuyDialog } from "./buy-dialog";
|
|
||||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||||
|
import { useStore as useUserStore } from "../stores/user-store";
|
||||||
|
import { getSubscriptionInfo } from "./settings/components/user-profile";
|
||||||
import { strings } from "@notesnook/intl";
|
import { strings } from "@notesnook/intl";
|
||||||
import { SettingsDialog } from "./settings";
|
|
||||||
|
|
||||||
type Step = {
|
type OnboardingDialogProps = BaseDialogProps<boolean>;
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
buttonText?: string;
|
|
||||||
image?: JSX.Element;
|
|
||||||
component?:
|
|
||||||
| (() => JSX.Element)
|
|
||||||
| ((props: { onNext: () => void }) => JSX.Element)
|
|
||||||
| ((props: { onClose: () => void }) => JSX.Element);
|
|
||||||
};
|
|
||||||
|
|
||||||
const newUserSteps: Step[] = [
|
|
||||||
{
|
|
||||||
title: strings.safeEncryptedNotes(),
|
|
||||||
subtitle: strings.writeWithFreedom(),
|
|
||||||
buttonText: strings.getStarted(),
|
|
||||||
image: (
|
|
||||||
<Image src={Note} style={{ flexShrink: 0, width: 120, height: 120 }} />
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: strings.chooseYourStyle(),
|
|
||||||
subtitle: strings.changeTheme(),
|
|
||||||
buttonText: strings.next(),
|
|
||||||
component: ThemeSelector
|
|
||||||
},
|
|
||||||
{
|
|
||||||
image: (
|
|
||||||
<Image src={E2E} style={{ flexShrink: 0, width: 180, height: 180 }} />
|
|
||||||
),
|
|
||||||
title: strings.crossPlatformEncrypted(),
|
|
||||||
subtitle: strings.encryptsEverything(),
|
|
||||||
component: CrossPlatform,
|
|
||||||
buttonText: strings.next()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: strings.joinTheCause(),
|
|
||||||
subtitle: strings.meetPrivacyMinded(),
|
|
||||||
component: JoinCause,
|
|
||||||
image: (
|
|
||||||
<Image src={Friends} style={{ flexShrink: 0, width: 140, height: 140 }} />
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
image: <Pro size={60} color="accent" />,
|
|
||||||
title: strings.notesnookPro(),
|
|
||||||
subtitle: strings.nextLevelPrivateNoteTaking(),
|
|
||||||
component: TrialOffer
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const proUserSteps: Step[] = [
|
|
||||||
{
|
|
||||||
title: strings.welcomeToNotesnookPro(),
|
|
||||||
subtitle: strings.thankYouPrivacy(),
|
|
||||||
buttonText: strings.next(),
|
|
||||||
image: (
|
|
||||||
<Image src={Nomad} style={{ flexShrink: 0, width: 120, height: 120 }} />
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: strings.weAreAlwaysListening(),
|
|
||||||
subtitle: strings.weAreAlwaysListening(),
|
|
||||||
buttonText: strings.next(),
|
|
||||||
component: Support
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: strings.importYourNotes(),
|
|
||||||
subtitle: strings.importYourNotes(),
|
|
||||||
component: Importer
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const trialUserSteps: Step[] = [
|
|
||||||
{
|
|
||||||
title: strings.congratulations(),
|
|
||||||
subtitle: strings.trialStarted(),
|
|
||||||
buttonText: strings.continue(),
|
|
||||||
image: (
|
|
||||||
<Image
|
|
||||||
src={WorkAnywhere}
|
|
||||||
style={{ flexShrink: 0, width: 160, height: 160 }}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const onboarding = {
|
|
||||||
new: newUserSteps,
|
|
||||||
pro: proUserSteps,
|
|
||||||
trial: trialUserSteps
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export function interruptedOnboarding(): keyof typeof onboarding | undefined {
|
|
||||||
for (const key in onboarding) {
|
|
||||||
const index = Config.get(key, undefined);
|
|
||||||
if (index === null || index === undefined) continue;
|
|
||||||
if (
|
|
||||||
index >= 0 &&
|
|
||||||
index < onboarding[key as keyof typeof onboarding].length - 1
|
|
||||||
)
|
|
||||||
return key as keyof typeof onboarding;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type OnboardingDialogProps = BaseDialogProps<boolean> & {
|
|
||||||
type: keyof typeof onboarding;
|
|
||||||
};
|
|
||||||
export const OnboardingDialog = DialogManager.register(
|
export const OnboardingDialog = DialogManager.register(
|
||||||
function OnboardingDialog({
|
function OnboardingDialog({ onClose }: OnboardingDialogProps) {
|
||||||
onClose: _onClose,
|
const user = useUserStore((store) => store.user);
|
||||||
type
|
const { title } = getSubscriptionInfo(user);
|
||||||
}: OnboardingDialogProps) {
|
|
||||||
const [step, setStep] = usePersistentState(type, 0);
|
|
||||||
console.log("STEPS", type);
|
|
||||||
const steps = onboarding[type];
|
|
||||||
|
|
||||||
const onClose = useCallback(
|
|
||||||
(result: boolean) => {
|
|
||||||
Config.set(type, steps.length);
|
|
||||||
_onClose(result);
|
|
||||||
},
|
|
||||||
[_onClose, type, steps]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onNext = useCallback(() => {
|
|
||||||
if (step === steps.length - 1) onClose(true);
|
|
||||||
else setStep((s) => ++s);
|
|
||||||
}, [onClose, setStep, step, steps.length]);
|
|
||||||
|
|
||||||
if (!steps || !steps[step] || !type) {
|
|
||||||
onClose(false);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
subtitle,
|
|
||||||
image,
|
|
||||||
component: Component,
|
|
||||||
buttonText
|
|
||||||
} = steps[step];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog isOpen={true} width={500}>
|
<Dialog isOpen={true} width={500} onClose={() => onClose(false)}>
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
@@ -202,9 +39,8 @@ export const OnboardingDialog = DialogManager.register(
|
|||||||
overflowY: "auto"
|
overflowY: "auto"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{image}
|
|
||||||
<Text variant={"heading"} mt={2}>
|
<Text variant={"heading"} mt={2}>
|
||||||
{title}
|
{strings.welcomeToPlan(title + " plan")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
variant={"body"}
|
variant={"body"}
|
||||||
@@ -214,316 +50,17 @@ export const OnboardingDialog = DialogManager.register(
|
|||||||
color: "var(--paragraph-secondary)"
|
color: "var(--paragraph-secondary)"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{subtitle}
|
{strings.thankYouPrivacy()}
|
||||||
</Text>
|
</Text>
|
||||||
{Component && (
|
<Button
|
||||||
<Component onClose={() => onClose(true)} onNext={onNext} />
|
variant="accent"
|
||||||
)}
|
sx={{ borderRadius: 50, px: 30, mb: 4, mt: 4 }}
|
||||||
{buttonText && (
|
onClick={() => onClose(false)}
|
||||||
<Button
|
>
|
||||||
variant="accent"
|
{strings.continue()}
|
||||||
sx={{ borderRadius: 50, px: 30, mb: 4, mt: Component ? 0 : 4 }}
|
</Button>
|
||||||
onClick={onNext}
|
|
||||||
>
|
|
||||||
{buttonText}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
function JoinCause({ onNext }: { onNext: () => void }) {
|
|
||||||
return (
|
|
||||||
<Flex mb={4} sx={{ flexDirection: "column" }}>
|
|
||||||
<Button
|
|
||||||
as="a"
|
|
||||||
mt={4}
|
|
||||||
variant="accent"
|
|
||||||
sx={{ borderRadius: 50, alignSelf: "center", px: 30 }}
|
|
||||||
onClick={() => {
|
|
||||||
window.open("https://go.notesnook.com/discord", "_blank");
|
|
||||||
onNext();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{strings.joinDiscord()}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={"anchor"}
|
|
||||||
mt={2}
|
|
||||||
onClick={() => onNext()}
|
|
||||||
sx={{ color: "var(--paragraph-secondary)" }}
|
|
||||||
>
|
|
||||||
{strings.skip()}
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const importers = [
|
|
||||||
{ title: "Evernote" },
|
|
||||||
{ title: "Simplenote" },
|
|
||||||
{ title: "HTML" },
|
|
||||||
{ title: "Markdown" },
|
|
||||||
{ title: "Google Keep" },
|
|
||||||
{ title: "Standard Notes" }
|
|
||||||
];
|
|
||||||
function Importer({ onClose }: { onClose: () => void }) {
|
|
||||||
return (
|
|
||||||
<Flex my={4} sx={{ flexDirection: "column" }}>
|
|
||||||
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 1 }}>
|
|
||||||
{importers.map((importer) => (
|
|
||||||
<Flex
|
|
||||||
key={importer.title}
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
borderRadius: "default",
|
|
||||||
border: "1px solid var(--border)",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
p: 1
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text variant={"body"} ml={1} sx={{ textAlign: "center" }}>
|
|
||||||
{importer.title}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
<Button
|
|
||||||
as="a"
|
|
||||||
mt={4}
|
|
||||||
variant="accent"
|
|
||||||
sx={{ borderRadius: 50, alignSelf: "center", px: 30 }}
|
|
||||||
onClick={() => {
|
|
||||||
SettingsDialog.show({
|
|
||||||
activeSection: "importer"
|
|
||||||
});
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{strings.startImportingNow()}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={"anchor"}
|
|
||||||
mt={2}
|
|
||||||
onClick={() => onClose()}
|
|
||||||
sx={{ color: "var(--paragraph-secondary)" }}
|
|
||||||
>
|
|
||||||
{strings.skip()}
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const supportChannels = [
|
|
||||||
{
|
|
||||||
key: "email",
|
|
||||||
url: "mailto:support@streetwriters.co",
|
|
||||||
title: strings.emailSupport(),
|
|
||||||
icon: Email
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "discord",
|
|
||||||
url: "https://discord.com/invite/zQBK97EE22",
|
|
||||||
title: strings.joinDiscord(),
|
|
||||||
icon: Discord
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "twitter",
|
|
||||||
url: "https://twitter.com/notesnook",
|
|
||||||
title: strings.followOnX(),
|
|
||||||
icon: Twitter
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "github",
|
|
||||||
url: "https://github.com/streetwriters/notesnook",
|
|
||||||
title: strings.reportAnIssue(),
|
|
||||||
icon: Github
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
function Support() {
|
|
||||||
return (
|
|
||||||
<Box my={4} sx={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
|
|
||||||
{supportChannels.map((channel) => (
|
|
||||||
<Button
|
|
||||||
key={channel.key}
|
|
||||||
as="a"
|
|
||||||
variant={"icon"}
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
borderRadius: "default",
|
|
||||||
alignItems: "center"
|
|
||||||
}}
|
|
||||||
onClick={() => window.open(channel.url)}
|
|
||||||
>
|
|
||||||
<channel.icon size={16} />
|
|
||||||
<Text variant={"body"} ml={1}>
|
|
||||||
{channel.title}
|
|
||||||
</Text>
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const themes = [
|
|
||||||
{ key: "light" as const, name: "Light", image: LightUI },
|
|
||||||
{ key: "dark" as const, name: "Dark", image: DarkUI }
|
|
||||||
];
|
|
||||||
|
|
||||||
function ThemeSelector() {
|
|
||||||
const currentTheme = useThemeStore((store) => store.colorScheme);
|
|
||||||
const setTheme = useThemeStore((store) => store.setColorScheme);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
sx={{ border: "1px solid var(--border)", borderRadius: "default" }}
|
|
||||||
my={4}
|
|
||||||
>
|
|
||||||
{themes.map((theme) => {
|
|
||||||
const isSelected = currentTheme === theme.key;
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
key={theme.key}
|
|
||||||
p={20}
|
|
||||||
sx={{
|
|
||||||
borderRight: "1px solid var(--border)",
|
|
||||||
bg: isSelected ? "shade" : "transparent",
|
|
||||||
cursor: "pointer",
|
|
||||||
":last-of-type": {
|
|
||||||
borderRight: "0px"
|
|
||||||
},
|
|
||||||
":hover": {
|
|
||||||
bg: "hover"
|
|
||||||
},
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center"
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
setTheme(theme.key);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={theme.image}
|
|
||||||
sx={{
|
|
||||||
borderRadius: "default",
|
|
||||||
border: isSelected ? "2px solid" : "none",
|
|
||||||
borderColor: "accent",
|
|
||||||
boxShadow: isSelected ? "0px 0px 10px 1px #00000016" : "none"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Text variant={"subtitle"} mt={2} sx={{ color: "icon" }}>
|
|
||||||
{theme.name}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CrossPlatform() {
|
|
||||||
return (
|
|
||||||
<Flex my={4} sx={{ alignItems: "center" }}>
|
|
||||||
{isMacStoreApp() ? null : (
|
|
||||||
<Link
|
|
||||||
href="https://play.google.com/store/apps/details?id=com.streetwriters.notesnook"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image src={GooglePlay} sx={{ flexShrink: 0, width: 135 }} />
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
<Link
|
|
||||||
href="https://apps.apple.com/us/app/notesnook-take-private-notes/id1544027013"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image src={AppleStore} sx={{ flexShrink: 0, width: 110 }} />
|
|
||||||
</Link>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TrialOffer({ onClose }: { onClose: () => void }) {
|
|
||||||
const [error, setError] = useState<string>();
|
|
||||||
const [loading, setLoading] = useState<boolean>();
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
my={4}
|
|
||||||
sx={{
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Features item={{ style: {} }} />
|
|
||||||
{error ? (
|
|
||||||
<ErrorText error={error} />
|
|
||||||
) : (
|
|
||||||
<Text
|
|
||||||
variant={"body"}
|
|
||||||
mt={2}
|
|
||||||
bg="var(--background-secondary)"
|
|
||||||
p={1}
|
|
||||||
sx={{ borderRadius: "default", color: "var(--paragraph-secondary)" }}
|
|
||||||
>
|
|
||||||
<b>Note:</b> Upgrade now and get 30% discount on all plans.
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Flex mt={2} sx={{ justifyContent: "center", width: "100%" }}>
|
|
||||||
<Button
|
|
||||||
variant="accent"
|
|
||||||
sx={{ borderRadius: 50, alignSelf: "center", mr: 2, width: "40%" }}
|
|
||||||
onClick={() => {
|
|
||||||
onClose();
|
|
||||||
BuyDialog.show({ plan: "monthly", couponCode: "TRIAL2PRO" });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{strings.upgradeToPro()}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={"secondary"}
|
|
||||||
sx={{ borderRadius: 50, alignSelf: "center", width: "40%" }}
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const result = await TaskManager.startTask({
|
|
||||||
type: "status",
|
|
||||||
id: "trialActivation",
|
|
||||||
title: strings.activatingTrial(),
|
|
||||||
action: () => db.user.activateTrial()
|
|
||||||
});
|
|
||||||
if (result) onClose();
|
|
||||||
} catch (e) {
|
|
||||||
setError(
|
|
||||||
`Could not activate trial. Please try again. Error: ${
|
|
||||||
(e as Error).message
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{loading ? <Loading size={16} /> : strings.tryFreeFor14Days()}
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
<Button
|
|
||||||
variant={"anchor"}
|
|
||||||
mt={2}
|
|
||||||
onClick={() => onClose()}
|
|
||||||
sx={{ color: "var(--paragraph-secondary)" }}
|
|
||||||
>
|
|
||||||
{strings.skip()}
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,63 +20,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Copy, Loading } from "../../../components/icons";
|
import { Copy, Loading } from "../../../components/icons";
|
||||||
import { Box, Button, Link, Flex, Text } from "@theme-ui/components";
|
import { Box, Button, Link, Flex, Text } from "@theme-ui/components";
|
||||||
import { getFormattedDate } from "@notesnook/common";
|
import { getFormattedDate, usePromise } from "@notesnook/common";
|
||||||
import { strings } from "@notesnook/intl";
|
import { strings } from "@notesnook/intl";
|
||||||
import { db } from "../../../common/db";
|
import { db } from "../../../common/db";
|
||||||
import { TransactionStatus, Transaction } from "@notesnook/core";
|
import { Transaction, TransactionV1 } from "@notesnook/core";
|
||||||
import { writeToClipboard } from "../../../utils/clipboard";
|
import { writeToClipboard } from "../../../utils/clipboard";
|
||||||
import { showToast } from "../../../utils/toast";
|
import { showToast } from "../../../utils/toast";
|
||||||
import { TaskManager } from "../../../common/task-manager";
|
import { TaskManager } from "../../../common/task-manager";
|
||||||
|
import { ErrorText } from "../../../components/error-text";
|
||||||
const TransactionStatusToText: Record<TransactionStatus, string> = {
|
|
||||||
completed: "Completed",
|
|
||||||
billed: "Billed",
|
|
||||||
canceled: "Canceled",
|
|
||||||
paid: "Paid",
|
|
||||||
past_due: "Past due"
|
|
||||||
};
|
|
||||||
|
|
||||||
export function BillingHistory() {
|
export function BillingHistory() {
|
||||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
const transactions = usePromise(() => db.subscriptions.transactions(), []);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<Error | undefined>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async function () {
|
|
||||||
try {
|
|
||||||
setError(undefined);
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
const transactions = await db.subscriptions.transactions();
|
|
||||||
if (!transactions) return;
|
|
||||||
setTransactions(transactions.filter((c) => !!c.billed_at));
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Error) setError(e);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isLoading ? (
|
{transactions.status === "pending" ? (
|
||||||
<Loading sx={{ mt: 2 }} />
|
<Loading sx={{ mt: 2 }} />
|
||||||
) : error ? (
|
) : transactions.status === "rejected" ? (
|
||||||
<Flex
|
<ErrorText error={transactions.reason} />
|
||||||
sx={{
|
) : transactions.value?.transactions.length === 0 ? (
|
||||||
bg: "var(--background-error)",
|
|
||||||
p: 1,
|
|
||||||
borderRadius: "default"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text variant="error" sx={{ whiteSpace: "pre-wrap" }}>
|
|
||||||
{error.message}
|
|
||||||
<br />
|
|
||||||
{error.stack}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
) : transactions.length === 0 ? (
|
|
||||||
<Text variant="body" sx={{ mt: 2, color: "paragraph-secondary" }}>
|
<Text variant="body" sx={{ mt: 2, color: "paragraph-secondary" }}>
|
||||||
You have not been billed yet.
|
You have not been billed yet.
|
||||||
</Text>
|
</Text>
|
||||||
@@ -86,96 +48,173 @@ export function BillingHistory() {
|
|||||||
cellPadding={0}
|
cellPadding={0}
|
||||||
cellSpacing={0}
|
cellSpacing={0}
|
||||||
>
|
>
|
||||||
<thead>
|
{transactions.value?.type === "v1" ? (
|
||||||
<Box
|
<>
|
||||||
as="tr"
|
<thead>
|
||||||
sx={{
|
<Box
|
||||||
height: 30,
|
as="tr"
|
||||||
th: { borderBottom: "1px solid var(--separator)" }
|
sx={{
|
||||||
}}
|
height: 30,
|
||||||
>
|
th: { borderBottom: "1px solid var(--separator)" }
|
||||||
{[
|
}}
|
||||||
{ id: "id", title: "ID", width: "5%" },
|
>
|
||||||
{ id: "billedAt", title: "Billed at", width: "20%" },
|
{[
|
||||||
{ id: "amount", title: strings.amount(), width: "20%" },
|
{ id: "date", title: strings.date(), width: "20%" },
|
||||||
{ id: "status", title: strings.status(), width: "20%" },
|
{ id: "orderId", title: strings.orderId(), width: "20%" },
|
||||||
{ id: "invoice", title: "Invoice", width: "20%" }
|
{ id: "amount", title: strings.amount(), width: "20%" },
|
||||||
].map((column) =>
|
{ id: "status", title: strings.status(), width: "20%" },
|
||||||
!column.title ? (
|
{ id: "receipt", title: strings.receipt(), width: "20%" }
|
||||||
<th key={column.id} />
|
].map((column) =>
|
||||||
) : (
|
!column.title ? (
|
||||||
<Box
|
<th key={column.id} />
|
||||||
as="th"
|
) : (
|
||||||
key={column.id}
|
<Box
|
||||||
sx={{
|
as="th"
|
||||||
width: column.width,
|
key={column.id}
|
||||||
px: 1,
|
sx={{
|
||||||
mb: 2,
|
width: column.width,
|
||||||
textAlign: "left"
|
px: 1,
|
||||||
}}
|
mb: 2,
|
||||||
>
|
textAlign: "left"
|
||||||
<Text
|
}}
|
||||||
variant="body"
|
>
|
||||||
sx={{ textAlign: "left", fontWeight: "normal" }}
|
<Text
|
||||||
>
|
variant="body"
|
||||||
{column.title}
|
sx={{ textAlign: "left", fontWeight: "normal" }}
|
||||||
|
>
|
||||||
|
{column.title}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{transactions.value?.transactions.map((transaction) => (
|
||||||
|
<Box key={transaction.order_id} as="tr" sx={{ height: 30 }}>
|
||||||
|
<Text as="td" variant="body">
|
||||||
|
{getFormattedDate(transaction.created_at, "date")}
|
||||||
|
</Text>
|
||||||
|
<Text as="td" variant="body">
|
||||||
|
{transaction.order_id}
|
||||||
|
</Text>
|
||||||
|
<Text as="td" variant="body">
|
||||||
|
{transaction.amount} {transaction.currency}
|
||||||
|
</Text>
|
||||||
|
<Text as="td" variant="body">
|
||||||
|
{strings.transactionStatusToText(transaction.status)}
|
||||||
|
</Text>
|
||||||
|
<Text as="td" variant="body">
|
||||||
|
<Link
|
||||||
|
href={transaction.receipt_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferer nofollow"
|
||||||
|
variant="text.subBody"
|
||||||
|
sx={{ color: "accent" }}
|
||||||
|
>
|
||||||
|
{strings.viewReceipt()}
|
||||||
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
))}
|
||||||
)}
|
</tbody>
|
||||||
</Box>
|
</>
|
||||||
</thead>
|
) : (
|
||||||
<tbody>
|
<>
|
||||||
{transactions.map((transaction) => (
|
<thead>
|
||||||
<Box key={transaction.id} as="tr" sx={{ height: 30 }}>
|
<Box
|
||||||
<Text as="td" variant="body">
|
as="tr"
|
||||||
<Copy
|
sx={{
|
||||||
size={16}
|
height: 30,
|
||||||
onClick={() =>
|
th: { borderBottom: "1px solid var(--separator)" }
|
||||||
writeToClipboard({ "text/plain": transaction.id })
|
}}
|
||||||
}
|
>
|
||||||
sx={{ cursor: "pointer" }}
|
{[
|
||||||
/>
|
{ id: "id", title: "ID", width: "5%" },
|
||||||
</Text>
|
{ id: "billedAt", title: "Billed at", width: "20%" },
|
||||||
<Text as="td" variant="body">
|
{ id: "amount", title: strings.amount(), width: "20%" },
|
||||||
{getFormattedDate(transaction.billed_at, "date")}
|
{ id: "status", title: strings.status(), width: "20%" },
|
||||||
</Text>
|
{ id: "invoice", title: "Invoice", width: "20%" }
|
||||||
<Text as="td" variant="body">
|
].map((column) =>
|
||||||
{(transaction.details.totals.grand_total / 100).toFixed(2)}{" "}
|
!column.title ? (
|
||||||
{transaction.details.totals.currency_code}
|
<th key={column.id} />
|
||||||
</Text>
|
) : (
|
||||||
<Text as="td" variant="body">
|
<Box
|
||||||
{strings.transactionStatusToText(transaction.status)}
|
as="th"
|
||||||
</Text>
|
key={column.id}
|
||||||
<Text as="td" variant="body">
|
sx={{
|
||||||
<Button
|
width: column.width,
|
||||||
variant="anchor"
|
px: 1,
|
||||||
onClick={async () => {
|
mb: 2,
|
||||||
const url = await TaskManager.startTask({
|
textAlign: "left"
|
||||||
type: "modal",
|
}}
|
||||||
title: "Getting invoice",
|
>
|
||||||
subtitle: "This might take a minute or two.",
|
<Text
|
||||||
action() {
|
variant="body"
|
||||||
return db.subscriptions.invoice(transaction.id);
|
sx={{ textAlign: "left", fontWeight: "normal" }}
|
||||||
|
>
|
||||||
|
{column.title}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{transactions.value?.transactions.map((transaction) => (
|
||||||
|
<Box key={transaction.id} as="tr" sx={{ height: 30 }}>
|
||||||
|
<Text as="td" variant="body">
|
||||||
|
<Copy
|
||||||
|
size={16}
|
||||||
|
onClick={() =>
|
||||||
|
writeToClipboard({ "text/plain": transaction.id })
|
||||||
}
|
}
|
||||||
});
|
sx={{ cursor: "pointer" }}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
<Text as="td" variant="body">
|
||||||
|
{getFormattedDate(transaction.billed_at, "date")}
|
||||||
|
</Text>
|
||||||
|
<Text as="td" variant="body">
|
||||||
|
{(transaction.details.totals.grand_total / 100).toFixed(
|
||||||
|
2
|
||||||
|
)}{" "}
|
||||||
|
{transaction.details.totals.currency_code}
|
||||||
|
</Text>
|
||||||
|
<Text as="td" variant="body">
|
||||||
|
{strings.transactionStatusToText(transaction.status)}
|
||||||
|
</Text>
|
||||||
|
<Text as="td" variant="body">
|
||||||
|
<Button
|
||||||
|
variant="anchor"
|
||||||
|
onClick={async () => {
|
||||||
|
const url = await TaskManager.startTask({
|
||||||
|
type: "modal",
|
||||||
|
title: "Getting invoice",
|
||||||
|
subtitle: "This might take a minute or two.",
|
||||||
|
action() {
|
||||||
|
return db.subscriptions.invoice(transaction.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (!url || url instanceof Error)
|
if (!url || url instanceof Error)
|
||||||
return showToast(
|
return showToast(
|
||||||
"error",
|
"error",
|
||||||
url instanceof Error
|
url instanceof Error
|
||||||
? `Failed to get invoice for this transaction: ${url.message}`
|
? `Failed to get invoice for this transaction: ${url.message}`
|
||||||
: "No invoice found for this transaction."
|
: "No invoice found for this transaction."
|
||||||
);
|
);
|
||||||
window.open(url, "_blank");
|
window.open(url, "_blank");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Download
|
Download
|
||||||
</Button>
|
</Button>
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</table>
|
</table>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -101,9 +101,11 @@ export function SubscriptionStatus() {
|
|||||||
variant="subBody"
|
variant="subBody"
|
||||||
>
|
>
|
||||||
{feature.id === "storage"
|
{feature.id === "storage"
|
||||||
? `${formatBytes(feature.used)}/${formatBytes(
|
? `${formatBytes(feature.used)}/${
|
||||||
feature.total
|
feature.total === Infinity
|
||||||
)}`
|
? "Unlimited"
|
||||||
|
: formatBytes(feature.total)
|
||||||
|
}`
|
||||||
: feature.total === Infinity
|
: feature.total === Infinity
|
||||||
? "Unlimited"
|
? "Unlimited"
|
||||||
: `${feature.used} of ${feature.total}`}
|
: `${feature.used} of ${feature.total}`}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import {
|
|||||||
User
|
User
|
||||||
} from "@notesnook/core";
|
} from "@notesnook/core";
|
||||||
|
|
||||||
export function getSubscriptionInfo(user: User | undefined): {
|
export function getSubscriptionInfo(user?: User): {
|
||||||
title: string;
|
title: string;
|
||||||
trial?: boolean;
|
trial?: boolean;
|
||||||
paused?: boolean;
|
paused?: boolean;
|
||||||
@@ -47,6 +47,7 @@ export function getSubscriptionInfo(user: User | undefined): {
|
|||||||
autoRenew?: boolean;
|
autoRenew?: boolean;
|
||||||
trialExpiryDate?: string;
|
trialExpiryDate?: string;
|
||||||
} {
|
} {
|
||||||
|
user = user || useUserStore.getState().user;
|
||||||
const { type, expiry, plan, status, provider } = user?.subscription || {};
|
const { type, expiry, plan, status, provider } = user?.subscription || {};
|
||||||
if (!expiry) return { title: "Free" };
|
if (!expiry) return { title: "Free" };
|
||||||
|
|
||||||
@@ -251,10 +252,17 @@ export function UserProfile({ minimal }: Props) {
|
|||||||
</Text>
|
</Text>
|
||||||
{user.totalStorage && !minimal ? (
|
{user.totalStorage && !minimal ? (
|
||||||
<Flex sx={{ maxWidth: 300, alignItems: "center", gap: 1 }}>
|
<Flex sx={{ maxWidth: 300, alignItems: "center", gap: 1 }}>
|
||||||
<Progress max={user.totalStorage} value={user.storageUsed || 0} />
|
<Progress
|
||||||
|
max={user.totalStorage === -1 ? Infinity : user.totalStorage}
|
||||||
|
value={user.storageUsed || 0}
|
||||||
|
color="var(--accent)"
|
||||||
|
/>
|
||||||
<Text variant="subBody" sx={{ flexShrink: 0 }}>
|
<Text variant="subBody" sx={{ flexShrink: 0 }}>
|
||||||
{formatBytes(user.storageUsed || 0)}/
|
{formatBytes(user.storageUsed || 0)}/
|
||||||
{formatBytes(user.totalStorage)} used
|
{user.totalStorage === -1
|
||||||
|
? "Unlimited"
|
||||||
|
: formatBytes(user.totalStorage)}{" "}
|
||||||
|
used
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
import { TaskManager } from "../../common/task-manager";
|
import { TaskManager } from "../../common/task-manager";
|
||||||
import { ConfirmDialog } from "../confirm";
|
import { ConfirmDialog } from "../confirm";
|
||||||
import { ChangePlanDialog } from "../buy-dialog/change-plan-dialog";
|
import { ChangePlanDialog } from "../buy-dialog/change-plan-dialog";
|
||||||
|
import { getSubscriptionInfo } from "./components/user-profile";
|
||||||
|
|
||||||
export const SubscriptionSettings: SettingsGroup[] = [
|
export const SubscriptionSettings: SettingsGroup[] = [
|
||||||
{
|
{
|
||||||
@@ -89,6 +90,7 @@ export const SubscriptionSettings: SettingsGroup[] = [
|
|||||||
const user = useUserStore.getState().user;
|
const user = useUserStore.getState().user;
|
||||||
const status = user?.subscription.status;
|
const status = user?.subscription.status;
|
||||||
return (
|
return (
|
||||||
|
getSubscriptionInfo(user).legacy ||
|
||||||
user?.subscription.provider !== SubscriptionProvider.PADDLE ||
|
user?.subscription.provider !== SubscriptionProvider.PADDLE ||
|
||||||
!isUserSubscribed(user) ||
|
!isUserSubscribed(user) ||
|
||||||
status === SubscriptionStatusEnum.CANCELED ||
|
status === SubscriptionStatusEnum.CANCELED ||
|
||||||
@@ -136,12 +138,12 @@ export const SubscriptionSettings: SettingsGroup[] = [
|
|||||||
title: strings.update(),
|
title: strings.update(),
|
||||||
action: async () => {
|
action: async () => {
|
||||||
try {
|
try {
|
||||||
const urls = await db.subscriptions.urls();
|
const url = await db.subscriptions.updateUrl();
|
||||||
if (!urls)
|
if (!url)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Failed to get subscription management urls. Please contact us at support@streetwriters.co so we can help you update your payment method."
|
"Failed to get subscription update url. Please contact us at support@streetwriters.co so we can help you update your payment method."
|
||||||
);
|
);
|
||||||
window.open(urls?.update_payment_method, "_blank");
|
window.open(url, "_blank");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) showToast("error", e.message);
|
if (e instanceof Error) showToast("error", e.message);
|
||||||
}
|
}
|
||||||
@@ -160,6 +162,7 @@ export const SubscriptionSettings: SettingsGroup[] = [
|
|||||||
const user = useUserStore.getState().user;
|
const user = useUserStore.getState().user;
|
||||||
const status = user?.subscription.status;
|
const status = user?.subscription.status;
|
||||||
return (
|
return (
|
||||||
|
getSubscriptionInfo(user).legacy ||
|
||||||
user?.subscription.provider !== SubscriptionProvider.PADDLE ||
|
user?.subscription.provider !== SubscriptionProvider.PADDLE ||
|
||||||
!isUserSubscribed(user) ||
|
!isUserSubscribed(user) ||
|
||||||
status !== SubscriptionStatusEnum.TRIAL
|
status !== SubscriptionStatusEnum.TRIAL
|
||||||
|
|||||||
@@ -23,33 +23,10 @@ import {
|
|||||||
SubscriptionType,
|
SubscriptionType,
|
||||||
User
|
User
|
||||||
} from "@notesnook/core";
|
} from "@notesnook/core";
|
||||||
import { SUBSCRIPTION_STATUS } from "../common/constants";
|
import { useStore as useUserStore } from "../stores/user-store";
|
||||||
import {
|
|
||||||
useStore as useUserStore,
|
|
||||||
store as userstore
|
|
||||||
} from "../stores/user-store";
|
|
||||||
|
|
||||||
export function useIsUserPremium() {
|
|
||||||
const user = useUserStore((store) => store.user);
|
|
||||||
return isUserPremium(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isUserPremium(user?: User) {
|
|
||||||
if (IS_TESTING) return !("isBasic" in window);
|
|
||||||
if (!user) user = userstore.get().user;
|
|
||||||
if (!user) return false;
|
|
||||||
|
|
||||||
const subStatus = user.subscription.type;
|
|
||||||
return (
|
|
||||||
subStatus === SUBSCRIPTION_STATUS.BETA ||
|
|
||||||
subStatus === SUBSCRIPTION_STATUS.PREMIUM ||
|
|
||||||
subStatus === SUBSCRIPTION_STATUS.PREMIUM_CANCELED ||
|
|
||||||
subStatus === SUBSCRIPTION_STATUS.TRIAL
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isActiveSubscription(user?: User) {
|
export function isActiveSubscription(user?: User) {
|
||||||
user = user || userstore.get().user;
|
user = user || useUserStore.getState().user;
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
|
|
||||||
const { status } = user?.subscription || {};
|
const { status } = user?.subscription || {};
|
||||||
@@ -59,7 +36,7 @@ export function isActiveSubscription(user?: User) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
export function isUserSubscribed(user?: User) {
|
export function isUserSubscribed(user?: User) {
|
||||||
user = user || userstore.get().user;
|
user = user || useUserStore.getState().user;
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
|
|
||||||
const { type, expiry, plan, status } = user?.subscription || {};
|
const { type, expiry, plan, status } = user?.subscription || {};
|
||||||
|
|||||||
@@ -21,10 +21,9 @@ import createStore from "../common/store";
|
|||||||
import { db } from "../common/db";
|
import { db } from "../common/db";
|
||||||
import BaseStore from "./index";
|
import BaseStore from "./index";
|
||||||
import Config from "../utils/config";
|
import Config from "../utils/config";
|
||||||
import { isUserPremium } from "../hooks/use-is-user-premium";
|
import { isUserSubscribed } from "../hooks/use-is-user-premium";
|
||||||
import { SUBSCRIPTION_STATUS } from "../common/constants";
|
|
||||||
import { appVersion } from "../utils/version";
|
import { appVersion } from "../utils/version";
|
||||||
import { findItemAndDelete } from "@notesnook/core";
|
import { findItemAndDelete, SubscriptionStatus } from "@notesnook/core";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends {BaseStore<AnnouncementStore>}
|
* @extends {BaseStore<AnnouncementStore>}
|
||||||
@@ -97,15 +96,12 @@ async function shouldShowAnnouncement(announcement) {
|
|||||||
if (!show) return false;
|
if (!show) return false;
|
||||||
|
|
||||||
const user = await db.user.getUser();
|
const user = await db.user.getUser();
|
||||||
const subStatus = user?.subscription?.type;
|
|
||||||
show = announcement.userTypes.some((userType) => {
|
show = announcement.userTypes.some((userType) => {
|
||||||
switch (userType) {
|
switch (userType) {
|
||||||
case "pro":
|
case "subscribed":
|
||||||
return isUserPremium(user);
|
return isUserSubscribed(user);
|
||||||
case "trial":
|
case "trial":
|
||||||
return subStatus === SUBSCRIPTION_STATUS.TRIAL;
|
return user?.subscription?.status === SubscriptionStatus.TRIAL;
|
||||||
case "trialExpired":
|
|
||||||
return subStatus === SUBSCRIPTION_STATUS.BASIC;
|
|
||||||
case "loggedOut":
|
case "loggedOut":
|
||||||
return !user;
|
return !user;
|
||||||
case "loggedIn":
|
case "loggedIn":
|
||||||
@@ -114,8 +110,6 @@ async function shouldShowAnnouncement(announcement) {
|
|||||||
return user && !user.isEmailConfirmed;
|
return user && !user.isEmailConfirmed;
|
||||||
case "verified":
|
case "verified":
|
||||||
return user && user.isEmailConfirmed;
|
return user && user.isEmailConfirmed;
|
||||||
case "proExpired":
|
|
||||||
return subStatus === SUBSCRIPTION_STATUS.PREMIUM_EXPIRED;
|
|
||||||
case "any":
|
case "any":
|
||||||
default:
|
default:
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -23,12 +23,11 @@ import BaseStore from "./index";
|
|||||||
import { EV, EVENTS } from "@notesnook/core";
|
import { EV, EVENTS } from "@notesnook/core";
|
||||||
import Config from "../utils/config";
|
import Config from "../utils/config";
|
||||||
import { hashNavigate } from "../navigation";
|
import { hashNavigate } from "../navigation";
|
||||||
import { isUserPremium } from "../hooks/use-is-user-premium";
|
|
||||||
import { SUBSCRIPTION_STATUS } from "../common/constants";
|
|
||||||
import { AuthenticatorType, User } from "@notesnook/core";
|
import { AuthenticatorType, User } from "@notesnook/core";
|
||||||
import { ConfirmDialog } from "../dialogs/confirm";
|
import { ConfirmDialog } from "../dialogs/confirm";
|
||||||
import { OnboardingDialog } from "../dialogs/onboarding-dialog";
|
import { OnboardingDialog } from "../dialogs/onboarding-dialog";
|
||||||
import { strings } from "@notesnook/intl";
|
import { strings } from "@notesnook/intl";
|
||||||
|
import { isUserSubscribed } from "../hooks/use-is-user-premium";
|
||||||
|
|
||||||
class UserStore extends BaseStore<UserStore> {
|
class UserStore extends BaseStore<UserStore> {
|
||||||
isLoggedIn?: boolean;
|
isLoggedIn?: boolean;
|
||||||
@@ -59,16 +58,12 @@ class UserStore extends BaseStore<UserStore> {
|
|||||||
if (Config.get("sessionExpired")) return;
|
if (Config.get("sessionExpired")) return;
|
||||||
|
|
||||||
EV.subscribe(EVENTS.userSubscriptionUpdated, (subscription) => {
|
EV.subscribe(EVENTS.userSubscriptionUpdated, (subscription) => {
|
||||||
const wasUserPremium = isUserPremium();
|
const wasSubscribed = isUserSubscribed();
|
||||||
this.set((state) => {
|
this.set((state) => {
|
||||||
if (!state.user) return;
|
if (!state.user) return;
|
||||||
state.user.subscription = subscription;
|
state.user.subscription = subscription;
|
||||||
});
|
});
|
||||||
if (!wasUserPremium && isUserPremium())
|
if (!wasSubscribed && isUserSubscribed()) OnboardingDialog.show({});
|
||||||
OnboardingDialog.show({
|
|
||||||
type:
|
|
||||||
subscription.type === SUBSCRIPTION_STATUS.TRIAL ? "trial" : "pro"
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
EV.subscribe(EVENTS.userEmailConfirmed, () => {
|
EV.subscribe(EVENTS.userEmailConfirmed, () => {
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
Server,
|
Server,
|
||||||
Clip
|
Clip
|
||||||
} from "@notesnook/web-clipper/dist/common/bridge";
|
} from "@notesnook/web-clipper/dist/common/bridge";
|
||||||
import { isUserPremium } from "../hooks/use-is-user-premium";
|
|
||||||
import { store as appstore } from "../stores/app-store";
|
import { store as appstore } from "../stores/app-store";
|
||||||
import { h } from "./html";
|
import { h } from "./html";
|
||||||
import { sanitizeFilename } from "@notesnook/common";
|
import { sanitizeFilename } from "@notesnook/common";
|
||||||
@@ -32,6 +31,7 @@ import { getFormattedDate } from "@notesnook/common";
|
|||||||
import { useStore as useThemeStore } from "../stores/theme-store";
|
import { useStore as useThemeStore } from "../stores/theme-store";
|
||||||
import { isCipher } from "@notesnook/core";
|
import { isCipher } from "@notesnook/core";
|
||||||
import { attachFiles } from "../components/editor/picker";
|
import { attachFiles } from "../components/editor/picker";
|
||||||
|
import { isUserSubscribed } from "../hooks/use-is-user-premium";
|
||||||
|
|
||||||
export class WebExtensionServer implements Server {
|
export class WebExtensionServer implements Server {
|
||||||
async login() {
|
async login() {
|
||||||
@@ -39,7 +39,7 @@ export class WebExtensionServer implements Server {
|
|||||||
const user = await db.user.getUser();
|
const user = await db.user.getUser();
|
||||||
const theme = colorScheme === "dark" ? darkTheme : lightTheme;
|
const theme = colorScheme === "dark" ? darkTheme : lightTheme;
|
||||||
if (!user) return { pro: false, theme };
|
if (!user) return { pro: false, theme };
|
||||||
return { email: user.email, pro: isUserPremium(user), theme };
|
return { email: user.email, pro: isUserSubscribed(user), theme };
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNotes(): Promise<ItemReference[] | undefined> {
|
async getNotes(): Promise<ItemReference[] | undefined> {
|
||||||
|
|||||||
@@ -542,11 +542,9 @@ export type FeatureUsage = {
|
|||||||
export async function getFeaturesUsage(): Promise<FeatureUsage[]> {
|
export async function getFeaturesUsage(): Promise<FeatureUsage[]> {
|
||||||
const { isLegacyPro, plan } = await getUserPlan();
|
const { isLegacyPro, plan } = await getUserPlan();
|
||||||
const usage: FeatureUsage[] = [];
|
const usage: FeatureUsage[] = [];
|
||||||
console.log(isLegacyPro, plan);
|
|
||||||
for (const key in features) {
|
for (const key in features) {
|
||||||
const feature = getFeature(key as FeatureId);
|
const feature = getFeature(key as FeatureId);
|
||||||
const limit = getFeatureLimitFromPlan(feature, plan, isLegacyPro);
|
const limit = getFeatureLimitFromPlan(feature, plan, isLegacyPro);
|
||||||
console.log(limit, feature, key);
|
|
||||||
if (!feature.used || typeof limit.value !== "number") continue;
|
if (!feature.used || typeof limit.value !== "number") continue;
|
||||||
usage.push({
|
usage.push({
|
||||||
id: key as FeatureId,
|
id: key as FeatureId,
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { planToId, SubscriptionPlan } from "../types.js";
|
import {
|
||||||
|
planToId,
|
||||||
|
SubscriptionPlan,
|
||||||
|
SubscriptionType,
|
||||||
|
User
|
||||||
|
} from "../types.js";
|
||||||
import hosts from "../utils/constants.js";
|
import hosts from "../utils/constants.js";
|
||||||
import http from "../utils/http.js";
|
import http from "../utils/http.js";
|
||||||
import Database from "./index.js";
|
import Database from "./index.js";
|
||||||
@@ -30,6 +35,33 @@ export type TransactionStatus =
|
|||||||
| "past_due"
|
| "past_due"
|
||||||
| "canceled";
|
| "canceled";
|
||||||
|
|
||||||
|
export type TransactionStatusV1 =
|
||||||
|
| "completed"
|
||||||
|
| "refunded"
|
||||||
|
| "partially_refunded"
|
||||||
|
| "disputed";
|
||||||
|
|
||||||
|
export type TransactionV1 = {
|
||||||
|
order_id: string;
|
||||||
|
checkout_id: string;
|
||||||
|
amount: string;
|
||||||
|
currency: string;
|
||||||
|
status: TransactionStatusV1;
|
||||||
|
created_at: string;
|
||||||
|
passthrough: null;
|
||||||
|
product_id: number;
|
||||||
|
is_subscription: boolean;
|
||||||
|
is_one_off: boolean;
|
||||||
|
subscription: SubscriptionV1;
|
||||||
|
user: User;
|
||||||
|
receipt_url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SubscriptionV1 = {
|
||||||
|
subscription_id: number;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
|
||||||
export interface Transaction {
|
export interface Transaction {
|
||||||
id: string;
|
id: string;
|
||||||
status: TransactionStatus;
|
status: TransactionStatus;
|
||||||
@@ -62,51 +94,76 @@ export default class Subscriptions {
|
|||||||
|
|
||||||
async cancel() {
|
async cancel() {
|
||||||
const token = await this.db.tokenManager.getAccessToken();
|
const token = await this.db.tokenManager.getAccessToken();
|
||||||
if (!token) return;
|
const user = await this.db.user.getUser();
|
||||||
await http.post(
|
if (!token || !user) return;
|
||||||
`${hosts.SUBSCRIPTIONS_HOST}/subscriptions/v2/cancel`,
|
const endpoint = isLegacySubscription(user)
|
||||||
null,
|
? `subscriptions/cancel`
|
||||||
token
|
: `subscriptions/v2/cancel`;
|
||||||
);
|
await http.post(`${hosts.SUBSCRIPTIONS_HOST}/${endpoint}`, null, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
async pause() {
|
async pause() {
|
||||||
const token = await this.db.tokenManager.getAccessToken();
|
const token = await this.db.tokenManager.getAccessToken();
|
||||||
if (!token) return;
|
const user = await this.db.user.getUser();
|
||||||
await http.post(
|
if (!token || !user) return;
|
||||||
`${hosts.SUBSCRIPTIONS_HOST}/subscriptions/v2/pause`,
|
const endpoint = isLegacySubscription(user)
|
||||||
null,
|
? `subscriptions/cancel?pause=true`
|
||||||
token
|
: `subscriptions/v2/pause`;
|
||||||
);
|
if (isLegacySubscription(user))
|
||||||
|
await http.delete(`${hosts.SUBSCRIPTIONS_HOST}/${endpoint}`, token);
|
||||||
|
else
|
||||||
|
await http.post(`${hosts.SUBSCRIPTIONS_HOST}/${endpoint}`, null, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
async resume() {
|
async resume() {
|
||||||
const token = await this.db.tokenManager.getAccessToken();
|
const token = await this.db.tokenManager.getAccessToken();
|
||||||
if (!token) return;
|
const user = await this.db.user.getUser();
|
||||||
await http.post(
|
if (!token || !user) return;
|
||||||
`${hosts.SUBSCRIPTIONS_HOST}/subscriptions/v2/resume`,
|
const endpoint = isLegacySubscription(user)
|
||||||
null,
|
? `subscriptions/resume`
|
||||||
token
|
: `subscriptions/v2/resume`;
|
||||||
);
|
await http.post(`${hosts.SUBSCRIPTIONS_HOST}/${endpoint}`, null, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
async refund(reason?: string) {
|
async refund(reason?: string) {
|
||||||
const token = await this.db.tokenManager.getAccessToken();
|
const token = await this.db.tokenManager.getAccessToken();
|
||||||
if (!token) return;
|
const user = await this.db.user.getUser();
|
||||||
|
if (!token || !user) return;
|
||||||
|
const endpoint = isLegacySubscription(user)
|
||||||
|
? `subscriptions/refund`
|
||||||
|
: `subscriptions/v2/refund`;
|
||||||
await http.post(
|
await http.post(
|
||||||
`${hosts.SUBSCRIPTIONS_HOST}/subscriptions/v2/refund`,
|
`${hosts.SUBSCRIPTIONS_HOST}/${endpoint}`,
|
||||||
{ reason },
|
{ reason },
|
||||||
token
|
token
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async transactions(): Promise<Transaction[] | undefined> {
|
async transactions(): Promise<
|
||||||
|
| { type: "v2"; transactions: Transaction[] }
|
||||||
|
| { type: "v1"; transactions: TransactionV1[] }
|
||||||
|
| undefined
|
||||||
|
> {
|
||||||
const token = await this.db.tokenManager.getAccessToken();
|
const token = await this.db.tokenManager.getAccessToken();
|
||||||
if (!token) return;
|
const user = await this.db.user.getUser();
|
||||||
return await http.get(
|
if (!token || !user) return;
|
||||||
`${hosts.SUBSCRIPTIONS_HOST}/subscriptions/v2/transactions`,
|
if (isLegacySubscription(user)) {
|
||||||
token
|
return {
|
||||||
);
|
type: "v1",
|
||||||
|
transactions: await http.get(
|
||||||
|
`${hosts.SUBSCRIPTIONS_HOST}/subscriptions/transactions`,
|
||||||
|
token
|
||||||
|
)
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
type: "v2",
|
||||||
|
transactions: await http.get(
|
||||||
|
`${hosts.SUBSCRIPTIONS_HOST}/subscriptions/v2/transactions`,
|
||||||
|
token
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async invoice(transactionId: string): Promise<string | undefined> {
|
async invoice(transactionId: string): Promise<string | undefined> {
|
||||||
@@ -119,15 +176,23 @@ export default class Subscriptions {
|
|||||||
return response.url;
|
return response.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
async urls(): Promise<
|
async updateUrl(): Promise<string | undefined> {
|
||||||
{ update_payment_method: string; cancel: string } | undefined
|
|
||||||
> {
|
|
||||||
const token = await this.db.tokenManager.getAccessToken();
|
const token = await this.db.tokenManager.getAccessToken();
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
return await http.get(
|
const user = await this.db.user.getUser();
|
||||||
`${hosts.SUBSCRIPTIONS_HOST}/subscriptions/v2/urls`,
|
if (!token || !user) return;
|
||||||
token
|
if (isLegacySubscription(user)) {
|
||||||
);
|
return await http.get(
|
||||||
|
`${hosts.SUBSCRIPTIONS_HOST}/subscriptions/update`,
|
||||||
|
token
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const result = await http.get(
|
||||||
|
`${hosts.SUBSCRIPTIONS_HOST}/subscriptions/v2/urls`,
|
||||||
|
token
|
||||||
|
);
|
||||||
|
return result.update_payment_method;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async redeemCode(code: string) {
|
async redeemCode(code: string) {
|
||||||
@@ -174,3 +239,14 @@ export default class Subscriptions {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isLegacySubscription(user: User) {
|
||||||
|
const type = user.subscription.type;
|
||||||
|
return (
|
||||||
|
type !== undefined &&
|
||||||
|
(type === SubscriptionType.BETA ||
|
||||||
|
type === SubscriptionType.PREMIUM ||
|
||||||
|
type === SubscriptionType.PREMIUM_CANCELED ||
|
||||||
|
type === SubscriptionType.TRIAL)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -7060,8 +7060,8 @@ msgid "Welcome back!"
|
|||||||
msgstr "Welcome back!"
|
msgstr "Welcome back!"
|
||||||
|
|
||||||
#: src/strings.ts:2574
|
#: src/strings.ts:2574
|
||||||
msgid "Welcome to Notesnook {plan} plan"
|
msgid "Welcome to Notesnook {plan}"
|
||||||
msgstr "Welcome to Notesnook {plan} plan"
|
msgstr "Welcome to Notesnook {plan}"
|
||||||
|
|
||||||
#: src/strings.ts:2072
|
#: src/strings.ts:2072
|
||||||
msgid "Welcome to Notesnook Pro"
|
msgid "Welcome to Notesnook Pro"
|
||||||
|
|||||||
@@ -7011,7 +7011,7 @@ msgid "Welcome back!"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/strings.ts:2574
|
#: src/strings.ts:2574
|
||||||
msgid "Welcome to Notesnook {plan} plan"
|
msgid "Welcome to Notesnook {plan}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/strings.ts:2072
|
#: src/strings.ts:2072
|
||||||
|
|||||||
Reference in New Issue
Block a user