mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-21 14:09:34 +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 { registerKeyMap } from "./common/key-map";
|
||||
import { updateStatus, removeStatus, getStatus } from "./hooks/use-status";
|
||||
import {
|
||||
interruptedOnboarding,
|
||||
OnboardingDialog
|
||||
} from "./dialogs/onboarding-dialog";
|
||||
import { hashNavigate } from "./navigation";
|
||||
import { desktop } from "./common/desktop-bridge";
|
||||
import { FeatureDialog } from "./dialogs/feature-dialog";
|
||||
@@ -69,8 +65,6 @@ export default function AppEffects() {
|
||||
await resetNotices();
|
||||
setIsVaultCreated(await db.vault.exists());
|
||||
|
||||
const onboardingKey = interruptedOnboarding();
|
||||
if (onboardingKey) await OnboardingDialog.show({ type: onboardingKey });
|
||||
await FeatureDialog.show({ featureName: "highlights" });
|
||||
await scheduleBackups();
|
||||
await scheduleFullBackups();
|
||||
|
||||
@@ -37,7 +37,6 @@ import {
|
||||
} from "./plan-list";
|
||||
import { useCheckoutStore } from "./store";
|
||||
import { getCurrencySymbol, toPricingInfo } from "./helpers";
|
||||
import { isMacStoreApp } from "../../utils/platform";
|
||||
import { isUserSubscribed } from "../../hooks/use-is-user-premium";
|
||||
import { SUBSCRIPTION_STATUS } from "../../common/constants";
|
||||
import BaseDialog from "../../components/dialog";
|
||||
@@ -126,7 +125,7 @@ export const BuyDialog = DialogManager.register(function BuyDialog(
|
||||
/>
|
||||
</ScopedThemeProvider>
|
||||
<CheckoutDetails
|
||||
onComplete={onCheckoutComplete ?? (() => {})}
|
||||
onComplete={onCheckoutComplete ?? (() => onClose(false))}
|
||||
user={user}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
@@ -30,6 +30,7 @@ import { ConfirmDialog } from "./confirm";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { getDeviceInfo } from "../utils/platform";
|
||||
import { getSubscriptionInfo } from "./settings/components/user-profile";
|
||||
|
||||
const PLACEHOLDERS = {
|
||||
title: strings.issueTitlePlaceholder(),
|
||||
@@ -141,7 +142,7 @@ export const IssueDialog = DialogManager.register(function IssueDialog(
|
||||
/
|
||||
</Text>
|
||||
<Text variant="subBody" mt={1}>
|
||||
{getDeviceInfo([`Pro: ${isUserPremium()}`])
|
||||
{getDeviceInfo([`Plan: ${getSubscriptionInfo().title}`])
|
||||
.split("\n")
|
||||
.map((t) => (
|
||||
<>
|
||||
@@ -168,7 +169,7 @@ function showIssueReportedDialog({ url }: { url: string }) {
|
||||
|
||||
const BODY_TEMPLATE = (body: string) => {
|
||||
const info = `**Device information:**\n${getDeviceInfo([
|
||||
`Pro: ${isUserPremium()}`
|
||||
`Plan: ${getSubscriptionInfo().title}`
|
||||
])}`;
|
||||
if (!body) return 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/>.
|
||||
*/
|
||||
|
||||
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 {
|
||||
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 { useStore as useUserStore } from "../stores/user-store";
|
||||
import { getSubscriptionInfo } from "./settings/components/user-profile";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { SettingsDialog } from "./settings";
|
||||
|
||||
type Step = {
|
||||
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;
|
||||
};
|
||||
type OnboardingDialogProps = BaseDialogProps<boolean>;
|
||||
export const OnboardingDialog = DialogManager.register(
|
||||
function OnboardingDialog({
|
||||
onClose: _onClose,
|
||||
type
|
||||
}: 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];
|
||||
function OnboardingDialog({ onClose }: OnboardingDialogProps) {
|
||||
const user = useUserStore((store) => store.user);
|
||||
const { title } = getSubscriptionInfo(user);
|
||||
|
||||
return (
|
||||
<Dialog isOpen={true} width={500}>
|
||||
<Dialog isOpen={true} width={500} onClose={() => onClose(false)}>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
@@ -202,9 +39,8 @@ export const OnboardingDialog = DialogManager.register(
|
||||
overflowY: "auto"
|
||||
}}
|
||||
>
|
||||
{image}
|
||||
<Text variant={"heading"} mt={2}>
|
||||
{title}
|
||||
{strings.welcomeToPlan(title + " plan")}
|
||||
</Text>
|
||||
<Text
|
||||
variant={"body"}
|
||||
@@ -214,316 +50,17 @@ export const OnboardingDialog = DialogManager.register(
|
||||
color: "var(--paragraph-secondary)"
|
||||
}}
|
||||
>
|
||||
{subtitle}
|
||||
{strings.thankYouPrivacy()}
|
||||
</Text>
|
||||
{Component && (
|
||||
<Component onClose={() => onClose(true)} onNext={onNext} />
|
||||
)}
|
||||
{buttonText && (
|
||||
<Button
|
||||
variant="accent"
|
||||
sx={{ borderRadius: 50, px: 30, mb: 4, mt: Component ? 0 : 4 }}
|
||||
onClick={onNext}
|
||||
sx={{ borderRadius: 50, px: 30, mb: 4, mt: 4 }}
|
||||
onClick={() => onClose(false)}
|
||||
>
|
||||
{buttonText}
|
||||
{strings.continue()}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</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 { Copy, Loading } from "../../../components/icons";
|
||||
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 { db } from "../../../common/db";
|
||||
import { TransactionStatus, Transaction } from "@notesnook/core";
|
||||
import { Transaction, TransactionV1 } from "@notesnook/core";
|
||||
import { writeToClipboard } from "../../../utils/clipboard";
|
||||
import { showToast } from "../../../utils/toast";
|
||||
import { TaskManager } from "../../../common/task-manager";
|
||||
|
||||
const TransactionStatusToText: Record<TransactionStatus, string> = {
|
||||
completed: "Completed",
|
||||
billed: "Billed",
|
||||
canceled: "Canceled",
|
||||
paid: "Paid",
|
||||
past_due: "Past due"
|
||||
};
|
||||
import { ErrorText } from "../../../components/error-text";
|
||||
|
||||
export function BillingHistory() {
|
||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||
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);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
const transactions = usePromise(() => db.subscriptions.transactions(), []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
{transactions.status === "pending" ? (
|
||||
<Loading sx={{ mt: 2 }} />
|
||||
) : error ? (
|
||||
<Flex
|
||||
sx={{
|
||||
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 ? (
|
||||
) : transactions.status === "rejected" ? (
|
||||
<ErrorText error={transactions.reason} />
|
||||
) : transactions.value?.transactions.length === 0 ? (
|
||||
<Text variant="body" sx={{ mt: 2, color: "paragraph-secondary" }}>
|
||||
You have not been billed yet.
|
||||
</Text>
|
||||
@@ -86,6 +48,79 @@ export function BillingHistory() {
|
||||
cellPadding={0}
|
||||
cellSpacing={0}
|
||||
>
|
||||
{transactions.value?.type === "v1" ? (
|
||||
<>
|
||||
<thead>
|
||||
<Box
|
||||
as="tr"
|
||||
sx={{
|
||||
height: 30,
|
||||
th: { borderBottom: "1px solid var(--separator)" }
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{ id: "date", title: strings.date(), width: "20%" },
|
||||
{ id: "orderId", title: strings.orderId(), width: "20%" },
|
||||
{ id: "amount", title: strings.amount(), width: "20%" },
|
||||
{ id: "status", title: strings.status(), width: "20%" },
|
||||
{ id: "receipt", title: strings.receipt(), width: "20%" }
|
||||
].map((column) =>
|
||||
!column.title ? (
|
||||
<th key={column.id} />
|
||||
) : (
|
||||
<Box
|
||||
as="th"
|
||||
key={column.id}
|
||||
sx={{
|
||||
width: column.width,
|
||||
px: 1,
|
||||
mb: 2,
|
||||
textAlign: "left"
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
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>
|
||||
</Box>
|
||||
))}
|
||||
</tbody>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<thead>
|
||||
<Box
|
||||
as="tr"
|
||||
@@ -126,7 +161,7 @@ export function BillingHistory() {
|
||||
</Box>
|
||||
</thead>
|
||||
<tbody>
|
||||
{transactions.map((transaction) => (
|
||||
{transactions.value?.transactions.map((transaction) => (
|
||||
<Box key={transaction.id} as="tr" sx={{ height: 30 }}>
|
||||
<Text as="td" variant="body">
|
||||
<Copy
|
||||
@@ -141,7 +176,9 @@ export function BillingHistory() {
|
||||
{getFormattedDate(transaction.billed_at, "date")}
|
||||
</Text>
|
||||
<Text as="td" variant="body">
|
||||
{(transaction.details.totals.grand_total / 100).toFixed(2)}{" "}
|
||||
{(transaction.details.totals.grand_total / 100).toFixed(
|
||||
2
|
||||
)}{" "}
|
||||
{transaction.details.totals.currency_code}
|
||||
</Text>
|
||||
<Text as="td" variant="body">
|
||||
@@ -176,6 +213,8 @@ export function BillingHistory() {
|
||||
</Box>
|
||||
))}
|
||||
</tbody>
|
||||
</>
|
||||
)}
|
||||
</table>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -101,9 +101,11 @@ export function SubscriptionStatus() {
|
||||
variant="subBody"
|
||||
>
|
||||
{feature.id === "storage"
|
||||
? `${formatBytes(feature.used)}/${formatBytes(
|
||||
feature.total
|
||||
)}`
|
||||
? `${formatBytes(feature.used)}/${
|
||||
feature.total === Infinity
|
||||
? "Unlimited"
|
||||
: formatBytes(feature.total)
|
||||
}`
|
||||
: feature.total === Infinity
|
||||
? "Unlimited"
|
||||
: `${feature.used} of ${feature.total}`}
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
User
|
||||
} from "@notesnook/core";
|
||||
|
||||
export function getSubscriptionInfo(user: User | undefined): {
|
||||
export function getSubscriptionInfo(user?: User): {
|
||||
title: string;
|
||||
trial?: boolean;
|
||||
paused?: boolean;
|
||||
@@ -47,6 +47,7 @@ export function getSubscriptionInfo(user: User | undefined): {
|
||||
autoRenew?: boolean;
|
||||
trialExpiryDate?: string;
|
||||
} {
|
||||
user = user || useUserStore.getState().user;
|
||||
const { type, expiry, plan, status, provider } = user?.subscription || {};
|
||||
if (!expiry) return { title: "Free" };
|
||||
|
||||
@@ -251,10 +252,17 @@ export function UserProfile({ minimal }: Props) {
|
||||
</Text>
|
||||
{user.totalStorage && !minimal ? (
|
||||
<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 }}>
|
||||
{formatBytes(user.storageUsed || 0)}/
|
||||
{formatBytes(user.totalStorage)} used
|
||||
{user.totalStorage === -1
|
||||
? "Unlimited"
|
||||
: formatBytes(user.totalStorage)}{" "}
|
||||
used
|
||||
</Text>
|
||||
</Flex>
|
||||
) : null}
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
import { TaskManager } from "../../common/task-manager";
|
||||
import { ConfirmDialog } from "../confirm";
|
||||
import { ChangePlanDialog } from "../buy-dialog/change-plan-dialog";
|
||||
import { getSubscriptionInfo } from "./components/user-profile";
|
||||
|
||||
export const SubscriptionSettings: SettingsGroup[] = [
|
||||
{
|
||||
@@ -89,6 +90,7 @@ export const SubscriptionSettings: SettingsGroup[] = [
|
||||
const user = useUserStore.getState().user;
|
||||
const status = user?.subscription.status;
|
||||
return (
|
||||
getSubscriptionInfo(user).legacy ||
|
||||
user?.subscription.provider !== SubscriptionProvider.PADDLE ||
|
||||
!isUserSubscribed(user) ||
|
||||
status === SubscriptionStatusEnum.CANCELED ||
|
||||
@@ -136,12 +138,12 @@ export const SubscriptionSettings: SettingsGroup[] = [
|
||||
title: strings.update(),
|
||||
action: async () => {
|
||||
try {
|
||||
const urls = await db.subscriptions.urls();
|
||||
if (!urls)
|
||||
const url = await db.subscriptions.updateUrl();
|
||||
if (!url)
|
||||
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) {
|
||||
if (e instanceof Error) showToast("error", e.message);
|
||||
}
|
||||
@@ -160,6 +162,7 @@ export const SubscriptionSettings: SettingsGroup[] = [
|
||||
const user = useUserStore.getState().user;
|
||||
const status = user?.subscription.status;
|
||||
return (
|
||||
getSubscriptionInfo(user).legacy ||
|
||||
user?.subscription.provider !== SubscriptionProvider.PADDLE ||
|
||||
!isUserSubscribed(user) ||
|
||||
status !== SubscriptionStatusEnum.TRIAL
|
||||
|
||||
@@ -23,33 +23,10 @@ import {
|
||||
SubscriptionType,
|
||||
User
|
||||
} from "@notesnook/core";
|
||||
import { SUBSCRIPTION_STATUS } from "../common/constants";
|
||||
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
|
||||
);
|
||||
}
|
||||
import { useStore as useUserStore } from "../stores/user-store";
|
||||
|
||||
export function isActiveSubscription(user?: User) {
|
||||
user = user || userstore.get().user;
|
||||
user = user || useUserStore.getState().user;
|
||||
if (!user) return false;
|
||||
|
||||
const { status } = user?.subscription || {};
|
||||
@@ -59,7 +36,7 @@ export function isActiveSubscription(user?: User) {
|
||||
);
|
||||
}
|
||||
export function isUserSubscribed(user?: User) {
|
||||
user = user || userstore.get().user;
|
||||
user = user || useUserStore.getState().user;
|
||||
if (!user) return false;
|
||||
|
||||
const { type, expiry, plan, status } = user?.subscription || {};
|
||||
|
||||
@@ -21,10 +21,9 @@ import createStore from "../common/store";
|
||||
import { db } from "../common/db";
|
||||
import BaseStore from "./index";
|
||||
import Config from "../utils/config";
|
||||
import { isUserPremium } from "../hooks/use-is-user-premium";
|
||||
import { SUBSCRIPTION_STATUS } from "../common/constants";
|
||||
import { isUserSubscribed } from "../hooks/use-is-user-premium";
|
||||
import { appVersion } from "../utils/version";
|
||||
import { findItemAndDelete } from "@notesnook/core";
|
||||
import { findItemAndDelete, SubscriptionStatus } from "@notesnook/core";
|
||||
|
||||
/**
|
||||
* @extends {BaseStore<AnnouncementStore>}
|
||||
@@ -97,15 +96,12 @@ async function shouldShowAnnouncement(announcement) {
|
||||
if (!show) return false;
|
||||
|
||||
const user = await db.user.getUser();
|
||||
const subStatus = user?.subscription?.type;
|
||||
show = announcement.userTypes.some((userType) => {
|
||||
switch (userType) {
|
||||
case "pro":
|
||||
return isUserPremium(user);
|
||||
case "subscribed":
|
||||
return isUserSubscribed(user);
|
||||
case "trial":
|
||||
return subStatus === SUBSCRIPTION_STATUS.TRIAL;
|
||||
case "trialExpired":
|
||||
return subStatus === SUBSCRIPTION_STATUS.BASIC;
|
||||
return user?.subscription?.status === SubscriptionStatus.TRIAL;
|
||||
case "loggedOut":
|
||||
return !user;
|
||||
case "loggedIn":
|
||||
@@ -114,8 +110,6 @@ async function shouldShowAnnouncement(announcement) {
|
||||
return user && !user.isEmailConfirmed;
|
||||
case "verified":
|
||||
return user && user.isEmailConfirmed;
|
||||
case "proExpired":
|
||||
return subStatus === SUBSCRIPTION_STATUS.PREMIUM_EXPIRED;
|
||||
case "any":
|
||||
default:
|
||||
return true;
|
||||
|
||||
@@ -23,12 +23,11 @@ import BaseStore from "./index";
|
||||
import { EV, EVENTS } from "@notesnook/core";
|
||||
import Config from "../utils/config";
|
||||
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 { ConfirmDialog } from "../dialogs/confirm";
|
||||
import { OnboardingDialog } from "../dialogs/onboarding-dialog";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { isUserSubscribed } from "../hooks/use-is-user-premium";
|
||||
|
||||
class UserStore extends BaseStore<UserStore> {
|
||||
isLoggedIn?: boolean;
|
||||
@@ -59,16 +58,12 @@ class UserStore extends BaseStore<UserStore> {
|
||||
if (Config.get("sessionExpired")) return;
|
||||
|
||||
EV.subscribe(EVENTS.userSubscriptionUpdated, (subscription) => {
|
||||
const wasUserPremium = isUserPremium();
|
||||
const wasSubscribed = isUserSubscribed();
|
||||
this.set((state) => {
|
||||
if (!state.user) return;
|
||||
state.user.subscription = subscription;
|
||||
});
|
||||
if (!wasUserPremium && isUserPremium())
|
||||
OnboardingDialog.show({
|
||||
type:
|
||||
subscription.type === SUBSCRIPTION_STATUS.TRIAL ? "trial" : "pro"
|
||||
});
|
||||
if (!wasSubscribed && isUserSubscribed()) OnboardingDialog.show({});
|
||||
});
|
||||
|
||||
EV.subscribe(EVENTS.userEmailConfirmed, () => {
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
Server,
|
||||
Clip
|
||||
} from "@notesnook/web-clipper/dist/common/bridge";
|
||||
import { isUserPremium } from "../hooks/use-is-user-premium";
|
||||
import { store as appstore } from "../stores/app-store";
|
||||
import { h } from "./html";
|
||||
import { sanitizeFilename } from "@notesnook/common";
|
||||
@@ -32,6 +31,7 @@ import { getFormattedDate } from "@notesnook/common";
|
||||
import { useStore as useThemeStore } from "../stores/theme-store";
|
||||
import { isCipher } from "@notesnook/core";
|
||||
import { attachFiles } from "../components/editor/picker";
|
||||
import { isUserSubscribed } from "../hooks/use-is-user-premium";
|
||||
|
||||
export class WebExtensionServer implements Server {
|
||||
async login() {
|
||||
@@ -39,7 +39,7 @@ export class WebExtensionServer implements Server {
|
||||
const user = await db.user.getUser();
|
||||
const theme = colorScheme === "dark" ? darkTheme : lightTheme;
|
||||
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> {
|
||||
|
||||
@@ -542,11 +542,9 @@ export type FeatureUsage = {
|
||||
export async function getFeaturesUsage(): Promise<FeatureUsage[]> {
|
||||
const { isLegacyPro, plan } = await getUserPlan();
|
||||
const usage: FeatureUsage[] = [];
|
||||
console.log(isLegacyPro, plan);
|
||||
for (const key in features) {
|
||||
const feature = getFeature(key as FeatureId);
|
||||
const limit = getFeatureLimitFromPlan(feature, plan, isLegacyPro);
|
||||
console.log(limit, feature, key);
|
||||
if (!feature.used || typeof limit.value !== "number") continue;
|
||||
usage.push({
|
||||
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/>.
|
||||
*/
|
||||
|
||||
import { planToId, SubscriptionPlan } from "../types.js";
|
||||
import {
|
||||
planToId,
|
||||
SubscriptionPlan,
|
||||
SubscriptionType,
|
||||
User
|
||||
} from "../types.js";
|
||||
import hosts from "../utils/constants.js";
|
||||
import http from "../utils/http.js";
|
||||
import Database from "./index.js";
|
||||
@@ -30,6 +35,33 @@ export type TransactionStatus =
|
||||
| "past_due"
|
||||
| "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 {
|
||||
id: string;
|
||||
status: TransactionStatus;
|
||||
@@ -62,51 +94,76 @@ export default class Subscriptions {
|
||||
|
||||
async cancel() {
|
||||
const token = await this.db.tokenManager.getAccessToken();
|
||||
if (!token) return;
|
||||
await http.post(
|
||||
`${hosts.SUBSCRIPTIONS_HOST}/subscriptions/v2/cancel`,
|
||||
null,
|
||||
token
|
||||
);
|
||||
const user = await this.db.user.getUser();
|
||||
if (!token || !user) return;
|
||||
const endpoint = isLegacySubscription(user)
|
||||
? `subscriptions/cancel`
|
||||
: `subscriptions/v2/cancel`;
|
||||
await http.post(`${hosts.SUBSCRIPTIONS_HOST}/${endpoint}`, null, token);
|
||||
}
|
||||
|
||||
async pause() {
|
||||
const token = await this.db.tokenManager.getAccessToken();
|
||||
if (!token) return;
|
||||
await http.post(
|
||||
`${hosts.SUBSCRIPTIONS_HOST}/subscriptions/v2/pause`,
|
||||
null,
|
||||
token
|
||||
);
|
||||
const user = await this.db.user.getUser();
|
||||
if (!token || !user) return;
|
||||
const endpoint = isLegacySubscription(user)
|
||||
? `subscriptions/cancel?pause=true`
|
||||
: `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() {
|
||||
const token = await this.db.tokenManager.getAccessToken();
|
||||
if (!token) return;
|
||||
await http.post(
|
||||
`${hosts.SUBSCRIPTIONS_HOST}/subscriptions/v2/resume`,
|
||||
null,
|
||||
token
|
||||
);
|
||||
const user = await this.db.user.getUser();
|
||||
if (!token || !user) return;
|
||||
const endpoint = isLegacySubscription(user)
|
||||
? `subscriptions/resume`
|
||||
: `subscriptions/v2/resume`;
|
||||
await http.post(`${hosts.SUBSCRIPTIONS_HOST}/${endpoint}`, null, token);
|
||||
}
|
||||
|
||||
async refund(reason?: string) {
|
||||
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(
|
||||
`${hosts.SUBSCRIPTIONS_HOST}/subscriptions/v2/refund`,
|
||||
`${hosts.SUBSCRIPTIONS_HOST}/${endpoint}`,
|
||||
{ reason },
|
||||
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();
|
||||
if (!token) return;
|
||||
return await http.get(
|
||||
const user = await this.db.user.getUser();
|
||||
if (!token || !user) return;
|
||||
if (isLegacySubscription(user)) {
|
||||
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> {
|
||||
@@ -119,15 +176,23 @@ export default class Subscriptions {
|
||||
return response.url;
|
||||
}
|
||||
|
||||
async urls(): Promise<
|
||||
{ update_payment_method: string; cancel: string } | undefined
|
||||
> {
|
||||
async updateUrl(): Promise<string | undefined> {
|
||||
const token = await this.db.tokenManager.getAccessToken();
|
||||
if (!token) return;
|
||||
const user = await this.db.user.getUser();
|
||||
if (!token || !user) return;
|
||||
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) {
|
||||
@@ -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!"
|
||||
|
||||
#: src/strings.ts:2574
|
||||
msgid "Welcome to Notesnook {plan} plan"
|
||||
msgstr "Welcome to Notesnook {plan} plan"
|
||||
msgid "Welcome to Notesnook {plan}"
|
||||
msgstr "Welcome to Notesnook {plan}"
|
||||
|
||||
#: src/strings.ts:2072
|
||||
msgid "Welcome to Notesnook Pro"
|
||||
|
||||
@@ -7011,7 +7011,7 @@ msgid "Welcome back!"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2574
|
||||
msgid "Welcome to Notesnook {plan} plan"
|
||||
msgid "Welcome to Notesnook {plan}"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2072
|
||||
|
||||
Reference in New Issue
Block a user