@tldraw/editor 3.16.0-next.fe14f1b4181f → 4.0.1

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 (143) hide show
  1. package/dist-cjs/index.d.ts +91 -111
  2. package/dist-cjs/index.js +3 -5
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +1 -7
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +11 -1
  7. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  8. package/dist-cjs/lib/config/TLUserPreferences.js +15 -4
  9. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  10. package/dist-cjs/lib/editor/Editor.js +58 -114
  11. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  12. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +4 -0
  13. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +2 -2
  14. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js +4 -2
  15. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +2 -2
  16. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +11 -6
  17. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  18. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +10 -0
  19. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  20. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  21. package/dist-cjs/lib/hooks/useCanvasEvents.js +19 -16
  22. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  23. package/dist-cjs/lib/hooks/useDocumentEvents.js +5 -5
  24. package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
  25. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js +1 -2
  26. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
  27. package/dist-cjs/lib/hooks/useGestureEvents.js +1 -1
  28. package/dist-cjs/lib/hooks/useGestureEvents.js.map +2 -2
  29. package/dist-cjs/lib/hooks/useHandleEvents.js +6 -6
  30. package/dist-cjs/lib/hooks/useHandleEvents.js.map +2 -2
  31. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js +4 -1
  32. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +2 -2
  33. package/dist-cjs/lib/hooks/useSelectionEvents.js +8 -8
  34. package/dist-cjs/lib/hooks/useSelectionEvents.js.map +2 -2
  35. package/dist-cjs/lib/license/LicenseManager.js +147 -59
  36. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  37. package/dist-cjs/lib/license/LicenseProvider.js +39 -1
  38. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  39. package/dist-cjs/lib/license/Watermark.js +144 -75
  40. package/dist-cjs/lib/license/Watermark.js.map +3 -3
  41. package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
  42. package/dist-cjs/lib/primitives/Vec.js +0 -4
  43. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  44. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +50 -20
  45. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  46. package/dist-cjs/lib/primitives/geometry/Group2d.js +8 -1
  47. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  48. package/dist-cjs/lib/utils/dom.js.map +2 -2
  49. package/dist-cjs/lib/utils/getPointerInfo.js +2 -3
  50. package/dist-cjs/lib/utils/getPointerInfo.js.map +2 -2
  51. package/dist-cjs/lib/utils/reparenting.js +7 -36
  52. package/dist-cjs/lib/utils/reparenting.js.map +3 -3
  53. package/dist-cjs/version.js +4 -4
  54. package/dist-cjs/version.js.map +1 -1
  55. package/dist-esm/index.d.mts +91 -111
  56. package/dist-esm/index.mjs +3 -5
  57. package/dist-esm/index.mjs.map +2 -2
  58. package/dist-esm/lib/TldrawEditor.mjs +1 -7
  59. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  60. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +11 -1
  61. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  62. package/dist-esm/lib/config/TLUserPreferences.mjs +15 -4
  63. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  64. package/dist-esm/lib/editor/Editor.mjs +58 -114
  65. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  66. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +4 -0
  67. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
  68. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs +4 -2
  69. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +2 -2
  70. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +11 -6
  71. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  72. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +10 -0
  73. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  74. package/dist-esm/lib/hooks/useCanvasEvents.mjs +20 -22
  75. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  76. package/dist-esm/lib/hooks/useDocumentEvents.mjs +6 -6
  77. package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
  78. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs +1 -2
  79. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
  80. package/dist-esm/lib/hooks/useGestureEvents.mjs +2 -2
  81. package/dist-esm/lib/hooks/useGestureEvents.mjs.map +2 -2
  82. package/dist-esm/lib/hooks/useHandleEvents.mjs +6 -6
  83. package/dist-esm/lib/hooks/useHandleEvents.mjs.map +2 -2
  84. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs +4 -1
  85. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +2 -2
  86. package/dist-esm/lib/hooks/useSelectionEvents.mjs +9 -14
  87. package/dist-esm/lib/hooks/useSelectionEvents.mjs.map +2 -2
  88. package/dist-esm/lib/license/LicenseManager.mjs +148 -60
  89. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  90. package/dist-esm/lib/license/LicenseProvider.mjs +39 -2
  91. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  92. package/dist-esm/lib/license/Watermark.mjs +145 -76
  93. package/dist-esm/lib/license/Watermark.mjs.map +3 -3
  94. package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
  95. package/dist-esm/lib/primitives/Vec.mjs +0 -4
  96. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  97. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +53 -21
  98. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  99. package/dist-esm/lib/primitives/geometry/Group2d.mjs +8 -1
  100. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  101. package/dist-esm/lib/utils/dom.mjs.map +2 -2
  102. package/dist-esm/lib/utils/getPointerInfo.mjs +2 -3
  103. package/dist-esm/lib/utils/getPointerInfo.mjs.map +2 -2
  104. package/dist-esm/lib/utils/reparenting.mjs +8 -41
  105. package/dist-esm/lib/utils/reparenting.mjs.map +2 -2
  106. package/dist-esm/version.mjs +4 -4
  107. package/dist-esm/version.mjs.map +1 -1
  108. package/editor.css +8 -3
  109. package/package.json +7 -7
  110. package/src/index.ts +2 -10
  111. package/src/lib/TldrawEditor.tsx +1 -15
  112. package/src/lib/components/default-components/DefaultCanvas.tsx +7 -1
  113. package/src/lib/config/TLUserPreferences.ts +16 -3
  114. package/src/lib/editor/Editor.test.ts +90 -0
  115. package/src/lib/editor/Editor.ts +77 -151
  116. package/src/lib/editor/derivations/notVisibleShapes.ts +6 -0
  117. package/src/lib/editor/managers/FocusManager/FocusManager.ts +6 -2
  118. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +30 -8
  119. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +10 -3
  120. package/src/lib/editor/shapes/ShapeUtil.ts +32 -0
  121. package/src/lib/editor/types/misc-types.ts +0 -6
  122. package/src/lib/hooks/useCanvasEvents.ts +20 -20
  123. package/src/lib/hooks/useDocumentEvents.ts +6 -6
  124. package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +1 -1
  125. package/src/lib/hooks/useGestureEvents.ts +2 -2
  126. package/src/lib/hooks/useHandleEvents.ts +6 -6
  127. package/src/lib/hooks/usePassThroughMouseOverEvents.ts +4 -1
  128. package/src/lib/hooks/useSelectionEvents.ts +9 -14
  129. package/src/lib/license/LicenseManager.test.ts +780 -377
  130. package/src/lib/license/LicenseManager.ts +207 -70
  131. package/src/lib/license/LicenseProvider.tsx +74 -2
  132. package/src/lib/license/Watermark.tsx +152 -77
  133. package/src/lib/license/useLicenseManagerState.ts +2 -2
  134. package/src/lib/primitives/Vec.ts +0 -5
  135. package/src/lib/primitives/geometry/Geometry2d.test.ts +420 -0
  136. package/src/lib/primitives/geometry/Geometry2d.ts +78 -21
  137. package/src/lib/primitives/geometry/Group2d.ts +10 -1
  138. package/src/lib/test/InFrontOfTheCanvas.test.tsx +187 -0
  139. package/src/lib/utils/dom.test.ts +103 -0
  140. package/src/lib/utils/dom.ts +8 -1
  141. package/src/lib/utils/getPointerInfo.ts +3 -2
  142. package/src/lib/utils/reparenting.ts +10 -70
  143. package/src/version.ts +4 -4
