diff --git a/app/assets/javascripts/preferences/panes/AccountPreferences.tsx b/app/assets/javascripts/preferences/panes/AccountPreferences.tsx index 732809ab4..0cdaddfbd 100644 --- a/app/assets/javascripts/preferences/panes/AccountPreferences.tsx +++ b/app/assets/javascripts/preferences/panes/AccountPreferences.tsx @@ -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 ( - - - - - - ); - } - - return ( - - - - - - - ); - } + ({ application, appState }: Props) => ( + + {!application.hasAccount() ? ( + + ) : ( + <> + + + + )} + + + + ) ); diff --git a/app/assets/javascripts/preferences/panes/account/index.ts b/app/assets/javascripts/preferences/panes/account/index.ts index 85800edb5..bc74bd926 100644 --- a/app/assets/javascripts/preferences/panes/account/index.ts +++ b/app/assets/javascripts/preferences/panes/account/index.ts @@ -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'; diff --git a/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx b/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx index c1c2476ac..b86aa4c7f 100644 --- a/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx +++ b/app/assets/javascripts/preferences/panes/account/subscription/Subscription.tsx @@ -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 = observer(({ - application, - subscriptionState, -}: Props) => { - const [loading, setLoading] = useState(false); - const [error, setError] = useState(false); +export const Subscription: FunctionComponent = 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 ( - - -
-
- Subscription - {error ? ( - No subscription information available. - ) : loading ? ( - Loading subscription information... - ) : userSubscription && userSubscription.endsAt > now ? ( - - ) : ( - - )} + return ( + + +
+
+ Subscription + {userSubscription && userSubscription.endsAt > now ? ( + + ) : ( + + )} +
-
- - - ); -}); + + + ); + } +); diff --git a/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionInformation.tsx b/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionInformation.tsx index be1485854..2c36d2b06 100644 --- a/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionInformation.tsx +++ b/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionInformation.tsx @@ -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 ( Your{' '} @@ -28,9 +27,8 @@ const StatusText = observer(({ subscriptionState }: Props) => { Standard Notes{userSubscriptionName ? ' ' : ''} {userSubscriptionName} {' '} - subscription has been canceled - {' '} - {expired ? ( + subscription has been canceled{' '} + {isUserSubscriptionExpired ? ( and expired on {expirationDateString} @@ -44,7 +42,7 @@ const StatusText = observer(({ subscriptionState }: Props) => { ); } - if (expired) { + if (isUserSubscriptionExpired) { return ( Your{' '} @@ -52,11 +50,9 @@ const StatusText = observer(({ subscriptionState }: Props) => { Standard Notes{userSubscriptionName ? ' ' : ''} {userSubscriptionName} {' '} - subscription {' '} - - expired on {expirationDateString} - - . You may resubscribe below if you wish. + subscription{' '} + expired on {expirationDateString}. + You may resubscribe below if you wish. ); } diff --git a/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionWrapper.tsx b/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionWrapper.tsx deleted file mode 100644 index 9a19112e6..000000000 --- a/app/assets/javascripts/preferences/panes/account/subscription/SubscriptionWrapper.tsx +++ /dev/null @@ -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 = ({ - application, -}) => { - const [subscriptionState] = useState(() => new SubscriptionState()); - return ( - - ); -}; diff --git a/app/assets/javascripts/preferences/panes/account/subscription/subscription_state.tsx b/app/assets/javascripts/preferences/panes/account/subscription/subscription_state.tsx deleted file mode 100644 index 38f6768a0..000000000 --- a/app/assets/javascripts/preferences/panes/account/subscription/subscription_state.tsx +++ /dev/null @@ -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; - } -} diff --git a/app/assets/javascripts/ui_models/app_state/app_state.ts b/app/assets/javascripts/ui_models/app_state/app_state.ts index 8c2665fbf..f406fec30 100644 --- a/app/assets/javascripts/ui_models/app_state/app_state.ts +++ b/app/assets/javascripts/ui_models/app_state/app_state.ts @@ -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, diff --git a/app/assets/javascripts/ui_models/app_state/subscription_state.ts b/app/assets/javascripts/ui_models/app_state/subscription_state.ts new file mode 100644 index 000000000..ff510c779 --- /dev/null +++ b/app/assets/javascripts/ui_models/app_state/subscription_state.ts @@ -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(); + } +}