From 3cb016ab1f3295ff60f269754d48c7f3b3ef2b91 Mon Sep 17 00:00:00 2001 From: Mo Date: Thu, 13 Oct 2022 09:08:03 -0500 Subject: [PATCH] feat: handle basic routes (#1784) --- packages/ui-services/package.json | 2 +- .../src/Preferences/PreferenceId.ts | 14 +++ packages/ui-services/src/Route/RouteParams.ts | 18 ++++ .../ui-services/src/Route/RouteParser.spec.ts | 51 +++++++++++ packages/ui-services/src/Route/RouteParser.ts | 88 +++++++++++++++++++ .../ui-services/src/Route/RouteService.ts | 50 +++++++++++ .../src/Route/RouteServiceEvent.ts | 3 + packages/ui-services/src/Route/RouteType.ts | 7 ++ packages/ui-services/src/index.ts | 6 ++ packages/web/src/javascripts/App.tsx | 39 +------- .../javascripts/Application/Application.ts | 43 ++++++--- .../Application/ApplicationGroup.ts | 27 +----- .../javascripts/Application/WebServices.ts | 12 +++ .../ApplicationView/ApplicationView.tsx | 17 ++-- .../Components/Footer/UpgradeNow.tsx | 2 +- .../NoAccountWarningContent.tsx | 4 +- .../SubscriptionSharing/InvitationsList.tsx | 2 +- .../SubscriptionSharing/NoProSubscription.tsx | 2 +- .../Components/Preferences/PreferencesMenu.ts | 16 +--- .../Preferences/PreferencesMenuView.tsx | 3 +- .../PurchaseFlow/PurchaseFlowFunctions.ts | 15 +++- .../Controllers/PreferencesController.ts | 23 +++-- .../Controllers/ViewControllerManager.ts | 45 +++++----- packages/web/src/javascripts/Utils/Utils.ts | 4 - .../javascripts/setDefaultMonospaceFont.tsx | 10 +++ .../setViewportHeightWithFallback.tsx | 24 +++++ packages/web/web.webpack.dev.js | 4 + 27 files changed, 391 insertions(+), 140 deletions(-) create mode 100644 packages/ui-services/src/Preferences/PreferenceId.ts create mode 100644 packages/ui-services/src/Route/RouteParams.ts create mode 100644 packages/ui-services/src/Route/RouteParser.spec.ts create mode 100644 packages/ui-services/src/Route/RouteParser.ts create mode 100644 packages/ui-services/src/Route/RouteService.ts create mode 100644 packages/ui-services/src/Route/RouteServiceEvent.ts create mode 100644 packages/ui-services/src/Route/RouteType.ts create mode 100644 packages/web/src/javascripts/Application/WebServices.ts create mode 100644 packages/web/src/javascripts/setDefaultMonospaceFont.tsx create mode 100644 packages/web/src/javascripts/setViewportHeightWithFallback.tsx diff --git a/packages/ui-services/package.json b/packages/ui-services/package.json index 40d8ce630..cd20b1aee 100644 --- a/packages/ui-services/package.json +++ b/packages/ui-services/package.json @@ -20,7 +20,7 @@ "prebuild": "yarn clean", "build": "tsc -p tsconfig.json", "lint": "eslint . --ext .ts", - "test": "jest spec --coverage --passWithNoTests" + "test": "jest spec" }, "dependencies": { "@standardnotes/common": "^1.39.0", diff --git a/packages/ui-services/src/Preferences/PreferenceId.ts b/packages/ui-services/src/Preferences/PreferenceId.ts new file mode 100644 index 000000000..a47d5ee46 --- /dev/null +++ b/packages/ui-services/src/Preferences/PreferenceId.ts @@ -0,0 +1,14 @@ +const PREFERENCE_IDS = [ + 'general', + 'account', + 'security', + 'appearance', + 'backups', + 'listed', + 'shortcuts', + 'accessibility', + 'get-free-month', + 'help-feedback', +] as const + +export type PreferenceId = typeof PREFERENCE_IDS[number] diff --git a/packages/ui-services/src/Route/RouteParams.ts b/packages/ui-services/src/Route/RouteParams.ts new file mode 100644 index 000000000..3cf73c08b --- /dev/null +++ b/packages/ui-services/src/Route/RouteParams.ts @@ -0,0 +1,18 @@ +import { PreferenceId } from '../Preferences/PreferenceId' + +export type OnboardingParams = { + fromHomepage: boolean +} + +export type SettingsParams = { + panel: PreferenceId +} + +export type DemoParams = { + token: string +} + +export type PurchaseParams = { + plan: string + period: string +} diff --git a/packages/ui-services/src/Route/RouteParser.spec.ts b/packages/ui-services/src/Route/RouteParser.spec.ts new file mode 100644 index 000000000..f242f586c --- /dev/null +++ b/packages/ui-services/src/Route/RouteParser.spec.ts @@ -0,0 +1,51 @@ +import { RouteParser } from './RouteParser' +import { RouteType } from './RouteType' + +describe('route parser', () => { + it('routes to onboarding', () => { + const url = 'https://app.standardnotes.com/onboard?from_homepage=true' + const parser = new RouteParser(url) + + expect(parser.type).toEqual(RouteType.Onboarding) + expect(parser.onboardingParams.fromHomepage).toEqual(true) + }) + + it('routes to demo', () => { + const url = 'https://app-demo.standardnotes.com/?demo-token=eyJhY2Nlc3NUb2tl' + const parser = new RouteParser(url) + + expect(parser.type).toEqual(RouteType.Demo) + expect(parser.demoParams.token).toEqual('eyJhY2Nlc3NUb2tl') + }) + + it('routes to settings', () => { + const url = 'https://app.standardnotes.com/?settings=account' + const parser = new RouteParser(url) + + expect(parser.type).toEqual(RouteType.Settings) + expect(parser.settingsParams.panel).toEqual('account') + }) + + it('routes to purchase', () => { + const url = 'https://app.standardnotes.com/?purchase=true&plan=PLUS_PLAN&period=year' + const parser = new RouteParser(url) + + expect(parser.type).toEqual(RouteType.Purchase) + expect(parser.purchaseParams.period).toEqual('year') + expect(parser.purchaseParams.plan).toEqual('PLUS_PLAN') + }) + + it('routes to none', () => { + const url = 'https://app.standardnotes.com/unknown?foo=bar' + const parser = new RouteParser(url) + + expect(parser.type).toEqual(RouteType.None) + }) + + it('accessing wrong params should throw', () => { + const url = 'https://app.standardnotes.com/item?uuid=123' + const parser = new RouteParser(url) + + expect(() => parser.onboardingParams).toThrowError('Accessing invalid params') + }) +}) diff --git a/packages/ui-services/src/Route/RouteParser.ts b/packages/ui-services/src/Route/RouteParser.ts new file mode 100644 index 000000000..09ecfe741 --- /dev/null +++ b/packages/ui-services/src/Route/RouteParser.ts @@ -0,0 +1,88 @@ +import { PreferenceId } from './../Preferences/PreferenceId' +import { DemoParams, OnboardingParams, PurchaseParams, SettingsParams } from './RouteParams' +import { RouteType } from './RouteType' + +enum RootRoutes { + Onboarding = '/onboard', + None = '/', +} + +enum RootQueryParam { + Purchase = 'purchase', + Settings = 'settings', + DemoToken = 'demo-token', +} + +export class RouteParser { + private url: URL + private readonly path: string + public readonly type: RouteType + private readonly searchParams: URLSearchParams + + constructor(url: string) { + this.url = new URL(url) + this.path = this.url.pathname + this.searchParams = this.url.searchParams + + const pathUsesRootQueryParams = this.path === RootRoutes.None + + if (pathUsesRootQueryParams) { + if (this.searchParams.has(RootQueryParam.Purchase)) { + this.type = RouteType.Purchase + } else if (this.searchParams.has(RootQueryParam.Settings)) { + this.type = RouteType.Settings + } else if (this.searchParams.has(RootQueryParam.DemoToken)) { + this.type = RouteType.Demo + } else { + this.type = RouteType.None + } + } else { + if (this.path === RootRoutes.Onboarding) { + this.type = RouteType.Onboarding + } else { + this.type = RouteType.None + } + } + } + + get demoParams(): DemoParams { + if (this.type !== RouteType.Demo) { + throw new Error('Accessing invalid params') + } + + return { + token: this.searchParams.get(RootQueryParam.DemoToken) as string, + } + } + + get settingsParams(): SettingsParams { + if (this.type !== RouteType.Settings) { + throw new Error('Accessing invalid params') + } + + return { + panel: this.searchParams.get(RootQueryParam.Settings) as PreferenceId, + } + } + + get purchaseParams(): PurchaseParams { + if (this.type !== RouteType.Purchase) { + throw new Error('Accessing invalid params') + } + + return { + plan: this.searchParams.get('plan') as string, + period: this.searchParams.get('period') as string, + } + } + + get onboardingParams(): OnboardingParams { + if (this.type !== RouteType.Onboarding) { + throw new Error('Accessing invalid params') + } + + return { + fromHomepage: !!this.searchParams.get('from_homepage'), + } + } +} diff --git a/packages/ui-services/src/Route/RouteService.ts b/packages/ui-services/src/Route/RouteService.ts new file mode 100644 index 000000000..e71a1ce2e --- /dev/null +++ b/packages/ui-services/src/Route/RouteService.ts @@ -0,0 +1,50 @@ +import { + AbstractService, + ApplicationEvent, + ApplicationInterface, + InternalEventBusInterface, +} from '@standardnotes/services' +import { RouteParser } from './RouteParser' +import { RouteServiceEvent } from './RouteServiceEvent' + +export class RouteService extends AbstractService { + private unsubApp!: () => void + + constructor( + private application: ApplicationInterface, + protected override internalEventBus: InternalEventBusInterface, + ) { + super(internalEventBus) + this.addAppEventObserver() + } + + override deinit() { + super.deinit() + ;(this.application as unknown) = undefined + this.unsubApp() + } + + public getRoute(): RouteParser { + return new RouteParser(window.location.href) + } + + public removeSettingsFromURLQueryParameters() { + const urlSearchParams = new URLSearchParams(window.location.search) + urlSearchParams.delete('settings') + + const newUrl = `${window.location.origin}${window.location.pathname}${urlSearchParams.toString()}` + window.history.replaceState(null, document.title, newUrl) + } + + private addAppEventObserver() { + this.unsubApp = this.application.addEventObserver(async (event: ApplicationEvent) => { + if (event === ApplicationEvent.LocalDataLoaded) { + void this.notifyRouteChange() + } + }) + } + + private notifyRouteChange() { + void this.notifyEvent(RouteServiceEvent.RouteChanged, this.getRoute()) + } +} diff --git a/packages/ui-services/src/Route/RouteServiceEvent.ts b/packages/ui-services/src/Route/RouteServiceEvent.ts new file mode 100644 index 000000000..11becff03 --- /dev/null +++ b/packages/ui-services/src/Route/RouteServiceEvent.ts @@ -0,0 +1,3 @@ +export enum RouteServiceEvent { + RouteChanged = 'route-changed', +} diff --git a/packages/ui-services/src/Route/RouteType.ts b/packages/ui-services/src/Route/RouteType.ts new file mode 100644 index 000000000..0de59b6af --- /dev/null +++ b/packages/ui-services/src/Route/RouteType.ts @@ -0,0 +1,7 @@ +export enum RouteType { + Onboarding = 'onboarding', + Settings = 'settings', + Purchase = 'purchase', + Demo = 'demo', + None = 'none', +} diff --git a/packages/ui-services/src/index.ts b/packages/ui-services/src/index.ts index 10a482bde..87157f2dc 100644 --- a/packages/ui-services/src/index.ts +++ b/packages/ui-services/src/index.ts @@ -2,6 +2,12 @@ export * from './Alert/Functions' export * from './Alert/WebAlertService' export * from './Archive/ArchiveManager' export * from './IO/IOService' +export * from './Preferences/PreferenceId' +export * from './Route/RouteParams' +export * from './Route/RouteParser' +export * from './Route/RouteType' +export * from './Route/RouteService' +export * from './Route/RouteServiceEvent' export * from './Security/AutolockService' export * from './Storage/LocalStorage' export * from './Theme/ThemeManager' diff --git a/packages/web/src/javascripts/App.tsx b/packages/web/src/javascripts/App.tsx index 86e9b56fe..3e8e7b92a 100644 --- a/packages/web/src/javascripts/App.tsx +++ b/packages/web/src/javascripts/App.tsx @@ -1,7 +1,5 @@ 'use strict' -import { disableIosTextFieldZoom, isDev } from '@/Utils' - declare global { interface Window { dashboardUrl?: string @@ -25,8 +23,9 @@ declare global { } } +import { disableIosTextFieldZoom } from '@/Utils' import { IsWebPlatform, WebAppVersion } from '@/Constants/Version' -import { DesktopManagerInterface, Environment, Platform, SNLog } from '@standardnotes/snjs' +import { DesktopManagerInterface, Environment, SNLog } from '@standardnotes/snjs' import ApplicationGroupView from './Components/ApplicationGroupView/ApplicationGroupView' import { WebDevice } from './Application/Device/WebDevice' import { StartApplication } from './Application/Device/StartApplication' @@ -36,44 +35,14 @@ import { WebApplication } from './Application/Application' import { createRoot, Root } from 'react-dom/client' import { ElementIds } from './Constants/ElementIDs' import { MediaQueryBreakpoints } from './Hooks/useMediaQuery' +import { setViewportHeightWithFallback } from './setViewportHeightWithFallback' +import { setDefaultMonospaceFont } from './setDefaultMonospaceFont' let keyCount = 0 const getKey = () => { return keyCount++ } -const ViewportHeightKey = '--viewport-height' - -export const setViewportHeightWithFallback = () => { - const currentHeight = parseInt(document.documentElement.style.getPropertyValue(ViewportHeightKey)) - const newValue = visualViewport && visualViewport.height > 0 ? visualViewport.height : window.innerHeight - - if (isDev) { - // eslint-disable-next-line no-console - console.log(`currentHeight: ${currentHeight}, newValue: ${newValue}`) - } - - if (currentHeight && newValue < currentHeight) { - return - } - - if (!newValue) { - document.documentElement.style.setProperty(ViewportHeightKey, '100vh') - return - } - - document.documentElement.style.setProperty(ViewportHeightKey, `${newValue}px`) -} - -const setDefaultMonospaceFont = (platform?: Platform) => { - if (platform === Platform.Android) { - document.documentElement.style.setProperty( - '--sn-stylekit-monospace-font', - '"Roboto Mono", "Droid Sans Mono", monospace', - ) - } -} - const startApplication: StartApplication = async function startApplication( defaultSyncServerHost: string, device: WebOrDesktopDevice, diff --git a/packages/web/src/javascripts/Application/Application.ts b/packages/web/src/javascripts/Application/Application.ts index 9309ed76c..ea2111acb 100644 --- a/packages/web/src/javascripts/Application/Application.ts +++ b/packages/web/src/javascripts/Application/Application.ts @@ -19,25 +19,25 @@ import { WebApplicationInterface, MobileDeviceInterface, MobileUnlockTiming, + InternalEventBus, } from '@standardnotes/snjs' import { makeObservable, observable } from 'mobx' import { PanelResizedData } from '@/Types/PanelResizedData' import { isDesktopApplication } from '@/Utils' import { DesktopManager } from './Device/DesktopManager' -import { ArchiveManager, AutolockService, IOService, WebAlertService, ThemeManager } from '@standardnotes/ui-services' +import { + ArchiveManager, + AutolockService, + IOService, + RouteService, + ThemeManager, + WebAlertService, +} from '@standardnotes/ui-services' import { MobileWebReceiver } from './MobileWebReceiver' import { AndroidBackHandler } from '@/NativeMobileWeb/AndroidBackHandler' import { PrefDefaults } from '@/Constants/PrefDefaults' -import { setViewportHeightWithFallback } from '@/App' - -type WebServices = { - viewControllerManager: ViewControllerManager - desktopService?: DesktopManager - autolockService?: AutolockService - archiveService: ArchiveManager - themeService: ThemeManager - io: IOService -} +import { setViewportHeightWithFallback } from '@/setViewportHeightWithFallback' +import { WebServices } from './WebServices' export type WebEventObserver = (event: WebAppEvent, data?: unknown) => void @@ -49,6 +49,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter private onVisibilityChange: () => void private mobileWebReceiver?: MobileWebReceiver private androidBackHandler?: AndroidBackHandler + public readonly routeService: RouteService constructor( deviceInterface: WebOrDesktopDevice, @@ -75,8 +76,25 @@ export class WebApplication extends SNApplication implements WebApplicationInter }) deviceInterface.setApplication(this) + const internalEventBus = new InternalEventBus() + this.itemControllerGroup = new ItemGroupController(this) this.iconsController = new IconsController() + this.routeService = new RouteService(this, internalEventBus) + + const viewControllerManager = new ViewControllerManager(this, deviceInterface) + const archiveService = new ArchiveManager(this) + const io = new IOService(platform === Platform.MacWeb || platform === Platform.MacDesktop) + const themeService = new ThemeManager(this, internalEventBus) + + this.setWebServices({ + viewControllerManager, + archiveService, + desktopService: isDesktopDevice(deviceInterface) ? new DesktopManager(this, deviceInterface) : undefined, + io, + autolockService: this.isNativeMobileWeb() ? undefined : new AutolockService(this, internalEventBus), + themeService, + }) if (this.isNativeMobileWeb()) { this.mobileWebReceiver = new MobileWebReceiver(this) @@ -121,6 +139,9 @@ export class WebApplication extends SNApplication implements WebApplicationInter ;(this.itemControllerGroup as unknown) = undefined ;(this.mobileWebReceiver as unknown) = undefined + this.routeService.deinit() + ;(this.routeService as unknown) = undefined + this.webEventObservers.length = 0 document.removeEventListener('visibilitychange', this.onVisibilityChange) diff --git a/packages/web/src/javascripts/Application/ApplicationGroup.ts b/packages/web/src/javascripts/Application/ApplicationGroup.ts index f9c292de8..9a2430913 100644 --- a/packages/web/src/javascripts/Application/ApplicationGroup.ts +++ b/packages/web/src/javascripts/Application/ApplicationGroup.ts @@ -1,17 +1,7 @@ import { WebApplication } from './Application' -import { - ApplicationDescriptor, - SNApplicationGroup, - Platform, - InternalEventBus, - isDesktopDevice, -} from '@standardnotes/snjs' -import { ArchiveManager, IOService, AutolockService, ThemeManager } from '@standardnotes/ui-services' - -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' +import { ApplicationDescriptor, SNApplicationGroup } from '@standardnotes/snjs' import { getPlatform, isDesktopApplication } from '@/Utils' import { WebOrDesktopDevice } from '@/Application/Device/WebOrDesktopDevice' -import { DesktopManager } from './Device/DesktopManager' const createApplication = ( descriptor: ApplicationDescriptor, @@ -30,21 +20,6 @@ const createApplication = ( webSocketUrl, ) - const viewControllerManager = new ViewControllerManager(application, device) - const archiveService = new ArchiveManager(application) - const io = new IOService(platform === Platform.MacWeb || platform === Platform.MacDesktop) - const internalEventBus = new InternalEventBus() - const themeService = new ThemeManager(application, internalEventBus) - - application.setWebServices({ - viewControllerManager, - archiveService, - desktopService: isDesktopDevice(device) ? new DesktopManager(application, device) : undefined, - io, - autolockService: application.isNativeMobileWeb() ? undefined : new AutolockService(application, internalEventBus), - themeService, - }) - return application } diff --git a/packages/web/src/javascripts/Application/WebServices.ts b/packages/web/src/javascripts/Application/WebServices.ts new file mode 100644 index 000000000..8e7fa5906 --- /dev/null +++ b/packages/web/src/javascripts/Application/WebServices.ts @@ -0,0 +1,12 @@ +import { ViewControllerManager } from '@/Controllers/ViewControllerManager' +import { DesktopManager } from './Device/DesktopManager' +import { ArchiveManager, AutolockService, IOService, ThemeManager } from '@standardnotes/ui-services' + +export type WebServices = { + viewControllerManager: ViewControllerManager + desktopService?: DesktopManager + autolockService?: AutolockService + archiveService: ArchiveManager + themeService: ThemeManager + io: IOService +} diff --git a/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx b/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx index bfe7c5fda..597f5c57a 100644 --- a/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx +++ b/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx @@ -1,8 +1,8 @@ import { ApplicationGroup } from '@/Application/ApplicationGroup' -import { getPlatformString, getWindowUrlParams } from '@/Utils' +import { getPlatformString } from '@/Utils' import { ApplicationEvent, Challenge, removeFromArray, WebAppEvent } from '@standardnotes/snjs' import { PANEL_NAME_NOTES, PANEL_NAME_NAVIGATION } from '@/Constants/Constants' -import { alertDialog } from '@standardnotes/ui-services' +import { alertDialog, RouteType } from '@standardnotes/ui-services' import { WebApplication } from '@/Application/Application' import Navigation from '@/Components/Navigation/Navigation' import NoteGroupView from '@/Components/NoteGroupView/NoteGroupView' @@ -79,8 +79,13 @@ const ApplicationView: FunctionComponent = ({ application, mainApplicatio setNeedsUnlock(application.hasPasscode()) }, [application]) - const handleDemoSignInFromParams = useCallback(() => { - const token = getWindowUrlParams().get('demo-token') + const handleDemoSignInFromParamsIfApplicable = useCallback(() => { + const route = application.routeService.getRoute() + if (route.type !== RouteType.Demo) { + return + } + + const token = route.demoParams.token if (!token || application.hasAccount()) { return } @@ -91,8 +96,8 @@ const ApplicationView: FunctionComponent = ({ application, mainApplicatio const onAppLaunch = useCallback(() => { setLaunched(true) setNeedsUnlock(false) - handleDemoSignInFromParams() - }, [handleDemoSignInFromParams]) + handleDemoSignInFromParamsIfApplicable() + }, [handleDemoSignInFromParamsIfApplicable]) useEffect(() => { if (application.isStarted()) { diff --git a/packages/web/src/javascripts/Components/Footer/UpgradeNow.tsx b/packages/web/src/javascripts/Components/Footer/UpgradeNow.tsx index cdf627b66..e721c9588 100644 --- a/packages/web/src/javascripts/Components/Footer/UpgradeNow.tsx +++ b/packages/web/src/javascripts/Components/Footer/UpgradeNow.tsx @@ -25,7 +25,7 @@ const UpgradeNow = ({ application, featuresController }: Props) => { application.getViewControllerManager().purchaseFlowController.openPurchaseFlow() }} > - Upgrade now + {hasAccount ? 'Unlock features' : 'Sign up to sync'} ) : null diff --git a/packages/web/src/javascripts/Components/NoAccountWarning/NoAccountWarningContent.tsx b/packages/web/src/javascripts/Components/NoAccountWarning/NoAccountWarningContent.tsx index 48e3b4def..4f4baf376 100644 --- a/packages/web/src/javascripts/Components/NoAccountWarning/NoAccountWarningContent.tsx +++ b/packages/web/src/javascripts/Components/NoAccountWarning/NoAccountWarningContent.tsx @@ -26,7 +26,9 @@ const NoAccountWarningContent = ({ accountMenuController, noAccountWarningContro return (

Data not backed up

-

Sign in or register to back up your notes.

+

+ Sign in or register to sync your notes to your other devices with end-to-end encryption. +

diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/SubscriptionSharing/InvitationsList.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/SubscriptionSharing/InvitationsList.tsx index d56eb98f2..d10d8f6b5 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/SubscriptionSharing/InvitationsList.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/SubscriptionSharing/InvitationsList.tsx @@ -46,7 +46,7 @@ const InvitationsList = ({ subscriptionState, application }: Props) => { } if (usedInvitationsCount === 0) { - return Make your first subscription invitation below. + return Make your first subscription invite below. } return ( diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/SubscriptionSharing/NoProSubscription.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/SubscriptionSharing/NoProSubscription.tsx index 8b5937e0d..008427ad7 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/SubscriptionSharing/NoProSubscription.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/SubscriptionSharing/NoProSubscription.tsx @@ -30,7 +30,7 @@ const NoProSubscription: FunctionComponent = ({ application }) => { <> Subscription sharing is available only on the Professional plan. Please - upgrade in order to share subscription. + upgrade in order to share your subscription. {isLoadingPurchaseFlow && Redirecting you to the subscription page...} {purchaseFlowError && {purchaseFlowError}} diff --git a/packages/web/src/javascripts/Components/Preferences/PreferencesMenu.ts b/packages/web/src/javascripts/Components/Preferences/PreferencesMenu.ts index 2781c10f0..c9b731dad 100644 --- a/packages/web/src/javascripts/Components/Preferences/PreferencesMenu.ts +++ b/packages/web/src/javascripts/Components/Preferences/PreferencesMenu.ts @@ -3,21 +3,7 @@ import { IconType } from '@standardnotes/snjs' import { WebApplication } from '@/Application/Application' import { PackageProvider } from './Panes/General/Advanced/Packages/Provider/PackageProvider' import { securityPrefsHasBubble } from './Panes/Security/securityPrefsHasBubble' - -const PREFERENCE_IDS = [ - 'general', - 'account', - 'security', - 'appearance', - 'backups', - 'listed', - 'shortcuts', - 'accessibility', - 'get-free-month', - 'help-feedback', -] as const - -export type PreferenceId = typeof PREFERENCE_IDS[number] +import { PreferenceId } from '@standardnotes/ui-services' interface PreferencesMenuItem { readonly id: PreferenceId diff --git a/packages/web/src/javascripts/Components/Preferences/PreferencesMenuView.tsx b/packages/web/src/javascripts/Components/Preferences/PreferencesMenuView.tsx index d1df9bd55..2aa9a5ae5 100644 --- a/packages/web/src/javascripts/Components/Preferences/PreferencesMenuView.tsx +++ b/packages/web/src/javascripts/Components/Preferences/PreferencesMenuView.tsx @@ -4,7 +4,8 @@ import styled from 'styled-components' import Dropdown from '../Dropdown/Dropdown' import { DropdownItem } from '../Dropdown/DropdownItem' import PreferencesMenuItem from './PreferencesComponents/MenuItem' -import { PreferenceId, PreferencesMenu } from './PreferencesMenu' +import { PreferencesMenu } from './PreferencesMenu' +import { PreferenceId } from '@standardnotes/ui-services' type Props = { menu: PreferencesMenu diff --git a/packages/web/src/javascripts/Components/PurchaseFlow/PurchaseFlowFunctions.ts b/packages/web/src/javascripts/Components/PurchaseFlow/PurchaseFlowFunctions.ts index f72288b04..d3fb0c353 100644 --- a/packages/web/src/javascripts/Components/PurchaseFlow/PurchaseFlowFunctions.ts +++ b/packages/web/src/javascripts/Components/PurchaseFlow/PurchaseFlowFunctions.ts @@ -1,24 +1,30 @@ import { WebApplication } from '@/Application/Application' -import { getWindowUrlParams, isDesktopApplication } from '@/Utils' +import { isDesktopApplication } from '@/Utils' +import { RouteType } from '@standardnotes/ui-services' export const getPurchaseFlowUrl = async (application: WebApplication): Promise => { const currentUrl = window.location.origin const successUrl = isDesktopApplication() ? 'standardnotes://' : currentUrl + if (application.noAccount()) { return `${window.purchaseUrl}/offline?&success_url=${successUrl}` } + const token = await application.getNewSubscriptionToken() if (token) { return `${window.purchaseUrl}?subscription_token=${token}&success_url=${successUrl}` } + return undefined } export const loadPurchaseFlowUrl = async (application: WebApplication): Promise => { const url = await getPurchaseFlowUrl(application) - const params = getWindowUrlParams() - const period = params.get('period') ? `&period=${params.get('period')}` : '' - const plan = params.get('plan') ? `&plan=${params.get('plan')}` : '' + const route = application.routeService.getRoute() + const params = route.type === RouteType.Purchase ? route.purchaseParams : { period: null, plan: null } + const period = params.period ? `&period=${params.period}` : '' + const plan = params.plan ? `&plan=${params.plan}` : '' + if (url) { const finalUrl = `${url}${period}${plan}` @@ -31,5 +37,6 @@ export const loadPurchaseFlowUrl = async (application: WebApplication): Promise< return true } + return false } diff --git a/packages/web/src/javascripts/Controllers/PreferencesController.ts b/packages/web/src/javascripts/Controllers/PreferencesController.ts index 846e516d6..187c779e6 100644 --- a/packages/web/src/javascripts/Controllers/PreferencesController.ts +++ b/packages/web/src/javascripts/Controllers/PreferencesController.ts @@ -1,13 +1,18 @@ -import { PreferenceId } from '@/Components/Preferences/PreferencesMenu' +import { InternalEventBus } from '@standardnotes/snjs' import { action, computed, makeObservable, observable } from 'mobx' +import { PreferenceId } from '@standardnotes/ui-services' +import { AbstractViewController } from './Abstract/AbstractViewController' +import { WebApplication } from '@/Application/Application' -const DEFAULT_PANE = 'account' +const DEFAULT_PANE: PreferenceId = 'account' -export class PreferencesController { +export class PreferencesController extends AbstractViewController { private _open = false currentPane: PreferenceId = DEFAULT_PANE - constructor() { + constructor(application: WebApplication, eventBus: InternalEventBus) { + super(application, eventBus) + makeObservable(this, { _open: observable, currentPane: observable, @@ -29,18 +34,10 @@ export class PreferencesController { closePreferences = (): void => { this._open = false this.currentPane = DEFAULT_PANE - this.removePreferencesToggleFromURLQueryParameters() + this.application.routeService.removeSettingsFromURLQueryParameters() } get isOpen(): boolean { return this._open } - - private removePreferencesToggleFromURLQueryParameters() { - const urlSearchParams = new URLSearchParams(window.location.search) - urlSearchParams.delete('settings') - - const newUrl = `${window.location.origin}${window.location.pathname}${urlSearchParams.toString()}` - window.history.replaceState(null, document.title, newUrl) - } } diff --git a/packages/web/src/javascripts/Controllers/ViewControllerManager.ts b/packages/web/src/javascripts/Controllers/ViewControllerManager.ts index 081b606a7..e27f3c411 100644 --- a/packages/web/src/javascripts/Controllers/ViewControllerManager.ts +++ b/packages/web/src/javascripts/Controllers/ViewControllerManager.ts @@ -1,4 +1,4 @@ -import { storage, StorageKey } from '@standardnotes/ui-services' +import { RouteType, storage, StorageKey } from '@standardnotes/ui-services' import { WebApplication } from '@/Application/Application' import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController' import { destroyAllObjectProperties } from '@/Utils' @@ -28,7 +28,6 @@ import { NavigationController } from './Navigation/NavigationController' import { FilePreviewModalController } from './FilePreviewModalController' import { SelectedItemsController } from './SelectedItemsController' import { HistoryModalController } from './NoteHistory/HistoryModalController' -import { PreferenceId } from '@/Components/Preferences/PreferencesMenu' import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane' import { LinkingController } from './LinkingController' @@ -47,7 +46,7 @@ export class ViewControllerManager { readonly noAccountWarningController: NoAccountWarningController readonly notesController: NotesController readonly itemListController: ItemListController - readonly preferencesController = new PreferencesController() + readonly preferencesController: PreferencesController readonly purchaseFlowController: PurchaseFlowController readonly quickSettingsMenuController = new QuickSettingsController() readonly searchOptionsController: SearchOptionsController @@ -72,6 +71,8 @@ export class ViewControllerManager { this.subscriptionManager = application.subscriptions + this.preferencesController = new PreferencesController(application, this.eventBus) + this.selectionController = new SelectedItemsController(application, this.eventBus) this.featuresController = new FeaturesController(application, this.eventBus) @@ -220,30 +221,34 @@ export class ViewControllerManager { addAppEventObserver() { this.unsubAppEventObserver = this.application.addEventObserver(async (eventName) => { - const urlSearchParams = new URLSearchParams(window.location.search) - switch (eventName) { case ApplicationEvent.Launched: - if (urlSearchParams.get('purchase')) { - this.purchaseFlowController.openPurchaseFlow() - } - if (urlSearchParams.get('settings')) { - const user = this.application.getUser() - if (user === undefined) { - this.accountMenuController.setShow(true) - this.accountMenuController.setCurrentPane(AccountMenuPane.SignIn) - - break + { + const route = this.application.routeService.getRoute() + if (route.type === RouteType.Purchase) { + this.purchaseFlowController.openPurchaseFlow() } + if (route.type === RouteType.Settings) { + const user = this.application.getUser() + if (user === undefined) { + this.accountMenuController.setShow(true) + this.accountMenuController.setCurrentPane(AccountMenuPane.SignIn) - this.preferencesController.openPreferences() - this.preferencesController.setCurrentPane(urlSearchParams.get('settings') as PreferenceId) + break + } + + this.preferencesController.openPreferences() + this.preferencesController.setCurrentPane(route.settingsParams.panel) + } } break case ApplicationEvent.SignedIn: - if (urlSearchParams.get('settings')) { - this.preferencesController.openPreferences() - this.preferencesController.setCurrentPane(urlSearchParams.get('settings') as PreferenceId) + { + const route = this.application.routeService.getRoute() + if (route.type === RouteType.Settings) { + this.preferencesController.openPreferences() + this.preferencesController.setCurrentPane(route.settingsParams.panel) + } } break case ApplicationEvent.SyncStatusChanged: diff --git a/packages/web/src/javascripts/Utils/Utils.ts b/packages/web/src/javascripts/Utils/Utils.ts index 00e1d8d5b..d5f3ade46 100644 --- a/packages/web/src/javascripts/Utils/Utils.ts +++ b/packages/web/src/javascripts/Utils/Utils.ts @@ -160,10 +160,6 @@ export const isEmailValid = (email: string): boolean => { return EMAIL_REGEX.test(email) } -export const getWindowUrlParams = (): URLSearchParams => { - return new URLSearchParams(window.location.search) -} - export const openInNewTab = (url: string) => { const newWindow = window.open(url, '_blank', 'noopener,noreferrer') if (newWindow) { diff --git a/packages/web/src/javascripts/setDefaultMonospaceFont.tsx b/packages/web/src/javascripts/setDefaultMonospaceFont.tsx new file mode 100644 index 000000000..83c2f460a --- /dev/null +++ b/packages/web/src/javascripts/setDefaultMonospaceFont.tsx @@ -0,0 +1,10 @@ +import { Platform } from '@standardnotes/snjs' + +export const setDefaultMonospaceFont = (platform?: Platform) => { + if (platform === Platform.Android) { + document.documentElement.style.setProperty( + '--sn-stylekit-monospace-font', + '"Roboto Mono", "Droid Sans Mono", monospace', + ) + } +} diff --git a/packages/web/src/javascripts/setViewportHeightWithFallback.tsx b/packages/web/src/javascripts/setViewportHeightWithFallback.tsx new file mode 100644 index 000000000..922bac990 --- /dev/null +++ b/packages/web/src/javascripts/setViewportHeightWithFallback.tsx @@ -0,0 +1,24 @@ +import { isDev } from '@/Utils' + +export const ViewportHeightKey = '--viewport-height' + +export const setViewportHeightWithFallback = () => { + const currentHeight = parseInt(document.documentElement.style.getPropertyValue(ViewportHeightKey)) + const newValue = visualViewport && visualViewport.height > 0 ? visualViewport.height : window.innerHeight + + if (isDev) { + // eslint-disable-next-line no-console + console.log(`currentHeight: ${currentHeight}, newValue: ${newValue}`) + } + + if (currentHeight && newValue < currentHeight) { + return + } + + if (!newValue) { + document.documentElement.style.setProperty(ViewportHeightKey, '100vh') + return + } + + document.documentElement.style.setProperty(ViewportHeightKey, `${newValue}px`) +} diff --git a/packages/web/web.webpack.dev.js b/packages/web/web.webpack.dev.js index 7aa5e7723..d9f166109 100644 --- a/packages/web/web.webpack.dev.js +++ b/packages/web/web.webpack.dev.js @@ -12,11 +12,15 @@ module.exports = (env, argv) => { optimization: { minimize: false, }, + output: { + publicPath: '/', + }, plugins: [new ReactRefreshWebpackPlugin()], devServer: { hot: true, static: './dist', port, + historyApiFallback: true, devMiddleware: { writeToDisk: argv.writeToDisk, },