global: introducing Notesnook Circle (#8779)

* core: add api for getting Notesnook Circle partners

* web: introduce notesnook circle

* core: export CirclePartner type

* mobile: add notesnook-circle

* web: use strings from intl for notesnook circle

* common: add notesnook circle to features

* web: fix local ips set for hosts

* mobile: fix db hosts

---------

Co-authored-by: Ammar Ahmed <ammarahmed6506@gmail.com>
This commit is contained in:
Abdullah Atta
2025-10-17 08:49:32 +05:00
committed by GitHub
parent b27f064ad4
commit 1f748a4026
21 changed files with 715 additions and 186 deletions

View File

@@ -39,7 +39,7 @@ export async function setupDatabase(password?: string) {
const key = await getDatabaseKey(password); const key = await getDatabaseKey(password);
if (!key) throw new Error(strings.databaseSetupFailed()); if (!key) throw new Error(strings.databaseSetupFailed());
// const base = `http://192.168.100.88`; // const base = `http://192.168.100.92`;
// database.host({ // database.host({
// API_HOST: `${base}:5264`, // API_HOST: `${base}:5264`,

View File

@@ -44,7 +44,8 @@
"@trpc/react-query": "^10.45.2", "@trpc/react-query": "^10.45.2",
"@trpc/server": "^10.45.2", "@trpc/server": "^10.45.2",
"@tanstack/react-query": "^4.36.1", "@tanstack/react-query": "^4.36.1",
"async-mutex": "0.5.0" "async-mutex": "0.5.0",
"react-async-hook": "^4.0.0"
}, },
"sideEffects": false "sideEffects": false
} }

View File

@@ -43,6 +43,7 @@ import { ServersConfiguration } from "./server-config";
import SoundPicker from "./sound-picker"; import SoundPicker from "./sound-picker";
import ThemeSelector from "./theme-selector"; import ThemeSelector from "./theme-selector";
import { TitleFormat } from "./title-format"; import { TitleFormat } from "./title-format";
import { NotesnookCircle } from "./notesnook-circle";
export const components: { [name: string]: ReactElement } = { export const components: { [name: string]: ReactElement } = {
homeselector: <HomePicker />, homeselector: <HomePicker />,
@@ -69,5 +70,6 @@ export const components: { [name: string]: ReactElement } = {
), ),
"sidebar-tab-selector": <SidebarTabPicker />, "sidebar-tab-selector": <SidebarTabPicker />,
"change-password": <ChangePassword />, "change-password": <ChangePassword />,
"change-email": <ChangeEmail /> "change-email": <ChangeEmail />,
"notesnook-circle": <NotesnookCircle />
}; };

View File

