@tldraw/editor 3.16.0-canary.acb40a76700b → 3.16.0-canary.b2da39015a73

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 (189) hide show
  1. package/dist-cjs/index.d.ts +113 -104
  2. package/dist-cjs/index.js +6 -6
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +7 -7
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/Shape.js +7 -10
  7. package/dist-cjs/lib/components/Shape.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +14 -23
  9. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
  11. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
  12. package/dist-cjs/lib/config/TLUserPreferences.js +1 -1
  13. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  14. package/dist-cjs/lib/editor/Editor.js +50 -114
  15. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  16. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +4 -0
  17. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +2 -2
  18. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +1 -1
  19. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  20. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +23 -0
  21. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  22. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  23. package/dist-cjs/lib/exports/getSvgJsx.js +34 -14
  24. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  25. package/dist-cjs/lib/hooks/useCanvasEvents.js +22 -17
  26. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  27. package/dist-cjs/lib/hooks/useDocumentEvents.js +5 -5
  28. package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
  29. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js +1 -2
  30. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
  31. package/dist-cjs/lib/hooks/useGestureEvents.js +1 -1
  32. package/dist-cjs/lib/hooks/useGestureEvents.js.map +2 -2
  33. package/dist-cjs/lib/hooks/useHandleEvents.js +3 -3
  34. package/dist-cjs/lib/hooks/useHandleEvents.js.map +2 -2
  35. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js +4 -1
  36. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +2 -2
  37. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +4 -1
  38. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  39. package/dist-cjs/lib/hooks/useSelectionEvents.js +4 -4
  40. package/dist-cjs/lib/hooks/useSelectionEvents.js.map +2 -2
  41. package/dist-cjs/lib/license/LicenseManager.js +143 -53
  42. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  43. package/dist-cjs/lib/license/LicenseProvider.js +39 -1
  44. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  45. package/dist-cjs/lib/license/Watermark.js +69 -7
  46. package/dist-cjs/lib/license/Watermark.js.map +3 -3
  47. package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
  48. package/dist-cjs/lib/options.js +6 -0
  49. package/dist-cjs/lib/options.js.map +2 -2
  50. package/dist-cjs/lib/primitives/Box.js +3 -0
  51. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  52. package/dist-cjs/lib/primitives/Vec.js +0 -4
  53. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  54. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +50 -20
  55. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  56. package/dist-cjs/lib/primitives/geometry/Group2d.js +8 -1
  57. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  58. package/dist-cjs/lib/utils/dom.js +12 -1
  59. package/dist-cjs/lib/utils/dom.js.map +2 -2
  60. package/dist-cjs/lib/utils/getPointerInfo.js +2 -2
  61. package/dist-cjs/lib/utils/getPointerInfo.js.map +2 -2
  62. package/dist-cjs/lib/utils/reparenting.js +2 -35
  63. package/dist-cjs/lib/utils/reparenting.js.map +3 -3
  64. package/dist-cjs/version.js +3 -3
  65. package/dist-cjs/version.js.map +1 -1
  66. package/dist-esm/index.d.mts +113 -104
  67. package/dist-esm/index.mjs +9 -7
  68. package/dist-esm/index.mjs.map +2 -2
  69. package/dist-esm/lib/TldrawEditor.mjs +8 -8
  70. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  71. package/dist-esm/lib/components/Shape.mjs +7 -10
  72. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  73. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +15 -24
  74. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  75. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
  76. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
  77. package/dist-esm/lib/config/TLUserPreferences.mjs +1 -1
  78. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  79. package/dist-esm/lib/editor/Editor.mjs +50 -114
  80. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  81. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +4 -0
  82. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
  83. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +1 -1
  84. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  85. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +23 -0
  86. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  87. package/dist-esm/lib/exports/getSvgJsx.mjs +34 -14
  88. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  89. package/dist-esm/lib/hooks/useCanvasEvents.mjs +24 -18
  90. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  91. package/dist-esm/lib/hooks/useDocumentEvents.mjs +11 -6
  92. package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
  93. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs +2 -3
  94. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
  95. package/dist-esm/lib/hooks/useGestureEvents.mjs +2 -2
  96. package/dist-esm/lib/hooks/useGestureEvents.mjs.map +2 -2
  97. package/dist-esm/lib/hooks/useHandleEvents.mjs +9 -4
  98. package/dist-esm/lib/hooks/useHandleEvents.mjs.map +2 -2
  99. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs +4 -1
  100. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +2 -2
  101. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +4 -1
  102. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  103. package/dist-esm/lib/hooks/useSelectionEvents.mjs +6 -5
  104. package/dist-esm/lib/hooks/useSelectionEvents.mjs.map +2 -2
  105. package/dist-esm/lib/license/LicenseManager.mjs +144 -54
  106. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  107. package/dist-esm/lib/license/LicenseProvider.mjs +39 -2
  108. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  109. package/dist-esm/lib/license/Watermark.mjs +70 -8
  110. package/dist-esm/lib/license/Watermark.mjs.map +3 -3
  111. package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
  112. package/dist-esm/lib/options.mjs +6 -0
  113. package/dist-esm/lib/options.mjs.map +2 -2
  114. package/dist-esm/lib/primitives/Box.mjs +4 -1
  115. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  116. package/dist-esm/lib/primitives/Vec.mjs +0 -4
  117. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  118. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +53 -21
  119. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  120. package/dist-esm/lib/primitives/geometry/Group2d.mjs +8 -1
  121. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  122. package/dist-esm/lib/utils/dom.mjs +12 -1
  123. package/dist-esm/lib/utils/dom.mjs.map +2 -2
  124. package/dist-esm/lib/utils/getPointerInfo.mjs +2 -2
  125. package/dist-esm/lib/utils/getPointerInfo.mjs.map +2 -2
  126. package/dist-esm/lib/utils/reparenting.mjs +3 -40
  127. package/dist-esm/lib/utils/reparenting.mjs.map +2 -2
  128. package/dist-esm/version.mjs +3 -3
  129. package/dist-esm/version.mjs.map +1 -1
  130. package/editor.css +16 -3
  131. package/package.json +14 -37
  132. package/src/index.ts +4 -9
  133. package/src/lib/TldrawEditor.tsx +9 -16
  134. package/src/lib/components/Shape.tsx +6 -12
  135. package/src/lib/components/default-components/DefaultCanvas.tsx +12 -23
  136. package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
  137. package/src/lib/config/TLUserPreferences.ts +1 -1
  138. package/src/lib/editor/Editor.test.ts +102 -11
  139. package/src/lib/editor/Editor.ts +65 -151
  140. package/src/lib/editor/derivations/notVisibleShapes.ts +6 -0
  141. package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +15 -14
  142. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +16 -15
  143. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +49 -48
  144. package/src/lib/editor/managers/FontManager/FontManager.test.ts +24 -23
  145. package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +7 -6
  146. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +12 -11
  147. package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +57 -50
  148. package/src/lib/editor/managers/TextManager/TextManager.test.ts +51 -26
  149. package/src/lib/editor/managers/TickManager/TickManager.test.ts +14 -13
  150. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +21 -26
  151. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +1 -1
  152. package/src/lib/editor/shapes/ShapeUtil.ts +46 -0
  153. package/src/lib/editor/types/misc-types.ts +0 -6
  154. package/src/lib/exports/getSvgJsx.test.ts +868 -0
  155. package/src/lib/exports/getSvgJsx.tsx +76 -19
  156. package/src/lib/hooks/useCanvasEvents.ts +23 -17
  157. package/src/lib/hooks/useDocumentEvents.ts +11 -6
  158. package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +2 -2
  159. package/src/lib/hooks/useGestureEvents.ts +2 -2
  160. package/src/lib/hooks/useHandleEvents.ts +9 -4
  161. package/src/lib/hooks/usePassThroughMouseOverEvents.ts +4 -1
  162. package/src/lib/hooks/usePassThroughWheelEvents.ts +6 -1
  163. package/src/lib/hooks/useSelectionEvents.ts +6 -5
  164. package/src/lib/license/LicenseManager.test.ts +724 -383
  165. package/src/lib/license/LicenseManager.ts +204 -58
  166. package/src/lib/license/LicenseProvider.tsx +74 -2
  167. package/src/lib/license/Watermark.test.tsx +2 -1
  168. package/src/lib/license/Watermark.tsx +75 -8
  169. package/src/lib/license/useLicenseManagerState.ts +2 -2
  170. package/src/lib/options.ts +6 -0
  171. package/src/lib/primitives/Box.test.ts +126 -0
  172. package/src/lib/primitives/Box.ts +10 -1
  173. package/src/lib/primitives/Vec.ts +0 -5
  174. package/src/lib/primitives/geometry/Geometry2d.test.ts +420 -0
  175. package/src/lib/primitives/geometry/Geometry2d.ts +78 -21
  176. package/src/lib/primitives/geometry/Group2d.ts +10 -1
  177. package/src/lib/test/InFrontOfTheCanvas.test.tsx +187 -0
  178. package/src/lib/utils/dom.test.ts +94 -0
  179. package/src/lib/utils/dom.ts +38 -1
  180. package/src/lib/utils/getPointerInfo.ts +2 -1
  181. package/src/lib/utils/reparenting.ts +3 -69
  182. package/src/lib/utils/sync/LocalIndexedDb.test.ts +2 -1
  183. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +15 -15
  184. package/src/version.ts +3 -3
  185. package/dist-cjs/lib/utils/nearestMultiple.js +0 -34
  186. package/dist-cjs/lib/utils/nearestMultiple.js.map +0 -7
  187. package/dist-esm/lib/utils/nearestMultiple.mjs +0 -14
  188. package/dist-esm/lib/utils/nearestMultiple.mjs.map +0 -7
  189. package/src/lib/utils/nearestMultiple.ts +0 -13
