@tldraw/editor 3.16.0-canary.c2c4563957ce → 3.16.0-canary.c360426d8b7a

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 (114) hide show
  1. package/dist-cjs/index.d.ts +59 -3
  2. package/dist-cjs/index.js +6 -2
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +2 -2
  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/editor/Editor.js +9 -4
  9. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  10. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +4 -0
  11. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +2 -2
  12. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js +4 -2
  13. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +2 -2
  14. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +10 -0
  15. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  16. package/dist-cjs/lib/hooks/useCanvasEvents.js +15 -12
  17. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  18. package/dist-cjs/lib/hooks/useDocumentEvents.js +5 -5
  19. package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
  20. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js +1 -2
  21. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
  22. package/dist-cjs/lib/hooks/useGestureEvents.js +1 -1
  23. package/dist-cjs/lib/hooks/useGestureEvents.js.map +2 -2
  24. package/dist-cjs/lib/hooks/useHandleEvents.js +3 -3
  25. package/dist-cjs/lib/hooks/useHandleEvents.js.map +2 -2
  26. package/dist-cjs/lib/hooks/useSelectionEvents.js +4 -4
  27. package/dist-cjs/lib/hooks/useSelectionEvents.js.map +2 -2
  28. package/dist-cjs/lib/license/LicenseManager.js +133 -38
  29. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  30. package/dist-cjs/lib/license/LicenseProvider.js +36 -3
  31. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  32. package/dist-cjs/lib/license/Watermark.js +143 -75
  33. package/dist-cjs/lib/license/Watermark.js.map +3 -3
  34. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +24 -2
  35. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  36. package/dist-cjs/lib/primitives/geometry/Group2d.js +5 -1
  37. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  38. package/dist-cjs/lib/utils/dom.js +12 -1
  39. package/dist-cjs/lib/utils/dom.js.map +2 -2
  40. package/dist-cjs/lib/utils/getPointerInfo.js +2 -2
  41. package/dist-cjs/lib/utils/getPointerInfo.js.map +2 -2
  42. package/dist-cjs/version.js +3 -3
  43. package/dist-cjs/version.js.map +1 -1
  44. package/dist-esm/index.d.mts +59 -3
  45. package/dist-esm/index.mjs +9 -3
  46. package/dist-esm/index.mjs.map +2 -2
  47. package/dist-esm/lib/TldrawEditor.mjs +3 -3
  48. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  49. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +12 -2
  50. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  51. package/dist-esm/lib/editor/Editor.mjs +9 -4
  52. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  53. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +4 -0
  54. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
  55. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs +4 -2
  56. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +2 -2
  57. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +10 -0
  58. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  59. package/dist-esm/lib/hooks/useCanvasEvents.mjs +17 -13
  60. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  61. package/dist-esm/lib/hooks/useDocumentEvents.mjs +11 -6
  62. package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
  63. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs +2 -3
  64. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
  65. package/dist-esm/lib/hooks/useGestureEvents.mjs +2 -2
  66. package/dist-esm/lib/hooks/useGestureEvents.mjs.map +2 -2
  67. package/dist-esm/lib/hooks/useHandleEvents.mjs +9 -4
  68. package/dist-esm/lib/hooks/useHandleEvents.mjs.map +2 -2
  69. package/dist-esm/lib/hooks/useSelectionEvents.mjs +6 -5
  70. package/dist-esm/lib/hooks/useSelectionEvents.mjs.map +2 -2
  71. package/dist-esm/lib/license/LicenseManager.mjs +134 -39
  72. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  73. package/dist-esm/lib/license/LicenseProvider.mjs +36 -4
  74. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  75. package/dist-esm/lib/license/Watermark.mjs +144 -76
  76. package/dist-esm/lib/license/Watermark.mjs.map +3 -3
  77. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +24 -2
  78. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  79. package/dist-esm/lib/primitives/geometry/Group2d.mjs +5 -1
  80. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  81. package/dist-esm/lib/utils/dom.mjs +12 -1
  82. package/dist-esm/lib/utils/dom.mjs.map +2 -2
  83. package/dist-esm/lib/utils/getPointerInfo.mjs +2 -2
  84. package/dist-esm/lib/utils/getPointerInfo.mjs.map +2 -2
  85. package/dist-esm/version.mjs +3 -3
  86. package/dist-esm/version.mjs.map +1 -1
  87. package/editor.css +8 -3
  88. package/package.json +7 -7
  89. package/src/index.ts +3 -0
  90. package/src/lib/TldrawEditor.tsx +3 -4
  91. package/src/lib/components/default-components/DefaultCanvas.tsx +8 -2
  92. package/src/lib/editor/Editor.test.ts +90 -0
  93. package/src/lib/editor/Editor.ts +16 -4
  94. package/src/lib/editor/derivations/notVisibleShapes.ts +6 -0
  95. package/src/lib/editor/managers/FocusManager/FocusManager.ts +6 -2
  96. package/src/lib/editor/shapes/ShapeUtil.ts +11 -0
  97. package/src/lib/hooks/useCanvasEvents.ts +17 -11
  98. package/src/lib/hooks/useDocumentEvents.ts +11 -6
  99. package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +2 -2
  100. package/src/lib/hooks/useGestureEvents.ts +2 -2
  101. package/src/lib/hooks/useHandleEvents.ts +9 -4
  102. package/src/lib/hooks/useSelectionEvents.ts +6 -5
  103. package/src/lib/license/LicenseManager.test.ts +719 -387
  104. package/src/lib/license/LicenseManager.ts +187 -49
  105. package/src/lib/license/LicenseProvider.tsx +69 -5
  106. package/src/lib/license/Watermark.tsx +151 -77
  107. package/src/lib/primitives/geometry/Geometry2d.test.ts +420 -0
  108. package/src/lib/primitives/geometry/Geometry2d.ts +29 -2
  109. package/src/lib/primitives/geometry/Group2d.ts +6 -1
  110. package/src/lib/test/InFrontOfTheCanvas.test.tsx +187 -0
  111. package/src/lib/utils/dom.test.ts +94 -0
  112. package/src/lib/utils/dom.ts +38 -1
  113. package/src/lib/utils/getPointerInfo.ts +2 -1
  114. package/src/version.ts +3 -3
