feat: move SubscriptionState to central AppState (#869)

This commit is contained in:
Aman Harwara
2022-02-15 21:00:05 +05:30
committed by GitHub
parent 00d57aa69d
commit dab8080da6
8 changed files with 190 additions and 201 deletions

View File

@@ -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>
);
}
); );

View File

@@ -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';

View File

@@ -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> );
); }
}); );

View File

@@ -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>
); );
} }

View File

@@ -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}
/>
);
};

View File

@@ -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;
}
}

View File

@@ -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,

View 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();
}
}