@@ -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,12 +80,19 @@ 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
91
  export type TestEnvironment = 'development' | 'production'
68
92
 
93
+ /** @internal */
94
+ export type TrackType = 'unlicensed' | 'with_watermark' | 'evaluation' | null
95
+
69
96
  /** @internal */
70
97
  export class LicenseManager {
71
98
  private publicKey =
@@ -73,10 +100,7 @@ export class LicenseManager {
73
100
  public isDevelopment: boolean
74
101
  public isTest: boolean
75
102
  public isCryptoAvailable: boolean
76
- state = atom<'pending' | 'licensed' | 'licensed-with-watermark' | 'unlicensed'>(
77
- 'license state',
78
- 'pending'
79
- )
103
+ state = atom<LicenseState>('license state', 'pending')
80
104
  public verbose = true
81
105
 
82
106
  constructor(
@@ -89,21 +113,22 @@ export class LicenseManager {
89
113
  this.publicKey = testPublicKey || this.publicKey
90
114
  this.isCryptoAvailable = !!crypto.subtle
91
115
 
92
- this.getLicenseFromKey(licenseKey).then((result) => {
93
- const isUnlicensed = isEditorUnlicensed(result)
116
+ this.getLicenseFromKey(licenseKey)
117
+ .then((result) => {
118
+ const licenseState = getLicenseState(
119
+ result,
120
+ (messages: string[]) => this.outputMessages(messages),
121
+ this.isDevelopment
122
+ )
94
123
 
95
- if (!this.isDevelopment && isUnlicensed) {
96
- fetch(WATERMARK_TRACK_SRC)
97
- }
124
+ this.maybeTrack(result, licenseState)
98
125
 
99
- if (isUnlicensed) {
126
+ this.state.set(licenseState)
127
+ })
128
+ .catch((error) => {
129
+ console.error('License validation failed:', error)
100
130
  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
- })
131
+ })
107
132
  }
108
133
 
109
134
  private getIsDevelopment(testEnvironment?: TestEnvironment) {
@@ -117,6 +142,50 @@ export class LicenseManager {
117
142
  )
118
143
  }
119
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
+
120
189
  private async extractLicenseKey(licenseKey: string): Promise<LicenseInfo> {
121
190
  const [data, signature] = licenseKey.split('.')
122
191
  const [prefix, encodedData] = data.split('/')
@@ -203,6 +272,9 @@ export class LicenseManager {
203
272
  const isAnnualLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.ANNUAL_LICENSE)
204
273
  const isPerpetualLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.PERPETUAL_LICENSE)
205
274
 
275
+ const isEvaluationLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.EVALUATION_LICENSE)
276
+ const daysSinceExpiry = this.getDaysSinceExpiry(expiryDate)
277
+
206
278
  const result: ValidLicenseKeyResult = {
207
279
  license: licenseInfo,
208
280
  isLicenseParseable: true,
@@ -214,7 +286,12 @@ export class LicenseManager {
214
286
  isPerpetualLicense,
215
287
  isPerpetualLicenseExpired: isPerpetualLicense && this.isPerpetualLicenseExpired(expiryDate),
216
288
  isInternalLicense: this.isFlagEnabled(licenseInfo.flags, FLAGS.INTERNAL_LICENSE),
289
+ isNativeLicense: this.isNativeLicense(licenseInfo),
217
290
  isLicensedWithWatermark: this.isFlagEnabled(licenseInfo.flags, FLAGS.WITH_WATERMARK),
291
+ isEvaluationLicense,
292
+ isEvaluationLicenseExpired:
293
+ isEvaluationLicense && this.isEvaluationLicenseExpired(expiryDate),
294
+ daysSinceExpiry,
218
295
  }
219
296
  this.outputLicenseInfoIfNeeded(result)
220
297
 
@@ -230,13 +307,13 @@ export class LicenseManager {
230
307
  const currentHostname = window.location.hostname.toLowerCase()
231
308
 
232
309
  return licenseInfo.hosts.some((host) => {
233
- const normalizedHost = host.toLowerCase().trim()
310
+ const normalizedHostOrUrlRegex = host.toLowerCase().trim()
234
311
 
235
312
  // Allow the domain if listed and www variations, 'example.com' allows 'example.com' and 'www.example.com'
236
313
  if (
237
- normalizedHost === currentHostname ||
238
- `www.${normalizedHost}` === currentHostname ||
239
- normalizedHost === `www.${currentHostname}`
314
+ normalizedHostOrUrlRegex === currentHostname ||
315
+ `www.${normalizedHostOrUrlRegex}` === currentHostname ||
316
+ normalizedHostOrUrlRegex === `www.${currentHostname}`
240
317
  ) {
241
318
  return true
242
319
  }
@@ -247,6 +324,12 @@ export class LicenseManager {
247
324
  return true
248
325
  }
249
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
+
250
333
  // Glob testing, we only support '*.somedomain.com' right now.
251
334
  if (host.includes('*')) {
252
335
  const globToRegex = new RegExp(host.replace(/\*/g, '.*?'))
@@ -257,7 +340,7 @@ export class LicenseManager {
257
340
  if (window.location.protocol === 'vscode-webview:') {
258
341
  const currentUrl = new URL(window.location.href)
259
342
  const extensionId = currentUrl.searchParams.get('extensionId')
260
- if (normalizedHost === extensionId) {
343
+ if (normalizedHostOrUrlRegex === extensionId) {
261
344
  return true
262
345
  }
263
346
  }
@@ -266,6 +349,10 @@ export class LicenseManager {
266
349
  })
267
350
  }
268
351
 
352
+ private isNativeLicense(licenseInfo: LicenseInfo) {
353
+ return this.isFlagEnabled(licenseInfo.flags, FLAGS.NATIVE_LICENSE)
354
+ }
355
+
269
356
  private getExpirationDateWithoutGracePeriod(expiryDate: Date) {
270
357
  return new Date(expiryDate.getFullYear(), expiryDate.getMonth(), expiryDate.getDate())
271
358
  }
@@ -280,15 +367,7 @@ export class LicenseManager {
280
367
 
281
368
  private isAnnualLicenseExpired(expiryDate: Date) {
282
369
  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
370
+ return new Date() >= expiration
292
371
  }
293
372
 
294
373
  private isPerpetualLicenseExpired(expiryDate: Date) {
@@ -301,6 +380,21 @@ export class LicenseManager {
301
380
  return dates.major >= expiration || dates.minor >= expiration
302
381
  }
303
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
+
304
398
  private isFlagEnabled(flags: number, flag: number) {
305
399
  return (flags & flag) === flag
306
400
  }
@@ -318,19 +412,6 @@ export class LicenseManager {
318
412
  }
319
413
 
320
414
  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
415
  // If we added a new flag it will be twice the value of the currently highest flag.
335
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.
336
417
  if (result.license.flags >= HIGHEST_FLAG * 2) {
@@ -367,15 +448,80 @@ export class LicenseManager {
367
448
  static className = 'tl-watermark_SEE-LICENSE'
368
449
  }
369
450
 
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.')
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'
376
497
  }
377
- return true
378
498
  }
379
499
 
380
- return false
500
+ // Handle expired regular licenses (both annual and perpetual)
501
+ if (result.isPerpetualLicenseExpired || result.isAnnualLicenseExpired) {
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'
519
+ }
520
+
521
+ // License is valid, determine if it has watermark
522
+ if (result.isLicensedWithWatermark) {
523
+ return 'licensed-with-watermark'
524
+ }
525
+
526
+ return 'licensed'
381
527
  }
@@ -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
+ }
@@ -1,10 +1,11 @@
1
1
  import { act, render, waitFor } from '@testing-library/react'
2
+ import { vi } from 'vitest'
2
3
  import { TldrawEditor } from '../TldrawEditor'
3
4
  import { LicenseManager } from './LicenseManager'
4
5
 
5
6
  let mockLicenseState = 'unlicensed'
6
7
 
7
- jest.mock('./useLicenseManagerState', () => ({
8
+ vi.mock('./useLicenseManagerState', () => ({
8
9
  useLicenseManagerState: () => mockLicenseState,
9
10
  }))
10
11
 
@@ -3,7 +3,7 @@ import { memo, useRef } from 'react'
3
3
  import { useCanvasEvents } from '../hooks/useCanvasEvents'
4
4
  import { useEditor } from '../hooks/useEditor'
5
5
  import { usePassThroughWheelEvents } from '../hooks/usePassThroughWheelEvents'
6
- import { preventDefault, stopEventPropagation } from '../utils/dom'
6
+ import { markEventAsHandled, preventDefault } from '../utils/dom'
7
7
  import { runtime } from '../utils/runtime'
8
8
  import { watermarkDesktopSvg, watermarkMobileSvg } from '../watermarks'
9
9
  import { LicenseManager } from './LicenseManager'
@@ -28,12 +28,74 @@ export const Watermark = memo(function Watermark() {
28
28
  return (
29
29
  <>
30
30
  <LicenseStyles />
31
- <WatermarkInner src={isMobile ? WATERMARK_MOBILE_LOCAL_SRC : WATERMARK_DESKTOP_LOCAL_SRC} />
31
+ <WatermarkInner
32
+ src={isMobile ? WATERMARK_MOBILE_LOCAL_SRC : WATERMARK_DESKTOP_LOCAL_SRC}
33
+ isUnlicensed={licenseManagerState === 'unlicensed'}
34
+ />
32
35
  </>
33
36
  )
34
37
  })
35
38
 
36
- const WatermarkInner = memo(function WatermarkInner({ src }: { src: string }) {
39
+ const UnlicensedWatermark = memo(function UnlicensedWatermark({
40
+ isDebugMode,
41
+ isMobile,
42
+ }: {
43
+ isDebugMode: boolean
44
+ isMobile: boolean
45
+ }) {
46
+ const events = useCanvasEvents()
47
+ const ref = useRef<HTMLDivElement>(null)
48
+ usePassThroughWheelEvents(ref)
49
+
50
+ const url = 'https://tldraw.dev/?utm_source=dotcom&utm_medium=organic&utm_campaign=watermark'
51
+
52
+ return (
53
+ <div
54
+ ref={ref}
55
+ className={LicenseManager.className}
56
+ data-debug={isDebugMode}
57
+ data-mobile={isMobile}
58
+ data-unlicensed={true}
59
+ data-testid="tl-watermark-unlicensed"
60
+ draggable={false}
61
+ {...events}
62
+ >
63
+ <button
64
+ draggable={false}
65
+ role="button"
66
+ onPointerDown={(e) => {
67
+ markEventAsHandled(e)
68
+ preventDefault(e)
69
+ }}
70
+ title="Unlicensed - click to get a license"
71
+ onClick={() => runtime.openWindow(url, '_blank')}
72
+ style={{
73
+ position: 'absolute',
74
+ pointerEvents: 'all',
75
+ cursor: 'pointer',
76
+ color: 'var(--tl-color-text)',
77
+ opacity: 0.8,
78
+ border: 0,
79
+ padding: 0,
80
+ backgroundColor: 'transparent',
81
+ fontSize: '11px',
82
+ fontWeight: '600',
83
+ textAlign: 'center',
84
+ }}
85
+ >
86
+ Unlicensed
87
+ </button>
88
+ </div>
89
+ )
90
+ })
91
+
92
+ const WatermarkInner = memo(function WatermarkInner({
93
+ src,
94
+ isUnlicensed,
95
+ }: {
96
+ src: string
97
+ isUnlicensed: boolean
98
+ }) {
37
99
  const editor = useEditor()
38
100
  const isDebugMode = useValue('debug mode', () => editor.getInstanceState().isDebugMode, [editor])
39
101
  const isMobile = useValue('is mobile', () => editor.getViewportScreenBounds().width < 700, [
@@ -47,12 +109,17 @@ const WatermarkInner = memo(function WatermarkInner({ src }: { src: string }) {
47
109
  const maskCss = `url('${src}') center 100% / 100% no-repeat`
48
110
  const url = 'https://tldraw.dev/?utm_source=dotcom&utm_medium=organic&utm_campaign=watermark'
49
111
 
112
+ if (isUnlicensed) {
113
+ return <UnlicensedWatermark isDebugMode={isDebugMode} isMobile={isMobile} />
114
+ }
115
+
50
116
  return (
51
117
  <div
52
118
  ref={ref}
53
119
  className={LicenseManager.className}
54
120
  data-debug={isDebugMode}
55
121
  data-mobile={isMobile}
122
+ data-testid="tl-watermark-licensed"
56
123
  draggable={false}
57
124
  {...events}
58
125
  >
@@ -60,7 +127,7 @@ const WatermarkInner = memo(function WatermarkInner({ src }: { src: string }) {
60
127
  draggable={false}
61
128
  role="button"
62
129
  onPointerDown={(e) => {
63
- stopEventPropagation(e)
130
+ markEventAsHandled(e)
64
131
  preventDefault(e)
65
132
  }}
66
133
  title="made with tldraw"
@@ -86,8 +153,8 @@ To remove the watermark, please purchase a license at tldraw.dev.
86
153
 
87
154
  .${className} {
88
155
  position: absolute;
89
- bottom: var(--tl-space-2);
90
- right: var(--tl-space-2);
156
+ bottom: max(var(--tl-space-2), env(safe-area-inset-bottom));
157
+ right: max(var(--tl-space-2), env(safe-area-inset-right));
91
158
  width: 96px;
92
159
  height: 32px;
93
160
  display: flex;
@@ -116,12 +183,12 @@ To remove the watermark, please purchase a license at tldraw.dev.
116
183
  }
117
184
 
118
185
  .${className}[data-debug='true'] {
119
- bottom: 46px;
186
+ bottom: max(46px, env(safe-area-inset-bottom));
120
187
  }
121
188
 
122
189
  .${className}[data-mobile='true'] {
123
190
  border-radius: 4px 0px 0px 4px;
124
- right: -2px;
191
+ right: max(-2px, calc(env(safe-area-inset-right) - 2px));
125
192
  width: 8px;
126
193
  height: 48px;
127
194
  }
@@ -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
  }
@@ -27,6 +27,8 @@ export interface TldrawOptions {
27
27
  readonly multiClickDurationMs: number
28
28
  readonly coarseDragDistanceSquared: number
29
29
  readonly dragDistanceSquared: number
30
+ readonly uiDragDistanceSquared: number
31
+ readonly uiCoarseDragDistanceSquared: number
30
32
  readonly defaultSvgPadding: number
31
33
  readonly cameraSlideFriction: number
32
34
  readonly gridSteps: readonly {
@@ -98,6 +100,10 @@ export const defaultTldrawOptions = {
98
100
  multiClickDurationMs: 200,
99
101
  coarseDragDistanceSquared: 36, // 6 squared
100
102
  dragDistanceSquared: 16, // 4 squared
103
+ uiDragDistanceSquared: 16, // 4 squared
104
+ // it's really easy to accidentally drag from the toolbar on mobile, so we use a much larger
105
+ // threshold than usual here to try and prevent accidental drags.
106
+ uiCoarseDragDistanceSquared: 625, // 25 squared
101
107
  defaultSvgPadding: 32,
102
108
  cameraSlideFriction: 0.09,
103
109
  gridSteps: [