@@ -1,16 +1,27 @@
1
1
  import { atom } from '@tldraw/state'
2
- import { fetch } from '@tldraw/utils'
3
- import { publishDates } from '../../version'
2
+ import { publishDates, version } from '../../version'
4
3
  import { getDefaultCdnBaseUrl } from '../utils/assets'
5
4
  import { importPublicKey, str2ab } from '../utils/licensing'
6
5
 
7
- const GRACE_PERIOD_DAYS = 5
6
+ const GRACE_PERIOD_DAYS = 30
8
7
 
9
8
  export const FLAGS = {
10
- ANNUAL_LICENSE: 0x1,
11
- PERPETUAL_LICENSE: 0x2,
12
- INTERNAL_LICENSE: 0x4,
13
- WITH_WATERMARK: 0x8,
9
+ // -- MUTUALLY EXCLUSIVE FLAGS --
10
+ // Annual means the license expires after a time period, usually 1 year.
11
+ ANNUAL_LICENSE: 1,
12
+ // Perpetual means the license never expires up to the max supported version.
13
+ PERPETUAL_LICENSE: 1 << 1,
14
+
15
+ // -- ADDITIVE FLAGS --
16
+ // Internal means the license is for internal use only.
17
+ INTERNAL_LICENSE: 1 << 2,
18
+ // Watermark means the product is watermarked.
19
+ WITH_WATERMARK: 1 << 3,
20
+ // Evaluation means the license is for evaluation purposes only.
21
+ EVALUATION_LICENSE: 1 << 4,
22
+ // Native means the license is for native apps which switches
23
+ // on special-case logic.
24
+ NATIVE_LICENSE: 1 << 5,
14
25
  }
