Merge pull request #629 from standardnotes/feature/subscription-info-in-preferences
feat: subscription info in preferences
This commit is contained in:
@@ -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 { PreferencesPane } from '@/preferences/components';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
@@ -11,6 +11,7 @@ export const AccountPreferences = observer(({application}: Props) => {
|
|||||||
<PreferencesPane>
|
<PreferencesPane>
|
||||||
<Credentials application={application} />
|
<Credentials application={application} />
|
||||||
<Sync application={application} />
|
<Sync application={application} />
|
||||||
|
<SubscriptionWrapper application={application} />
|
||||||
</PreferencesPane>
|
</PreferencesPane>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
|
export { SubscriptionWrapper } from './subscription/SubscriptionWrapper';
|
||||||
export { Sync } from './Sync';
|
export { Sync } from './Sync';
|
||||||
export { Credentials } from './Credentials';
|
export { Credentials } from './Credentials';
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { FunctionalComponent } from "preact";
|
||||||
|
import { Text } from '@/preferences/components';
|
||||||
|
import { Button } from '@/components/Button';
|
||||||
|
|
||||||
|
export const NoSubscription: FunctionalComponent = () => (
|
||||||
|
<>
|
||||||
|
<Text>You don't have a Standard Notes subscription yet.</Text>
|
||||||
|
<div className="flex">
|
||||||
|
<Button
|
||||||
|
className="min-w-20 mt-3 mr-3"
|
||||||
|
type="normal"
|
||||||
|
label="Refresh"
|
||||||
|
onClick={() => null}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="min-w-20 mt-3"
|
||||||
|
type="primary"
|
||||||
|
label="Purchase subscription"
|
||||||
|
onClick={() => null}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
@@ -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 (
|
||||||
|
<PreferencesGroup>
|
||||||
|
<PreferencesSegment>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<div className="flex-grow flex flex-col">
|
||||||
|
<Title>Subscription</Title>
|
||||||
|
{error ? (
|
||||||
|
<Text>No subscription information available.</Text>
|
||||||
|
) : loading ? (
|
||||||
|
<Text>Loading subscription information...</Text>
|
||||||
|
) : userSubscription && userSubscription.endsAt > now ? (
|
||||||
|
<SubscriptionInformation subscriptionState={subscriptionState} />
|
||||||
|
) : (
|
||||||
|
<NoSubscription />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PreferencesSegment>
|
||||||
|
</PreferencesGroup>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -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 ? (
|
||||||
|
<Text>
|
||||||
|
Your{' '}
|
||||||
|
<span className="font-bold">
|
||||||
|
Standard Notes{userSubscriptionName ? ' ' : ''}
|
||||||
|
{userSubscriptionName}
|
||||||
|
</span>{' '}
|
||||||
|
subscription has been{' '}
|
||||||
|
<span className="font-bold">
|
||||||
|
canceled but will remain valid until{' '}
|
||||||
|
{expirationDate}
|
||||||
|
</span>
|
||||||
|
. You may resubscribe below if you wish.
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text>
|
||||||
|
Your{' '}
|
||||||
|
<span className="font-bold">
|
||||||
|
Standard Notes{userSubscriptionName ? ' ' : ''}
|
||||||
|
{userSubscriptionName}
|
||||||
|
</span>{' '}
|
||||||
|
subscription will be{' '}
|
||||||
|
<span className="font-bold">
|
||||||
|
renewed on {expirationDate}
|
||||||
|
</span>
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const PrimaryButton = observer(({ subscriptionState }: Props) => {
|
||||||
|
const { userSubscription } = subscriptionState;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className="min-w-20 mt-3"
|
||||||
|
type="primary"
|
||||||
|
label={userSubscription!.cancelled ? "Renew subscription" : "Cancel subscription"}
|
||||||
|
onClick={() => null}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SubscriptionInformation = observer(
|
||||||
|
({ subscriptionState }: Props) => (
|
||||||
|
<>
|
||||||
|
<StatusText
|
||||||
|
subscriptionState={subscriptionState}
|
||||||
|
/>
|
||||||
|
<div className="flex">
|
||||||
|
<Button
|
||||||
|
className="min-w-20 mt-3 mr-3"
|
||||||
|
type="normal"
|
||||||
|
label="Refresh"
|
||||||
|
onClick={() => null}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="min-w-20 mt-3 mr-3"
|
||||||
|
type="normal"
|
||||||
|
label="Change plan"
|
||||||
|
onClick={() => null}
|
||||||
|
/>
|
||||||
|
<PrimaryButton
|
||||||
|
subscriptionState={subscriptionState}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -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<Props> = ({
|
||||||
|
application,
|
||||||
|
}) => {
|
||||||
|
const [subscriptionState] = useState(() => new SubscriptionState());
|
||||||
|
return (
|
||||||
|
<Subscription
|
||||||
|
application={application}
|
||||||
|
subscriptionState={subscriptionState}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import { NativeExtManager } from '@/services/nativeExtManager';
|
|||||||
import { StatusManager } from '@/services/statusManager';
|
import { StatusManager } from '@/services/statusManager';
|
||||||
import { ThemeManager } from '@/services/themeManager';
|
import { ThemeManager } from '@/services/themeManager';
|
||||||
import { AppVersion } from '@/version';
|
import { AppVersion } from '@/version';
|
||||||
|
import { isDev } from '@/utils';
|
||||||
|
|
||||||
type WebServices = {
|
type WebServices = {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
@@ -64,7 +65,8 @@ export class WebApplication extends SNApplication {
|
|||||||
identifier,
|
identifier,
|
||||||
[],
|
[],
|
||||||
defaultSyncServerHost,
|
defaultSyncServerHost,
|
||||||
AppVersion
|
AppVersion,
|
||||||
|
isDev,
|
||||||
);
|
);
|
||||||
this.$compile = $compile;
|
this.$compile = $compile;
|
||||||
this.scope = scope;
|
this.scope = scope;
|
||||||
|
|||||||
@@ -149,3 +149,7 @@
|
|||||||
.sn-title {
|
.sn-title {
|
||||||
@extend .font-bold;
|
@extend .font-bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mr-3 {
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -71,7 +71,8 @@
|
|||||||
"@reach/checkbox": "^0.13.2",
|
"@reach/checkbox": "^0.13.2",
|
||||||
"@reach/dialog": "^0.13.0",
|
"@reach/dialog": "^0.13.0",
|
||||||
"@standardnotes/sncrypto-web": "1.5.2",
|
"@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": "^6.3.2",
|
||||||
"mobx-react-lite": "^3.2.0",
|
"mobx-react-lite": "^3.2.0",
|
||||||
"preact": "^10.5.12",
|
"preact": "^10.5.12",
|
||||||
|
|||||||
26
yarn.lock
26
yarn.lock
@@ -2011,7 +2011,14 @@
|
|||||||
prop-types "^15.7.2"
|
prop-types "^15.7.2"
|
||||||
tslib "^2.1.0"
|
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"
|
version "3.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.7.1.tgz#d0b1eb63f605e04ecb077fdb5ef83e3fe6db33f9"
|
resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.7.1.tgz#d0b1eb63f605e04ecb077fdb5ef83e3fe6db33f9"
|
||||||
integrity sha512-xtjAvtikLW3Xv75X/kYA1KTm8FJVPPlXvl+ofnrf/ijkIaRkbUW/3TUhMES+G5CMiG2TZv6uVn32GqJipqgQQQ==
|
integrity sha512-xtjAvtikLW3Xv75X/kYA1KTm8FJVPPlXvl+ofnrf/ijkIaRkbUW/3TUhMES+G5CMiG2TZv6uVn32GqJipqgQQQ==
|
||||||
@@ -2030,6 +2037,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/auth" "^3.7.0"
|
"@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":
|
"@standardnotes/features@1.6.2":
|
||||||
version "1.6.2"
|
version "1.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.6.2.tgz#98c5998426d9f93e06c2846c5bc7b6aef8d31063"
|
resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.6.2.tgz#98c5998426d9f93e06c2846c5bc7b6aef8d31063"
|
||||||
@@ -2055,12 +2069,12 @@
|
|||||||
"@standardnotes/sncrypto-common" "^1.5.2"
|
"@standardnotes/sncrypto-common" "^1.5.2"
|
||||||
libsodium-wrappers "^0.7.8"
|
libsodium-wrappers "^0.7.8"
|
||||||
|
|
||||||
"@standardnotes/snjs@2.13.0":
|
"@standardnotes/snjs@2.14.3":
|
||||||
version "2.13.0"
|
version "2.14.3"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.13.0.tgz#fd80dfafab839916aa8055c60909695e01752561"
|
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.14.3.tgz#9d39508d5144db87359b3893ef05da8b0b793053"
|
||||||
integrity sha512-tm7KRFNmY0aMU0eMhBL4Rx6Q1PU9Z0JdqZv8+fYaZWXejlw/36IlW0tmUxgLonINDxweMCGqDrOX98D+Njav9Q==
|
integrity sha512-pX0WoWgY11ZMLdePvRNjnNoYVpTjWn4MLVnZGdSXp+Qz5rra/Y/ZBXFusrHG4+KqAqJ0AVhD6y5AwxtxuJwISQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/auth" "3.7.1"
|
"@standardnotes/auth" "3.7.2"
|
||||||
"@standardnotes/common" "1.1.0"
|
"@standardnotes/common" "1.1.0"
|
||||||
"@standardnotes/domain-events" "2.1.0"
|
"@standardnotes/domain-events" "2.1.0"
|
||||||
"@standardnotes/features" "1.6.2"
|
"@standardnotes/features" "1.6.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user