@@ -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
 
@@ -36,11 +47,12 @@ export interface LicenseInfo {
36
47
 
37
48
  /** @internal */
38
49
  export type LicenseState =
39
- | 'pending'
40
- | 'licensed'
41
- | 'licensed-with-watermark'
42
- | 'unlicensed'
43
- | 'internal-expired'
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)
44
56
  /** @internal */
45
57
  export type InvalidLicenseReason =
46
58
  | 'invalid-license-key'
@@ -68,12 +80,19 @@ export interface ValidLicenseKeyResult {
68
80
  isPerpetualLicense: boolean
69
81
  isPerpetualLicenseExpired: boolean
70
82
  isInternalLicense: boolean
83
+ isNativeLicense: boolean
71
84
  isLicensedWithWatermark: boolean
85
+ isEvaluationLicense: boolean
86
+ isEvaluationLicenseExpired: boolean
87
+ daysSinceExpiry: number
72
88
  }
73
89
 
74
90
  /** @internal */
75
91
  export type TestEnvironment = 'development' | 'production'
76
92
 
93
+ /** @internal */
94
+ export type TrackType = 'unlicensed' | 'with_watermark' | 'evaluation' | null
95
+
77
96
  /** @internal */
78
97
  export class LicenseManager {
79
98
  private publicKey =
@@ -96,11 +115,13 @@ export class LicenseManager {
96
115
 
97
116
  this.getLicenseFromKey(licenseKey)
98
117
  .then((result) => {
99
- const licenseState = getLicenseState(result)
118
+ const licenseState = getLicenseState(
119
+ result,
120
+ (messages: string[]) => this.outputMessages(messages),
121
+ this.isDevelopment
122
+ )
100
123
 
101
- if (!this.isDevelopment && licenseState === 'unlicensed') {
102
- fetch(WATERMARK_TRACK_SRC)
103
- }
124
+ this.maybeTrack(result, licenseState)
104
125
 
105
126
  this.state.set(licenseState)
106
127
  })
@@ -121,6 +142,50 @@ export class LicenseManager {
121
142
  )
122
143
  }
123
144
 
145
+ private getTrackType(result: LicenseFromKeyResult, licenseState: LicenseState): TrackType {
146
+ // Track watermark for unlicensed production deployments
147
+ if (licenseState === 'unlicensed-production') {
148
+ return 'unlicensed'
149
+ }
150
+
151
+ if (this.isDevelopment) {
152
+ return null
153
+ }
154
+
155
+ if (!result.isLicenseParseable) {
156
+ return null
157
+ }
158
+
159
+ // Track evaluation licenses (for analytics, even though no watermark is shown)
160
+ if (result.isEvaluationLicense) {
161
+ return 'evaluation'
162
+ }
163
+
164
+ // Track licenses that show watermarks
165
+ if (licenseState === 'licensed-with-watermark') {
166
+ return 'with_watermark'
167
+ }
168
+
169
+ return null
170
+ }
171
+
172
+ private maybeTrack(result: LicenseFromKeyResult, licenseState: LicenseState): void {
173
+ const trackType = this.getTrackType(result, licenseState)
174
+ if (!trackType) {
175
+ return
176
+ }
177
+
178
+ const url = new URL(WATERMARK_TRACK_SRC)
179
+ url.searchParams.set('version', version)
180
+ url.searchParams.set('license_type', trackType)
181
+ if ('license' in result) {
182
+ url.searchParams.set('license_id', result.license.id)
183
+ }
184
+
185
+ // eslint-disable-next-line no-restricted-globals
186
+ fetch(url.toString())
187
+ }
188
+
124
189
  private async extractLicenseKey(licenseKey: string): Promise<LicenseInfo> {
125
190
  const [data, signature] = licenseKey.split('.')
126
191
  const [prefix, encodedData] = data.split('/')
@@ -207,6 +272,9 @@ export class LicenseManager {
207
272
  const isAnnualLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.ANNUAL_LICENSE)
208
273
  const isPerpetualLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.PERPETUAL_LICENSE)
209
274
 
275
+ const isEvaluationLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.EVALUATION_LICENSE)
276
+ const daysSinceExpiry = this.getDaysSinceExpiry(expiryDate)
277
+
210
278
  const result: ValidLicenseKeyResult = {
211
279
  license: licenseInfo,
212
280
  isLicenseParseable: true,
@@ -218,7 +286,12 @@ export class LicenseManager {
218
286
  isPerpetualLicense,
219
287
  isPerpetualLicenseExpired: isPerpetualLicense && this.isPerpetualLicenseExpired(expiryDate),
220
288
  isInternalLicense: this.isFlagEnabled(licenseInfo.flags, FLAGS.INTERNAL_LICENSE),
289
+ isNativeLicense: this.isNativeLicense(licenseInfo),
221
290
  isLicensedWithWatermark: this.isFlagEnabled(licenseInfo.flags, FLAGS.WITH_WATERMARK),
291
+ isEvaluationLicense,
292
+ isEvaluationLicenseExpired:
293
+ isEvaluationLicense && this.isEvaluationLicenseExpired(expiryDate),
294
+ daysSinceExpiry,
222
295
  }
223
296
  this.outputLicenseInfoIfNeeded(result)
224
297
 
@@ -234,13 +307,13 @@ export class LicenseManager {
234
307
  const currentHostname = window.location.hostname.toLowerCase()
235
308
 
236
309
  return licenseInfo.hosts.some((host) => {
237
- const normalizedHost = host.toLowerCase().trim()
310
+ const normalizedHostOrUrlRegex = host.toLowerCase().trim()
238
311
 
239
312
  // Allow the domain if listed and www variations, 'example.com' allows 'example.com' and 'www.example.com'
240
313
  if (
241
- normalizedHost === currentHostname ||
242
- `www.${normalizedHost}` === currentHostname ||
243
- normalizedHost === `www.${currentHostname}`
314
+ normalizedHostOrUrlRegex === currentHostname ||
315
+ `www.${normalizedHostOrUrlRegex}` === currentHostname ||
316
+ normalizedHostOrUrlRegex === `www.${currentHostname}`
244
317
  ) {
245
318
  return true
246
319
  }
@@ -251,6 +324,12 @@ export class LicenseManager {
251
324
  return true
252
325
  }
253
326
 
327
+ // Native license support
328
+ // In this case, `normalizedHost` is actually a protocol, e.g. `app-bundle:`
329
+ if (this.isNativeLicense(licenseInfo)) {
330
+ return new RegExp(normalizedHostOrUrlRegex).test(window.location.href)
331
+ }
332
+
254
333
  // Glob testing, we only support '*.somedomain.com' right now.
255
334
  if (host.includes('*')) {
256
335
  const globToRegex = new RegExp(host.replace(/\*/g, '.*?'))
@@ -261,7 +340,7 @@ export class LicenseManager {
261
340
  if (window.location.protocol === 'vscode-webview:') {
262
341
  const currentUrl = new URL(window.location.href)
263
342
  const extensionId = currentUrl.searchParams.get('extensionId')
264
- if (normalizedHost === extensionId) {
343
+ if (normalizedHostOrUrlRegex === extensionId) {
265
344
  return true
266
345
  }
267
346
  }
@@ -270,6 +349,10 @@ export class LicenseManager {
270
349
  })
271
350
  }
272
351
 
352
+ private isNativeLicense(licenseInfo: LicenseInfo) {
353
+ return this.isFlagEnabled(licenseInfo.flags, FLAGS.NATIVE_LICENSE)
354
+ }
355
+
273
356
  private getExpirationDateWithoutGracePeriod(expiryDate: Date) {
274
357
  return new Date(expiryDate.getFullYear(), expiryDate.getMonth(), expiryDate.getDate())
275
358
  }
@@ -284,15 +367,7 @@ export class LicenseManager {
284
367
 
285
368
  private isAnnualLicenseExpired(expiryDate: Date) {
286
369
  const expiration = this.getExpirationDateWithGracePeriod(expiryDate)
287
- const isExpired = new Date() >= expiration
288
- // If it is not expired yet (including the grace period), but after the expiry date we warn the users
289
- if (!isExpired && new Date() >= this.getExpirationDateWithoutGracePeriod(expiryDate)) {
290
- this.outputMessages([
291
- 'tldraw license is about to expire, you are in a grace period.',
292
- `Please reach out to ${LICENSE_EMAIL} if you would like to renew your license.`,
293
- ])
294
- }
295
- return isExpired
370
+ return new Date() >= expiration
296
371
  }
297
372
 
298
373
  private isPerpetualLicenseExpired(expiryDate: Date) {
@@ -305,6 +380,21 @@ export class LicenseManager {
305
380
  return dates.major >= expiration || dates.minor >= expiration
306
381
  }
307
382
 
383
+ private getDaysSinceExpiry(expiryDate: Date): number {
384
+ const now = new Date()
385
+ const expiration = this.getExpirationDateWithoutGracePeriod(expiryDate)
386
+ const diffTime = now.getTime() - expiration.getTime()
387
+ const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
388
+ return Math.max(0, diffDays)
389
+ }
390
+
391
+ private isEvaluationLicenseExpired(expiryDate: Date): boolean {
392
+ // Evaluation licenses have no grace period - they expire immediately
393
+ const now = new Date()
394
+ const expiration = this.getExpirationDateWithoutGracePeriod(expiryDate)
395
+ return now >= expiration
396
+ }
397
+
308
398
  private isFlagEnabled(flags: number, flag: number) {
309
399
  return (flags & flag) === flag
310
400
  }
@@ -322,19 +412,6 @@ export class LicenseManager {
322
412
  }
323
413
 
324
414
  private outputLicenseInfoIfNeeded(result: ValidLicenseKeyResult) {
325
- if (result.isAnnualLicenseExpired) {
326
- this.outputMessages([
327
- 'Your tldraw license has expired!',
328
- `Please reach out to ${LICENSE_EMAIL} to renew.`,
329
- ])
330
- }
331
-
332
- if (!result.isDomainValid && !result.isDevelopment) {
333
- this.outputMessages([
334
- 'This tldraw license key is not valid for this domain!',
335
- `Please reach out to ${LICENSE_EMAIL} if you would like to use tldraw on other domains.`,
336
- ])
337
- }
338
415
  // If we added a new flag it will be twice the value of the currently highest flag.
339
416
  // 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.
340
417
  if (result.license.flags >= HIGHEST_FLAG * 2) {
@@ -371,13 +448,74 @@ export class LicenseManager {
371
448
  static className = 'tl-watermark_SEE-LICENSE'
372
449
  }
373
450
 
374
- export function getLicenseState(result: LicenseFromKeyResult): LicenseState {
375
- if (!result.isLicenseParseable) return 'unlicensed'
376
- if (!result.isDomainValid && !result.isDevelopment) return 'unlicensed'
451
+ export function getLicenseState(
452
+ result: LicenseFromKeyResult,
453
+ outputMessages: (messages: string[]) => void,
454
+ isDevelopment: boolean
455
+ ): LicenseState {
456
+ if (!result.isLicenseParseable) {
457
+ if (isDevelopment) {
458
+ return 'unlicensed'
459
+ }
460
+
461
+ // All unlicensed scenarios should not work in production
462
+ if (result.reason === 'no-key-provided') {
463
+ outputMessages([
464
+ 'No tldraw license key provided!',
465
+ 'A license is required for production deployments.',
466
+ `Please reach out to ${LICENSE_EMAIL} to purchase a license.`,
467
+ ])
468
+ } else {
469
+ outputMessages([
470
+ 'Invalid license key. tldraw requires a valid license for production use.',
471
+ `Please reach out to ${LICENSE_EMAIL} to purchase a license.`,
472
+ ])
473
+ }
474
+ return 'unlicensed-production'
475
+ }
476
+
477
+ if (!result.isDomainValid && !result.isDevelopment) {
478
+ outputMessages([
479
+ 'License key is not valid for this domain.',
480
+ 'A license is required for production deployments.',
481
+ `Please reach out to ${LICENSE_EMAIL} to purchase a license.`,
482
+ ])
483
+ return 'unlicensed-production'
484
+ }
485
+
486
+ // Handle evaluation licenses - they expire immediately with no grace period
487
+ if (result.isEvaluationLicense) {
488
+ if (result.isEvaluationLicenseExpired) {
489
+ outputMessages([
490
+ 'Your tldraw evaluation license has expired!',
491
+ `Please reach out to ${LICENSE_EMAIL} to purchase a full license.`,
492
+ ])
493
+ return 'expired'
494
+ } else {
495
+ // Valid evaluation license - tracked but no watermark shown
496
+ return 'licensed'
497
+ }
498
+ }
499
+
500
+ // Handle expired regular licenses (both annual and perpetual)
377
501
  if (result.isPerpetualLicenseExpired || result.isAnnualLicenseExpired) {
378
- // Check if it's an expired internal license with valid domain
379
- const internalExpired = result.isInternalLicense && result.isDomainValid
380
- return internalExpired ? 'internal-expired' : 'unlicensed'
502
+ outputMessages([
503
+ 'Your tldraw license has been expired for more than 30 days!',
504
+ `Please reach out to ${LICENSE_EMAIL} to renew your license.`,
505
+ ])
506
+ return 'expired'
507
+ }
508
+
509
+ // Check if license is past expiry date but within grace period
510
+ const daysSinceExpiry = result.daysSinceExpiry
511
+ if (daysSinceExpiry > 0 && !result.isEvaluationLicense) {
512
+ outputMessages([
513
+ 'Your tldraw license has expired.',
514
+ `License expired ${daysSinceExpiry} days ago.`,
515
+ `Please reach out to ${LICENSE_EMAIL} to renew your license.`,
516
+ ])
517
+ // Within 30-day grace period: still licensed (no watermark)
518
+ return 'licensed'
381
519
  }
382
520
 
383
521
  // License is valid, determine if it has watermark
@@ -1,5 +1,5 @@
1
1
  import { useValue } from '@tldraw/state-react'
2
- import { createContext, ReactNode, useContext, useState } from 'react'
2
+ import { createContext, ReactNode, useContext, useEffect, useState } from 'react'
3
3
  import { LicenseManager } from './LicenseManager'
4
4
 
5
5
  /** @internal */
@@ -8,9 +8,16 @@ export const LicenseContext = createContext({} as LicenseManager)
8
8
  /** @internal */
9
9
  export const useLicenseContext = () => useContext(LicenseContext)
10
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
+
11
18
  /** @internal */
12
19
  export function LicenseProvider({
13
- licenseKey,
20
+ licenseKey = getLicenseKeyFromEnv() ?? undefined,
14
21
  children,
15
22
  }: {
16
23
  licenseKey?: string
@@ -18,11 +25,68 @@ export function LicenseProvider({
18
25
  }) {
19
26
  const [licenseManager] = useState(() => new LicenseManager(licenseKey))
20
27
  const licenseState = useValue(licenseManager.state)
28
+ const [showEditor, setShowEditor] = useState(true)
21
29
 
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' }} />
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 />
25
45
  }
26
46
 
27
47
  return <LicenseContext.Provider value={licenseManager}>{children}</LicenseContext.Provider>
28
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
+ }