@@ -0,0 +1,190 @@
import { CirclePartner, SubscriptionStatus } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
import { useEffect, useState } from "react";
import {
ActivityIndicator,
Image,
ScrollView,
TouchableOpacity,
View
} from "react-native";
import { db } from "../../common/database";
import AppIcon from "../../components/ui/AppIcon";
import { Button } from "../../components/ui/button";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import { ToastManager } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import PremiumService from "../../services/premium";
import { useUserStore } from "../../stores/use-user-store";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { useAsync } from "react-async-hook";
import { Notice } from "../../components/ui/notice";
export const NotesnookCircle = () => {
const user = useUserStore((state) => state.user);
const isOnTrial =
PremiumService.get() &&
user?.subscription?.status === SubscriptionStatus.TRIAL;
const isFree = !PremiumService.get();
const partners = useAsync(db.circle.partners, []);
return (
<ScrollView
contentContainerStyle={{
gap: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: DefaultAppStyles.GAP,
paddingTop: DefaultAppStyles.GAP_VERTICAL
}}
>
{!isFree && !isOnTrial ? null : (
<View>
<Paragraph>
{isFree
? strings.freeUserCircleNotice()
: strings.trialUserCircleNotice()}
</Paragraph>
{!isOnTrial ? null : (
<Button
title={strings.upgradePlan()}
onPress={() => {
Navigation.navigate("PayWall", {
canGoBack: true,
context: useUserStore.getState().user
? "logged-in"
: "logged-out"
});
}}
style={{
alignSelf: "flex-start",
paddingHorizontal: 0
}}
/>
)}
</View>
)}
{partners.loading ? <ActivityIndicator /> : null}
{partners.error ? (
<Notice type="alert" text={partners.error.message} />
) : null}
{partners.result?.map((item) => (
<Partner item={item} available={!isFree && !isOnTrial} />
))}
</ScrollView>
);
};
const Partner = ({
item,
available
}: {
item: CirclePartner;
available: boolean;
}) => {
const { colors } = useThemeColors();
const [code, setCode] = useState<string>();
return (
<View
style={{
borderRadius: defaultBorderRadius,
borderWidth: 1,
borderColor: colors.primary.border,
padding: DefaultAppStyles.GAP,
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center"
}}
>
<Heading>{item.name}</Heading>
<Image
src={item.logoBase64}
style={{
width: 40,
height: 40
}}
/>
</View>
<Paragraph style={{ textAlign: "justify" }}>
{item.longDescription.trim()}
</Paragraph>
<Paragraph
style={{
alignSelf: "center"
}}
color={colors.primary.accent}
>
{item.offerDescription}
</Paragraph>
{available ? (
<>
{!code ? (
<Button
type="accent"
title={strings.redeemCode()}
width="100%"
onPress={() => {
if (!PremiumService.get()) {
Navigation.navigate("PayWall", {
canGoBack: true,
context: useUserStore.getState().user
? "logged-in"
: "logged-out"
});
return;
}
db.circle
.redeem(item.id)
.then((result) => setCode(result?.code))
.catch((e) => ToastManager.error(e));
}}
/>
) : (
<TouchableOpacity
style={{
backgroundColor: colors.secondary.background,
borderRadius: defaultBorderRadius,
alignItems: "center",
justifyContent: "center",
padding: DefaultAppStyles.GAP_SMALL,
borderWidth: 0.5,
borderColor: colors.secondary.border,
flexDirection: "row",
gap: DefaultAppStyles.GAP_SMALL
}}
activeOpacity={0.9}
onPress={() => {
Clipboard.setString(code);
}}
>
<Paragraph
size={AppFontSize.lg}
color={colors.secondary.paragraph}
>
{code}
</Paragraph>
<AppIcon name="content-copy" />
</TouchableOpacity>
)}
</>
) : null}
</View>
);
};

View File

@@ -615,6 +615,14 @@ export const settingsGroups: SettingSection[] = [
} }
} }
] ]
},
{
id: "notesnook-circle",
name: strings.notesnookCircle(),
icon: "circle-outline",
type: "screen",
description: strings.notesnookCircleDesc(),
component: "notesnook-circle"
} }
] ]
}, },

File diff suppressed because it is too large Load Diff

View File

@@ -56,4 +56,4 @@
"react": "18.2.0", "react": "18.2.0",
"react-native": "0.77.2" "react-native": "0.77.2"
} }
} }

View File

@@ -70,6 +70,7 @@ const EXTRA_ICON_NAMES = [
"chevron-down", "chevron-down",
"calendar", "calendar",
"minus-circle-outline", "minus-circle-outline",
"circle-outline",
"close-circle-outline", "close-circle-outline",
"qrcode", "qrcode",
"text", "text",

View File

@@ -53,8 +53,8 @@ async function initializeDatabase(persistence: DatabasePersistence) {
AUTH_HOST: "https://auth.streetwriters.co", AUTH_HOST: "https://auth.streetwriters.co",
SSE_HOST: "https://events.streetwriters.co", SSE_HOST: "https://events.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co", ISSUES_HOST: "https://issues.streetwriters.co",
MONOGRAPH_HOST: "https://monogr.ph",
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co", SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
MONOGRAPH_HOST: "https://monogr.ph",
NOTESNOOK_HOST: "https://notesnook.com", NOTESNOOK_HOST: "https://notesnook.com",
...Config.get("serverUrls", {}) ...Config.get("serverUrls", {})
}); });

