refactor: repo (#1070)

This commit is contained in:
Mo
2022-06-07 07:18:41 -05:00
committed by GitHub
parent 4c65784421
commit f4ef63693c
1102 changed files with 5786 additions and 3366 deletions

View File

@@ -0,0 +1,35 @@
import { observer } from 'mobx-react-lite'
import { WebApplication } from '@/Application/Application'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import Authentication from './Authentication'
import Credentials from './Credentials'
import Sync from './Sync'
import Subscription from './Subscription/Subscription'
import SignOutWrapper from './SignOutView'
import FilesSection from './Files'
import PreferencesPane from '../../PreferencesComponents/PreferencesPane'
type Props = {
application: WebApplication
viewControllerManager: ViewControllerManager
}
const AccountPreferences = ({ application, viewControllerManager }: Props) => (
<PreferencesPane>
{!application.hasAccount() ? (
<Authentication application={application} viewControllerManager={viewControllerManager} />
) : (
<>
<Credentials application={application} viewControllerManager={viewControllerManager} />
<Sync application={application} />
</>
)}
<Subscription application={application} viewControllerManager={viewControllerManager} />
{application.hasAccount() && viewControllerManager.featuresController.hasFiles && (
<FilesSection application={application} />
)}
<SignOutWrapper application={application} viewControllerManager={viewControllerManager} />
</PreferencesPane>
)
export default observer(AccountPreferences)

View File

@@ -0,0 +1,39 @@
import { FunctionComponent } from 'react'
import OfflineSubscription from '@/Components/Preferences/Panes/Account/OfflineSubscription'
import { WebApplication } from '@/Application/Application'
import { observer } from 'mobx-react-lite'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import Extensions from '@/Components/Preferences/Panes/Extensions/Extensions'
import { ExtensionsLatestVersions } from '@/Components/Preferences/Panes/Extensions/ExtensionsLatestVersions'
import AccordionItem from '@/Components/Shared/AccordionItem'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
type Props = {
application: WebApplication
viewControllerManager: ViewControllerManager
extensionsLatestVersions: ExtensionsLatestVersions
}
const Advanced: FunctionComponent<Props> = ({ application, viewControllerManager, extensionsLatestVersions }) => {
return (
<PreferencesGroup>
<PreferencesSegment>
<AccordionItem title={'Advanced Settings'}>
<div className="flex flex-row items-center">
<div className="flex-grow flex flex-col">
<OfflineSubscription application={application} viewControllerManager={viewControllerManager} />
<Extensions
className={'mt-3'}
application={application}
extensionsLatestVersions={extensionsLatestVersions}
/>
</div>
</div>
</AccordionItem>
</PreferencesSegment>
</PreferencesGroup>
)
}
export default observer(Advanced)

View File

@@ -0,0 +1,52 @@
import Button from '@/Components/Button/Button'
import { Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
import { WebApplication } from '@/Application/Application'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'react'
import { AccountIllustration } from '@standardnotes/icons'
import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
type Props = {
application: WebApplication
viewControllerManager: ViewControllerManager
}
const Authentication: FunctionComponent<Props> = ({ viewControllerManager }) => {
const clickSignIn = () => {
viewControllerManager.preferencesController.closePreferences()
viewControllerManager.accountMenuController.setCurrentPane(AccountMenuPane.SignIn)
viewControllerManager.accountMenuController.setShow(true)
}
const clickRegister = () => {
viewControllerManager.preferencesController.closePreferences()
viewControllerManager.accountMenuController.setCurrentPane(AccountMenuPane.Register)
viewControllerManager.accountMenuController.setShow(true)
}
return (
<PreferencesGroup>
<PreferencesSegment>
<div className="flex flex-col items-center px-12">
<AccountIllustration className="mb-3" />
<Title>You're not signed in</Title>
<Text className="text-center mb-3">
Sign in to sync your notes and preferences across all your devices and enable end-to-end encryption.
</Text>
<Button variant="primary" label="Create free account" onClick={clickRegister} className="mb-3" />
<div className="text-input">
Already have an account?{' '}
<button className="border-0 p-0 bg-default color-info underline cursor-pointer" onClick={clickSignIn}>
Sign in
</button>
</div>
</div>
</PreferencesSegment>
</PreferencesGroup>
)
}
export default observer(Authentication)

View File

@@ -0,0 +1,159 @@
import ModalDialog from '@/Components/Shared/ModalDialog'
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
import Button from '@/Components/Button/Button'
import { FunctionComponent, useState } from 'react'
import { WebApplication } from '@/Application/Application'
import { useBeforeUnload } from '@/Hooks/useBeforeUnload'
import ChangeEmailForm from './ChangeEmailForm'
import ChangeEmailSuccess from './ChangeEmailSuccess'
import { isEmailValid } from '@/Utils'
enum SubmitButtonTitles {
Default = 'Continue',
GeneratingKeys = 'Generating Keys...',
Finish = 'Finish',
}
enum Steps {
InitialStep,
FinishStep,
}
type Props = {
onCloseDialog: () => void
application: WebApplication
}
const ChangeEmail: FunctionComponent<Props> = ({ onCloseDialog, application }) => {
const [currentPassword, setCurrentPassword] = useState('')
const [newEmail, setNewEmail] = useState('')
const [isContinuing, setIsContinuing] = useState(false)
const [lockContinue, setLockContinue] = useState(false)
const [submitButtonTitle, setSubmitButtonTitle] = useState(SubmitButtonTitles.Default)
const [currentStep, setCurrentStep] = useState(Steps.InitialStep)
useBeforeUnload()
const applicationAlertService = application.alertService
const validateCurrentPassword = async () => {
if (!currentPassword || currentPassword.length === 0) {
applicationAlertService.alert('Please enter your current password.').catch(console.error)
return false
}
const success = await application.validateAccountPassword(currentPassword)
if (!success) {
applicationAlertService
.alert('The current password you entered is not correct. Please try again.')
.catch(console.error)
return false
}
return success
}
const validateNewEmail = async () => {
if (!isEmailValid(newEmail)) {
applicationAlertService
.alert('The email you entered has an invalid format. Please review your input and try again.')
.catch(console.error)
return false
}
return true
}
const resetProgressState = () => {
setSubmitButtonTitle(SubmitButtonTitles.Default)
setIsContinuing(false)
}
const processEmailChange = async () => {
await application.downloadBackup()
setLockContinue(true)
const response = await application.changeEmail(newEmail, currentPassword)
const success = !response.error
setLockContinue(false)
return success
}
const dismiss = () => {
if (lockContinue) {
applicationAlertService.alert('Cannot close window until pending tasks are complete.').catch(console.error)
} else {
onCloseDialog()
}
}
const handleSubmit = async () => {
if (lockContinue || isContinuing) {
return
}
if (currentStep === Steps.FinishStep) {
dismiss()
return
}
setIsContinuing(true)
setSubmitButtonTitle(SubmitButtonTitles.GeneratingKeys)
const valid = (await validateCurrentPassword()) && (await validateNewEmail())
if (!valid) {
resetProgressState()
return
}
const success = await processEmailChange()
if (!success) {
resetProgressState()
return
}
setIsContinuing(false)
setSubmitButtonTitle(SubmitButtonTitles.Finish)
setCurrentStep(Steps.FinishStep)
}
const handleDialogClose = () => {
if (lockContinue) {
applicationAlertService.alert('Cannot close window until pending tasks are complete.').catch(console.error)
} else {
onCloseDialog()
}
}
return (
<div>
<ModalDialog>
<ModalDialogLabel closeDialog={handleDialogClose}>Change Email</ModalDialogLabel>
<ModalDialogDescription className="px-4.5">
{currentStep === Steps.InitialStep && (
<ChangeEmailForm setNewEmail={setNewEmail} setCurrentPassword={setCurrentPassword} />
)}
{currentStep === Steps.FinishStep && <ChangeEmailSuccess />}
</ModalDialogDescription>
<ModalDialogButtons className="px-4.5">
<Button className="min-w-20" variant="primary" label={submitButtonTitle} onClick={handleSubmit} />
</ModalDialogButtons>
</ModalDialog>
</div>
)
}
export default ChangeEmail

View File

@@ -0,0 +1,45 @@
import { Dispatch, SetStateAction, FunctionComponent } from 'react'
type Props = {
setNewEmail: Dispatch<SetStateAction<string>>
setCurrentPassword: Dispatch<SetStateAction<string>>
}
const labelClassName = 'block mb-1'
const inputClassName = 'sk-input contrast'
const ChangeEmailForm: FunctionComponent<Props> = ({ setNewEmail, setCurrentPassword }) => {
return (
<div className="w-full flex flex-col">
<div className="mt-2 mb-3">
<label className={labelClassName} htmlFor="change-email-email-input">
New Email:
</label>
<input
id="change-email-email-input"
className={inputClassName}
type="email"
onChange={({ target }) => {
setNewEmail((target as HTMLInputElement).value)
}}
/>
</div>
<div className="mb-2">
<label className={labelClassName} htmlFor="change-email-password-input">
Current Password:
</label>
<input
id="change-email-password-input"
className={inputClassName}
type="password"
onChange={({ target }) => {
setCurrentPassword((target as HTMLInputElement).value)
}}
/>
</div>
</div>
)
}
export default ChangeEmailForm

View File

@@ -0,0 +1,15 @@
import { FunctionComponent } from 'react'
const ChangeEmailSuccess: FunctionComponent = () => {
return (
<div>
<div className={'sk-label sk-bold info mt-2'}>Your email has been successfully changed.</div>
<p className={'sk-p'}>
Please ensure you are running the latest version of Standard Notes on all platforms to ensure maximum
compatibility.
</p>
</div>
)
}
export default ChangeEmailSuccess

View File

@@ -0,0 +1,30 @@
import Button from '@/Components/Button/Button'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'react'
import { Title, Text } from '../../PreferencesComponents/Content'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
const ClearSessionDataView: FunctionComponent<{
viewControllerManager: ViewControllerManager
}> = ({ viewControllerManager }) => {
return (
<PreferencesGroup>
<PreferencesSegment>
<Title>Clear workspace</Title>
<Text>Remove all data related to the current workspace from the application.</Text>
<div className="min-h-3" />
<Button
dangerStyle={true}
label="Clear workspace"
onClick={() => {
viewControllerManager.accountMenuController.setSigningOut(true)
}}
/>
</PreferencesSegment>
</PreferencesGroup>
)
}
export default observer(ClearSessionDataView)

View File

@@ -0,0 +1,71 @@
import { Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
import Button from '@/Components/Button/Button'
import { WebApplication } from '@/Application/Application'
import { observer } from 'mobx-react-lite'
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
import { dateToLocalizedString } from '@standardnotes/snjs'
import { useCallback, useState, FunctionComponent } from 'react'
import ChangeEmail from '@/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmail'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import PasswordWizard from '@/Components/PasswordWizard/PasswordWizard'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
type Props = {
application: WebApplication
viewControllerManager: ViewControllerManager
}
const Credentials: FunctionComponent<Props> = ({ application }: Props) => {
const [isChangeEmailDialogOpen, setIsChangeEmailDialogOpen] = useState(false)
const [shouldShowPasswordWizard, setShouldShowPasswordWizard] = useState(false)
const user = application.getUser()
const passwordCreatedAtTimestamp = application.getUserPasswordCreationDate() as Date
const passwordCreatedOn = dateToLocalizedString(passwordCreatedAtTimestamp)
const presentPasswordWizard = useCallback(() => {
setShouldShowPasswordWizard(true)
}, [])
const dismissPasswordWizard = useCallback(() => {
setShouldShowPasswordWizard(false)
}, [])
return (
<>
<PreferencesGroup>
<PreferencesSegment>
<Title>Credentials</Title>
<Subtitle>Email</Subtitle>
<Text>
You're signed in as <span className="font-bold wrap">{user?.email}</span>
</Text>
<Button
className="min-w-20 mt-3"
variant="normal"
label="Change email"
onClick={() => {
setIsChangeEmailDialogOpen(true)
}}
/>
<HorizontalSeparator classes="my-4" />
<Subtitle>Password</Subtitle>
<Text>
Current password was set on <span className="font-bold">{passwordCreatedOn}</span>
</Text>
<Button className="min-w-20 mt-3" variant="normal" label="Change password" onClick={presentPasswordWizard} />
{isChangeEmailDialogOpen && (
<ChangeEmail onCloseDialog={() => setIsChangeEmailDialogOpen(false)} application={application} />
)}
</PreferencesSegment>
</PreferencesGroup>
{shouldShowPasswordWizard ? (
<PasswordWizard application={application} dismissModal={dismissPasswordWizard} />
) : null}
</>
)
}
export default observer(Credentials)

View File

@@ -0,0 +1,68 @@
import { WebApplication } from '@/Application/Application'
import { formatSizeToReadableString } from '@standardnotes/filepicker'
import { SubscriptionSettingName } from '@standardnotes/snjs'
import { FunctionComponent, useEffect, useState } from 'react'
import { Subtitle, Title } from '../../PreferencesComponents/Content'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
type Props = {
application: WebApplication
}
const FilesSection: FunctionComponent<Props> = ({ application }) => {
const [isLoading, setIsLoading] = useState(true)
const [filesQuotaUsed, setFilesQuotaUsed] = useState<number>(0)
const [filesQuotaTotal, setFilesQuotaTotal] = useState<number>(0)
useEffect(() => {
const getFilesQuota = async () => {
const filesQuotaUsed = await application.settings.getSubscriptionSetting(
SubscriptionSettingName.FileUploadBytesUsed,
)
const filesQuotaTotal = await application.settings.getSubscriptionSetting(
SubscriptionSettingName.FileUploadBytesLimit,
)
if (filesQuotaUsed) {
setFilesQuotaUsed(parseFloat(filesQuotaUsed))
}
if (filesQuotaTotal) {
setFilesQuotaTotal(parseFloat(filesQuotaTotal))
}
setIsLoading(false)
}
getFilesQuota().catch(console.error)
}, [application])
return (
<PreferencesGroup>
<PreferencesSegment>
<Title>Files</Title>
<Subtitle>Storage Quota</Subtitle>
{isLoading ? (
<div className="mt-2">
<div className="sk-spinner spinner-info w-3 h-3"></div>
</div>
) : (
<>
<div className="mt-1 mb-1">
<span className="font-semibold">{formatSizeToReadableString(filesQuotaUsed)}</span> of{' '}
<span>{formatSizeToReadableString(filesQuotaTotal)}</span> used
</div>
<progress
className="w-full progress-bar"
aria-label="Files storage used"
value={filesQuotaUsed}
max={filesQuotaTotal}
/>
</>
)}
</PreferencesSegment>
</PreferencesGroup>
)
}
export default FilesSection

View File

@@ -0,0 +1,127 @@
import React, { FunctionComponent, useEffect, useState } from 'react'
import { Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
import DecoratedInput from '@/Components/Input/DecoratedInput'
import Button from '@/Components/Button/Button'
import { WebApplication } from '@/Application/Application'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { observer } from 'mobx-react-lite'
import { STRING_REMOVE_OFFLINE_KEY_CONFIRMATION } from '@/Constants/Strings'
import { ButtonType, ClientDisplayableError } from '@standardnotes/snjs'
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
type Props = {
application: WebApplication
viewControllerManager: ViewControllerManager
}
const OfflineSubscription: FunctionComponent<Props> = ({ application }) => {
const [activationCode, setActivationCode] = useState('')
const [isSuccessfullyActivated, setIsSuccessfullyActivated] = useState(false)
const [isSuccessfullyRemoved, setIsSuccessfullyRemoved] = useState(false)
const [hasUserPreviouslyStoredCode, setHasUserPreviouslyStoredCode] = useState(false)
useEffect(() => {
if (application.features.hasOfflineRepo()) {
setHasUserPreviouslyStoredCode(true)
}
}, [application])
const shouldShowOfflineSubscription = () => {
return !application.hasAccount() || application.isThirdPartyHostUsed() || hasUserPreviouslyStoredCode
}
const handleSubscriptionCodeSubmit = async (event: React.FormEvent) => {
event.preventDefault()
const result = await application.features.setOfflineFeaturesCode(activationCode)
if (result instanceof ClientDisplayableError) {
await application.alertService.alert(result.text)
} else {
setIsSuccessfullyActivated(true)
setHasUserPreviouslyStoredCode(true)
setIsSuccessfullyRemoved(false)
}
}
const handleRemoveOfflineKey = async () => {
await application.features.deleteOfflineFeatureRepo()
setIsSuccessfullyActivated(false)
setHasUserPreviouslyStoredCode(false)
setActivationCode('')
setIsSuccessfullyRemoved(true)
}
const handleRemoveClick = async () => {
application.alertService
.confirm(
STRING_REMOVE_OFFLINE_KEY_CONFIRMATION,
'Remove offline key?',
'Remove Offline Key',
ButtonType.Danger,
'Cancel',
)
.then(async (shouldRemove: boolean) => {
if (shouldRemove) {
await handleRemoveOfflineKey()
}
})
.catch((err: string) => {
application.alertService.alert(err).catch(console.error)
})
}
if (!shouldShowOfflineSubscription()) {
return null
}
return (
<>
<div className="flex items-center justify-between">
<div className="flex flex-col mt-3 w-full">
<Subtitle>{!hasUserPreviouslyStoredCode && 'Activate'} Offline Subscription</Subtitle>
<form onSubmit={handleSubscriptionCodeSubmit}>
<div className={'mt-2'}>
{!hasUserPreviouslyStoredCode && (
<DecoratedInput
onChange={(code) => setActivationCode(code)}
placeholder={'Offline Subscription Code'}
value={activationCode}
disabled={isSuccessfullyActivated}
className={'mb-3'}
/>
)}
</div>
{(isSuccessfullyActivated || isSuccessfullyRemoved) && (
<div className={'mt-3 mb-3 info'}>
Your offline subscription code has been successfully {isSuccessfullyActivated ? 'activated' : 'removed'}
.
</div>
)}
{hasUserPreviouslyStoredCode && (
<Button
dangerStyle={true}
label="Remove offline key"
onClick={() => {
handleRemoveClick().catch(console.error)
}}
/>
)}
{!hasUserPreviouslyStoredCode && !isSuccessfullyActivated && (
<Button
label={'Submit'}
variant="primary"
disabled={activationCode === ''}
onClick={(event) => handleSubscriptionCodeSubmit(event)}
/>
)}
</form>
</div>
</div>
<HorizontalSeparator classes="mt-8 mb-5" />
</>
)
}
export default observer(OfflineSubscription)

View File

@@ -0,0 +1,71 @@
import Button from '@/Components/Button/Button'
import OtherSessionsSignOutContainer from '@/Components/OtherSessionsSignOut/OtherSessionsSignOut'
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
import { WebApplication } from '@/Application/Application'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'react'
import { Subtitle, Title, Text } from '../../PreferencesComponents/Content'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
import ClearSessionDataView from './ClearSessionDataView'
type Props = {
application: WebApplication
viewControllerManager: ViewControllerManager
}
const SignOutView: FunctionComponent<Props> = observer(({ application, viewControllerManager }) => {
return (
<>
<PreferencesGroup>
<PreferencesSegment>
<Title>Sign out</Title>
<Subtitle>Other devices</Subtitle>
<Text>Want to sign out on all devices except this one?</Text>
<div className="min-h-3" />
<div className="flex flex-row">
<Button
className="mr-3"
variant="normal"
label="Sign out other sessions"
onClick={() => {
viewControllerManager.accountMenuController.setOtherSessionsSignOut(true)
}}
/>
<Button
variant="normal"
label="Manage sessions"
onClick={() => viewControllerManager.openSessionsModal()}
/>
</div>
</PreferencesSegment>
<HorizontalSeparator classes="my-4" />
<PreferencesSegment>
<Subtitle>This workspace</Subtitle>
<Text>Remove all data related to the current workspace from the application.</Text>
<div className="min-h-3" />
<Button
dangerStyle={true}
label="Sign out workspace"
onClick={() => {
viewControllerManager.accountMenuController.setSigningOut(true)
}}
/>
</PreferencesSegment>
</PreferencesGroup>
<OtherSessionsSignOutContainer viewControllerManager={viewControllerManager} application={application} />
</>
)
})
SignOutView.displayName = 'SignOutView'
const SignOutWrapper: FunctionComponent<Props> = ({ application, viewControllerManager }) => {
if (!application.hasAccount()) {
return <ClearSessionDataView viewControllerManager={viewControllerManager} />
}
return <SignOutView viewControllerManager={viewControllerManager} application={application} />
}
export default observer(SignOutWrapper)

View File

@@ -0,0 +1,44 @@
import { FunctionComponent, useState } from 'react'
import { LinkButton, Text } from '@/Components/Preferences/PreferencesComponents/Content'
import Button from '@/Components/Button/Button'
import { WebApplication } from '@/Application/Application'
import { loadPurchaseFlowUrl } from '@/Components/PurchaseFlow/PurchaseFlowFunctions'
type Props = {
application: WebApplication
}
const NoSubscription: FunctionComponent<Props> = ({ application }) => {
const [isLoadingPurchaseFlow, setIsLoadingPurchaseFlow] = useState(false)
const [purchaseFlowError, setPurchaseFlowError] = useState<string | undefined>(undefined)
const onPurchaseClick = async () => {
const errorMessage = 'There was an error when attempting to redirect you to the subscription page.'
setIsLoadingPurchaseFlow(true)
try {
if (!(await loadPurchaseFlowUrl(application))) {
setPurchaseFlowError(errorMessage)
}
} catch (e) {
setPurchaseFlowError(errorMessage)
} finally {
setIsLoadingPurchaseFlow(false)
}
}
return (
<>
<Text>You don't have a Standard Notes subscription yet.</Text>
{isLoadingPurchaseFlow && <Text>Redirecting you to the subscription page...</Text>}
{purchaseFlowError && <Text className="color-danger">{purchaseFlowError}</Text>}
<div className="flex">
<LinkButton className="min-w-20 mt-3 mr-3" label="Learn More" link={window.plansUrl as string} />
{application.hasAccount() && (
<Button className="min-w-20 mt-3" variant="primary" label="Subscribe" onClick={onPurchaseClick} />
)}
</div>
</>
)
}
export default NoSubscription

View File

@@ -0,0 +1,61 @@
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
import { observer } from 'mobx-react-lite'
import { Text } from '@/Components/Preferences/PreferencesComponents/Content'
type Props = { subscriptionState: SubscriptionController }
const StatusText = ({ subscriptionState }: Props) => {
const {
userSubscriptionName,
userSubscriptionExpirationDate,
isUserSubscriptionExpired,
isUserSubscriptionCanceled,
} = subscriptionState
const expirationDateString = userSubscriptionExpirationDate?.toLocaleString()
if (isUserSubscriptionCanceled) {
return (
<Text className="mt-1">
Your{' '}
<span className="font-bold">
Standard Notes{userSubscriptionName ? ' ' : ''}
{userSubscriptionName}
</span>{' '}
subscription has been canceled{' '}
{isUserSubscriptionExpired ? (
<span className="font-bold">and expired on {expirationDateString}</span>
) : (
<span className="font-bold">but will remain valid until {expirationDateString}</span>
)}
. You may resubscribe below if you wish.
</Text>
)
}
if (isUserSubscriptionExpired) {
return (
<Text className="mt-1">
Your{' '}
<span className="font-bold">
Standard Notes{userSubscriptionName ? ' ' : ''}
{userSubscriptionName}
</span>{' '}
subscription <span className="font-bold">expired on {expirationDateString}</span>. You may resubscribe below if
you wish.
</Text>
)
}
return (
<Text className="mt-1">
Your{' '}
<span className="font-bold">
Standard Notes{userSubscriptionName ? ' ' : ''}
{userSubscriptionName}
</span>{' '}
subscription will be <span className="font-bold">renewed on {expirationDateString}</span>.
</Text>
)
}
export default observer(StatusText)

View File

@@ -0,0 +1,40 @@
import { Title } from '@/Components/Preferences/PreferencesComponents/Content'
import { WebApplication } from '@/Application/Application'
import SubscriptionInformation from './SubscriptionInformation'
import NoSubscription from './NoSubscription'
import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'react'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
type Props = {
application: WebApplication
viewControllerManager: ViewControllerManager
}
const Subscription: FunctionComponent<Props> = ({ application, viewControllerManager }: Props) => {
const subscriptionState = viewControllerManager.subscriptionController
const { userSubscription } = subscriptionState
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>
{userSubscription && userSubscription.endsAt > now ? (
<SubscriptionInformation subscriptionState={subscriptionState} application={application} />
) : (
<NoSubscription application={application} />
)}
</div>
</div>
</PreferencesSegment>
</PreferencesGroup>
)
}
export default observer(Subscription)

View File

@@ -0,0 +1,31 @@
import { observer } from 'mobx-react-lite'
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
import Button from '@/Components/Button/Button'
import { WebApplication } from '@/Application/Application'
import { openSubscriptionDashboard } from '@/Utils/ManageSubscription'
import StatusText from './StatusText'
type Props = {
subscriptionState: SubscriptionController
application: WebApplication
}
const SubscriptionInformation = ({ subscriptionState, application }: Props) => {
const manageSubscription = async () => {
openSubscriptionDashboard(application)
}
return (
<>
<StatusText subscriptionState={subscriptionState} />
<Button
className="min-w-20 mt-3 mr-3"
variant="normal"
label="Manage subscription"
onClick={manageSubscription}
/>
</>
)
}
export default observer(SubscriptionInformation)

View File

@@ -0,0 +1,58 @@
import { Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
import Button from '@/Components/Button/Button'
import { SyncQueueStrategy } from '@standardnotes/snjs'
import { STRING_GENERIC_SYNC_ERROR } from '@/Constants/Strings'
import { observer } from 'mobx-react-lite'
import { WebApplication } from '@/Application/Application'
import { FunctionComponent, useState } from 'react'
import { formatLastSyncDate } from '@/Utils/FormatLastSyncDate'
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
type Props = {
application: WebApplication
}
const Sync: FunctionComponent<Props> = ({ application }: Props) => {
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false)
const [lastSyncDate, setLastSyncDate] = useState(formatLastSyncDate(application.sync.getLastSyncDate() as Date))
const doSynchronization = async () => {
setIsSyncingInProgress(true)
const response = await application.sync.sync({
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
checkIntegrity: true,
})
setIsSyncingInProgress(false)
if (response && (response as any).error) {
application.alertService.alert(STRING_GENERIC_SYNC_ERROR).catch(console.error)
} else {
setLastSyncDate(formatLastSyncDate(application.sync.getLastSyncDate() as Date))
}
}
return (
<PreferencesGroup>
<PreferencesSegment>
<div className="flex flex-row items-center">
<div className="flex-grow flex flex-col">
<Title>Sync</Title>
<Text>
Last synced <span className="font-bold">on {lastSyncDate}</span>
</Text>
<Button
className="min-w-20 mt-3"
variant="normal"
label="Sync now"
disabled={isSyncingInProgress}
onClick={doSynchronization}
/>
</div>
</div>
</PreferencesSegment>
</PreferencesGroup>
)
}
export default observer(Sync)