15
26
  const HIGHEST_FLAG = Math.max(...Object.values(FLAGS))
16
27
 
@@ -33,6 +44,15 @@ export interface LicenseInfo {
33
44
  flags: number
34
45
  expiryDate: string
35
46
  }
47
+
48
+ /** @internal */
49
+ export type LicenseState =
50
+ | 'pending' // License validation is in progress
51
+ | 'licensed' // License is valid and active (no restrictions)
52
+ | 'licensed-with-watermark' // License is valid but shows watermark (evaluation licenses, WITH_WATERMARK licenses)
53
+ | 'unlicensed' // No valid license found or license is invalid (development)
54
+ | 'unlicensed-production' // No valid license in production deployment (missing, invalid, or wrong domain)
55
+ | 'expired' // License has been expired (30 days past expiration for regular licenses, immediately for evaluation licenses)
36
56
  /** @internal */
37
57
  export type InvalidLicenseReason =
38
58
  | 'invalid-license-key'
@@ -60,11 +80,15 @@ export interface ValidLicenseKeyResult {
60
80
  isPerpetualLicense: boolean
61
81
  isPerpetualLicenseExpired: boolean
62
82
  isInternalLicense: boolean
83
+ isNativeLicense: boolean
63
84
  isLicensedWithWatermark: boolean
85
+ isEvaluationLicense: boolean
86
+ isEvaluationLicenseExpired: boolean
87
+ daysSinceExpiry: number
64
88
  }
65
89
 
66
90
  /** @internal */
67
- export type TestEnvironment = 'development' | 'production'
91
+ export type TrackType = 'unlicensed' | 'with_watermark' | 'evaluation' | null
68
92
 
69
93
  /** @internal */
