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 {
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>
)
);

View File

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

View File

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

View File

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

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

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