feat: move SubscriptionState to central AppState (#869)
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
Sync,
|
||||
SubscriptionWrapper,
|
||||
Subscription,
|
||||
Credentials,
|
||||
SignOutWrapper,
|
||||
Authentication
|
||||
Authentication,
|
||||
} from '@/preferences/panes/account';
|
||||
import { PreferencesPane } from '@/preferences/components';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
@@ -16,25 +16,18 @@ type Props = {
|
||||
};
|
||||
|
||||
export const AccountPreferences = observer(
|
||||
({ application, appState }: Props) => {
|
||||
|
||||
if (!application.hasAccount()) {
|
||||
return (
|
||||
<PreferencesPane>
|
||||
<Authentication application={application} appState={appState} />
|
||||
<SubscriptionWrapper application={application} />
|
||||
<SignOutWrapper application={application} appState={appState} />
|
||||
</PreferencesPane>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PreferencesPane>
|
||||
<Credentials application={application} appState={appState} />
|
||||
<Sync application={application} />
|
||||
<SubscriptionWrapper application={application} />
|
||||
<SignOutWrapper application={application} appState={appState} />
|
||||
</PreferencesPane>
|
||||
);
|
||||
}
|
||||
({ application, appState }: Props) => (
|
||||
<PreferencesPane>
|
||||
{!application.hasAccount() ? (
|
||||
<Authentication application={application} appState={appState} />
|
||||
) : (
|
||||
<>
|
||||
<Credentials application={application} appState={appState} />
|
||||
<Sync application={application} />
|
||||
</>
|
||||
)}
|
||||
<Subscription application={application} appState={appState} />
|
||||
<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 { Credentials } from './Credentials';
|
||||
export { SignOutWrapper } from './SignOutView';
|
||||
|
||||
@@ -4,101 +4,42 @@ import {
|
||||
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';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { ApplicationEvent } from '@standardnotes/snjs';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
subscriptionState: SubscriptionState;
|
||||
appState: AppState;
|
||||
};
|
||||
|
||||
export const Subscription: FunctionComponent<Props> = observer(({
|
||||
application,
|
||||
subscriptionState,
|
||||
}: Props) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
export const Subscription: FunctionComponent<Props> = observer(
|
||||
({ application, appState }: Props) => {
|
||||
const subscriptionState = appState.subscription;
|
||||
const { userSubscription } = subscriptionState;
|
||||
|
||||
const { userSubscription } = subscriptionState;
|
||||
const now = new Date().getTime();
|
||||
|
||||
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(() => {
|
||||
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} />
|
||||
)}
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex-grow flex flex-col">
|
||||
<Title>Subscription</Title>
|
||||
{userSubscription && userSubscription.endsAt > now ? (
|
||||
<SubscriptionInformation
|
||||
subscriptionState={subscriptionState}
|
||||
application={application}
|
||||
/>
|
||||
) : (
|
||||
<NoSubscription application={application} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
});
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
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 { Button } from '@/components/Button';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { convertTimestampToMilliseconds } from '@standardnotes/snjs';
|
||||
import { openSubscriptionDashboard } from '@/hooks/manageSubscription';
|
||||
|
||||
type Props = {
|
||||
@@ -12,15 +11,15 @@ type Props = {
|
||||
};
|
||||
|
||||
const StatusText = observer(({ subscriptionState }: Props) => {
|
||||
const { userSubscription, userSubscriptionName } = subscriptionState;
|
||||
const expirationDate = new Date(
|
||||
convertTimestampToMilliseconds(userSubscription!.endsAt)
|
||||
);
|
||||
const expirationDateString = expirationDate.toLocaleString();
|
||||
const expired = expirationDate.getTime() < new Date().getTime();
|
||||
const canceled = userSubscription!.cancelled;
|
||||
const {
|
||||
userSubscriptionName,
|
||||
userSubscriptionExpirationDate,
|
||||
isUserSubscriptionExpired,
|
||||
isUserSubscriptionCanceled,
|
||||
} = subscriptionState;
|
||||
const expirationDateString = userSubscriptionExpirationDate?.toLocaleString();
|
||||
|
||||
if (canceled) {
|
||||
if (isUserSubscriptionCanceled) {
|
||||
return (
|
||||
<Text className="mt-1">
|
||||
Your{' '}
|
||||
@@ -28,9 +27,8 @@ const StatusText = observer(({ subscriptionState }: Props) => {
|
||||
Standard Notes{userSubscriptionName ? ' ' : ''}
|
||||
{userSubscriptionName}
|
||||
</span>{' '}
|
||||
subscription has been canceled
|
||||
{' '}
|
||||
{expired ? (
|
||||
subscription has been canceled{' '}
|
||||
{isUserSubscriptionExpired ? (
|
||||
<span className="font-bold">
|
||||
and expired on {expirationDateString}
|
||||
</span>
|
||||
@@ -44,7 +42,7 @@ const StatusText = observer(({ subscriptionState }: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (expired) {
|
||||
if (isUserSubscriptionExpired) {
|
||||
return (
|
||||
<Text className="mt-1">
|
||||
Your{' '}
|
||||
@@ -52,11 +50,9 @@ const StatusText = observer(({ subscriptionState }: Props) => {
|
||||
Standard Notes{userSubscriptionName ? ' ' : ''}
|
||||
{userSubscriptionName}
|
||||
</span>{' '}
|
||||
subscription {' '}
|
||||
<span className="font-bold">
|
||||
expired on {expirationDateString}
|
||||
</span>
|
||||
. You may resubscribe below if you wish.
|
||||
subscription{' '}
|
||||
<span className="font-bold">expired on {expirationDateString}</span>.
|
||||
You may resubscribe below if you wish.
|
||||
</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 { QuickSettingsState } from './quick_settings_state';
|
||||
import { SearchOptionsState } from './search_options_state';
|
||||
import { SubscriptionState } from './subscription_state';
|
||||
import { SyncState } from './sync_state';
|
||||
import { TagsState } from './tags_state';
|
||||
|
||||
@@ -86,6 +87,7 @@ export class AppState {
|
||||
readonly features: FeaturesState;
|
||||
readonly tags: TagsState;
|
||||
readonly notesView: NotesViewState;
|
||||
readonly subscription: SubscriptionState;
|
||||
|
||||
isSessionsModalVisible = false;
|
||||
|
||||
@@ -126,6 +128,10 @@ export class AppState {
|
||||
application,
|
||||
this.appEventObserverRemovers
|
||||
);
|
||||
this.subscription = new SubscriptionState(
|
||||
application,
|
||||
this.appEventObserverRemovers
|
||||
);
|
||||
this.purchaseFlow = new PurchaseFlowState(application);
|
||||
this.notesView = new NotesViewState(
|
||||
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