@tldraw/editor 3.15.4 → 3.15.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist-cjs/index.d.ts +2 -0
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/license/LicenseManager.js +28 -28
  5. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  6. package/dist-cjs/lib/license/LicenseProvider.js +5 -0
  7. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  8. package/dist-cjs/lib/license/Watermark.js +4 -4
  9. package/dist-cjs/lib/license/Watermark.js.map +1 -1
  10. package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
  11. package/dist-cjs/version.js +2 -2
  12. package/dist-cjs/version.js.map +1 -1
  13. package/dist-esm/index.d.mts +2 -0
  14. package/dist-esm/index.mjs +1 -1
  15. package/dist-esm/index.mjs.map +2 -2
  16. package/dist-esm/lib/license/LicenseManager.mjs +28 -28
  17. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  18. package/dist-esm/lib/license/LicenseProvider.mjs +5 -0
  19. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  20. package/dist-esm/lib/license/Watermark.mjs +4 -4
  21. package/dist-esm/lib/license/Watermark.mjs.map +1 -1
  22. package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
  23. package/dist-esm/version.mjs +2 -2
  24. package/dist-esm/version.mjs.map +1 -1
  25. package/package.json +7 -7
  26. package/src/index.ts +1 -0
  27. package/src/lib/license/LicenseManager.test.ts +58 -51
  28. package/src/lib/license/LicenseManager.ts +43 -30
  29. package/src/lib/license/LicenseProvider.tsx +8 -0
  30. package/src/lib/license/Watermark.tsx +4 -4
  31. package/src/lib/license/useLicenseManagerState.ts +2 -2
  32. package/src/version.ts +2 -2
@@ -3,7 +3,7 @@ import { publishDates } from '../../version'
3
3
  import { str2ab } from '../utils/licensing'
4
4
  import {
5
5
  FLAGS,
6
- isEditorUnlicensed,
6
+ getLicenseState,
7
7
  LicenseManager,
8
8
  PROPERTIES,
9
9
  ValidLicenseKeyResult,
@@ -485,115 +485,122 @@ function getDefaultLicenseResult(overrides: Partial<ValidLicenseKeyResult>): Val
485
485
  }
486
486
  }
487
487
 
