diff --git a/app/assets/javascripts/preferences/panes/AccountPreferences.tsx b/app/assets/javascripts/preferences/panes/AccountPreferences.tsx
index b9ceb494d..49eb88a7d 100644
--- a/app/assets/javascripts/preferences/panes/AccountPreferences.tsx
+++ b/app/assets/javascripts/preferences/panes/AccountPreferences.tsx
@@ -1,4 +1,4 @@
-import { Credentials, Sync } from '@/preferences/panes/account';
+import { Sync, SubscriptionWrapper, Credentials } from '@/preferences/panes/account';
import { PreferencesPane } from '@/preferences/components';
import { observer } from 'mobx-react-lite';
import { WebApplication } from '@/ui_models/application';
@@ -11,6 +11,7 @@ export const AccountPreferences = observer(({application}: Props) => {
+
);
});
diff --git a/app/assets/javascripts/preferences/panes/account/index.ts b/app/assets/javascripts/preferences/panes/account/index.ts
index 4910df077..933896877 100644
--- a/app/assets/javascripts/preferences/panes/account/index.ts
+++ b/app/assets/javascripts/preferences/panes/account/index.ts
@@ -1,2 +1,3 @@
+export { SubscriptionWrapper } from './subscription/SubscriptionWrapper';
export { Sync } from './Sync';
export { Credentials } from './Credentials';
diff --git a/app/assets/javascripts/preferences/panes/account/subscription/NoSubscription.tsx b/app/assets/javascripts/preferences/panes/account/subscription/NoSubscription.tsx
new file mode 100644
index 000000000..5042ea992
--- /dev/null
+++ b/app/assets/javascripts/preferences/panes/account/subscription/NoSubscription.tsx
@@ -0,0 +1,23 @@
+import { FunctionalComponent } from "preact";
+import { Text } from '@/preferences/components';
+import { Button } from '@/components/Button';
+
+export const NoSubscription: FunctionalComponent = () => (
+ <>
+ You don't have a Standard Notes subscription yet.
+
+
+ >
+);
diff --git a/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx b/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx
new file mode 100644
index 000000000..4f159cb0c
--- /dev/null
+++ b/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx
@@ -0,0 +1,86 @@
+import {
+ PreferencesGroup,
+ PreferencesSegment,
+ Title,
+} from '@/preferences/components';
+import { WebApplication } from '@/ui_models/application';
+import { useCallback, useEffect, useState } from 'preact/hooks';
+import { SubscriptionState } from './subscription_state';
+import { SubscriptionInformation } from './SubscriptionInformation';
+import { NoSubscription } from './NoSubscription';
+import { Text } from '@/preferences/components';
+import { observer } from 'mobx-react-lite';
+
+type Props = {
+ application: WebApplication;
+ subscriptionState: SubscriptionState;
+};
+
+export const Subscription = observer(({
+ application,
+ subscriptionState,
+}: Props) => {
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(false);
+
+ const { userSubscription } = subscriptionState;
+
+ const getSubscriptions = useCallback(async () => {
+ try {
+ const subscriptions = await application.getAvailableSubscriptions();
+ if (subscriptions) {
+ subscriptionState.setAvailableSubscriptions(subscriptions);
+ }
+ } catch (e) {
+ // Error in this call will only prevent the plan name from showing
+ }
+ }, [application, subscriptionState]);
+
+ const getSubscription = useCallback(async () => {
+ try {
+ const subscription = await application.getUserSubscription();
+ if (subscription) {
+ subscriptionState.setUserSubscription(subscription);
+ }
+ } catch (e) {
+ setError(true);
+ }
+ }, [application, subscriptionState]);
+
+ const getSubscriptionInfo = useCallback(async () => {
+ setLoading(true);
+ try {
+ await getSubscription();
+ await getSubscriptions();
+ } finally {
+ setLoading(false);
+ }
+ }, [getSubscription, getSubscriptions]);
+
+ useEffect(() => {
+ getSubscriptionInfo();
+ }, [getSubscriptionInfo]);
+
+ const now = new Date().getTime();
+
+ return (
+
+
+
+
+
Subscription
+ {error ? (
+ No subscription information available.
+ ) : loading ? (
+ Loading subscription information...
+ ) : userSubscription && userSubscription.endsAt > now ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+});
diff --git a/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionInformation.tsx b/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionInformation.tsx
new file mode 100644
index 000000000..a68b9c53f
--- /dev/null
+++ b/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionInformation.tsx
@@ -0,0 +1,82 @@
+import { observer } from 'mobx-react-lite';
+import { SubscriptionState } from './subscription_state';
+import { Text } from '@/preferences/components';
+import { Button } from '@/components/Button';
+
+type Props = {
+ subscriptionState: SubscriptionState;
+};
+
+const StatusText = observer(({ subscriptionState }: Props) => {
+ const { userSubscription, userSubscriptionName } = subscriptionState;
+ const expirationDate = new Date(userSubscription!.endsAt / 1000).toLocaleString();
+
+ return userSubscription!.cancelled ? (
+
+ Your{' '}
+
+ Standard Notes{userSubscriptionName ? ' ' : ''}
+ {userSubscriptionName}
+ {' '}
+ subscription has been{' '}
+
+ canceled but will remain valid until{' '}
+ {expirationDate}
+
+ . You may resubscribe below if you wish.
+
+ ) : (
+
+ Your{' '}
+
+ Standard Notes{userSubscriptionName ? ' ' : ''}
+ {userSubscriptionName}
+ {' '}
+ subscription will be{' '}
+
+ renewed on {expirationDate}
+
+ .
+
+ );
+});
+
+const PrimaryButton = observer(({ subscriptionState }: Props) => {
+ const { userSubscription } = subscriptionState;
+
+ return (
+ null}
+ />
+ );
+});
+
+export const SubscriptionInformation = observer(
+ ({ subscriptionState }: Props) => (
+ <>
+
+
+
null}
+ />
+ null}
+ />
+
+
+ >
+ )
+);
diff --git a/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionWrapper.tsx b/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionWrapper.tsx
new file mode 100644
index 000000000..9a19112e6
--- /dev/null
+++ b/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionWrapper.tsx
@@ -0,0 +1,21 @@
+import { WebApplication } from '@/ui_models/application';
+import { FunctionalComponent } from 'preact';
+import { useState } from 'preact/hooks';
+import { Subscription } from './Subscription';
+import { SubscriptionState } from './subscription_state';
+
+type Props = {
+ application: WebApplication;
+};
+
+export const SubscriptionWrapper: FunctionalComponent = ({
+ application,
+}) => {
+ const [subscriptionState] = useState(() => new SubscriptionState());
+ return (
+
+ );
+};
diff --git a/app/assets/javascripts/preferences/panes/account/subscription/subscription_state.tsx b/app/assets/javascripts/preferences/panes/account/subscription/subscription_state.tsx
new file mode 100644
index 000000000..38f6768a0
--- /dev/null
+++ b/app/assets/javascripts/preferences/panes/account/subscription/subscription_state.tsx
@@ -0,0 +1,51 @@
+import { action, computed, makeObservable, observable } from 'mobx';
+
+type Subscription = {
+ planName: string;
+ cancelled: boolean;
+ endsAt: number;
+};
+
+type AvailableSubscriptions = {
+ [key: string]: {
+ name: string;
+ };
+};
+
+export class SubscriptionState {
+ userSubscription: Subscription | undefined = undefined;
+ availableSubscriptions: AvailableSubscriptions | undefined = undefined;
+
+ constructor() {
+ makeObservable(this, {
+ userSubscription: observable,
+ availableSubscriptions: observable,
+
+ userSubscriptionName: computed,
+
+ setUserSubscription: action,
+ setAvailableSubscriptions: action,
+ });
+ }
+
+ get userSubscriptionName(): string {
+ if (
+ this.availableSubscriptions &&
+ this.userSubscription &&
+ this.availableSubscriptions[this.userSubscription.planName]
+ ) {
+ return this.availableSubscriptions[this.userSubscription.planName].name;
+ }
+ return '';
+ }
+
+ public setUserSubscription(subscription: Subscription): void {
+ this.userSubscription = subscription;
+ }
+
+ public setAvailableSubscriptions(
+ subscriptions: AvailableSubscriptions
+ ): void {
+ this.availableSubscriptions = subscriptions;
+ }
+}
diff --git a/app/assets/javascripts/ui_models/application.ts b/app/assets/javascripts/ui_models/application.ts
index fba8edf08..99161903f 100644
--- a/app/assets/javascripts/ui_models/application.ts
+++ b/app/assets/javascripts/ui_models/application.ts
@@ -25,6 +25,7 @@ import { NativeExtManager } from '@/services/nativeExtManager';
import { StatusManager } from '@/services/statusManager';
import { ThemeManager } from '@/services/themeManager';
import { AppVersion } from '@/version';
+import { isDev } from '@/utils';
type WebServices = {
appState: AppState;
@@ -64,7 +65,8 @@ export class WebApplication extends SNApplication {
identifier,
[],
defaultSyncServerHost,
- AppVersion
+ AppVersion,
+ isDev,
);
this.$compile = $compile;
this.scope = scope;
diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss
index 32baa1cb4..a8a4bed8b 100644
--- a/app/assets/stylesheets/_sn.scss
+++ b/app/assets/stylesheets/_sn.scss
@@ -149,3 +149,7 @@
.sn-title {
@extend .font-bold;
}
+
+.mr-3 {
+ margin-right: 0.75rem;
+}
diff --git a/package.json b/package.json
index b9684b96a..3e0e2ad59 100644
--- a/package.json
+++ b/package.json
@@ -71,7 +71,8 @@
"@reach/checkbox": "^0.13.2",
"@reach/dialog": "^0.13.0",
"@standardnotes/sncrypto-web": "1.5.2",
- "@standardnotes/snjs": "2.13.0",
+ "@standardnotes/features": "1.6.1",
+ "@standardnotes/snjs": "2.14.3",
"mobx": "^6.3.2",
"mobx-react-lite": "^3.2.0",
"preact": "^10.5.12",
diff --git a/yarn.lock b/yarn.lock
index 33f4f423f..3efd11a6f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2011,7 +2011,14 @@
prop-types "^15.7.2"
tslib "^2.1.0"
-"@standardnotes/auth@3.7.1", "@standardnotes/auth@^3.7.0":
+"@standardnotes/auth@3.7.2":
+ version "3.7.2"
+ resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.7.2.tgz#de553ca38c64ae76b3ee3a3aa12ea20311030adb"
+ integrity sha512-YED+iWX1FxMpn4UJ0Yo37/K0Py/xNYoqcFSlgEcXNorNllRHpLXGXKZ3ILAQVRa0R1oYXpmsthx4bjg2JSptiA==
+ dependencies:
+ "@standardnotes/common" "^1.1.0"
+
+"@standardnotes/auth@^3.7.0":
version "3.7.1"
resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.7.1.tgz#d0b1eb63f605e04ecb077fdb5ef83e3fe6db33f9"
integrity sha512-xtjAvtikLW3Xv75X/kYA1KTm8FJVPPlXvl+ofnrf/ijkIaRkbUW/3TUhMES+G5CMiG2TZv6uVn32GqJipqgQQQ==
@@ -2030,6 +2037,13 @@
dependencies:
"@standardnotes/auth" "^3.7.0"
+"@standardnotes/features@1.6.1":
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.6.1.tgz#bfa227bd231dc1b54449936663731f5132b08e23"
+ integrity sha512-IC6fEotUqs23JdZx96JnEgARxwYzjmPz3UwU/uVn8hHjxPev/W0nyZFRiSlj4v+dod0jSa6FTR8iLLsOQ6M4Ug==
+ dependencies:
+ "@standardnotes/common" "^1.1.0"
+
"@standardnotes/features@1.6.2":
version "1.6.2"
resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.6.2.tgz#98c5998426d9f93e06c2846c5bc7b6aef8d31063"
@@ -2055,12 +2069,12 @@
"@standardnotes/sncrypto-common" "^1.5.2"
libsodium-wrappers "^0.7.8"
-"@standardnotes/snjs@2.13.0":
- version "2.13.0"
- resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.13.0.tgz#fd80dfafab839916aa8055c60909695e01752561"
- integrity sha512-tm7KRFNmY0aMU0eMhBL4Rx6Q1PU9Z0JdqZv8+fYaZWXejlw/36IlW0tmUxgLonINDxweMCGqDrOX98D+Njav9Q==
+"@standardnotes/snjs@2.14.3":
+ version "2.14.3"
+ resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.14.3.tgz#9d39508d5144db87359b3893ef05da8b0b793053"
+ integrity sha512-pX0WoWgY11ZMLdePvRNjnNoYVpTjWn4MLVnZGdSXp+Qz5rra/Y/ZBXFusrHG4+KqAqJ0AVhD6y5AwxtxuJwISQ==
dependencies:
- "@standardnotes/auth" "3.7.1"
+ "@standardnotes/auth" "3.7.2"
"@standardnotes/common" "1.1.0"
"@standardnotes/domain-events" "2.1.0"
"@standardnotes/features" "1.6.2"