feat: move SubscriptionState to central AppState (#869)
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
Sync,
|
Sync,
|
||||||
SubscriptionWrapper,
|
Subscription,
|
||||||
Credentials,
|
Credentials,
|
||||||
SignOutWrapper,
|
SignOutWrapper,
|
||||||
Authentication
|
Authentication,
|
||||||
} from '@/preferences/panes/account';
|
} 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';
|
||||||
@@ -16,25 +16,18 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AccountPreferences = observer(
|
export const AccountPreferences = observer(
|
||||||
({ application, appState }: Props) => {
|
({ application, appState }: Props) => (
|
||||||
|
<PreferencesPane>
|
||||||
if (!application.hasAccount()) {
|
{!application.hasAccount() ? (
|
||||||
return (
|
<Authentication application={application} appState={appState} />
|
||||||
<PreferencesPane>
|
) : (
|
||||||
<Authentication application={application} appState={appState} />
|
<>
|
||||||
<SubscriptionWrapper application={application} />
|
<Credentials application={application} appState={appState} />
|
||||||
<SignOutWrapper application={application} appState={appState} />
|
<Sync application={application} />
|
||||||
</PreferencesPane>
|
</>
|
||||||
);
|
)}
|
||||||
}
|
<Subscription application={application} appState={appState} />
|
||||||
|
<SignOutWrapper application={application} appState={appState} />
|
||||||
return (
|
</PreferencesPane>
|
||||||
<PreferencesPane>
|
)
|
||||||
<Credentials application={application} appState={appState} />
|
|
||||||
<Sync application={application} />
|
|
||||||
<SubscriptionWrapper application={application} />
|
|
||||||
<SignOutWrapper application={application} appState={appState} />
|
|
||||||
</PreferencesPane>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export { SubscriptionWrapper } from './subscription/SubscriptionWrapper';
|
export { Subscription } from './subscription/Subscription';
|
||||||
export { Sync } from './Sync';
|
export { Sync } from './Sync';
|
||||||
export { Credentials } from './Credentials';
|
export { Credentials } from './Credentials';
|
||||||
export { SignOutWrapper } from './SignOutView';
|
export { SignOutWrapper } from './SignOutView';
|
||||||
|
|||||||
@@ -4,101 +4,42 @@ import {
|
|||||||
Title,
|
Title,
|
||||||
} from '@/preferences/components';
|
} from '@/preferences/components';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { useCallback, useEffect, useState } from 'preact/hooks';
|
|
||||||
import { SubscriptionState } from './subscription_state';
|
|
||||||
import { SubscriptionInformation } from './SubscriptionInformation';
|
import { SubscriptionInformation } from './SubscriptionInformation';
|
||||||
import { NoSubscription } from './NoSubscription';
|
import { NoSubscription } from './NoSubscription';
|
||||||
import { Text } from '@/preferences/components';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { ApplicationEvent } from '@standardnotes/snjs';
|
import { AppState } from '@/ui_models/app_state';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
subscriptionState: SubscriptionState;
|
appState: AppState;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Subscription: FunctionComponent<Props> = observer(({
|
export const Subscription: FunctionComponent<Props> = observer(
|
||||||
application,
|
({ application, appState }: Props) => {
|
||||||
subscriptionState,
|
const subscriptionState = appState.subscription;
|
||||||
}: Props) => {
|
const { userSubscription } = subscriptionState;
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState(false);
|
|
||||||
|
|
||||||
const { userSubscription } = subscriptionState;
|
const now = new Date().getTime();
|
||||||
|
|
||||||
const getSubscriptions = useCallback(async () => {
|
return (
|
||||||
try {
|
<PreferencesGroup>
|
||||||
const subscriptions = await application.getAvailableSubscriptions();
|
<PreferencesSegment>
|
||||||
if (subscriptions) {
|
<div className="flex flex-row items-center">
|
||||||
subscriptionState.setAvailableSubscriptions(subscriptions);
|
<div className="flex-grow flex flex-col">
|
||||||
}
|
<Title>Subscription</Title>
|
||||||
} catch (e) {
|
{userSubscription && userSubscription.endsAt > now ? (
|
||||||
// Error in this call will only prevent the plan name from showing
|
<SubscriptionInformation
|
||||||
}
|
subscriptionState={subscriptionState}
|
||||||
}, [application, subscriptionState]);
|
application={application}
|
||||||
|
/>
|
||||||
const getSubscription = useCallback(async () => {
|
) : (
|
||||||
try {
|
<NoSubscription application={application} />
|
||||||
const subscription = await application.getUserSubscription();
|
)}
|
||||||
if (subscription) {
|
</div>
|
||||||
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(() => {
|
|
||||||
const removeUserRoleObserver = application.addEventObserver(
|
|
||||||
async () => {
|
|
||||||
await getSubscription();
|
|
||||||
await getSubscriptions();
|
|
||||||
},
|
|
||||||
ApplicationEvent.UserRolesChanged
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
removeUserRoleObserver();
|
|
||||||
};
|
|
||||||
}, [application, getSubscription, getSubscriptions]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (application.hasAccount()) {
|
|
||||||
getSubscriptionInfo();
|
|
||||||
}
|
|
||||||
}, [application, 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} application={application} />
|
|
||||||
) : (
|
|
||||||
<NoSubscription application={application} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PreferencesSegment>
|
||||||
</PreferencesSegment>
|
</PreferencesGroup>
|
||||||
</PreferencesGroup>
|
);
|
||||||
);
|
}
|
||||||
});
|
);
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { SubscriptionState } from './subscription_state';
|
import { SubscriptionState } from '../../../../ui_models/app_state/subscription_state';
|
||||||
import { Text } from '@/preferences/components';
|
import { Text } from '@/preferences/components';
|
||||||
import { Button } from '@/components/Button';
|
import { Button } from '@/components/Button';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { convertTimestampToMilliseconds } from '@standardnotes/snjs';
|
|
||||||
import { openSubscriptionDashboard } from '@/hooks/manageSubscription';
|
import { openSubscriptionDashboard } from '@/hooks/manageSubscription';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -12,15 +11,15 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const StatusText = observer(({ subscriptionState }: Props) => {
|
const StatusText = observer(({ subscriptionState }: Props) => {
|
||||||
const { userSubscription, userSubscriptionName } = subscriptionState;
|
const {
|
||||||
const expirationDate = new Date(
|
userSubscriptionName,
|
||||||
convertTimestampToMilliseconds(userSubscription!.endsAt)
|
userSubscriptionExpirationDate,
|
||||||
);
|
isUserSubscriptionExpired,
|
||||||
const expirationDateString = expirationDate.toLocaleString();
|
isUserSubscriptionCanceled,
|
||||||
const expired = expirationDate.getTime() < new Date().getTime();
|
} = subscriptionState;
|
||||||
const canceled = userSubscription!.cancelled;
|
const expirationDateString = userSubscriptionExpirationDate?.toLocaleString();
|
||||||
|
|
||||||
if (canceled) {
|
if (isUserSubscriptionCanceled) {
|
||||||
return (
|
return (
|
||||||
<Text className="mt-1">
|
<Text className="mt-1">
|
||||||
Your{' '}
|
Your{' '}
|
||||||
@@ -28,9 +27,8 @@ const StatusText = observer(({ subscriptionState }: Props) => {
|
|||||||
Standard Notes{userSubscriptionName ? ' ' : ''}
|
Standard Notes{userSubscriptionName ? ' ' : ''}
|
||||||
{userSubscriptionName}
|
{userSubscriptionName}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
subscription has been canceled
|
subscription has been canceled{' '}
|
||||||
{' '}
|
{isUserSubscriptionExpired ? (
|
||||||
{expired ? (
|
|
||||||
<span className="font-bold">
|
<span className="font-bold">
|
||||||
and expired on {expirationDateString}
|
and expired on {expirationDateString}
|
||||||
</span>
|
</span>
|
||||||
@@ -44,7 +42,7 @@ const StatusText = observer(({ subscriptionState }: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expired) {
|
if (isUserSubscriptionExpired) {
|
||||||
return (
|
return (
|
||||||
<Text className="mt-1">
|
<Text className="mt-1">
|
||||||
Your{' '}
|
Your{' '}
|
||||||
@@ -52,11 +50,9 @@ const StatusText = observer(({ subscriptionState }: Props) => {
|
|||||||
Standard Notes{userSubscriptionName ? ' ' : ''}
|
Standard Notes{userSubscriptionName ? ' ' : ''}
|
||||||
{userSubscriptionName}
|
{userSubscriptionName}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
subscription {' '}
|
subscription{' '}
|
||||||
<span className="font-bold">
|
<span className="font-bold">expired on {expirationDateString}</span>.
|
||||||
expired on {expirationDateString}
|
You may resubscribe below if you wish.
|
||||||
</span>
|
|
||||||
. You may resubscribe below if you wish.
|
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -33,6 +33,7 @@ import { PreferencesState } from './preferences_state';
|
|||||||
import { PurchaseFlowState } from './purchase_flow_state';
|
import { PurchaseFlowState } from './purchase_flow_state';
|
||||||
import { QuickSettingsState } from './quick_settings_state';
|
import { QuickSettingsState } from './quick_settings_state';
|
||||||
import { SearchOptionsState } from './search_options_state';
|
import { SearchOptionsState } from './search_options_state';
|
||||||
|
import { SubscriptionState } from './subscription_state';
|
||||||
import { SyncState } from './sync_state';
|
import { SyncState } from './sync_state';
|
||||||
import { TagsState } from './tags_state';
|
import { TagsState } from './tags_state';
|
||||||
|
|
||||||
@@ -86,6 +87,7 @@ export class AppState {
|
|||||||
readonly features: FeaturesState;
|
readonly features: FeaturesState;
|
||||||
readonly tags: TagsState;
|
readonly tags: TagsState;
|
||||||
readonly notesView: NotesViewState;
|
readonly notesView: NotesViewState;
|
||||||
|
readonly subscription: SubscriptionState;
|
||||||
|
|
||||||
isSessionsModalVisible = false;
|
isSessionsModalVisible = false;
|
||||||
|
|
||||||
@@ -126,6 +128,10 @@ export class AppState {
|
|||||||
application,
|
application,
|
||||||
this.appEventObserverRemovers
|
this.appEventObserverRemovers
|
||||||
);
|
);
|
||||||
|
this.subscription = new SubscriptionState(
|
||||||
|
application,
|
||||||
|
this.appEventObserverRemovers
|
||||||
|
);
|
||||||
this.purchaseFlow = new PurchaseFlowState(application);
|
this.purchaseFlow = new PurchaseFlowState(application);
|
||||||
this.notesView = new NotesViewState(
|
this.notesView = new NotesViewState(
|
||||||
application,
|
application,
|
||||||
|
|||||||
125
app/assets/javascripts/ui_models/app_state/subscription_state.ts
Normal file
125
app/assets/javascripts/ui_models/app_state/subscription_state.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import {
|
||||||
|
ApplicationEvent,
|
||||||
|
convertTimestampToMilliseconds,
|
||||||
|
} from '@standardnotes/snjs';
|
||||||
|
import { action, computed, makeObservable, observable } from 'mobx';
|
||||||
|
import { WebApplication } from '../application';
|
||||||
|
|
||||||
|
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(
|
||||||
|
private application: WebApplication,
|
||||||
|
appObservers: (() => void)[]
|
||||||
|
) {
|
||||||
|
makeObservable(this, {
|
||||||
|
userSubscription: observable,
|
||||||
|
availableSubscriptions: observable,
|
||||||
|
|
||||||
|
userSubscriptionName: computed,
|
||||||
|
userSubscriptionExpirationDate: computed,
|
||||||
|
isUserSubscriptionExpired: computed,
|
||||||
|
isUserSubscriptionCanceled: computed,
|
||||||
|
|
||||||
|
setUserSubscription: action,
|
||||||
|
setAvailableSubscriptions: action,
|
||||||
|
});
|
||||||
|
|
||||||
|
appObservers.push(
|
||||||
|
application.addEventObserver(async () => {
|
||||||
|
if (application.hasAccount()) {
|
||||||
|
this.getSubscriptionInfo();
|
||||||
|
}
|
||||||
|
}, ApplicationEvent.Launched),
|
||||||
|
application.addEventObserver(async () => {
|
||||||
|
this.getSubscriptionInfo();
|
||||||
|
}, ApplicationEvent.SignedIn),
|
||||||
|
application.addEventObserver(async () => {
|
||||||
|
this.getSubscriptionInfo();
|
||||||
|
}, ApplicationEvent.UserRolesChanged)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get userSubscriptionName(): string {
|
||||||
|
if (
|
||||||
|
this.availableSubscriptions &&
|
||||||
|
this.userSubscription &&
|
||||||
|
this.availableSubscriptions[this.userSubscription.planName]
|
||||||
|
) {
|
||||||
|
return this.availableSubscriptions[this.userSubscription.planName].name;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
get userSubscriptionExpirationDate(): Date | undefined {
|
||||||
|
if (!this.userSubscription) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(
|
||||||
|
convertTimestampToMilliseconds(this.userSubscription.endsAt)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isUserSubscriptionExpired(): boolean {
|
||||||
|
if (!this.userSubscriptionExpirationDate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.userSubscriptionExpirationDate.getTime() < new Date().getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
get isUserSubscriptionCanceled(): boolean {
|
||||||
|
return Boolean(this.userSubscription?.cancelled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setUserSubscription(subscription: Subscription): void {
|
||||||
|
this.userSubscription = subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setAvailableSubscriptions(
|
||||||
|
subscriptions: AvailableSubscriptions
|
||||||
|
): void {
|
||||||
|
this.availableSubscriptions = subscriptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAvailableSubscriptions() {
|
||||||
|
try {
|
||||||
|
const subscriptions = await this.application.getAvailableSubscriptions();
|
||||||
|
if (subscriptions) {
|
||||||
|
this.setAvailableSubscriptions(subscriptions);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getSubscription() {
|
||||||
|
try {
|
||||||
|
const subscription = await this.application.getUserSubscription();
|
||||||
|
if (subscription) {
|
||||||
|
this.setUserSubscription(subscription);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getSubscriptionInfo() {
|
||||||
|
await this.getSubscription();
|
||||||
|
await this.getAvailableSubscriptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user