488
- describe(isEditorUnlicensed, () => {
489
- it('shows watermark when license is not parseable', () => {
488
+ describe('getLicenseState', () => {
489
+ it('returns "unlicensed" for unparseable license', () => {
490
490
  const licenseResult = getDefaultLicenseResult({
491
491
  // @ts-ignore
492
492
  isLicenseParseable: false,
493
493
  })
494
- expect(isEditorUnlicensed(licenseResult)).toBe(true)
494
+ expect(getLicenseState(licenseResult)).toBe('unlicensed')
495
495
  })
496
496
 
497
- it('shows watermark when domain is not valid', () => {
497
+ it('returns "unlicensed" for invalid domain in production', () => {
498
498
  const licenseResult = getDefaultLicenseResult({
499
499
  isDomainValid: false,
500
+ isDevelopment: false,
500
501
  })
501
- expect(isEditorUnlicensed(licenseResult)).toBe(true)
502
+ expect(getLicenseState(licenseResult)).toBe('unlicensed')
502
503
  })
503
504
 
504
- it('shows watermark when annual license has expired', () => {
505
+ it('returns "licensed" for invalid domain in development mode', () => {
506
+ const licenseResult = getDefaultLicenseResult({
507
+ isDomainValid: false,
508
+ isDevelopment: true,
509
+ })
510
+ expect(getLicenseState(licenseResult)).toBe('licensed')
511
+ })
512
+
513
+ it('returns "unlicensed" for expired annual license', () => {
505
514
  const licenseResult = getDefaultLicenseResult({
506
515
  isAnnualLicense: true,
507
516
  isAnnualLicenseExpired: true,
517
+ isInternalLicense: false,
508
518
  })
509
- expect(isEditorUnlicensed(licenseResult)).toBe(true)
519
+ expect(getLicenseState(licenseResult)).toBe('unlicensed')
510
520
  })
511
521
 
512
- it('shows watermark when annual license has expired, even if dev mode', () => {
522
+ it('returns "unlicensed" for expired annual license even in dev mode', () => {
513
523
  const licenseResult = getDefaultLicenseResult({
514
524
  isAnnualLicense: true,
515
525
  isAnnualLicenseExpired: true,
516
526
  isDevelopment: true,
527
+ isInternalLicense: false,
517
528
  })
518
- expect(isEditorUnlicensed(licenseResult)).toBe(true)
529
+ expect(getLicenseState(licenseResult)).toBe('unlicensed')
519
530
  })
520
531
 
521
- it('shows watermark when perpetual license has expired', () => {
532
+ it('returns "unlicensed" for expired perpetual license', () => {
522
533
  const licenseResult = getDefaultLicenseResult({
523
534
  isPerpetualLicense: true,
524
535
  isPerpetualLicenseExpired: true,
536
+ isInternalLicense: false,
525
537
  })
526
- expect(isEditorUnlicensed(licenseResult)).toBe(true)
538
+ expect(getLicenseState(licenseResult)).toBe('unlicensed')
527
539
  })
528
540
 
529
- it('does not show watermark when license is valid and not expired', () => {
541
+ it('returns "internal-expired" for expired internal annual license with valid domain', () => {
542
+ const expiryDate = new Date(2023, 1, 1)
530
543
  const licenseResult = getDefaultLicenseResult({
531
544
  isAnnualLicense: true,
532
- isAnnualLicenseExpired: false,
533
- isInternalLicense: false,
545
+ isAnnualLicenseExpired: true,
546
+ isInternalLicense: true,
547
+ isDomainValid: true,
548
+ expiryDate,
534
549
  })
535
- expect(isEditorUnlicensed(licenseResult)).toBe(false)
550
+ expect(getLicenseState(licenseResult)).toBe('internal-expired')
536
551
  })
537
552
 
538
- it('does not show watermark when perpetual license is valid and not expired', () => {
553
+ it('returns "internal-expired" for expired internal perpetual license with valid domain', () => {
554
+ const expiryDate = new Date(2023, 1, 1)
539
555
  const licenseResult = getDefaultLicenseResult({
540
556
  isPerpetualLicense: true,
541
- isPerpetualLicenseExpired: false,
542
- isInternalLicense: false,
543
- })
544
- expect(isEditorUnlicensed(licenseResult)).toBe(false)
545
- })
546
-
547
- it('does not show watermark when in development mode', () => {
548
- const licenseResult = getDefaultLicenseResult({
549
- isDevelopment: true,
557
+ isPerpetualLicenseExpired: true,
558
+ isInternalLicense: true,
559
+ isDomainValid: true,
560
+ expiryDate,
550
561
  })
551
- expect(isEditorUnlicensed(licenseResult)).toBe(false)
562
+ expect(getLicenseState(licenseResult)).toBe('internal-expired')
552
563
  })
553
564
 
554
- it('does not show watermark when license is parseable and domain is valid', () => {
565
+ it('returns "unlicensed" for expired internal license with invalid domain', () => {
566
+ const expiryDate = new Date(2023, 1, 1)
555
567
  const licenseResult = getDefaultLicenseResult({
556
- isLicenseParseable: true,
557
- isDomainValid: true,
568
+ isAnnualLicense: true,
569
+ isAnnualLicenseExpired: true,
570
+ isInternalLicense: true,
571
+ isDomainValid: false,
572
+ expiryDate,
558
573
  })
559
- expect(isEditorUnlicensed(licenseResult)).toBe(false)
574
+ expect(getLicenseState(licenseResult)).toBe('unlicensed')
560
575
  })
561
576
 
562
- it('does not show watermark when license is parseable and domain is not valid and dev mode', () => {
577
+ it('returns "licensed-with-watermark" for watermarked license', () => {
563
578
  const licenseResult = getDefaultLicenseResult({
564
- isLicenseParseable: true,
565
- isDomainValid: false,
566
- isDevelopment: true,
579
+ isLicensedWithWatermark: true,
567
580
  })
568
- expect(isEditorUnlicensed(licenseResult)).toBe(false)
581
+ expect(getLicenseState(licenseResult)).toBe('licensed-with-watermark')
569
582
  })
570
583
 
571
- it('throws when an internal annual license has expired', () => {
572
- const expiryDate = new Date(2023, 1, 1)
584
+ it('returns "licensed" for valid annual license', () => {
573
585
  const licenseResult = getDefaultLicenseResult({
574
586
  isAnnualLicense: true,
575
- isAnnualLicenseExpired: true,
576
- isInternalLicense: true,
577
- expiryDate,
587
+ isAnnualLicenseExpired: false,
578
588
  })
579
- expect(() => isEditorUnlicensed(licenseResult)).toThrow(/License: Internal license expired/)
589
+ expect(getLicenseState(licenseResult)).toBe('licensed')
580
590
  })
581
591
 
582
- it('throws when an internal perpetual license has expired', () => {
583
- const expiryDate = new Date(2023, 1, 1)
592
+ it('returns "licensed" for valid perpetual license', () => {
584
593
  const licenseResult = getDefaultLicenseResult({
585
594
  isPerpetualLicense: true,
586
- isPerpetualLicenseExpired: true,
587
- isInternalLicense: true,
588
- expiryDate,
595
+ isPerpetualLicenseExpired: false,
589
596
  })
590
- expect(() => isEditorUnlicensed(licenseResult)).toThrow(/License: Internal license expired/)
597
+ expect(getLicenseState(licenseResult)).toBe('licensed')
591
598
  })
592
599
 
593
- it('shows watermark when license has that flag specified', () => {
600
+ it('returns "licensed" for valid license in development mode', () => {
594
601
  const licenseResult = getDefaultLicenseResult({
595
- isLicensedWithWatermark: true,
602
+ isDevelopment: true,
596
603
  })
597
- expect(isEditorUnlicensed(licenseResult)).toBe(false)
604
+ expect(getLicenseState(licenseResult)).toBe('licensed')
598
605
  })
599
606
  })
@@ -33,6 +33,14 @@ export interface LicenseInfo {
33
33
  flags: number
34
34
  expiryDate: string
35
35
  }
36
+
37
+ /** @internal */
38
+ export type LicenseState =
39
+ | 'pending'
40
+ | 'licensed'
41
+ | 'licensed-with-watermark'
42
+ | 'unlicensed'
43
+ | 'internal-expired'
36
44
  /** @internal */
37
45
  export type InvalidLicenseReason =
38
46
  | 'invalid-license-key'
@@ -73,10 +81,7 @@ export class LicenseManager {
73
81
  public isDevelopment: boolean
74
82
  public isTest: boolean
75
83
  public isCryptoAvailable: boolean
76
- state = atom<'pending' | 'licensed' | 'licensed-with-watermark' | 'unlicensed'>(
77
- 'license state',
78
- 'pending'
79
- )
84
+ state = atom<LicenseState>('license state', 'pending')
80
85
  public verbose = true
81
86
 
82
87
  constructor(
@@ -89,21 +94,20 @@ export class LicenseManager {
89
94
  this.publicKey = testPublicKey || this.publicKey
90
95
  this.isCryptoAvailable = !!crypto.subtle
91
96
 
92
- this.getLicenseFromKey(licenseKey).then((result) => {
93
- const isUnlicensed = isEditorUnlicensed(result)
97
+ this.getLicenseFromKey(licenseKey)
98
+ .then((result) => {
99
+ const licenseState = getLicenseState(result)
94
100
 
95
- if (!this.isDevelopment && isUnlicensed) {
96
- fetch(WATERMARK_TRACK_SRC)
97
- }
101
+ if (!this.isDevelopment && licenseState === 'unlicensed') {
102
+ fetch(WATERMARK_TRACK_SRC)
103
+ }
98
104
 
99
- if (isUnlicensed) {
105
+ this.state.set(licenseState)
106
+ })
107
+ .catch((error) => {
108
+ console.error('License validation failed:', error)
100
109
  this.state.set('unlicensed')
101
- } else if ((result as ValidLicenseKeyResult).isLicensedWithWatermark) {
102
- this.state.set('licensed-with-watermark')
103
- } else {
104
- this.state.set('licensed')
105
- }
106
- })
110
+ })
107
111
  }
108
112
 
109
113
  private getIsDevelopment(testEnvironment?: TestEnvironment) {
@@ -334,22 +338,27 @@ export class LicenseManager {
334
338
  // If we added a new flag it will be twice the value of the currently highest flag.
335
339
  // And if all the current flags are on we would get the `HIGHEST_FLAG * 2 - 1`, so anything higher than that means there are new flags.
336
340
  if (result.license.flags >= HIGHEST_FLAG * 2) {
337
- this.outputMessages([
338
- 'This tldraw license contains some unknown flags.',
339
- 'You may want to update tldraw packages to a newer version to get access to new functionality.',
340
- ])
341
+ this.outputMessages(
342
+ [
343
+ 'Warning: This tldraw license contains some unknown flags.',
344
+ 'This will still work, however, you may want to update tldraw packages to a newer version to get access to new functionality.',
345
+ ],
346
+ 'warning'
347
+ )
341
348
  }
342
349
  }
343
350
 
344
- private outputMessages(messages: string[]) {
351
+ private outputMessages(messages: string[], type: 'warning' | 'error' = 'error') {
345
352
  if (this.isTest) return
346
353
  if (this.verbose) {
347
354
  this.outputDelimiter()
348
355
  for (const message of messages) {
356
+ const color = type === 'warning' ? 'orange' : 'crimson'
357
+ const bgColor = type === 'warning' ? 'orange' : 'crimson'
349
358
  // eslint-disable-next-line no-console
350
359
  console.log(
351
360
  `%c${message}`,
352
- `color: white; background: crimson; padding: 2px; border-radius: 3px;`
361
+ `color: ${color}; background: ${bgColor}; padding: 2px; border-radius: 3px;`
353
362
  )
354
363
  }
355
364
  this.outputDelimiter()
@@ -367,15 +376,19 @@ export class LicenseManager {
367
376
  static className = 'tl-watermark_SEE-LICENSE'
368
377
  }
369
378
 
370
- export function isEditorUnlicensed(result: LicenseFromKeyResult) {
371
- if (!result.isLicenseParseable) return true
372
- if (!result.isDomainValid && !result.isDevelopment) return true
379
+ export function getLicenseState(result: LicenseFromKeyResult): LicenseState {
380
+ if (!result.isLicenseParseable) return 'unlicensed'
381
+ if (!result.isDomainValid && !result.isDevelopment) return 'unlicensed'
373
382
  if (result.isPerpetualLicenseExpired || result.isAnnualLicenseExpired) {
374
- if (result.isInternalLicense) {
375
- throw new Error('License: Internal license expired.')
376
- }
377
- return true
383
+ // Check if it's an expired internal license with valid domain
384
+ const internalExpired = result.isInternalLicense && result.isDomainValid
385
+ return internalExpired ? 'internal-expired' : 'unlicensed'
386
+ }
387
+
388
+ // License is valid, determine if it has watermark
389
+ if (result.isLicensedWithWatermark) {
390
+ return 'licensed-with-watermark'
378
391
  }
379
392
 
380
- return false
393
+ return 'licensed'
381
394
  }
@@ -1,3 +1,4 @@
1
+ import { useValue } from '@tldraw/state-react'
1
2
  import { createContext, ReactNode, useContext, useState } from 'react'
2
3
  import { LicenseManager } from './LicenseManager'
3
4
 
@@ -16,5 +17,12 @@ export function LicenseProvider({
16
17
  children: ReactNode
17
18
  }) {
18
19
  const [licenseManager] = useState(() => new LicenseManager(licenseKey))
20
+ const licenseState = useValue(licenseManager.state)
21
+
22
+ // If internal license has expired, don't render the editor at all
23
+ if (licenseState === 'internal-expired') {
24
+ return <div data-testid="tl-license-expired" style={{ display: 'none' }} />
25
+ }
26
+
19
27
  return <LicenseContext.Provider value={licenseManager}>{children}</LicenseContext.Provider>
20
28
  }
@@ -86,8 +86,8 @@ To remove the watermark, please purchase a license at tldraw.dev.
86
86
 
87
87
  .${className} {
88
88
  position: absolute;
89
- bottom: var(--space-2);
90
- right: var(--space-2);
89
+ bottom: max(var(--space-2), env(safe-area-inset-bottom));
90
+ right: max(var(--space-2), env(safe-area-inset-right));
91
91
  width: 96px;
92
92
  height: 32px;
93
93
  display: flex;
@@ -116,12 +116,12 @@ To remove the watermark, please purchase a license at tldraw.dev.
116
116
  }
117
117
 
118
118
  .${className}[data-debug='true'] {
119
- bottom: 46px;
119
+ bottom: max(46px, env(safe-area-inset-bottom));
120
120
  }
121
121
 
122
122
  .${className}[data-mobile='true'] {
123
123
  border-radius: 4px 0px 0px 4px;
124
- right: -2px;
124
+ right: max(-2px, calc(env(safe-area-inset-right) - 2px));
125
125
  width: 8px;
126
126
  height: 48px;
127
127
  }
@@ -1,7 +1,7 @@
1
1
  import { useValue } from '@tldraw/state-react'
2
- import { LicenseManager } from './LicenseManager'
2
+ import { LicenseManager, LicenseState } from './LicenseManager'
3
3
 
4
4
  /** @internal */
5
- export function useLicenseManagerState(licenseManager: LicenseManager) {
5
+ export function useLicenseManagerState(licenseManager: LicenseManager): LicenseState {
6
6
  return useValue('watermarkState', () => licenseManager.state.get(), [licenseManager])
7
7
  }
package/src/version.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  // This file is automatically generated by internal/scripts/refresh-assets.ts.
2
2
  // Do not edit manually. Or do, I'm a comment, not a cop.
3
3
 
4
- export const version = '3.15.4'
4
+ export const version = '3.15.5'
5
5
  export const publishDates = {
6
6
  major: '2024-09-13T14:36:29.063Z',
7
7
  minor: '2025-07-30T09:07:27.887Z',
8
- patch: '2025-08-28T08:59:06.034Z',
8
+ patch: '2025-09-30T13:16:39.779Z',
9
9
  }