View File

@@ -0,0 +1,173 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useState } from "react";
import { Copy, Loading } from "../../../components/icons";
import { Button, Link, Flex, Text, Grid } from "@theme-ui/components";
import { usePromise } from "@notesnook/common";
import { db } from "../../../common/db";
import { writeToClipboard } from "../../../utils/clipboard";
import { showToast } from "../../../utils/toast";
import { ErrorText } from "../../../components/error-text";
import { useStore as useUserStore } from "../../../stores/user-store";
import { SubscriptionPlan, SubscriptionStatus } from "@notesnook/core";
import { BuyDialog } from "../../buy-dialog";
import { strings } from "@notesnook/intl";
export function CirclePartners() {
const partners = usePromise(() => db.circle.partners(), []);
const [redeemedCode, setRedeemedCode] = useState<{
partnerId: string;
code: string;
}>();
const subscription = useUserStore((store) => store.user?.subscription);
const isFree =
!subscription ||
subscription?.plan === SubscriptionPlan.FREE ||
subscription?.status === SubscriptionStatus.EXPIRED;
const isTrial = subscription?.status === SubscriptionStatus.TRIAL;
const canRedeem = !isFree && !isTrial;
return (
<>
{partners.status === "pending" ? (
<Loading sx={{ mt: 2 }} />
) : partners.status === "rejected" ? (
<ErrorText error={partners.reason} />
) : (
<Grid columns={2} sx={{ mt: 2 }}>
{partners.value?.map((partner) => (
<Flex
key={partner.id}
sx={{
flexDirection: "column",
border: "1px solid var(--border)",
borderRadius: "dialog",
padding: 2,
gap: 1
}}
>
<Flex
sx={{ justifyContent: "space-between", alignItems: "center" }}
>
<Text variant="title">{partner.name}</Text>
<img
src={partner.logoBase64}
alt={partner.name}
style={{ width: 30, height: "auto", borderRadius: 8 }}
/>
</Flex>
<Text variant="body" sx={{ whiteSpace: "pre-wrap" }}>
{partner.shortDescription}
</Text>
<Text
variant="subBody"
sx={{
fontStyle: "italic",
color: "accent",
textAlign: "center"
}}
>
{partner.offerDescription}
</Text>
{redeemedCode?.partnerId === partner.id ? (
<>
<Flex
sx={{
bg: "background-secondary",
border: "1px solid var(--border)",
borderRadius: "default",
alignSelf: "center",
alignItems: "center",
gap: 1,
p: "small"
}}
>
<Text
variant="body"
className="selectable"
sx={{
fontFamily: "monospace",
fontSize: 14
}}
>
{redeemedCode.code}
</Text>
<Button
variant="icon"
onClick={() =>
writeToClipboard({ "text/plain": redeemedCode.code })
}
>
<Copy size={12} />
</Button>
</Flex>
{partner.codeRedeemUrl ? (
<Text variant="subBody" sx={{ textAlign: "center" }}>
<Link
target="_blank"
href={partner.codeRedeemUrl.replace(
"{{code}}",
redeemedCode.code
)}
>
Click here
</Link>{" "}
to directly claim the promotion.
</Text>
) : null}
</>
) : (
<Button
variant="accent"
sx={{ mt: 1 }}
onClick={async () => {
if (isFree) return BuyDialog.show({});
if (isTrial) {
return showToast(
"error",
strings.trialUserCircleNotice()
);
}
if (!canRedeem) return;
const result = await db.circle
.redeem(partner.id)
.catch((e) => {
showToast("error", e.message);
});
if (result?.code) {
setRedeemedCode({
partnerId: partner.id,
code: result.code
});
showToast("success", "Code redeemed successfully");
}
}}
>
{isFree ? strings.upgradeToRedeem() : strings.redeemCode()}
</Button>
)}
</Flex>
))}
</Grid>
)}
</>
);
}

View File

