This commit is contained in:
Mo
2022-11-13 09:28:16 -06:00
committed by GitHub
parent e56a960bbf
commit d519aca685
49 changed files with 512 additions and 151 deletions

View File

@@ -204,10 +204,6 @@ export class WebApplication extends SNApplication implements WebApplicationInter
return this.isNativeMobileWeb() && this.platform === Platform.Ios
}
get hideSubscriptionMarketing() {
return this.isNativeIOS()
}
mobileDevice(): MobileDeviceInterface {
if (!this.isNativeMobileWeb()) {
throw Error('Attempting to access device as mobile device on non mobile platform')

View File

@@ -199,7 +199,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
<AndroidBackHandlerProvider application={application}>
<DarkModeHandler application={application} />
<ResponsivePaneProvider paneController={application.getViewControllerManager().paneController}>
<PremiumModalProvider application={application} viewControllerManager={viewControllerManager}>
<PremiumModalProvider application={application} featuresController={viewControllerManager.featuresController}>
<div className={platformString + ' main-ui-view sn-component h-full'}>
<div id="app" className="app app-column-container" ref={appColumnContainerRef}>
<FileDragNDropProvider

View File

@@ -213,16 +213,14 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
'Create powerful workflows and organizational layouts with per-tag display preferences.'}
</p>
{!application.hideSubscriptionMarketing && (
<Button
primary
small
className="col-start-1 col-end-3 mt-3 justify-self-start uppercase"
onClick={() => application.openPurchaseFlow()}
>
Upgrade Features
</Button>
)}
<Button
primary
small
className="col-start-1 col-end-3 mt-3 justify-self-start uppercase"
onClick={() => application.openPurchaseFlow()}
>
Upgrade Features
</Button>
</div>
)

View File

@@ -2,7 +2,6 @@ import { WebApplication } from '@/Application/Application'
import { FeaturesController } from '@/Controllers/FeaturesController'
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
import { observer } from 'mobx-react-lite'
import { loadPurchaseFlowUrl } from '../PurchaseFlow/PurchaseFlowFunctions'
type Props = {
application: WebApplication
@@ -14,22 +13,11 @@ const UpgradeNow = ({ application, featuresController, subscriptionContoller }:
const shouldShowCTA = !featuresController.hasFolders
const hasAccount = subscriptionContoller.hasAccount
if (hasAccount && subscriptionContoller.hideSubscriptionMarketing) {
return null
}
return shouldShowCTA ? (
<div className="flex h-full items-center px-2">
<button
className="rounded bg-info py-0.5 px-1.5 text-sm font-bold uppercase text-info-contrast hover:brightness-125 lg:text-xs"
onClick={() => {
if (hasAccount) {
void loadPurchaseFlowUrl(application)
return
}
application.getViewControllerManager().purchaseFlowController.openPurchaseFlow()
}}
onClick={() => application.openPurchaseFlow()}
>
{hasAccount ? 'Unlock features' : 'Sign up to sync'}
</button>

View File

@@ -2,7 +2,6 @@ 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
@@ -16,9 +15,7 @@ const NoSubscription: FunctionComponent<Props> = ({ application }) => {
const errorMessage = 'There was an error when attempting to redirect you to the subscription page.'
setIsLoadingPurchaseFlow(true)
try {
if (!(await loadPurchaseFlowUrl(application))) {
setPurchaseFlowError(errorMessage)
}
application.openPurchaseFlow()
} catch (e) {
setPurchaseFlowError(errorMessage)
} finally {
@@ -31,14 +28,12 @@ const NoSubscription: FunctionComponent<Props> = ({ application }) => {
<Text>You don't have a Standard Notes subscription yet.</Text>
{isLoadingPurchaseFlow && <Text>Redirecting you to the subscription page...</Text>}
{purchaseFlowError && <Text className="text-danger">{purchaseFlowError}</Text>}
{!application.hideSubscriptionMarketing && (
<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="Subscribe" onClick={onPurchaseClick} />
)}
</div>
)}
<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="Subscribe" onClick={onPurchaseClick} />
)}
</div>
</>
)
}

View File

