@tldraw/editor 3.16.0-canary.cb97f41de62b → 3.16.0-canary.cc5427cdff41
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.
- package/dist-cjs/index.d.ts +52 -101
- package/dist-cjs/index.js +3 -5
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +5 -5
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/components/Shape.js +7 -10
- package/dist-cjs/lib/components/Shape.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +4 -23
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +31 -109
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js +13 -0
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
- package/dist-cjs/lib/exports/getSvgJsx.js +34 -14
- package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +7 -5
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js +4 -1
- package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +4 -1
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
- package/dist-cjs/lib/license/LicenseManager.js +120 -50
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/license/LicenseProvider.js +22 -0
- package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
- package/dist-cjs/lib/license/Watermark.js +68 -6
- package/dist-cjs/lib/license/Watermark.js.map +3 -3
- package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
- package/dist-cjs/lib/primitives/Box.js +3 -0
- package/dist-cjs/lib/primitives/Box.js.map +2 -2
- package/dist-cjs/lib/primitives/Vec.js +0 -4
- package/dist-cjs/lib/primitives/Vec.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js +26 -18
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Group2d.js +3 -0
- package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
- package/dist-cjs/lib/utils/reparenting.js +2 -35
- package/dist-cjs/lib/utils/reparenting.js.map +3 -3
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +52 -101
- package/dist-esm/index.mjs +3 -5
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +5 -5
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/Shape.mjs +7 -10
- package/dist-esm/lib/components/Shape.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +4 -23
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +31 -109
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +13 -0
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/exports/getSvgJsx.mjs +34 -14
- package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +7 -5
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs +4 -1
- package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +4 -1
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseManager.mjs +121 -51
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseProvider.mjs +23 -1
- package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
- package/dist-esm/lib/license/Watermark.mjs +68 -6
- package/dist-esm/lib/license/Watermark.mjs.map +3 -3
- package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
- package/dist-esm/lib/primitives/Box.mjs +4 -1
- package/dist-esm/lib/primitives/Box.mjs.map +2 -2
- package/dist-esm/lib/primitives/Vec.mjs +0 -4
- package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +29 -19
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Group2d.mjs +3 -0
- package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
- package/dist-esm/lib/utils/reparenting.mjs +3 -40
- package/dist-esm/lib/utils/reparenting.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +16 -3
- package/package.json +7 -7
- package/src/index.ts +2 -9
- package/src/lib/TldrawEditor.tsx +6 -12
- package/src/lib/components/Shape.tsx +6 -12
- package/src/lib/components/default-components/DefaultCanvas.tsx +5 -22
- package/src/lib/editor/Editor.ts +38 -146
- package/src/lib/editor/shapes/ShapeUtil.ts +35 -0
- package/src/lib/editor/types/misc-types.ts +0 -6
- package/src/lib/exports/getSvgJsx.test.ts +868 -0
- package/src/lib/exports/getSvgJsx.tsx +76 -19
- package/src/lib/hooks/useCanvasEvents.ts +6 -6
- package/src/lib/hooks/usePassThroughMouseOverEvents.ts +4 -1
- package/src/lib/hooks/usePassThroughWheelEvents.ts +6 -1
- package/src/lib/license/LicenseManager.test.ts +645 -382
- package/src/lib/license/LicenseManager.ts +173 -53
- package/src/lib/license/LicenseProvider.tsx +34 -1
- package/src/lib/license/Watermark.tsx +73 -6
- package/src/lib/license/useLicenseManagerState.ts +2 -2
- package/src/lib/primitives/Box.test.ts +126 -0
- package/src/lib/primitives/Box.ts +10 -1
- package/src/lib/primitives/Vec.ts +0 -5
- package/src/lib/primitives/geometry/Geometry2d.ts +49 -19
- package/src/lib/primitives/geometry/Group2d.ts +4 -0
- package/src/lib/utils/reparenting.ts +3 -69
- package/src/version.ts +3 -3
- package/dist-cjs/lib/utils/nearestMultiple.js +0 -34
- package/dist-cjs/lib/utils/nearestMultiple.js.map +0 -7
- package/dist-esm/lib/utils/nearestMultiple.mjs +0 -14
- package/dist-esm/lib/utils/nearestMultiple.mjs.map +0 -7
- package/src/lib/utils/nearestMultiple.ts +0 -13
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { atom } from '@tldraw/state'
|
|
2
|
-
import {
|
|
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 =
|
|
6
|
+
const GRACE_PERIOD_DAYS = 30
|
|
8
7
|
|
|
9
8
|
export const FLAGS = {
|
|
10
|
-
ANNUAL_LICENSE:
|
|
11
|
-
PERPETUAL_LICENSE:
|
|
12
|
-
INTERNAL_LICENSE:
|
|
13
|
-
WITH_WATERMARK:
|
|
9
|
+
ANNUAL_LICENSE: 1,
|
|
10
|
+
PERPETUAL_LICENSE: 1 << 1,
|
|
11
|
+
INTERNAL_LICENSE: 1 << 2,
|
|
12
|
+
WITH_WATERMARK: 1 << 3,
|
|
13
|
+
EVALUATION_LICENSE: 1 << 4,
|
|
14
14
|
}
|
|
15
15
|
const HIGHEST_FLAG = Math.max(...Object.values(FLAGS))
|
|
16
16
|
|
|
@@ -33,6 +33,15 @@ export interface LicenseInfo {
|
|
|
33
33
|
flags: number
|
|
34
34
|
expiryDate: string
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
/** @internal */
|
|
38
|
+
export type LicenseState =
|
|
39
|
+
| 'pending' // License validation is in progress
|
|
40
|
+
| 'licensed' // License is valid and active (no restrictions)
|
|
41
|
+
| 'licensed-with-watermark' // License is valid but shows watermark (evaluation licenses, WITH_WATERMARK licenses)
|
|
42
|
+
| 'unlicensed' // No valid license found or license is invalid (development)
|
|
43
|
+
| 'unlicensed-production' // No valid license in production deployment (missing, invalid, or wrong domain)
|
|
44
|
+
| 'expired' // License has been expired (30 days past expiration for regular licenses, immediately for evaluation licenses)
|
|
36
45
|
/** @internal */
|
|
37
46
|
export type InvalidLicenseReason =
|
|
38
47
|
| 'invalid-license-key'
|
|
@@ -61,11 +70,17 @@ export interface ValidLicenseKeyResult {
|
|
|
61
70
|
isPerpetualLicenseExpired: boolean
|
|
62
71
|
isInternalLicense: boolean
|
|
63
72
|
isLicensedWithWatermark: boolean
|
|
73
|
+
isEvaluationLicense: boolean
|
|
74
|
+
isEvaluationLicenseExpired: boolean
|
|
75
|
+
daysSinceExpiry: number
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
/** @internal */
|
|
67
79
|
export type TestEnvironment = 'development' | 'production'
|
|
68
80
|
|
|
81
|
+
/** @internal */
|
|
82
|
+
export type TrackType = 'unlicensed' | 'with_watermark' | 'evaluation' | null
|
|
83
|
+
|
|
69
84
|
/** @internal */
|
|
70
85
|
export class LicenseManager {
|
|
71
86
|
private publicKey =
|
|
@@ -73,10 +88,7 @@ export class LicenseManager {
|
|
|
73
88
|
public isDevelopment: boolean
|
|
74
89
|
public isTest: boolean
|
|
75
90
|
public isCryptoAvailable: boolean
|
|
76
|
-
state = atom<'
|
|
77
|
-
'license state',
|
|
78
|
-
'pending'
|
|
79
|
-
)
|
|
91
|
+
state = atom<LicenseState>('license state', 'pending')
|
|
80
92
|
public verbose = true
|
|
81
93
|
|
|
82
94
|
constructor(
|
|
@@ -89,21 +101,22 @@ export class LicenseManager {
|
|
|
89
101
|
this.publicKey = testPublicKey || this.publicKey
|
|
90
102
|
this.isCryptoAvailable = !!crypto.subtle
|
|
91
103
|
|
|
92
|
-
this.getLicenseFromKey(licenseKey)
|
|
93
|
-
|
|
104
|
+
this.getLicenseFromKey(licenseKey)
|
|
105
|
+
.then((result) => {
|
|
106
|
+
const licenseState = getLicenseState(
|
|
107
|
+
result,
|
|
108
|
+
(messages: string[]) => this.outputMessages(messages),
|
|
109
|
+
this.isDevelopment
|
|
110
|
+
)
|
|
94
111
|
|
|
95
|
-
|
|
96
|
-
fetch(WATERMARK_TRACK_SRC)
|
|
97
|
-
}
|
|
112
|
+
this.maybeTrack(result, licenseState)
|
|
98
113
|
|
|
99
|
-
|
|
114
|
+
this.state.set(licenseState)
|
|
115
|
+
})
|
|
116
|
+
.catch((error) => {
|
|
117
|
+
console.error('License validation failed:', error)
|
|
100
118
|
this.state.set('unlicensed')
|
|
101
|
-
}
|
|
102
|
-
this.state.set('licensed-with-watermark')
|
|
103
|
-
} else {
|
|
104
|
-
this.state.set('licensed')
|
|
105
|
-
}
|
|
106
|
-
})
|
|
119
|
+
})
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
private getIsDevelopment(testEnvironment?: TestEnvironment) {
|
|
@@ -117,6 +130,47 @@ export class LicenseManager {
|
|
|
117
130
|
)
|
|
118
131
|
}
|
|
119
132
|
|
|
133
|
+
private getTrackType(result: LicenseFromKeyResult, licenseState: LicenseState): TrackType {
|
|
134
|
+
// Track watermark for unlicensed production deployments
|
|
135
|
+
if (licenseState === 'unlicensed-production') {
|
|
136
|
+
return 'unlicensed'
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (this.isDevelopment) {
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!result.isLicenseParseable) {
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Track evaluation licenses (for analytics, even though no watermark is shown)
|
|
148
|
+
if (result.isEvaluationLicense) {
|
|
149
|
+
return 'evaluation'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Track licenses that show watermarks
|
|
153
|
+
if (licenseState === 'licensed-with-watermark') {
|
|
154
|
+
return 'with_watermark'
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private maybeTrack(result: LicenseFromKeyResult, licenseState: LicenseState): void {
|
|
161
|
+
const trackType = this.getTrackType(result, licenseState)
|
|
162
|
+
if (!trackType) {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const url = new URL(WATERMARK_TRACK_SRC)
|
|
167
|
+
url.searchParams.set('version', version)
|
|
168
|
+
url.searchParams.set('license_type', trackType)
|
|
169
|
+
|
|
170
|
+
// eslint-disable-next-line no-restricted-globals
|
|
171
|
+
fetch(url.toString())
|
|
172
|
+
}
|
|
173
|
+
|
|
120
174
|
private async extractLicenseKey(licenseKey: string): Promise<LicenseInfo> {
|
|
121
175
|
const [data, signature] = licenseKey.split('.')
|
|
122
176
|
const [prefix, encodedData] = data.split('/')
|
|
@@ -203,6 +257,9 @@ export class LicenseManager {
|
|
|
203
257
|
const isAnnualLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.ANNUAL_LICENSE)
|
|
204
258
|
const isPerpetualLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.PERPETUAL_LICENSE)
|
|
205
259
|
|
|
260
|
+
const isEvaluationLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.EVALUATION_LICENSE)
|
|
261
|
+
const daysSinceExpiry = this.getDaysSinceExpiry(expiryDate)
|
|
262
|
+
|
|
206
263
|
const result: ValidLicenseKeyResult = {
|
|
207
264
|
license: licenseInfo,
|
|
208
265
|
isLicenseParseable: true,
|
|
@@ -215,6 +272,10 @@ export class LicenseManager {
|
|
|
215
272
|
isPerpetualLicenseExpired: isPerpetualLicense && this.isPerpetualLicenseExpired(expiryDate),
|
|
216
273
|
isInternalLicense: this.isFlagEnabled(licenseInfo.flags, FLAGS.INTERNAL_LICENSE),
|
|
217
274
|
isLicensedWithWatermark: this.isFlagEnabled(licenseInfo.flags, FLAGS.WITH_WATERMARK),
|
|
275
|
+
isEvaluationLicense,
|
|
276
|
+
isEvaluationLicenseExpired:
|
|
277
|
+
isEvaluationLicense && this.isEvaluationLicenseExpired(expiryDate),
|
|
278
|
+
daysSinceExpiry,
|
|
218
279
|
}
|
|
219
280
|
this.outputLicenseInfoIfNeeded(result)
|
|
220
281
|
|
|
@@ -280,15 +341,7 @@ export class LicenseManager {
|
|
|
280
341
|
|
|
281
342
|
private isAnnualLicenseExpired(expiryDate: Date) {
|
|
282
343
|
const expiration = this.getExpirationDateWithGracePeriod(expiryDate)
|
|
283
|
-
|
|
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
|
|
344
|
+
return new Date() >= expiration
|
|
292
345
|
}
|
|
293
346
|
|
|
294
347
|
private isPerpetualLicenseExpired(expiryDate: Date) {
|
|
@@ -301,6 +354,21 @@ export class LicenseManager {
|
|
|
301
354
|
return dates.major >= expiration || dates.minor >= expiration
|
|
302
355
|
}
|
|
303
356
|
|
|
357
|
+
private getDaysSinceExpiry(expiryDate: Date): number {
|
|
358
|
+
const now = new Date()
|
|
359
|
+
const expiration = this.getExpirationDateWithoutGracePeriod(expiryDate)
|
|
360
|
+
const diffTime = now.getTime() - expiration.getTime()
|
|
361
|
+
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
|
|
362
|
+
return Math.max(0, diffDays)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private isEvaluationLicenseExpired(expiryDate: Date): boolean {
|
|
366
|
+
// Evaluation licenses have no grace period - they expire immediately
|
|
367
|
+
const now = new Date()
|
|
368
|
+
const expiration = this.getExpirationDateWithoutGracePeriod(expiryDate)
|
|
369
|
+
return now >= expiration
|
|
370
|
+
}
|
|
371
|
+
|
|
304
372
|
private isFlagEnabled(flags: number, flag: number) {
|
|
305
373
|
return (flags & flag) === flag
|
|
306
374
|
}
|
|
@@ -318,19 +386,6 @@ export class LicenseManager {
|
|
|
318
386
|
}
|
|
319
387
|
|
|
320
388
|
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
389
|
// If we added a new flag it will be twice the value of the currently highest flag.
|
|
335
390
|
// 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
391
|
if (result.license.flags >= HIGHEST_FLAG * 2) {
|
|
@@ -367,15 +422,80 @@ export class LicenseManager {
|
|
|
367
422
|
static className = 'tl-watermark_SEE-LICENSE'
|
|
368
423
|
}
|
|
369
424
|
|
|
370
|
-
export function
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
425
|
+
export function getLicenseState(
|
|
426
|
+
result: LicenseFromKeyResult,
|
|
427
|
+
outputMessages: (messages: string[]) => void,
|
|
428
|
+
isDevelopment: boolean
|
|
429
|
+
): LicenseState {
|
|
430
|
+
if (!result.isLicenseParseable) {
|
|
431
|
+
if (isDevelopment) {
|
|
432
|
+
return 'unlicensed'
|
|
376
433
|
}
|
|
377
|
-
|
|
434
|
+
|
|
435
|
+
// All unlicensed scenarios should not work in production
|
|
436
|
+
if (result.reason === 'no-key-provided') {
|
|
437
|
+
outputMessages([
|
|
438
|
+
'No tldraw license key provided!',
|
|
439
|
+
'A license is required for production deployments.',
|
|
440
|
+
`Please reach out to ${LICENSE_EMAIL} to purchase a license.`,
|
|
441
|
+
])
|
|
442
|
+
} else {
|
|
443
|
+
outputMessages([
|
|
444
|
+
'Invalid license key. tldraw requires a valid license for production use.',
|
|
445
|
+
`Please reach out to ${LICENSE_EMAIL} to purchase a license.`,
|
|
446
|
+
])
|
|
447
|
+
}
|
|
448
|
+
return 'unlicensed-production'
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (!result.isDomainValid && !result.isDevelopment) {
|
|
452
|
+
outputMessages([
|
|
453
|
+
'License key is not valid for this domain.',
|
|
454
|
+
'A license is required for production deployments.',
|
|
455
|
+
`Please reach out to ${LICENSE_EMAIL} to purchase a license.`,
|
|
456
|
+
])
|
|
457
|
+
return 'unlicensed-production'
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Handle evaluation licenses - they expire immediately with no grace period
|
|
461
|
+
if (result.isEvaluationLicense) {
|
|
462
|
+
if (result.isEvaluationLicenseExpired) {
|
|
463
|
+
outputMessages([
|
|
464
|
+
'Your tldraw evaluation license has expired!',
|
|
465
|
+
`Please reach out to ${LICENSE_EMAIL} to purchase a full license.`,
|
|
466
|
+
])
|
|
467
|
+
return 'expired'
|
|
468
|
+
} else {
|
|
469
|
+
// Valid evaluation license - tracked but no watermark shown
|
|
470
|
+
return 'licensed'
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Handle expired regular licenses (both annual and perpetual)
|
|
475
|
+
if (result.isPerpetualLicenseExpired || result.isAnnualLicenseExpired) {
|
|
476
|
+
outputMessages([
|
|
477
|
+
'Your tldraw license has been expired for more than 30 days!',
|
|
478
|
+
`Please reach out to ${LICENSE_EMAIL} to renew your license.`,
|
|
479
|
+
])
|
|
480
|
+
return 'expired'
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Check if license is past expiry date but within grace period
|
|
484
|
+
const daysSinceExpiry = result.daysSinceExpiry
|
|
485
|
+
if (daysSinceExpiry > 0 && !result.isEvaluationLicense) {
|
|
486
|
+
outputMessages([
|
|
487
|
+
'Your tldraw license has expired.',
|
|
488
|
+
`License expired ${daysSinceExpiry} days ago.`,
|
|
489
|
+
`Please reach out to ${LICENSE_EMAIL} to renew your license.`,
|
|
490
|
+
])
|
|
491
|
+
// Within 30-day grace period: still licensed (no watermark)
|
|
492
|
+
return 'licensed'
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// License is valid, determine if it has watermark
|
|
496
|
+
if (result.isLicensedWithWatermark) {
|
|
497
|
+
return 'licensed-with-watermark'
|
|
378
498
|
}
|
|
379
499
|
|
|
380
|
-
return
|
|
500
|
+
return 'licensed'
|
|
381
501
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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,6 +8,13 @@ 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
20
|
licenseKey,
|
|
@@ -16,5 +24,30 @@ export function LicenseProvider({
|
|
|
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
|
+
}
|
|
@@ -28,12 +28,74 @@ export const Watermark = memo(function Watermark() {
|
|
|
28
28
|
return (
|
|
29
29
|
<>
|
|
30
30
|
<LicenseStyles />
|
|
31
|
-
<WatermarkInner
|
|
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
|
|
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
|
+
stopEventPropagation(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
|
>
|
|
@@ -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
|
}
|
|
@@ -510,6 +510,132 @@ describe('Box', () => {
|
|
|
510
510
|
})
|
|
511
511
|
})
|
|
512
512
|
|
|
513
|
+
describe('Box.ContainsApproximately', () => {
|
|
514
|
+
it('returns true when first box exactly contains second', () => {
|
|
515
|
+
const boxA = new Box(0, 0, 100, 100)
|
|
516
|
+
const boxB = new Box(10, 10, 50, 50)
|
|
517
|
+
expect(Box.ContainsApproximately(boxA, boxB)).toBe(true)
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
it('returns false when first box clearly does not contain second', () => {
|
|
521
|
+
const boxA = new Box(0, 0, 50, 50)
|
|
522
|
+
const boxB = new Box(10, 10, 100, 100)
|
|
523
|
+
expect(Box.ContainsApproximately(boxA, boxB)).toBe(false)
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
it('returns true when containment is within default precision tolerance', () => {
|
|
527
|
+
// Box B extends very slightly outside A (within floating-point precision)
|
|
528
|
+
const boxA = new Box(0, 0, 100, 100)
|
|
529
|
+
const boxB = new Box(10, 10, 80, 80)
|
|
530
|
+
// Move B's max edges just slightly outside A's bounds
|
|
531
|
+
boxB.w = 90.000000000001 // maxX = 100.000000000001 (slightly beyond 100)
|
|
532
|
+
boxB.h = 90.000000000001 // maxY = 100.000000000001 (slightly beyond 100)
|
|
533
|
+
|
|
534
|
+
expect(Box.ContainsApproximately(boxA, boxB)).toBe(true)
|
|
535
|
+
expect(Box.Contains(boxA, boxB)).toBe(false) // strict contains would fail
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
it('returns false when containment exceeds default precision tolerance', () => {
|
|
539
|
+
const boxA = new Box(0, 0, 100, 100)
|
|
540
|
+
const boxB = new Box(10, 10, 80, 80)
|
|
541
|
+
// Move B's max edges clearly outside A's bounds
|
|
542
|
+
boxB.w = 95 // maxX = 105 (clearly beyond 100)
|
|
543
|
+
boxB.h = 95 // maxY = 105 (clearly beyond 100)
|
|
544
|
+
|
|
545
|
+
expect(Box.ContainsApproximately(boxA, boxB)).toBe(false)
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
it('respects custom precision parameter', () => {
|
|
549
|
+
const boxA = new Box(0, 0, 100, 100)
|
|
550
|
+
const boxB = new Box(10, 10, 85, 85) // maxX=95, maxY=95
|
|
551
|
+
|
|
552
|
+
// With loose precision (10), should contain (95 is within 100-10=90 tolerance)
|
|
553
|
+
expect(Box.ContainsApproximately(boxA, boxB, 10)).toBe(true)
|
|
554
|
+
|
|
555
|
+
// With tight precision (4), should still contain (95 is within 100-4=96)
|
|
556
|
+
expect(Box.ContainsApproximately(boxA, boxB, 4)).toBe(true)
|
|
557
|
+
|
|
558
|
+
// Since 95 < 100, the precision parameter doesn't affect containment here
|
|
559
|
+
expect(Box.ContainsApproximately(boxA, boxB, 4.9)).toBe(true)
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
it('handles negative coordinates correctly', () => {
|
|
563
|
+
const boxA = new Box(-50, -50, 100, 100) // bounds: (-50,-50) to (50,50)
|
|
564
|
+
const boxB = new Box(-40, -40, 79.999999999, 79.999999999) // bounds: (-40,-40) to (39.999999999, 39.999999999)
|
|
565
|
+
|
|
566
|
+
expect(Box.ContainsApproximately(boxA, boxB)).toBe(true)
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
it('handles edge case where boxes are identical', () => {
|
|
570
|
+
const boxA = new Box(10, 20, 100, 200)
|
|
571
|
+
const boxB = new Box(10, 20, 100, 200)
|
|
572
|
+
|
|
573
|
+
expect(Box.ContainsApproximately(boxA, boxB)).toBe(true)
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
it('handles edge case where inner box touches outer box edges', () => {
|
|
577
|
+
const boxA = new Box(0, 0, 100, 100)
|
|
578
|
+
const boxB = new Box(0, 0, 100, 100) // exactly the same
|
|
579
|
+
|
|
580
|
+
expect(Box.ContainsApproximately(boxA, boxB)).toBe(true)
|
|
581
|
+
|
|
582
|
+
// Slightly smaller inner box
|
|
583
|
+
const boxC = new Box(0.000001, 0.000001, 99.999998, 99.999998)
|
|
584
|
+
expect(Box.ContainsApproximately(boxA, boxC)).toBe(true)
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
it('handles floating-point precision issues in real-world scenarios', () => {
|
|
588
|
+
// Simulate common floating-point arithmetic issues
|
|
589
|
+
const containerBox = new Box(0, 0, 100, 100)
|
|
590
|
+
|
|
591
|
+
// Box that should be contained but has floating-point errors
|
|
592
|
+
const innerBox = new Box(10, 10, 80, 80)
|
|
593
|
+
// Simulate floating-point arithmetic that results in tiny overruns
|
|
594
|
+
innerBox.w = 90.00000000000001 // maxX = 100.00000000000001 (tiny overrun)
|
|
595
|
+
innerBox.h = 90.00000000000001 // maxY = 100.00000000000001 (tiny overrun)
|
|
596
|
+
|
|
597
|
+
expect(Box.ContainsApproximately(containerBox, innerBox)).toBe(true)
|
|
598
|
+
expect(Box.Contains(containerBox, innerBox)).toBe(false) // strict contains fails due to precision
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
it('fails when any edge exceeds tolerance', () => {
|
|
602
|
+
const boxA = new Box(10, 10, 100, 100) // bounds: (10,10) to (110,110)
|
|
603
|
+
|
|
604
|
+
// Test each edge exceeding tolerance
|
|
605
|
+
const testCases = [
|
|
606
|
+
{ name: 'left edge', box: new Box(5, 20, 80, 80) }, // minX too small
|
|
607
|
+
{ name: 'top edge', box: new Box(20, 5, 80, 80) }, // minY too small
|
|
608
|
+
{ name: 'right edge', box: new Box(20, 20, 95, 80) }, // maxX too large (20+95=115 > 110)
|
|
609
|
+
{ name: 'bottom edge', box: new Box(20, 20, 80, 95) }, // maxY too large (20+95=115 > 110)
|
|
610
|
+
]
|
|
611
|
+
|
|
612
|
+
testCases.forEach(({ box }) => {
|
|
613
|
+
expect(Box.ContainsApproximately(boxA, box, 1)).toBe(false) // tight precision
|
|
614
|
+
})
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
it('works with zero-sized dimensions', () => {
|
|
618
|
+
const boxA = new Box(0, 0, 100, 100)
|
|
619
|
+
const boxB = new Box(50, 50, 0, 0) // zero-sized box (point)
|
|
620
|
+
|
|
621
|
+
expect(Box.ContainsApproximately(boxA, boxB)).toBe(true)
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
it('handles precision parameter edge cases', () => {
|
|
625
|
+
const boxA = new Box(0, 0, 100, 100)
|
|
626
|
+
const boxB = new Box(10, 10, 91, 91) // maxX=101, maxY=101 (clearly outside)
|
|
627
|
+
|
|
628
|
+
// Zero precision should work like strict Contains
|
|
629
|
+
expect(Box.ContainsApproximately(boxA, boxB, 0)).toBe(false)
|
|
630
|
+
|
|
631
|
+
// Small precision should still fail (101 > 100)
|
|
632
|
+
expect(Box.ContainsApproximately(boxA, boxB, 0.5)).toBe(false)
|
|
633
|
+
|
|
634
|
+
// Sufficient precision should succeed (101 <= 100 + 2)
|
|
635
|
+
expect(Box.ContainsApproximately(boxA, boxB, 2)).toBe(true)
|
|
636
|
+
})
|
|
637
|
+
})
|
|
638
|
+
|
|
513
639
|
describe('Box.Includes', () => {
|
|
514
640
|
it('returns true when boxes collide or contain', () => {
|
|
515
641
|
const boxA = new Box(0, 0, 50, 50)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BoxModel } from '@tldraw/tlschema'
|
|
2
2
|
import { Vec, VecLike } from './Vec'
|
|
3
|
-
import { PI, PI2, toPrecision } from './utils'
|
|
3
|
+
import { approximatelyLte, PI, PI2, toPrecision } from './utils'
|
|
4
4
|
|
|
5
5
|
/** @public */
|
|
6
6
|
export type BoxLike = BoxModel | Box
|
|
@@ -417,6 +417,15 @@ export class Box {
|
|
|
417
417
|
return A.minX < B.minX && A.minY < B.minY && A.maxY > B.maxY && A.maxX > B.maxX
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
+
static ContainsApproximately(A: Box, B: Box, precision?: number) {
|
|
421
|
+
return (
|
|
422
|
+
approximatelyLte(A.minX, B.minX, precision) &&
|
|
423
|
+
approximatelyLte(A.minY, B.minY, precision) &&
|
|
424
|
+
approximatelyLte(B.maxX, A.maxX, precision) &&
|
|
425
|
+
approximatelyLte(B.maxY, A.maxY, precision)
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
|
|
420
429
|
static Includes(A: Box, B: Box) {
|
|
421
430
|
return Box.Collides(A, B) || Box.Contains(A, B)
|
|
422
431
|
}
|