@@ -39,7 +39,8 @@ import {
Servers, Servers,
ShieldLock, ShieldLock,
Sync, Sync,
Inbox Inbox,
CircleEmpty
} from "../../components/icons"; } from "../../components/icons";
import NavigationItem from "../../components/navigation-menu/navigation-item"; import NavigationItem from "../../components/navigation-menu/navigation-item";
import { FlexScrollContainer } from "../../components/scroll-container"; import { FlexScrollContainer } from "../../components/scroll-container";
@@ -80,6 +81,7 @@ import { strings } from "@notesnook/intl";
import { mdToHtml } from "../../utils/md"; import { mdToHtml } from "../../utils/md";
import { InboxSettings } from "./inbox-settings"; import { InboxSettings } from "./inbox-settings";
import { withFeatureCheck } from "../../common"; import { withFeatureCheck } from "../../common";
import { NotesnookCircleSettings } from "./notesnook-circle-settings";
type SettingsDialogProps = BaseDialogProps<false> & { type SettingsDialogProps = BaseDialogProps<false> & {
activeSection?: SectionKeys; activeSection?: SectionKeys;
@@ -109,6 +111,12 @@ const sectionGroups: SectionGroup[] = [
icon: Sync, icon: Sync,
isHidden: () => !useUserStore.getState().isLoggedIn isHidden: () => !useUserStore.getState().isLoggedIn
}, },
{
key: "circle",
title: "Notesnook Circle",
icon: CircleEmpty,
isHidden: () => !useUserStore.getState().isLoggedIn
},
{ {
key: "inbox", key: "inbox",
title: "Inbox", title: "Inbox",
@@ -185,7 +193,8 @@ const SettingsGroups = [
...AboutSettings, ...AboutSettings,
...SubscriptionSettings, ...SubscriptionSettings,
...ServersSettings, ...ServersSettings,
...InboxSettings ...InboxSettings,
...NotesnookCircleSettings
]; ];
// Thoughts: // Thoughts:

View File

@@ -0,0 +1,43 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { strings } from "@notesnook/intl";
import { CirclePartners } from "./components/circle-partners";
import { SettingsGroup } from "./types";
export const NotesnookCircleSettings: SettingsGroup[] = [
{
header: strings.notesnookCircle(),
key: "notesnook-circle",
section: "circle",
settings: [
{
key: "partners",
title: "",
description: strings.notesnookCircleDesc(),
components: [
{
type: "custom",
component: CirclePartners
}
]
}
]
}
];

View File

@@ -41,7 +41,8 @@ export type SectionKeys =
| "legal" | "legal"
| "developer" | "developer"
| "about" | "about"
| "inbox"; | "inbox"
| "circle";
export type SectionGroupKeys = export type SectionGroupKeys =
| "account" | "account"

View File

@@ -447,6 +447,17 @@ const features = {
believer: createLimit(true), believer: createLimit(true),
legacyPro: createLimit(true) legacyPro: createLimit(true)
} }
}),
notesnookCircle: createFeature({
id: "notesnookCircle",
title: "Notesnook Circle",
availability: {
free: createLimit(false),
essential: createLimit(true),
pro: createLimit(true),
believer: createLimit(true),
legacyPro: createLimit(true)
}
}) })
}; };

View File

@@ -0,0 +1,48 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import hosts from "../utils/constants.js";
import http from "../utils/http.js";
import Database from "./index.js";
export type CirclePartner = {
id: string;
name: string;
url: string;
logoBase64: string;
shortDescription: string;
longDescription: string;
offerDescription: string;
codeRedeemUrl?: string;
};
export class Circle {
constructor(private readonly db: Database) {}
partners(): Promise<CirclePartner[] | undefined> {
return http.get(`${hosts.SUBSCRIPTIONS_HOST}/circle/partners`);
}
async redeem(partnerId: string): Promise<{ code?: string } | undefined> {
const token = await this.db.tokenManager.getAccessToken();
return http.get(
`${hosts.SUBSCRIPTIONS_HOST}/circle/redeem?partnerId=${partnerId}`,
token
);
}
}

View File

