Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/snjs/lib/Services/Session/SessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
}

private setSession(session: Session, persist = true): void {
this.httpService.setAuthorizationToken(session.authorizationValue)

this.apiService.setSession(session, persist)
}

Expand Down Expand Up @@ -621,8 +623,6 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S

this.httpService.setHost(host)

this.httpService.setAuthorizationToken(session.authorizationValue)

await this.setSession(session)

this.webSocketsService.startWebSocketConnection(session.authorizationValue)
Expand Down
1 change: 1 addition & 0 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"clean": "rm -fr dist && rm -rf src/components",
"format": "prettier --write src/javascripts",
"lint": "NODE_OPTIONS=\"--max-old-space-size=4096\" eslint src/javascripts",
"lint:fix": "NODE_OPTIONS=\"--max-old-space-size=4096\" eslint src/javascripts --fix",
"start": "webpack-dev-server --config web.webpack.dev.js",
"start-secure": "yarn start --server-type https",
"test": "jest --config jest.config.js --coverage",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Subscription from './Subscription/Subscription'
import SignOutWrapper from './SignOutView'
import FilesSection from './Files'
import PreferencesPane from '../../PreferencesComponents/PreferencesPane'
import SubscriptionSharing from './SubscriptionSharing/SubscriptionSharing'

type Props = {
application: WebApplication
Expand All @@ -25,6 +26,7 @@ const AccountPreferences = ({ application, viewControllerManager }: Props) => (
</>
)}
<Subscription application={application} viewControllerManager={viewControllerManager} />
<SubscriptionSharing application={application} viewControllerManager={viewControllerManager} />
{application.hasAccount() && viewControllerManager.featuresController.hasFiles && (
<FilesSection application={application} />
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useState } from 'react'
import { observer } from 'mobx-react-lite'
import { InvitationStatus, Uuid } from '@standardnotes/snjs'

import { SubtitleLight, Text } from '@/Components/Preferences/PreferencesComponents/Content'
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
import Button from '@/Components/Button/Button'
import { WebApplication } from '@/Application/Application'
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'

type Props = {
subscriptionState: SubscriptionController
application: WebApplication
}

const InvitationsList = ({ subscriptionState, application }: Props) => {
const [lockContinue, setLockContinue] = useState(false)

const { usedInvitationsCount, subscriptionInvitations } = subscriptionState

const activeSubscriptions = subscriptionInvitations?.filter((invitation) =>
[InvitationStatus.Sent, InvitationStatus.Accepted].includes(invitation.status),
)
const inActiveSubscriptions = subscriptionInvitations?.filter((invitation) =>
[InvitationStatus.Declined, InvitationStatus.Canceled].includes(invitation.status),
)

const handleCancel = async (invitationUuid: Uuid) => {
if (lockContinue) {
application.alertService.alert('Cancelation already in progress.').catch(console.error)

return
}

setLockContinue(true)

const success = await subscriptionState.cancelSubscriptionInvitation(invitationUuid)

setLockContinue(false)

if (!success) {
application.alertService
.alert('Could not cancel invitation. Please try again or contact support if the issue persists.')
.catch(console.error)
}
}

if (usedInvitationsCount === 0) {
return <Text className="mt-1 mb-3">Make your first subscription invitation below.</Text>
}

return (
<div>
<SubtitleLight className="mb-2 text-info">Active Invitations:</SubtitleLight>
{activeSubscriptions?.map((invitation) => (
<div key={invitation.uuid} className="mt-1 mb-4">
<Text>
{invitation.inviteeIdentifier} <span className="text-info">({invitation.status})</span>
</Text>
{invitation.status !== InvitationStatus.Canceled && (
<Button className="mt-2 min-w-20" label="Cancel" onClick={() => handleCancel(invitation.uuid)} />
)}
</div>
))}
{!!inActiveSubscriptions?.length && (
<>
<SubtitleLight className="mb-2 text-info">Inactive Invitations:</SubtitleLight>
<div>
{inActiveSubscriptions?.map((invitation) => (
<div key={invitation.uuid} className="mb-3 first:mt-2">
<Text className="mt-1">
{invitation.inviteeIdentifier} <span className="text-info">({invitation.status})</span>
</Text>
</div>
))}
</div>
</>
)}
{!subscriptionState.allInvitationsUsed && <HorizontalSeparator classes="my-4" />}
</div>
)
}

export default observer(InvitationsList)
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { FunctionComponent, useState } from 'react'

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 { WebApplication } from '@/Application/Application'
import { isEmailValid } from '@/Utils'
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'

import InviteForm from './InviteForm'
import InviteSuccess from './InviteSuccess'

enum SubmitButtonTitles {
Default = 'Send Invitation',
Sending = 'Sending...',
Finish = 'Finish',
}

enum Steps {
InitialStep,
FinishStep,
}

type Props = {
onCloseDialog: () => void
application: WebApplication
subscriptionState: SubscriptionController
}

const Invite: FunctionComponent<Props> = ({ onCloseDialog, application, subscriptionState }) => {
const [submitButtonTitle, setSubmitButtonTitle] = useState(SubmitButtonTitles.Default)
const [inviteeEmail, setInviteeEmail] = useState('')
const [isContinuing, setIsContinuing] = useState(false)
const [lockContinue, setLockContinue] = useState(false)
const [currentStep, setCurrentStep] = useState(Steps.InitialStep)

const validateInviteeEmail = async () => {
if (!isEmailValid(inviteeEmail)) {
application.alertService
.alert('The email you entered has an invalid format. Please review your input and try again.')
.catch(console.error)

return false
}

return true
}

const handleDialogClose = () => {
if (lockContinue) {
application.alertService.alert('Cannot close window until pending tasks are complete.').catch(console.error)
} else {
onCloseDialog()
}
}

const resetProgressState = () => {
setSubmitButtonTitle(SubmitButtonTitles.Default)
setIsContinuing(false)
}

const processInvite = async () => {
setLockContinue(true)

const success = await subscriptionState.sendSubscriptionInvitation(inviteeEmail)

setLockContinue(false)

return success
}

const handleSubmit = async () => {
if (lockContinue || isContinuing) {
return
}

if (currentStep === Steps.FinishStep) {
handleDialogClose()

return
}

setIsContinuing(true)
setSubmitButtonTitle(SubmitButtonTitles.Sending)

const valid = await validateInviteeEmail()

if (!valid) {
resetProgressState()

return
}

const success = await processInvite()
if (!success) {
application.alertService
.alert('We could not send the invitation. Please try again or contact support if the issue persists.')
.catch(console.error)

resetProgressState()

return
}

setIsContinuing(false)
setSubmitButtonTitle(SubmitButtonTitles.Finish)
setCurrentStep(Steps.FinishStep)
}

return (
<div>
<ModalDialog>
<ModalDialogLabel closeDialog={handleDialogClose}>Invite</ModalDialogLabel>
<ModalDialogDescription className="flex flex-row items-center px-4.5">
{currentStep === Steps.InitialStep && <InviteForm setInviteeEmail={setInviteeEmail} />}
{currentStep === Steps.FinishStep && <InviteSuccess />}
</ModalDialogDescription>
<ModalDialogButtons className="px-4.5">
<Button className="min-w-20" primary label={submitButtonTitle} onClick={handleSubmit} />
</ModalDialogButtons>
</ModalDialog>
</div>
)
}

export default Invite
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Dispatch, FunctionComponent, SetStateAction } from 'react'