70
94
  export class LicenseManager {
@@ -73,50 +97,86 @@ export class LicenseManager {
73
97
  public isDevelopment: boolean
74
98
  public isTest: boolean
75
99
  public isCryptoAvailable: boolean
76
- state = atom<'pending' | 'licensed' | 'licensed-with-watermark' | 'unlicensed'>(
77
- 'license state',
78
- 'pending'
79
- )
100
+ state = atom<LicenseState>('license state', 'pending')
80
101
  public verbose = true
81
102
 
82
- constructor(
83
- licenseKey: string | undefined,
84
- testPublicKey?: string,
85
- testEnvironment?: TestEnvironment
86
- ) {
103
+ constructor(licenseKey: string | undefined, testPublicKey?: string) {
87
104
  this.isTest = process.env.NODE_ENV === 'test'
88
- this.isDevelopment = this.getIsDevelopment(testEnvironment)
105
+ this.isDevelopment = this.getIsDevelopment()
89
106
  this.publicKey = testPublicKey || this.publicKey
90
107
  this.isCryptoAvailable = !!crypto.subtle
91
108
 
92
- this.getLicenseFromKey(licenseKey).then((result) => {
93
- const isUnlicensed = isEditorUnlicensed(result)
109
+ this.getLicenseFromKey(licenseKey)
110
+ .then((result) => {
111
+ const licenseState = getLicenseState(
112
+ result,
113
+ (messages: string[]) => this.outputMessages(messages),
114
+ this.isDevelopment
115
+ )
94
116
 
95
- if (!this.isDevelopment && isUnlicensed) {
96
- fetch(WATERMARK_TRACK_SRC)
97
- }
117
+ this.maybeTrack(result, licenseState)
98
118
 
99
- if (isUnlicensed) {
119
+ this.state.set(licenseState)
120
+ })
121
+ .catch((error) => {
122
+ console.error('License validation failed:', error)
100
123
  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
- })
124
+ })
107
125
  }
108
126
 
109
- private getIsDevelopment(testEnvironment?: TestEnvironment) {
110
- if (testEnvironment === 'development') return true
111
- if (testEnvironment === 'production') return false
112
-
127
+ private getIsDevelopment() {
113
128
  // If we are using https on a non-localhost domain we assume it's a production env and a development one otherwise
114
129
  return (
115
130
  !['https:', 'vscode-webview:'].includes(window.location.protocol) ||
116
- window.location.hostname === 'localhost'
131
+ window.location.hostname === 'localhost' ||
132
+ process.env.NODE_ENV !== 'production'
117
133
  )
118
134
  }
119
135
 
136
+ private getTrackType(result: LicenseFromKeyResult, licenseState: LicenseState): TrackType {
137
+ // Track watermark for unlicensed production deployments
138
+ if (licenseState === 'unlicensed-production') {
139
+ return 'unlicensed'
140
+ }
141
+
142
+ if (this.isDevelopment) {
143
+ return null
144
+ }
145
+
146
+ if (!result.isLicenseParseable) {
147
+ return null
148
+ }
149
+
150
+ // Track evaluation licenses (for analytics, even though no watermark is shown)
151
+ if (result.isEvaluationLicense) {
152
+ return 'evaluation'
153
+ }
154
+
155
+ // Track licenses that show watermarks
156
+ if (licenseState === 'licensed-with-watermark') {
157
+ return 'with_watermark'
158
+ }
159
+
160
+ return null
161
+ }
162
+
163
+ private maybeTrack(result: LicenseFromKeyResult, licenseState: LicenseState): void {
164
+ const trackType = this.getTrackType(result, licenseState)
165
+ if (!trackType) {
166
+ return
167
+ }
168
+
169
+ const url = new URL(WATERMARK_TRACK_SRC)
170
+ url.searchParams.set('version', version)
171
+ url.searchParams.set('license_type', trackType)
172
+ if ('license' in result) {
173
+ url.searchParams.set('license_id', result.license.id)
174
+ }
175
+
176
+ // eslint-disable-next-line no-restricted-globals
177
+ fetch(url.toString())
178
+ }
179
+
120
180
  private async extractLicenseKey(licenseKey: string): Promise<LicenseInfo> {
121
181
  const [data, signature] = licenseKey.split('.')
122
182
  const [prefix, encodedData] = data.split('/')
@@ -203,6 +263,9 @@ export class LicenseManager {
203
263
  const isAnnualLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.ANNUAL_LICENSE)
204
264
  const isPerpetualLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.PERPETUAL_LICENSE)
205
265
 
266
+ const isEvaluationLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.EVALUATION_LICENSE)
267
+ const daysSinceExpiry = this.getDaysSinceExpiry(expiryDate)
268
+
206
269
  const result: ValidLicenseKeyResult = {
207
270
  license: licenseInfo,
208
271
  isLicenseParseable: true,
@@ -214,7 +277,12 @@ export class LicenseManager {
214
277
  isPerpetualLicense,
215
278
  isPerpetualLicenseExpired: isPerpetualLicense && this.isPerpetualLicenseExpired(expiryDate),
216
279
  isInternalLicense: this.isFlagEnabled(licenseInfo.flags, FLAGS.INTERNAL_LICENSE),
280
+ isNativeLicense: this.isNativeLicense(licenseInfo),
217
281
  isLicensedWithWatermark: this.isFlagEnabled(licenseInfo.flags, FLAGS.WITH_WATERMARK),
282
+ isEvaluationLicense,
283
+ isEvaluationLicenseExpired:
284
+ isEvaluationLicense && this.isEvaluationLicenseExpired(expiryDate),
285
+ daysSinceExpiry,
218
286
  }
219
287
  this.outputLicenseInfoIfNeeded(result)
220
288
 
@@ -230,13 +298,13 @@ export class LicenseManager {
230
298
  const currentHostname = window.location.hostname.toLowerCase()
231
299
 
232
300
  return licenseInfo.hosts.some((host) => {
233
- const normalizedHost = host.toLowerCase().trim()
301
+ const normalizedHostOrUrlRegex = host.toLowerCase().trim()
234
302
 
235
303
  // Allow the domain if listed and www variations, 'example.com' allows 'example.com' and 'www.example.com'
236
304
  if (
237
- normalizedHost === currentHostname ||
238
- `www.${normalizedHost}` === currentHostname ||
239
- normalizedHost === `www.${currentHostname}`
305
+ normalizedHostOrUrlRegex === currentHostname ||
306
+ `www.${normalizedHostOrUrlRegex}` === currentHostname ||
307
+ normalizedHostOrUrlRegex === `www.${currentHostname}`
240
308
  ) {
241
309
  return true
242
310
  }
@@ -247,6 +315,12 @@ export class LicenseManager {
247
315
  return true
248
316
  }
249
317
 
318
+ // Native license support
319
+ // In this case, `normalizedHost` is actually a protocol, e.g. `app-bundle:`
320
+ if (this.isNativeLicense(licenseInfo)) {
321
+ return new RegExp(normalizedHostOrUrlRegex).test(window.location.href)
322
+ }
323
+
250
324
  // Glob testing, we only support '*.somedomain.com' right now.
251
325
  if (host.includes('*')) {
252
326
  const globToRegex = new RegExp(host.replace(/\*/g, '.*?'))
@@ -257,7 +331,7 @@ export class LicenseManager {
257
331
  if (window.location.protocol === 'vscode-webview:') {
258
332
  const currentUrl = new URL(window.location.href)
259
333
  const extensionId = currentUrl.searchParams.get('extensionId')
260
- if (normalizedHost === extensionId) {
334
+ if (normalizedHostOrUrlRegex === extensionId) {
261
335
  return true
262
336
  }
263
337
  }
@@ -266,6 +340,10 @@ export class LicenseManager {
266
340
  })
267
341
  }
268
342
 
343
+ private isNativeLicense(licenseInfo: LicenseInfo) {
344
+ return this.isFlagEnabled(licenseInfo.flags, FLAGS.NATIVE_LICENSE)
345
+ }
346
+
269
347
  private getExpirationDateWithoutGracePeriod(expiryDate: Date) {
270
348
  return new Date(expiryDate.getFullYear(), expiryDate.getMonth(), expiryDate.getDate())
271
349
  }
@@ -280,15 +358,7 @@ export class LicenseManager {
280
358
 
281
359
  private isAnnualLicenseExpired(expiryDate: Date) {
282
360
  const expiration = this.getExpirationDateWithGracePeriod(expiryDate)
283
- const isExpired = new Date() >= expiration
284
- // If it is not expired yet (including the grace period), but after the expiry date we warn the users
285
- if (!isExpired && new Date() >= this.getExpirationDateWithoutGracePeriod(expiryDate)) {
286
- this.outputMessages([
287
- 'tldraw license is about to expire, you are in a grace period.',
288
- `Please reach out to ${LICENSE_EMAIL} if you would like to renew your license.`,
289
- ])
290
- }
291
- return isExpired
361
+ return new Date() >= expiration
292
362
  }
293
363
 
294
364
  private isPerpetualLicenseExpired(expiryDate: Date) {
@@ -301,6 +371,21 @@ export class LicenseManager {
301
371
  return dates.major >= expiration || dates.minor >= expiration
302
372
  }
303
373
 
374
+ private getDaysSinceExpiry(expiryDate: Date): number {
375
+ const now = new Date()
376
+ const expiration = this.getExpirationDateWithoutGracePeriod(expiryDate)
377
+ const diffTime = now.getTime() - expiration.getTime()
378
+ const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
379
+ return Math.max(0, diffDays)
380
+ }
381
+
382
+ private isEvaluationLicenseExpired(expiryDate: Date): boolean {
383
+ // Evaluation licenses have no grace period - they expire immediately
384
+ const now = new Date()
385
+ const expiration = this.getExpirationDateWithoutGracePeriod(expiryDate)
386
+ return now >= expiration
387
+ }
388
+
304
389
  private isFlagEnabled(flags: number, flag: number) {
305
390
  return (flags & flag) === flag
306
391
  }
@@ -318,19 +403,6 @@ export class LicenseManager {
318
403
  }
319
404
 
320
405
  private outputLicenseInfoIfNeeded(result: ValidLicenseKeyResult) {
321
- if (result.isAnnualLicenseExpired) {
322
- this.outputMessages([
323
- 'Your tldraw license has expired!',
324
- `Please reach out to ${LICENSE_EMAIL} to renew.`,
325
- ])
326
- }
327
-
328
- if (!result.isDomainValid && !result.isDevelopment) {
329
- this.outputMessages([
330
- 'This tldraw license key is not valid for this domain!',
331
- `Please reach out to ${LICENSE_EMAIL} if you would like to use tldraw on other domains.`,
332
- ])
333
- }
334
406
  // If we added a new flag it will be twice the value of the currently highest flag.
335
407
  // 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
408
  if (result.license.flags >= HIGHEST_FLAG * 2) {
@@ -367,15 +439,80 @@ export class LicenseManager {
367
439
  static className = 'tl-watermark_SEE-LICENSE'
368
440
  }
369
441
 
370
- export function isEditorUnlicensed(result: LicenseFromKeyResult) {
371
- if (!result.isLicenseParseable) return true
372
- if (!result.isDomainValid && !result.isDevelopment) return true
373
- if (result.isPerpetualLicenseExpired || result.isAnnualLicenseExpired) {
374
- if (result.isInternalLicense) {
375
- throw new Error('License: Internal license expired.')
442
+ export function getLicenseState(
443
+ result: LicenseFromKeyResult,
444
+ outputMessages: (messages: string[]) => void,
445
+ isDevelopment: boolean
446
+ ): LicenseState {
447
+ if (!result.isLicenseParseable) {
448
+ if (isDevelopment) {
449
+ return 'unlicensed'
376
450
  }
377
- return true
451
+
452
+ // All unlicensed scenarios should not work in production
453
+ if (result.reason === 'no-key-provided') {
454
+ outputMessages([
455
+ 'No tldraw license key provided!',
456
+ 'A license is required for production deployments.',
457
+ `Please reach out to ${LICENSE_EMAIL} to purchase a license.`,
458
+ ])
459
+ } else {
460
+ outputMessages([
461
+ 'Invalid license key. tldraw requires a valid license for production use.',
462
+ `Please reach out to ${LICENSE_EMAIL} to purchase a license.`,
463
+ ])
464
+ }
465
+ return 'unlicensed-production'
466
+ }
467
+
468
+ if (!result.isDomainValid && !result.isDevelopment) {
469
+ outputMessages([
470
+ 'License key is not valid for this domain.',
471
+ 'A license is required for production deployments.',
472
+ `Please reach out to ${LICENSE_EMAIL} to purchase a license.`,
473
+ ])
474
+ return 'unlicensed-production'
475
+ }
476
+
477
+ // Handle evaluation licenses - they expire immediately with no grace period
478
+ if (result.isEvaluationLicense) {
479
+ if (result.isEvaluationLicenseExpired) {
480
+ outputMessages([
481
+ 'Your tldraw evaluation license has expired!',
482
+ `Please reach out to ${LICENSE_EMAIL} to purchase a full license.`,
483
+ ])
484
+ return 'expired'
485
+ } else {
486
+ // Valid evaluation license - tracked but no watermark shown
487
+ return 'licensed'
488
+ }
489
+ }
490
+
491
+ // Handle expired regular licenses (both annual and perpetual)
492
+ if (result.isPerpetualLicenseExpired || result.isAnnualLicenseExpired) {
493
+ outputMessages([
494
+ 'Your tldraw license has been expired for more than 30 days!',
495
+ `Please reach out to ${LICENSE_EMAIL} to renew your license.`,
496
+ ])
497
+ return 'expired'
498
+ }
499
+
500
+ // Check if license is past expiry date but within grace period
501
+ const daysSinceExpiry = result.daysSinceExpiry
502
+ if (daysSinceExpiry > 0 && !result.isEvaluationLicense) {
503
+ outputMessages([
504
+ 'Your tldraw license has expired.',
505
+ `License expired ${daysSinceExpiry} days ago.`,
506
+ `Please reach out to ${LICENSE_EMAIL} to renew your license.`,
507
+ ])
508
+ // Within 30-day grace period: still licensed (no watermark)
509
+ return 'licensed'
510
+ }
511
+
512
+ // License is valid, determine if it has watermark
513
+ if (result.isLicensedWithWatermark) {
514
+ return 'licensed-with-watermark'
378
515
  }
379
516
 
380
- return false
517
+ return 'licensed'
381
518
  }
@@ -1,4 +1,5 @@
1
- import { createContext, ReactNode, useContext, useState } from 'react'
1
+ import { useValue } from '@tldraw/state-react'
2
+ import { createContext, ReactNode, useContext, useEffect, useState } from 'react'
2
3
  import { LicenseManager } from './LicenseManager'
3
4
 
4
5
  /** @internal */
@@ -7,14 +8,85 @@ export const LicenseContext = createContext({} as LicenseManager)
7
8
  /** @internal */
8
9
  export const useLicenseContext = () => useContext(LicenseContext)
9
10
 
11
+ function shouldHideEditorAfterDelay(licenseState: string): boolean {
12
+ return licenseState === 'expired' || licenseState === 'unlicensed-production'
13
+ }
14
+
15
+ /** @internal */
16
+ export const LICENSE_TIMEOUT = 5000
17
+
10
18
  /** @internal */
11
19
  export function LicenseProvider({
12
- licenseKey,
20
+ licenseKey = getLicenseKeyFromEnv() ?? undefined,
13
21
  children,
14
22
  }: {
15
23
  licenseKey?: string
16
24
  children: ReactNode
17
25
  }) {
18
26
  const [licenseManager] = useState(() => new LicenseManager(licenseKey))
27
+ const licenseState = useValue(licenseManager.state)
28
+ const [showEditor, setShowEditor] = useState(true)
29
+
30
+ // When license expires or no license in production, show for 5 seconds then hide
31
+ useEffect(() => {
32
+ if (shouldHideEditorAfterDelay(licenseState) && showEditor) {
33
+ // eslint-disable-next-line no-restricted-globals
34
+ const timer = setTimeout(() => {
35
+ setShowEditor(false)
36
+ }, LICENSE_TIMEOUT)
37
+
38
+ return () => clearTimeout(timer)
39
+ }
40
+ }, [licenseState, showEditor])
41
+
42
+ // If license is expired or no license in production and 5 seconds have passed, don't render anything (blank screen)
43
+ if (shouldHideEditorAfterDelay(licenseState) && !showEditor) {
44
+ return <LicenseGate />
45
+ }
46
+
19
47
  return <LicenseContext.Provider value={licenseManager}>{children}</LicenseContext.Provider>
20
48
  }
49
+
50
+ // Renders as a hidden div that can be detected by tests
51
+ function LicenseGate() {
52
+ return <div data-testid="tl-license-expired" style={{ display: 'none' }} />
53
+ }
54
+
55
+ let envLicenseKey: string | undefined | null = undefined
56
+ function getLicenseKeyFromEnv() {
57
+ if (envLicenseKey !== undefined) {
58
+ return envLicenseKey
59
+ }
60
+ // it's important here that we write out the full process.env.WHATEVER expression instead of
61
+ // doing something like process.env[someVariable]. This is because most bundlers do something
62
+ // like a find-replace inject environment variables, and so won't pick up on dynamic ones. It
63
+ // also means we can't do checks like `process.env && process.env.WHATEVER`, which is why we use
64
+ // the `getEnv` try/catch approach.
65
+
66
+ // framework-specific prefixes borrowed from the ones vercel uses, but trimmed down to just the
67
+ // react-y ones: https://vercel.com/docs/environment-variables/framework-environment-variables
68
+ envLicenseKey =
69
+ getEnv(() => process.env.TLDRAW_LICENSE_KEY) ||
70
+ getEnv(() => process.env.NEXT_PUBLIC_TLDRAW_LICENSE_KEY) ||
71
+ getEnv(() => process.env.REACT_APP_TLDRAW_LICENSE_KEY) ||
72
+ getEnv(() => process.env.GATSBY_TLDRAW_LICENSE_KEY) ||
73
+ getEnv(() => process.env.VITE_TLDRAW_LICENSE_KEY) ||
74
+ getEnv(() => process.env.PUBLIC_TLDRAW_LICENSE_KEY) ||
75
+ getEnv(() => (import.meta as any).env.TLDRAW_LICENSE_KEY) ||
76
+ getEnv(() => (import.meta as any).env.NEXT_PUBLIC_TLDRAW_LICENSE_KEY) ||
77
+ getEnv(() => (import.meta as any).env.REACT_APP_TLDRAW_LICENSE_KEY) ||
78
+ getEnv(() => (import.meta as any).env.GATSBY_TLDRAW_LICENSE_KEY) ||
79
+ getEnv(() => (import.meta as any).env.VITE_TLDRAW_LICENSE_KEY) ||
80
+ getEnv(() => (import.meta as any).env.PUBLIC_TLDRAW_LICENSE_KEY) ||
81
+ null
82
+
83
+ return envLicenseKey
84
+ }
85
+
86
+ function getEnv(cb: () => string | undefined) {
87
+ try {
88
+ return cb()
89
+ } catch {
90
+ return undefined
91
+ }
92
+ }