@@ -82,6 +82,7 @@ import { NNMigrationProvider } from "../database/migrations.js";
import { ConfigStorage } from "../database/config.js"; import { ConfigStorage } from "../database/config.js";
import { LazyPromise } from "../utils/lazy-promise.js"; import { LazyPromise } from "../utils/lazy-promise.js";
import { InboxApiKeys } from "./inbox-api-keys.js"; import { InboxApiKeys } from "./inbox-api-keys.js";
import { Circle } from "./circle.js";
type EventSourceConstructor = new ( type EventSourceConstructor = new (
uri: string, uri: string,
@@ -192,6 +193,7 @@ class Database {
tokenManager = new TokenManager(this.kv); tokenManager = new TokenManager(this.kv);
mfa = new MFAManager(this.tokenManager); mfa = new MFAManager(this.tokenManager);
subscriptions = new Subscriptions(this); subscriptions = new Subscriptions(this);
circle = new Circle(this);
offers = Offers; offers = Offers;
debug = new Debug(); debug = new Debug();
pricing = Pricing; pricing = Pricing;

View File

@@ -39,6 +39,7 @@ export * from "./api/debug.js";
export * from "./api/monographs.js"; export * from "./api/monographs.js";
export * from "./api/subscriptions.js"; export * from "./api/subscriptions.js";
export * from "./api/pricing.js"; export * from "./api/pricing.js";
export * from "./api/circle.js";
export { VAULT_ERRORS } from "./api/vault.js"; export { VAULT_ERRORS } from "./api/vault.js";
export type { SyncOptions } from "./api/sync/index.js"; export type { SyncOptions } from "./api/sync/index.js";
export { sanitizeTag } from "./collections/tags.js"; export { sanitizeTag } from "./collections/tags.js";

View File

@@ -4217,6 +4217,14 @@ msgstr "notes imported"
msgid "Notesnook" msgid "Notesnook"
msgstr "Notesnook" msgstr "Notesnook"
#: src/strings.ts:2596
msgid "Notesnook Circle"
msgstr "Notesnook Circle"
#: src/strings.ts:2598
msgid "Notesnook Circle brings together trusted partners who share our commitment to privacy, transparency, and user freedom."
msgstr "Notesnook Circle brings together trusted partners who share our commitment to privacy, transparency, and user freedom."
#: src/strings.ts:2066 #: src/strings.ts:2066
msgid "Notesnook encrypts everything offline before syncing to your other devices. This means that no one can read your notes except you. Not even us." msgid "Notesnook encrypts everything offline before syncing to your other devices. This means that no one can read your notes except you. Not even us."
msgstr "Notesnook encrypts everything offline before syncing to your other devices. This means that no one can read your notes except you. Not even us." msgstr "Notesnook encrypts everything offline before syncing to your other devices. This means that no one can read your notes except you. Not even us."
@@ -5006,6 +5014,10 @@ msgstr "Recovery successful!"
msgid "Redeem" msgid "Redeem"
msgstr "Redeem" msgstr "Redeem"
#: src/strings.ts:2595
msgid "Redeem code"
msgstr "Redeem code"
#: src/strings.ts:2431 #: src/strings.ts:2431
msgid "Redeem gift code" msgid "Redeem gift code"
msgstr "Redeem gift code" msgstr "Redeem gift code"
@@ -6367,6 +6379,10 @@ msgstr "The {title} at {url} is not compatible with this client."
msgid "The information above will be publically available at" msgid "The information above will be publically available at"
msgstr "The information above will be publically available at" msgstr "The information above will be publically available at"
#: src/strings.ts:2602
msgid "The Notesnook Circle is exclusive to subscribers. Please consider subscribing to gain access to Notesnook Circle and enjoy additional benefits."
msgstr "The Notesnook Circle is exclusive to subscribers. Please consider subscribing to gain access to Notesnook Circle and enjoy additional benefits."
#: src/strings.ts:2081 #: src/strings.ts:2081
msgid "The password/pin for unlocking the app." msgid "The password/pin for unlocking the app."
msgstr "The password/pin for unlocking the app." msgstr "The password/pin for unlocking the app."
@@ -6764,6 +6780,10 @@ msgstr "Upgrade to Notesnook Pro to create more tags."
msgid "Upgrade to Pro" msgid "Upgrade to Pro"
msgstr "Upgrade to Pro" msgstr "Upgrade to Pro"
#: src/strings.ts:2594
msgid "Upgrade to redeem"
msgstr "Upgrade to redeem"
#: src/strings.ts:509 #: src/strings.ts:509
msgid "Upload" msgid "Upload"
msgstr "Upload" msgstr "Upload"

View File

@@ -4197,6 +4197,14 @@ msgstr ""
msgid "Notesnook" msgid "Notesnook"
msgstr "" msgstr ""
#: src/strings.ts:2596
msgid "Notesnook Circle"
msgstr ""
#: src/strings.ts:2598
msgid "Notesnook Circle brings together trusted partners who share our commitment to privacy, transparency, and user freedom."
msgstr ""
#: src/strings.ts:2066 #: src/strings.ts:2066
msgid "Notesnook encrypts everything offline before syncing to your other devices. This means that no one can read your notes except you. Not even us." msgid "Notesnook encrypts everything offline before syncing to your other devices. This means that no one can read your notes except you. Not even us."
msgstr "" msgstr ""
@@ -4980,6 +4988,10 @@ msgstr ""
msgid "Redeem" msgid "Redeem"
msgstr "" msgstr ""
#: src/strings.ts:2595
msgid "Redeem code"
msgstr ""
#: src/strings.ts:2431 #: src/strings.ts:2431
msgid "Redeem gift code" msgid "Redeem gift code"
msgstr "" msgstr ""
@@ -6326,6 +6338,10 @@ msgstr ""
msgid "The information above will be publically available at" msgid "The information above will be publically available at"
msgstr "" msgstr ""
#: src/strings.ts:2602
msgid "The Notesnook Circle is exclusive to subscribers. Please consider subscribing to gain access to Notesnook Circle and enjoy additional benefits."
msgstr ""
#: src/strings.ts:2081 #: src/strings.ts:2081
msgid "The password/pin for unlocking the app." msgid "The password/pin for unlocking the app."
msgstr "" msgstr ""
@@ -6723,6 +6739,10 @@ msgstr ""
msgid "Upgrade to Pro" msgid "Upgrade to Pro"
msgstr "" msgstr ""
#: src/strings.ts:2594
msgid "Upgrade to redeem"
msgstr ""
#: src/strings.ts:509 #: src/strings.ts:509
msgid "Upload" msgid "Upload"
msgstr "" msgstr ""

View File

@@ -2590,5 +2590,14 @@ Use this if changes from other devices are not appearing on this device. This wi
t`You can change your subscription plan from the web app`, t`You can change your subscription plan from the web app`,
announcement: () => t`ANNOUNCEMENT`, announcement: () => t`ANNOUNCEMENT`,
cannotChangePlan: () => cannotChangePlan: () =>
t`Your current subscription does not allow changing plans` t`Your current subscription does not allow changing plans`,
upgradeToRedeem: () => t`Upgrade to redeem`,
redeemCode: () => t`Redeem code`,
notesnookCircle: () => t`Notesnook Circle`,
notesnookCircleDesc: () =>
t`Notesnook Circle brings together trusted partners who share our commitment to privacy, transparency, and user freedom.`,
trialUserCircleNotice: () =>
`Notesnook Circle is reserved for members with an active subscription. You'll get full access after your trial period is over and your subscription is confirmed.`,
freeUserCircleNotice: () =>
t`The Notesnook Circle is exclusive to subscribers. Please consider subscribing to gain access to Notesnook Circle and enjoy additional benefits.`
}; };

View File

@@ -67,7 +67,7 @@ await Promise.all([
IS_WATCH ? "--watch" : "" IS_WATCH ? "--watch" : ""
), ),
cmd( cmd(
TSCGO, TSC,
"--emitDeclarationOnly", "--emitDeclarationOnly",
"--outDir", "--outDir",
"dist/types", "dist/types",