feat: handle basic routes (#1784)
This commit is contained in:
@@ -20,7 +20,7 @@
|
|||||||
"prebuild": "yarn clean",
|
"prebuild": "yarn clean",
|
||||||
"build": "tsc -p tsconfig.json",
|
"build": "tsc -p tsconfig.json",
|
||||||
"lint": "eslint . --ext .ts",
|
"lint": "eslint . --ext .ts",
|
||||||
"test": "jest spec --coverage --passWithNoTests"
|
"test": "jest spec"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standardnotes/common": "^1.39.0",
|
"@standardnotes/common": "^1.39.0",
|
||||||
|
|||||||
14
packages/ui-services/src/Preferences/PreferenceId.ts
Normal file
14
packages/ui-services/src/Preferences/PreferenceId.ts
Normal file
@@ -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]
|
||||||
18
packages/ui-services/src/Route/RouteParams.ts
Normal file
18
packages/ui-services/src/Route/RouteParams.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
51
packages/ui-services/src/Route/RouteParser.spec.ts
Normal file
51
packages/ui-services/src/Route/RouteParser.spec.ts
Normal file
@@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
88
packages/ui-services/src/Route/RouteParser.ts
Normal file
88
packages/ui-services/src/Route/RouteParser.ts
Normal file
@@ -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'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
packages/ui-services/src/Route/RouteService.ts
Normal file
50
packages/ui-services/src/Route/RouteService.ts
Normal file
@@ -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<RouteServiceEvent, RouteParser> {
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
3
packages/ui-services/src/Route/RouteServiceEvent.ts
Normal file
3
packages/ui-services/src/Route/RouteServiceEvent.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export enum RouteServiceEvent {
|
||||||
|
RouteChanged = 'route-changed',
|
||||||
|
}
|
||||||
7
packages/ui-services/src/Route/RouteType.ts
Normal file
7
packages/ui-services/src/Route/RouteType.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export enum RouteType {
|
||||||
|
Onboarding = 'onboarding',
|
||||||
|
Settings = 'settings',
|
||||||
|
Purchase = 'purchase',
|
||||||
|
Demo = 'demo',
|
||||||
|
None = 'none',
|
||||||
|
}
|
||||||
@@ -2,6 +2,12 @@ export * from './Alert/Functions'
|
|||||||
export * from './Alert/WebAlertService'
|
export * from './Alert/WebAlertService'
|
||||||
export * from './Archive/ArchiveManager'
|
export * from './Archive/ArchiveManager'
|
||||||
export * from './IO/IOService'
|
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 './Security/AutolockService'
|
||||||
export * from './Storage/LocalStorage'
|
export * from './Storage/LocalStorage'
|
||||||
export * from './Theme/ThemeManager'
|
export * from './Theme/ThemeManager'
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
import { disableIosTextFieldZoom, isDev } from '@/Utils'
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
dashboardUrl?: string
|
dashboardUrl?: string
|
||||||
@@ -25,8 +23,9 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { disableIosTextFieldZoom } from '@/Utils'
|
||||||
import { IsWebPlatform, WebAppVersion } from '@/Constants/Version'
|
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 ApplicationGroupView from './Components/ApplicationGroupView/ApplicationGroupView'
|
||||||
import { WebDevice } from './Application/Device/WebDevice'
|
import { WebDevice } from './Application/Device/WebDevice'
|
||||||
import { StartApplication } from './Application/Device/StartApplication'
|
import { StartApplication } from './Application/Device/StartApplication'
|
||||||
@@ -36,44 +35,14 @@ import { WebApplication } from './Application/Application'
|
|||||||
import { createRoot, Root } from 'react-dom/client'
|
import { createRoot, Root } from 'react-dom/client'
|
||||||
import { ElementIds } from './Constants/ElementIDs'
|
import { ElementIds } from './Constants/ElementIDs'
|
||||||
import { MediaQueryBreakpoints } from './Hooks/useMediaQuery'
|
import { MediaQueryBreakpoints } from './Hooks/useMediaQuery'
|
||||||
|
import { setViewportHeightWithFallback } from './setViewportHeightWithFallback'
|
||||||
|
import { setDefaultMonospaceFont } from './setDefaultMonospaceFont'
|
||||||
|
|
||||||
let keyCount = 0
|
let keyCount = 0
|
||||||
const getKey = () => {
|
const getKey = () => {
|
||||||
return keyCount++
|
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(
|
const startApplication: StartApplication = async function startApplication(
|
||||||
defaultSyncServerHost: string,
|
defaultSyncServerHost: string,
|
||||||
device: WebOrDesktopDevice,
|
device: WebOrDesktopDevice,
|
||||||
|
|||||||
@@ -19,25 +19,25 @@ import {
|
|||||||
WebApplicationInterface,
|
WebApplicationInterface,
|
||||||
MobileDeviceInterface,
|
MobileDeviceInterface,
|
||||||
MobileUnlockTiming,
|
MobileUnlockTiming,
|
||||||
|
InternalEventBus,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { makeObservable, observable } from 'mobx'
|
import { makeObservable, observable } from 'mobx'
|
||||||
import { PanelResizedData } from '@/Types/PanelResizedData'
|
import { PanelResizedData } from '@/Types/PanelResizedData'
|
||||||
import { isDesktopApplication } from '@/Utils'
|
import { isDesktopApplication } from '@/Utils'
|
||||||
import { DesktopManager } from './Device/DesktopManager'
|
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 { MobileWebReceiver } from './MobileWebReceiver'
|
||||||
import { AndroidBackHandler } from '@/NativeMobileWeb/AndroidBackHandler'
|
import { AndroidBackHandler } from '@/NativeMobileWeb/AndroidBackHandler'
|
||||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||||
import { setViewportHeightWithFallback } from '@/App'
|
import { setViewportHeightWithFallback } from '@/setViewportHeightWithFallback'
|
||||||
|
import { WebServices } from './WebServices'
|
||||||
type WebServices = {
|
|
||||||
viewControllerManager: ViewControllerManager
|
|
||||||
desktopService?: DesktopManager
|
|
||||||
autolockService?: AutolockService
|
|
||||||
archiveService: ArchiveManager
|
|
||||||
themeService: ThemeManager
|
|
||||||
io: IOService
|
|
||||||
}
|
|
||||||
|
|
||||||
export type WebEventObserver = (event: WebAppEvent, data?: unknown) => void
|
export type WebEventObserver = (event: WebAppEvent, data?: unknown) => void
|
||||||
|
|
||||||
@@ -49,6 +49,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
private onVisibilityChange: () => void
|
private onVisibilityChange: () => void
|
||||||
private mobileWebReceiver?: MobileWebReceiver
|
private mobileWebReceiver?: MobileWebReceiver
|
||||||
private androidBackHandler?: AndroidBackHandler
|
private androidBackHandler?: AndroidBackHandler
|
||||||
|
public readonly routeService: RouteService
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
deviceInterface: WebOrDesktopDevice,
|
deviceInterface: WebOrDesktopDevice,
|
||||||
@@ -75,8 +76,25 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
})
|
})
|
||||||
|
|
||||||
deviceInterface.setApplication(this)
|
deviceInterface.setApplication(this)
|
||||||
|
const internalEventBus = new InternalEventBus()
|
||||||
|
|
||||||
this.itemControllerGroup = new ItemGroupController(this)
|
this.itemControllerGroup = new ItemGroupController(this)
|
||||||
this.iconsController = new IconsController()
|
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()) {
|
if (this.isNativeMobileWeb()) {
|
||||||
this.mobileWebReceiver = new MobileWebReceiver(this)
|
this.mobileWebReceiver = new MobileWebReceiver(this)
|
||||||
@@ -121,6 +139,9 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
;(this.itemControllerGroup as unknown) = undefined
|
;(this.itemControllerGroup as unknown) = undefined
|
||||||
;(this.mobileWebReceiver as unknown) = undefined
|
;(this.mobileWebReceiver as unknown) = undefined
|
||||||
|
|
||||||
|
this.routeService.deinit()
|
||||||
|
;(this.routeService as unknown) = undefined
|
||||||
|
|
||||||
this.webEventObservers.length = 0
|
this.webEventObservers.length = 0
|
||||||
|
|
||||||
document.removeEventListener('visibilitychange', this.onVisibilityChange)
|
document.removeEventListener('visibilitychange', this.onVisibilityChange)
|
||||||
|
|||||||
@@ -1,17 +1,7 @@
|
|||||||
import { WebApplication } from './Application'
|
import { WebApplication } from './Application'
|
||||||
import {
|
import { ApplicationDescriptor, SNApplicationGroup } from '@standardnotes/snjs'
|
||||||
ApplicationDescriptor,
|
|
||||||
SNApplicationGroup,
|
|
||||||
Platform,
|
|
||||||
InternalEventBus,
|
|
||||||
isDesktopDevice,
|
|
||||||
} from '@standardnotes/snjs'
|
|
||||||
import { ArchiveManager, IOService, AutolockService, ThemeManager } from '@standardnotes/ui-services'
|
|
||||||
|
|
||||||
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
|
||||||
import { getPlatform, isDesktopApplication } from '@/Utils'
|
import { getPlatform, isDesktopApplication } from '@/Utils'
|
||||||
import { WebOrDesktopDevice } from '@/Application/Device/WebOrDesktopDevice'
|
import { WebOrDesktopDevice } from '@/Application/Device/WebOrDesktopDevice'
|
||||||
import { DesktopManager } from './Device/DesktopManager'
|
|
||||||
|
|
||||||
const createApplication = (
|
const createApplication = (
|
||||||
descriptor: ApplicationDescriptor,
|
descriptor: ApplicationDescriptor,
|
||||||
@@ -30,21 +20,6 @@ const createApplication = (
|
|||||||
webSocketUrl,
|
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
|
return application
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
packages/web/src/javascripts/Application/WebServices.ts
Normal file
12
packages/web/src/javascripts/Application/WebServices.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ApplicationGroup } from '@/Application/ApplicationGroup'
|
import { ApplicationGroup } from '@/Application/ApplicationGroup'
|
||||||
import { getPlatformString, getWindowUrlParams } from '@/Utils'
|
import { getPlatformString } from '@/Utils'
|
||||||
import { ApplicationEvent, Challenge, removeFromArray, WebAppEvent } from '@standardnotes/snjs'
|
import { ApplicationEvent, Challenge, removeFromArray, WebAppEvent } from '@standardnotes/snjs'
|
||||||
import { PANEL_NAME_NOTES, PANEL_NAME_NAVIGATION } from '@/Constants/Constants'
|
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 { WebApplication } from '@/Application/Application'
|
||||||
import Navigation from '@/Components/Navigation/Navigation'
|
import Navigation from '@/Components/Navigation/Navigation'
|
||||||
import NoteGroupView from '@/Components/NoteGroupView/NoteGroupView'
|
import NoteGroupView from '@/Components/NoteGroupView/NoteGroupView'
|
||||||
@@ -79,8 +79,13 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
|||||||
setNeedsUnlock(application.hasPasscode())
|
setNeedsUnlock(application.hasPasscode())
|
||||||
}, [application])
|
}, [application])
|
||||||
|
|
||||||
const handleDemoSignInFromParams = useCallback(() => {
|
const handleDemoSignInFromParamsIfApplicable = useCallback(() => {
|
||||||
const token = getWindowUrlParams().get('demo-token')
|
const route = application.routeService.getRoute()
|
||||||
|
if (route.type !== RouteType.Demo) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = route.demoParams.token
|
||||||
if (!token || application.hasAccount()) {
|
if (!token || application.hasAccount()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -91,8 +96,8 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
|||||||
const onAppLaunch = useCallback(() => {
|
const onAppLaunch = useCallback(() => {
|
||||||
setLaunched(true)
|
setLaunched(true)
|
||||||
setNeedsUnlock(false)
|
setNeedsUnlock(false)
|
||||||
handleDemoSignInFromParams()
|
handleDemoSignInFromParamsIfApplicable()
|
||||||
}, [handleDemoSignInFromParams])
|
}, [handleDemoSignInFromParamsIfApplicable])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (application.isStarted()) {
|
if (application.isStarted()) {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const UpgradeNow = ({ application, featuresController }: Props) => {
|
|||||||
application.getViewControllerManager().purchaseFlowController.openPurchaseFlow()
|
application.getViewControllerManager().purchaseFlowController.openPurchaseFlow()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Upgrade now
|
{hasAccount ? 'Unlock features' : 'Sign up to sync'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null
|
) : null
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ const NoAccountWarningContent = ({ accountMenuController, noAccountWarningContro
|
|||||||
return (
|
return (
|
||||||
<div className="mt-4 grid grid-cols-1 rounded-md border border-border p-4">
|
<div className="mt-4 grid grid-cols-1 rounded-md border border-border p-4">
|
||||||
<h1 className="sk-h3 m-0 text-sm font-semibold">Data not backed up</h1>
|
<h1 className="sk-h3 m-0 text-sm font-semibold">Data not backed up</h1>
|
||||||
<p className="col-start-1 col-end-3 m-0 mt-1 text-sm">Sign in or register to back up your notes.</p>
|
<p className="col-start-1 col-end-3 m-0 mt-1 text-sm">
|
||||||
|
Sign in or register to sync your notes to your other devices with end-to-end encryption.
|
||||||
|
</p>
|
||||||
<Button primary small className="col-start-1 col-end-3 mt-3 justify-self-start" onClick={showAccountMenu}>
|
<Button primary small className="col-start-1 col-end-3 mt-3 justify-self-start" onClick={showAccountMenu}>
|
||||||
Open Account menu
|
Open Account menu
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const InvitationsList = ({ subscriptionState, application }: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (usedInvitationsCount === 0) {
|
if (usedInvitationsCount === 0) {
|
||||||
return <Text className="mt-1 mb-3">Make your first subscription invitation below.</Text>
|
return <Text className="mt-1 mb-3">Make your first subscription invite below.</Text>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const NoProSubscription: FunctionComponent<Props> = ({ application }) => {
|
|||||||
<>
|
<>
|
||||||
<Text>
|
<Text>
|
||||||
Subscription sharing is available only on the <span className="font-bold">Professional</span> plan. Please
|
Subscription sharing is available only on the <span className="font-bold">Professional</span> plan. Please
|
||||||
upgrade in order to share subscription.
|
upgrade in order to share your subscription.
|
||||||
</Text>
|
</Text>
|
||||||
{isLoadingPurchaseFlow && <Text>Redirecting you to the subscription page...</Text>}
|
{isLoadingPurchaseFlow && <Text>Redirecting you to the subscription page...</Text>}
|
||||||
{purchaseFlowError && <Text className="text-danger">{purchaseFlowError}</Text>}
|
{purchaseFlowError && <Text className="text-danger">{purchaseFlowError}</Text>}
|
||||||
|
|||||||
@@ -3,21 +3,7 @@ import { IconType } from '@standardnotes/snjs'
|
|||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import { PackageProvider } from './Panes/General/Advanced/Packages/Provider/PackageProvider'
|
import { PackageProvider } from './Panes/General/Advanced/Packages/Provider/PackageProvider'
|
||||||
import { securityPrefsHasBubble } from './Panes/Security/securityPrefsHasBubble'
|
import { securityPrefsHasBubble } from './Panes/Security/securityPrefsHasBubble'
|
||||||
|
import { PreferenceId } from '@standardnotes/ui-services'
|
||||||
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]
|
|
||||||
|
|
||||||
interface PreferencesMenuItem {
|
interface PreferencesMenuItem {
|
||||||
readonly id: PreferenceId
|
readonly id: PreferenceId
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import styled from 'styled-components'
|
|||||||
import Dropdown from '../Dropdown/Dropdown'
|
import Dropdown from '../Dropdown/Dropdown'
|
||||||
import { DropdownItem } from '../Dropdown/DropdownItem'
|
import { DropdownItem } from '../Dropdown/DropdownItem'
|
||||||
import PreferencesMenuItem from './PreferencesComponents/MenuItem'
|
import PreferencesMenuItem from './PreferencesComponents/MenuItem'
|
||||||
import { PreferenceId, PreferencesMenu } from './PreferencesMenu'
|
import { PreferencesMenu } from './PreferencesMenu'
|
||||||
|
import { PreferenceId } from '@standardnotes/ui-services'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
menu: PreferencesMenu
|
menu: PreferencesMenu
|
||||||
|
|||||||
@@ -1,24 +1,30 @@
|
|||||||
import { WebApplication } from '@/Application/Application'
|
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<string | undefined> => {
|
export const getPurchaseFlowUrl = async (application: WebApplication): Promise<string | undefined> => {
|
||||||
const currentUrl = window.location.origin
|
const currentUrl = window.location.origin
|
||||||
const successUrl = isDesktopApplication() ? 'standardnotes://' : currentUrl
|
const successUrl = isDesktopApplication() ? 'standardnotes://' : currentUrl
|
||||||
|
|
||||||
if (application.noAccount()) {
|
if (application.noAccount()) {
|
||||||
return `${window.purchaseUrl}/offline?&success_url=${successUrl}`
|
return `${window.purchaseUrl}/offline?&success_url=${successUrl}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = await application.getNewSubscriptionToken()
|
const token = await application.getNewSubscriptionToken()
|
||||||
if (token) {
|
if (token) {
|
||||||
return `${window.purchaseUrl}?subscription_token=${token}&success_url=${successUrl}`
|
return `${window.purchaseUrl}?subscription_token=${token}&success_url=${successUrl}`
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadPurchaseFlowUrl = async (application: WebApplication): Promise<boolean> => {
|
export const loadPurchaseFlowUrl = async (application: WebApplication): Promise<boolean> => {
|
||||||
const url = await getPurchaseFlowUrl(application)
|
const url = await getPurchaseFlowUrl(application)
|
||||||
const params = getWindowUrlParams()
|
const route = application.routeService.getRoute()
|
||||||
const period = params.get('period') ? `&period=${params.get('period')}` : ''
|
const params = route.type === RouteType.Purchase ? route.purchaseParams : { period: null, plan: null }
|
||||||
const plan = params.get('plan') ? `&plan=${params.get('plan')}` : ''
|
const period = params.period ? `&period=${params.period}` : ''
|
||||||
|
const plan = params.plan ? `&plan=${params.plan}` : ''
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
const finalUrl = `${url}${period}${plan}`
|
const finalUrl = `${url}${period}${plan}`
|
||||||
|
|
||||||
@@ -31,5 +37,6 @@ export const loadPurchaseFlowUrl = async (application: WebApplication): Promise<
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { PreferenceId } from '@/Components/Preferences/PreferencesMenu'
|
import { InternalEventBus } from '@standardnotes/snjs'
|
||||||
import { action, computed, makeObservable, observable } from 'mobx'
|
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
|
private _open = false
|
||||||
currentPane: PreferenceId = DEFAULT_PANE
|
currentPane: PreferenceId = DEFAULT_PANE
|
||||||
|
|
||||||
constructor() {
|
constructor(application: WebApplication, eventBus: InternalEventBus) {
|
||||||
|
super(application, eventBus)
|
||||||
|
|
||||||
makeObservable<PreferencesController, '_open'>(this, {
|
makeObservable<PreferencesController, '_open'>(this, {
|
||||||
_open: observable,
|
_open: observable,
|
||||||
currentPane: observable,
|
currentPane: observable,
|
||||||
@@ -29,18 +34,10 @@ export class PreferencesController {
|
|||||||
closePreferences = (): void => {
|
closePreferences = (): void => {
|
||||||
this._open = false
|
this._open = false
|
||||||
this.currentPane = DEFAULT_PANE
|
this.currentPane = DEFAULT_PANE
|
||||||
this.removePreferencesToggleFromURLQueryParameters()
|
this.application.routeService.removeSettingsFromURLQueryParameters()
|
||||||
}
|
}
|
||||||
|
|
||||||
get isOpen(): boolean {
|
get isOpen(): boolean {
|
||||||
return this._open
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { WebApplication } from '@/Application/Application'
|
||||||
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
|
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
|
||||||
import { destroyAllObjectProperties } from '@/Utils'
|
import { destroyAllObjectProperties } from '@/Utils'
|
||||||
@@ -28,7 +28,6 @@ import { NavigationController } from './Navigation/NavigationController'
|
|||||||
import { FilePreviewModalController } from './FilePreviewModalController'
|
import { FilePreviewModalController } from './FilePreviewModalController'
|
||||||
import { SelectedItemsController } from './SelectedItemsController'
|
import { SelectedItemsController } from './SelectedItemsController'
|
||||||
import { HistoryModalController } from './NoteHistory/HistoryModalController'
|
import { HistoryModalController } from './NoteHistory/HistoryModalController'
|
||||||
import { PreferenceId } from '@/Components/Preferences/PreferencesMenu'
|
|
||||||
import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane'
|
import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane'
|
||||||
import { LinkingController } from './LinkingController'
|
import { LinkingController } from './LinkingController'
|
||||||
|
|
||||||
@@ -47,7 +46,7 @@ export class ViewControllerManager {
|
|||||||
readonly noAccountWarningController: NoAccountWarningController
|
readonly noAccountWarningController: NoAccountWarningController
|
||||||
readonly notesController: NotesController
|
readonly notesController: NotesController
|
||||||
readonly itemListController: ItemListController
|
readonly itemListController: ItemListController
|
||||||
readonly preferencesController = new PreferencesController()
|
readonly preferencesController: PreferencesController
|
||||||
readonly purchaseFlowController: PurchaseFlowController
|
readonly purchaseFlowController: PurchaseFlowController
|
||||||
readonly quickSettingsMenuController = new QuickSettingsController()
|
readonly quickSettingsMenuController = new QuickSettingsController()
|
||||||
readonly searchOptionsController: SearchOptionsController
|
readonly searchOptionsController: SearchOptionsController
|
||||||
@@ -72,6 +71,8 @@ export class ViewControllerManager {
|
|||||||
|
|
||||||
this.subscriptionManager = application.subscriptions
|
this.subscriptionManager = application.subscriptions
|
||||||
|
|
||||||
|
this.preferencesController = new PreferencesController(application, this.eventBus)
|
||||||
|
|
||||||
this.selectionController = new SelectedItemsController(application, this.eventBus)
|
this.selectionController = new SelectedItemsController(application, this.eventBus)
|
||||||
|
|
||||||
this.featuresController = new FeaturesController(application, this.eventBus)
|
this.featuresController = new FeaturesController(application, this.eventBus)
|
||||||
@@ -220,30 +221,34 @@ export class ViewControllerManager {
|
|||||||
|
|
||||||
addAppEventObserver() {
|
addAppEventObserver() {
|
||||||
this.unsubAppEventObserver = this.application.addEventObserver(async (eventName) => {
|
this.unsubAppEventObserver = this.application.addEventObserver(async (eventName) => {
|
||||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
|
||||||
|
|
||||||
switch (eventName) {
|
switch (eventName) {
|
||||||
case ApplicationEvent.Launched:
|
case ApplicationEvent.Launched:
|
||||||
if (urlSearchParams.get('purchase')) {
|
{
|
||||||
this.purchaseFlowController.openPurchaseFlow()
|
const route = this.application.routeService.getRoute()
|
||||||
}
|
if (route.type === RouteType.Purchase) {
|
||||||
if (urlSearchParams.get('settings')) {
|
this.purchaseFlowController.openPurchaseFlow()
|
||||||
const user = this.application.getUser()
|
|
||||||
if (user === undefined) {
|
|
||||||
this.accountMenuController.setShow(true)
|
|
||||||
this.accountMenuController.setCurrentPane(AccountMenuPane.SignIn)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
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()
|
break
|
||||||
this.preferencesController.setCurrentPane(urlSearchParams.get('settings') as PreferenceId)
|
}
|
||||||
|
|
||||||
|
this.preferencesController.openPreferences()
|
||||||
|
this.preferencesController.setCurrentPane(route.settingsParams.panel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case ApplicationEvent.SignedIn:
|
case ApplicationEvent.SignedIn:
|
||||||
if (urlSearchParams.get('settings')) {
|
{
|
||||||
this.preferencesController.openPreferences()
|
const route = this.application.routeService.getRoute()
|
||||||
this.preferencesController.setCurrentPane(urlSearchParams.get('settings') as PreferenceId)
|
if (route.type === RouteType.Settings) {
|
||||||
|
this.preferencesController.openPreferences()
|
||||||
|
this.preferencesController.setCurrentPane(route.settingsParams.panel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case ApplicationEvent.SyncStatusChanged:
|
case ApplicationEvent.SyncStatusChanged:
|
||||||
|
|||||||
@@ -160,10 +160,6 @@ export const isEmailValid = (email: string): boolean => {
|
|||||||
return EMAIL_REGEX.test(email)
|
return EMAIL_REGEX.test(email)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getWindowUrlParams = (): URLSearchParams => {
|
|
||||||
return new URLSearchParams(window.location.search)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const openInNewTab = (url: string) => {
|
export const openInNewTab = (url: string) => {
|
||||||
const newWindow = window.open(url, '_blank', 'noopener,noreferrer')
|
const newWindow = window.open(url, '_blank', 'noopener,noreferrer')
|
||||||
if (newWindow) {
|
if (newWindow) {
|
||||||
|
|||||||
10
packages/web/src/javascripts/setDefaultMonospaceFont.tsx
Normal file
10
packages/web/src/javascripts/setDefaultMonospaceFont.tsx
Normal file
@@ -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',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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`)
|
||||||
|
}
|
||||||
@@ -12,11 +12,15 @@ module.exports = (env, argv) => {
|
|||||||
optimization: {
|
optimization: {
|
||||||
minimize: false,
|
minimize: false,
|
||||||
},
|
},
|
||||||
|
output: {
|
||||||
|
publicPath: '/',
|
||||||
|
},
|
||||||
plugins: [new ReactRefreshWebpackPlugin()],
|
plugins: [new ReactRefreshWebpackPlugin()],
|
||||||
devServer: {
|
devServer: {
|
||||||
hot: true,
|
hot: true,
|
||||||
static: './dist',
|
static: './dist',
|
||||||
port,
|
port,
|
||||||
|
historyApiFallback: true,
|
||||||
devMiddleware: {
|
devMiddleware: {
|
||||||
writeToDisk: argv.writeToDisk,
|
writeToDisk: argv.writeToDisk,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user