Files
standardnotes-app-web/packages/services/src/Domain/Subscription/SubscriptionManager.ts

235 lines
7.2 KiB
TypeScript

import { StorageKey } from './../Storage/StorageKeys'
import { ApplicationStage } from './../Application/ApplicationStage'
import { StorageServiceInterface } from './../Storage/StorageServiceInterface'
import { InternalEventInterface } from './../Internal/InternalEventInterface'
import { SessionsClientInterface } from './../Session/SessionsClientInterface'
import { SubscriptionName } from '@standardnotes/common'
import { convertTimestampToMilliseconds } from '@standardnotes/utils'
import { ApplicationEvent } from './../Event/ApplicationEvent'
import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface'
import { Invitation } from '@standardnotes/models'
import { SubscriptionApiServiceInterface } from '@standardnotes/api'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { AbstractService } from '../Service/AbstractService'
import { SubscriptionManagerInterface } from './SubscriptionManagerInterface'
import { AppleIAPReceipt } from './AppleIAPReceipt'
import { AvailableSubscriptions, getErrorFromErrorResponse, isErrorResponse } from '@standardnotes/responses'
import { Subscription } from '@standardnotes/security'
import { SubscriptionManagerEvent } from './SubscriptionManagerEvent'
export class SubscriptionManager
extends AbstractService<SubscriptionManagerEvent>
implements SubscriptionManagerInterface, InternalEventHandlerInterface
{
private onlineSubscription?: Subscription
private availableSubscriptions?: AvailableSubscriptions | undefined
constructor(
private subscriptionApiService: SubscriptionApiServiceInterface,
private sessions: SessionsClientInterface,
private storage: StorageServiceInterface,
protected override internalEventBus: InternalEventBusInterface,
) {
super(internalEventBus)
internalEventBus.addEventHandler(this, ApplicationEvent.UserRolesChanged)
internalEventBus.addEventHandler(this, ApplicationEvent.Launched)
internalEventBus.addEventHandler(this, ApplicationEvent.SignedIn)
}
async handleEvent(event: InternalEventInterface): Promise<void> {
switch (event.type) {
case ApplicationEvent.Launched: {
void this.fetchOnlineSubscription()
void this.fetchAvailableSubscriptions()
break
}
case ApplicationEvent.UserRolesChanged:
case ApplicationEvent.SignedIn:
void this.fetchOnlineSubscription()
break
}
}
public override async handleApplicationStage(stage: ApplicationStage): Promise<void> {
if (stage === ApplicationStage.StorageDecrypted_09) {
this.onlineSubscription = this.storage.getValue(StorageKey.Subscription)
void this.notifyEvent(SubscriptionManagerEvent.DidFetchSubscription)
}
}
hasOnlineSubscription(): boolean {
return this.onlineSubscription != undefined
}
getOnlineSubscription(): Subscription | undefined {
return this.onlineSubscription
}
getAvailableSubscriptions(): AvailableSubscriptions | undefined {
return this.availableSubscriptions
}
get userSubscriptionName(): string {
if (!this.onlineSubscription) {
throw new Error('Attempting to get subscription name without a subscription.')
}
if (
this.availableSubscriptions &&
this.availableSubscriptions[this.onlineSubscription.planName as SubscriptionName]
) {
return this.availableSubscriptions[this.onlineSubscription.planName as SubscriptionName].name
}
return ''
}
get userSubscriptionExpirationDate(): Date | undefined {
if (!this.onlineSubscription) {
return undefined
}
return new Date(convertTimestampToMilliseconds(this.onlineSubscription.endsAt))
}
get isUserSubscriptionExpired(): boolean {
if (!this.onlineSubscription) {
throw new Error('Attempting to check subscription expiration without a subscription.')
}
if (!this.userSubscriptionExpirationDate) {
return false
}
return this.userSubscriptionExpirationDate.getTime() < new Date().getTime()
}
get isUserSubscriptionCanceled(): boolean {
if (!this.onlineSubscription) {
throw new Error('Attempting to check subscription expiration without a subscription.')
}
return this.onlineSubscription.cancelled
}
async acceptInvitation(inviteUuid: string): Promise<{ success: true } | { success: false; message: string }> {
try {
const result = await this.subscriptionApiService.acceptInvite(inviteUuid)
if (isErrorResponse(result)) {
return { success: false, message: getErrorFromErrorResponse(result).message }
}
return result.data
} catch (error) {
return { success: false, message: 'Could not accept invitation.' }
}
}
async listSubscriptionInvitations(): Promise<Invitation[]> {
try {
const result = await this.subscriptionApiService.listInvites()
if (isErrorResponse(result)) {
return []
}
return result.data.invitations ?? []
} catch (error) {
return []
}
}
async inviteToSubscription(inviteeEmail: string): Promise<boolean> {
try {
const result = await this.subscriptionApiService.invite(inviteeEmail)
if (isErrorResponse(result)) {
return false
}
return result.data.success === true
} catch (error) {
return false
}
}
async cancelInvitation(inviteUuid: string): Promise<boolean> {
try {
const result = await this.subscriptionApiService.cancelInvite(inviteUuid)
if (isErrorResponse(result)) {
return false
}
return result.data.success === true
} catch (error) {
return false
}
}
private async fetchOnlineSubscription(): Promise<void> {
if (!this.sessions.isSignedIn()) {
return
}
try {
const result = await this.subscriptionApiService.getUserSubscription({ userUuid: this.sessions.userUuid })
if (isErrorResponse(result)) {
return
}
const subscription = result.data.subscription
this.handleReceivedOnlineSubscriptionFromServer(subscription)
} catch (error) {
void error
}
}
private handleReceivedOnlineSubscriptionFromServer(subscription: Subscription | undefined): void {
this.onlineSubscription = subscription
this.storage.setValue(StorageKey.Subscription, subscription)
void this.notifyEvent(SubscriptionManagerEvent.DidFetchSubscription)
}
private async fetchAvailableSubscriptions(): Promise<void> {
try {
const response = await this.subscriptionApiService.getAvailableSubscriptions()
if (isErrorResponse(response)) {
return
}
this.availableSubscriptions = response.data
} catch (error) {
void error
}
}
async confirmAppleIAP(
params: AppleIAPReceipt,
subscriptionToken: string,
): Promise<{ success: true } | { success: false; message: string }> {
try {
const result = await this.subscriptionApiService.confirmAppleIAP({
...params,
subscription_token: subscriptionToken,
})
if (isErrorResponse(result)) {
return { success: false, message: getErrorFromErrorResponse(result).message }
}
return result.data
} catch (error) {
return { success: false, message: 'Could not confirm IAP.' }
}
}
}