import DecoratedInput from '@/Components/Input/DecoratedInput'

type Props = {
setInviteeEmail: Dispatch<SetStateAction<string>>
}

const InviteForm: FunctionComponent<Props> = ({ setInviteeEmail }) => {
return (
<div className="flex w-full flex-col">
<div className="mb-3">
<label className="mb-1 block" htmlFor="invite-email-input">
Invitee Email:
</label>
<DecoratedInput
type="email"
id="invite-email-input"
onChange={(email) => {
setInviteeEmail(email)
}}
/>
</div>
</div>
)
}

export default InviteForm
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { FunctionComponent } from 'react'

const InviteSuccess: FunctionComponent = () => {
return (
<div>
<div className={'mb-2 font-bold text-info'}>Your invitation has been successfully sent.</div>
</div>
)
}

export default InviteSuccess
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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 NoProSubscription: 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>
Subscription sharing is available only on the <span className="font-bold">Professional</span> plan. Please
upgrade in order to share subscription.
</Text>
{isLoadingPurchaseFlow && <Text>Redirecting you to the subscription page...</Text>}
{purchaseFlowError && <Text className="text-danger">{purchaseFlowError}</Text>}
<div className="flex">
<LinkButton className="mt-3 mr-3 min-w-20" label="Learn More" link={window.plansUrl as string} />
{application.hasAccount() && (
<Button className="mt-3 min-w-20" primary label="Upgrade" onClick={onPurchaseClick} />
)}
</div>
</>
)
}

export default NoProSubscription
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
import { observer } from 'mobx-react-lite'
import { Text } from '@/Components/Preferences/PreferencesComponents/Content'

type Props = { subscriptionState: SubscriptionController }

const SharingStatusText = ({ subscriptionState }: Props) => {
const { usedInvitationsCount, allowedInvitationsCount } = subscriptionState

return (
<Text className="mt-1">
You have have used <span className="font-bold">{usedInvitationsCount}</span> out of {allowedInvitationsCount}{' '}
subscription invitations.
</Text>
)
}

export default observer(SharingStatusText)
Loading