@@ -2,7 +2,6 @@ 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
@@ -16,9 +15,7 @@ const NoProSubscription: FunctionComponent<Props> = ({ application }) => {
const errorMessage = 'There was an error when attempting to redirect you to the subscription page.'
setIsLoadingPurchaseFlow(true)
try {
if (!(await loadPurchaseFlowUrl(application))) {
setPurchaseFlowError(errorMessage)
}
application.openPurchaseFlow()
} catch (e) {
setPurchaseFlowError(errorMessage)
} finally {
@@ -35,14 +32,12 @@ const NoProSubscription: FunctionComponent<Props> = ({ application }) => {
{isLoadingPurchaseFlow && <Text>Redirecting you to the subscription page...</Text>}
{purchaseFlowError && <Text className="text-danger">{purchaseFlowError}</Text>}
{!application.hideSubscriptionMarketing && (
<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>
)}
<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>
</>
)
}

View File

@@ -0,0 +1,4 @@
export enum PremiumFeatureModalType {
UpgradePrompt,
UpgradeSuccess,
}

View File

@@ -4,7 +4,7 @@ import Icon from '@/Components/Icon/Icon'
import { WebApplication } from '@/Application/Application'
import { openSubscriptionDashboard } from '@/Utils/ManageSubscription'
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
import { loadPurchaseFlowUrl } from '../PurchaseFlow/PurchaseFlowFunctions'
import { PremiumFeatureModalType } from './PremiumFeatureModalType'
type Props = {
application: WebApplication
@@ -13,6 +13,7 @@ type Props = {
hasAccount: boolean
onClose: () => void
showModal: boolean
type: PremiumFeatureModalType
}
const PremiumFeaturesModal: FunctionComponent<Props> = ({
@@ -22,6 +23,7 @@ const PremiumFeaturesModal: FunctionComponent<Props> = ({
hasAccount,
onClose,
showModal,
type = PremiumFeatureModalType.UpgradePrompt,
}) => {
const plansButtonRef = useRef<HTMLButtonElement>(null)
@@ -29,11 +31,58 @@ const PremiumFeaturesModal: FunctionComponent<Props> = ({
if (hasSubscription) {
void openSubscriptionDashboard(application)
} else if (hasAccount) {
void loadPurchaseFlowUrl(application)
void application.openPurchaseFlow()
} else if (window.plansUrl) {
window.location.assign(window.plansUrl)
}
}, [application, hasSubscription, hasAccount])
onClose()
}, [application, hasSubscription, hasAccount, onClose])
const UpgradePrompt = (
<>
<AlertDialogDescription className="mb-2 px-4.5 text-center text-sm text-passive-1">
To take advantage of <span className="font-semibold">{featureName}</span> and other advanced features, upgrade
your current plan.
</AlertDialogDescription>
<div className="p-4">
<button
onClick={handleClick}
className="no-border w-full cursor-pointer rounded bg-info py-2 font-bold text-info-contrast hover:brightness-125 focus:brightness-125"
ref={plansButtonRef}
>
Upgrade
</button>
</div>
</>
)
const SuccessPrompt = (
<>
<AlertDialogDescription className="mb-2 px-4.5 text-center text-sm text-passive-1">
Enjoy your new powered up experience.
</AlertDialogDescription>
<div className="p-4">
<button
onClick={onClose}
className="no-border w-full cursor-pointer rounded bg-info py-2 font-bold text-info-contrast hover:brightness-125 focus:brightness-125"
ref={plansButtonRef}
>
Continue
</button>
</div>
</>
)
const title =
type === PremiumFeatureModalType.UpgradePrompt ? 'Enable Advanced Features' : 'Your purchase was successful!'
const iconName = type === PremiumFeatureModalType.UpgradePrompt ? PremiumFeatureIconName : '🎉'
const iconClass =
type === PremiumFeatureModalType.UpgradePrompt
? `h-12 w-12 ${PremiumFeatureIconClass}`
: 'px-7 py-2 h-24 w-24 text-[50px]'
return showModal ? (
<AlertDialog leastDestructiveRef={plansButtonRef} className="p-0">
@@ -53,25 +102,11 @@ const PremiumFeaturesModal: FunctionComponent<Props> = ({
className="mx-auto mb-5 flex h-24 w-24 items-center justify-center rounded-[50%] bg-contrast"
aria-hidden={true}
>
<Icon className={`h-12 w-12 ${PremiumFeatureIconClass}`} type={PremiumFeatureIconName} />
<Icon className={iconClass} size={'custom'} type={iconName} />
</div>
<div className="mb-1 text-center text-lg font-bold">Enable Advanced Features</div>
<div className="mb-1 text-center text-lg font-bold">{title}</div>
</AlertDialogLabel>
<AlertDialogDescription className="mb-2 px-4.5 text-center text-sm text-passive-1">
To take advantage of <span className="font-semibold">{featureName}</span> and other advanced features,
upgrade your current plan.
</AlertDialogDescription>
{!application.hideSubscriptionMarketing && (
<div className="p-4">
<button
onClick={handleClick}
className="no-border w-full cursor-pointer rounded bg-info py-2 font-bold text-info-contrast hover:brightness-125 focus:brightness-125"
ref={plansButtonRef}
>
Upgrade
</button>
</div>
)}
{type === PremiumFeatureModalType.UpgradePrompt ? UpgradePrompt : SuccessPrompt}
</div>
</div>
</AlertDialog>

View File

@@ -7,7 +7,6 @@ import { ChangeEventHandler, FunctionComponent, useEffect, useRef, useState } fr
import FloatingLabelInput from '@/Components/Input/FloatingLabelInput'
import { isEmailValid } from '@/Utils'
import { BlueDotIcon, CircleIcon, DiamondIcon, CreateAccountIllustration } from '@standardnotes/icons'
import { loadPurchaseFlowUrl } from '../PurchaseFlowFunctions'
type Props = {
viewControllerManager: ViewControllerManager
@@ -52,10 +51,7 @@ const CreateAccount: FunctionComponent<Props> = ({ viewControllerManager, applic
}
const subscribeWithoutAccount = () => {
loadPurchaseFlowUrl(application).catch((err) => {
console.error(err)
application.alertService.alert(err).catch(console.error)
})
application.getViewControllerManager().purchaseFlowController.openPurchaseWebpage()
}
const handleCreateAccount = async () => {
@@ -93,13 +89,7 @@ const CreateAccount: FunctionComponent<Props> = ({ viewControllerManager, applic
await application.register(email, password)
viewControllerManager.purchaseFlowController.closePurchaseFlow()
if (!application.hideSubscriptionMarketing) {
loadPurchaseFlowUrl(application).catch((err) => {
console.error(err)
application.alertService.alert(err).catch(console.error)
})
}
viewControllerManager.purchaseFlowController.openPurchaseFlow()
} catch (err) {
console.error(err)
application.alertService.alert(err as string).catch(console.error)
@@ -170,13 +160,15 @@ const CreateAccount: FunctionComponent<Props> = ({ viewControllerManager, applic
>
Sign in instead
</button>
<button
onClick={subscribeWithoutAccount}
disabled={isCreatingAccount}
className="flex cursor-pointer items-start border-0 bg-default p-0 font-medium text-info hover:underline"
>
Subscribe without account
</button>
{!application.isNativeIOS() && (
<button
onClick={subscribeWithoutAccount}
disabled={isCreatingAccount}
className="flex cursor-pointer items-start border-0 bg-default p-0 font-medium text-info hover:underline"
>
Subscribe without account
</button>
)}
</div>
<Button
className="mb-4 py-2.5 md:mb-0"

View File

@@ -7,7 +7,6 @@ import { ChangeEventHandler, FunctionComponent, useEffect, useRef, useState } fr
import FloatingLabelInput from '@/Components/Input/FloatingLabelInput'
import { isEmailValid } from '@/Utils'
import { BlueDotIcon, CircleIcon, DiamondIcon } from '@standardnotes/icons'
import { loadPurchaseFlowUrl } from '../PurchaseFlowFunctions'
type Props = {
viewControllerManager: ViewControllerManager
@@ -75,13 +74,7 @@ const SignIn: FunctionComponent<Props> = ({ viewControllerManager, application }
throw new Error(response.error?.message || response.data?.error?.message)
} else {
viewControllerManager.purchaseFlowController.closePurchaseFlow()
if (!application.hideSubscriptionMarketing) {
loadPurchaseFlowUrl(application).catch((err) => {
console.error(err)
application.alertService.alert(err).catch(console.error)
})
}
viewControllerManager.purchaseFlowController.openPurchaseFlow()
}
} catch (err) {
console.error(err)

View File

@@ -1,4 +1,5 @@
import { WebApplication } from '@/Application/Application'
import { PremiumFeatureModalType } from '@/Components/PremiumFeaturesModal/PremiumFeatureModalType'
import { destroyAllObjectProperties } from '@/Utils'
import {
ApplicationEvent,
@@ -16,6 +17,7 @@ export class FeaturesController extends AbstractViewController {
hasSmartViews: boolean
hasFiles: boolean
premiumAlertFeatureName: string | undefined
premiumAlertType: PremiumFeatureModalType | undefined = undefined
override deinit() {
super.deinit()
@@ -25,6 +27,7 @@ export class FeaturesController extends AbstractViewController {
;(this.hasSmartViews as unknown) = undefined
;(this.hasFiles as unknown) = undefined
;(this.premiumAlertFeatureName as unknown) = undefined
;(this.premiumAlertType as unknown) = undefined
destroyAllObjectProperties(this)
}
@@ -43,10 +46,11 @@ export class FeaturesController extends AbstractViewController {
hasFolders: observable,
hasSmartViews: observable,
hasFiles: observable,
premiumAlertType: observable,
premiumAlertFeatureName: observable,
showPremiumAlert: action,
closePremiumAlert: action,
showPurchaseSuccessAlert: action,
})
this.showPremiumAlert = this.showPremiumAlert.bind(this)
@@ -55,6 +59,9 @@ export class FeaturesController extends AbstractViewController {
this.disposers.push(
application.addEventObserver(async (event) => {
switch (event) {
case ApplicationEvent.DidPurchaseSubscription:
this.showPurchaseSuccessAlert()
break
case ApplicationEvent.FeaturesUpdated:
case ApplicationEvent.Launched:
runInAction(() => {
@@ -76,11 +83,17 @@ export class FeaturesController extends AbstractViewController {
public async showPremiumAlert(featureName: string): Promise<void> {
this.premiumAlertFeatureName = featureName
return when(() => this.premiumAlertFeatureName === undefined)
this.premiumAlertType = PremiumFeatureModalType.UpgradePrompt
return when(() => this.premiumAlertType === undefined)
}
showPurchaseSuccessAlert = () => {
this.premiumAlertType = PremiumFeatureModalType.UpgradeSuccess
}
public closePremiumAlert() {
this.premiumAlertFeatureName = undefined
this.premiumAlertType = undefined
}
private isEntitledToFiles(): boolean {

View File

@@ -1,5 +1,6 @@
import { LoggingDomain, log } from '@/Logging'
import { loadPurchaseFlowUrl } from '@/Components/PurchaseFlow/PurchaseFlowFunctions'
import { InternalEventBus } from '@standardnotes/snjs'
import { InternalEventBus, AppleIAPProductId } from '@standardnotes/snjs'
import { action, makeObservable, observable } from 'mobx'
import { WebApplication } from '../../Application/Application'
import { AbstractViewController } from '../Abstract/AbstractViewController'
@@ -26,15 +27,67 @@ export class PurchaseFlowController extends AbstractViewController {
this.currentPane = currentPane
}
openPurchaseFlow = (): void => {
openPurchaseFlow = (plan = AppleIAPProductId.ProPlanYearly): void => {
const user = this.application.getUser()
if (!user) {
this.isOpen = true
return
}
if (this.application.isNativeIOS()) {
void this.beginIosIapPurchaseFlow(plan)
} else {
loadPurchaseFlowUrl(this.application).catch(console.error)
}
}
openPurchaseWebpage = () => {
loadPurchaseFlowUrl(this.application).catch((err) => {
console.error(err)
this.application.alertService.alert(err).catch(console.error)
})
}
beginIosIapPurchaseFlow = async (plan: AppleIAPProductId): Promise<void> => {
const result = await this.application.mobileDevice().purchaseSubscriptionIAP(plan)
log(LoggingDomain.Purchasing, 'BeginIosIapPurchaseFlow result', result)
if (!result) {
void this.application.alertService.alert('Your purchase was canceled or failed. Please try again.')
return
}
const showGenericError = () => {
void this.application.alertService.alert(
'There was an error confirming your purchase. Please contact support at help@standardnotes.com.',
)
}
log(LoggingDomain.Purchasing, 'Confirming result with our server')
const token = await this.application.getNewSubscriptionToken()
if (!token) {
log(LoggingDomain.Purchasing, 'Unable to generate subscription token')
showGenericError()
return
}
const confirmResult = await this.application.subscriptions.confirmAppleIAP(result, token)
log(LoggingDomain.Purchasing, 'Server confirm result', confirmResult)
if (confirmResult) {
void this.application.alerts.alert(
'Please allow a few minutes for your subscription benefits to activate. You will see a confirmation alert in the app when your subscription is ready.',
'Your purchase was successful!',
)
} else {
showGenericError()
}
}
closePurchaseFlow = (): void => {
this.isOpen = false
}

View File

@@ -21,7 +21,6 @@ export class SubscriptionController extends AbstractViewController {
userSubscription: Subscription | undefined = undefined
availableSubscriptions: AvailableSubscriptions | undefined = undefined
subscriptionInvitations: Invitation[] | undefined = undefined
hideSubscriptionMarketing: boolean
hasAccount: boolean
override deinit() {
@@ -39,14 +38,12 @@ export class SubscriptionController extends AbstractViewController {
private subscriptionManager: SubscriptionClientInterface,
) {
super(application, eventBus)
this.hideSubscriptionMarketing = application.hideSubscriptionMarketing
this.hasAccount = application.hasAccount()
makeObservable(this, {
userSubscription: observable,
availableSubscriptions: observable,
subscriptionInvitations: observable,
hideSubscriptionMarketing: observable,
hasAccount: observable,
userSubscriptionName: computed,

View File

@@ -1,8 +1,8 @@
import { WebApplication } from '@/Application/Application'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, createContext, useCallback, useContext, ReactNode } from 'react'
import PremiumFeaturesModal from '@/Components/PremiumFeaturesModal/PremiumFeaturesModal'
import { FeaturesController } from '@/Controllers/FeaturesController'
type PremiumModalContextData = {
activate: (featureName: string) => void
@@ -24,15 +24,13 @@ export const usePremiumModal = (): PremiumModalContextData => {
interface Props {
application: WebApplication
viewControllerManager: ViewControllerManager
featuresController: FeaturesController
children: ReactNode
}
const PremiumModalProvider: FunctionComponent<Props> = observer(
({ application, viewControllerManager, children }: Props) => {
const featureName = viewControllerManager.featuresController.premiumAlertFeatureName || ''
const showModal = !!featureName
({ application, featuresController, children }: Props) => {
const featureName = featuresController.premiumAlertFeatureName || ''
const hasSubscription = application.hasValidSubscription()
@@ -40,25 +38,26 @@ const PremiumModalProvider: FunctionComponent<Props> = observer(
const activate = useCallback(
(feature: string) => {
viewControllerManager.featuresController.showPremiumAlert(feature).catch(console.error)
featuresController.showPremiumAlert(feature).catch(console.error)
},
[viewControllerManager],
[featuresController],
)
const close = useCallback(() => {
viewControllerManager.featuresController.closePremiumAlert()
}, [viewControllerManager])
featuresController.closePremiumAlert()
}, [featuresController])
return (
<>
{showModal && (
{featuresController.premiumAlertType != undefined && (
<PremiumFeaturesModal
application={application}
featureName={featureName}
hasSubscription={hasSubscription}
hasAccount={hasAccount}
onClose={close}
showModal={!!featureName}
showModal={featuresController.premiumAlertType != undefined}
type={featuresController.premiumAlertType}
/>
)}
<PremiumModalProvider_ value={{ activate }}>{children}</PremiumModalProvider_>
@@ -71,12 +70,10 @@ PremiumModalProvider.displayName = 'PremiumModalProvider'
const PremiumModalProviderWithDeallocateHandling: FunctionComponent<Props> = ({
application,
viewControllerManager,
featuresController,
children,
}) => {
return (
<PremiumModalProvider application={application} viewControllerManager={viewControllerManager} children={children} />
)
return <PremiumModalProvider application={application} featuresController={featuresController} children={children} />
}
export default observer(PremiumModalProviderWithDeallocateHandling)

View File

@@ -9,6 +9,7 @@ export enum LoggingDomain {
Viewport,
Selection,
BlockEditor,
Purchasing,
}
const LoggingStatus: Record<LoggingDomain, boolean> = {
@@ -18,7 +19,8 @@ const LoggingStatus: Record<LoggingDomain, boolean> = {
[LoggingDomain.NavigationList]: false,
[LoggingDomain.Viewport]: false,
[LoggingDomain.Selection]: false,
[LoggingDomain.BlockEditor]: true,
[LoggingDomain.BlockEditor]: false,
[LoggingDomain.Purchasing]: true,
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any