@tldraw/editor 3.16.0-canary.e9c30b532b82 → 3.16.0-canary.ea008b31887f
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 +71 -101
- package/dist-cjs/index.js +3 -5
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +6 -6
- 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/components/default-components/DefaultErrorFallback.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
- package/dist-cjs/lib/config/TLUserPreferences.js +1 -1
- package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +44 -112
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +4 -0
- package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +2 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +1 -1
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js +23 -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 +138 -50
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/license/LicenseProvider.js +39 -1
- 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/options.js +6 -0
- package/dist-cjs/lib/options.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 +50 -20
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Group2d.js +8 -1
- 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 +71 -101
- package/dist-esm/index.mjs +3 -5
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +6 -6
- 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/components/default-components/DefaultErrorFallback.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
- package/dist-esm/lib/config/TLUserPreferences.mjs +1 -1
- package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +44 -112
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +4 -0
- package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +1 -1
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +23 -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 +139 -51
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseProvider.mjs +39 -2
- 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/options.mjs +6 -0
- package/dist-esm/lib/options.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 +53 -21
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Group2d.mjs +8 -1
- 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 +14 -37
- package/src/index.ts +2 -9
- package/src/lib/TldrawEditor.tsx +7 -14
- package/src/lib/components/Shape.tsx +6 -12
- package/src/lib/components/default-components/DefaultCanvas.tsx +5 -22
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
- package/src/lib/config/TLUserPreferences.ts +1 -1
- package/src/lib/editor/Editor.test.ts +12 -11
- package/src/lib/editor/Editor.ts +53 -149
- package/src/lib/editor/derivations/notVisibleShapes.ts +6 -0
- package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +15 -14
- package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +16 -15
- package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +49 -48
- package/src/lib/editor/managers/FontManager/FontManager.test.ts +24 -23
- package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +7 -6
- package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +12 -11
- package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +57 -50
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +51 -26
- package/src/lib/editor/managers/TickManager/TickManager.test.ts +14 -13
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +21 -26
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +1 -1
- package/src/lib/editor/shapes/ShapeUtil.ts +46 -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 +692 -383
- package/src/lib/license/LicenseManager.ts +197 -53
- package/src/lib/license/LicenseProvider.tsx +74 -2
- package/src/lib/license/Watermark.test.tsx +2 -1
- package/src/lib/license/Watermark.tsx +73 -6
- package/src/lib/license/useLicenseManagerState.ts +2 -2
- package/src/lib/options.ts +6 -0
- 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.test.ts +420 -0
- package/src/lib/primitives/geometry/Geometry2d.ts +78 -21
- package/src/lib/primitives/geometry/Group2d.ts +10 -1
- package/src/lib/utils/reparenting.ts +3 -69
- package/src/lib/utils/sync/LocalIndexedDb.test.ts +2 -1
- package/src/lib/utils/sync/TLLocalSyncClient.test.ts +15 -15
- 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,27 @@
|
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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<'
|
|
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)
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
fetch(WATERMARK_TRACK_SRC)
|
|
97
|
-
}
|
|
124
|
+
this.maybeTrack(result, licenseState)
|
|
98
125
|
|
|
99
|
-
|
|
126
|
+
this.state.set(licenseState)
|
|
127
|
+
})
|
|
128
|
+
.catch((error) => {
|
|
129
|
+
console.error('License validation failed:', error)
|
|
100
130
|
this.state.set('unlicensed')
|
|
101
|
-
}
|
|
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,47 @@ 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
|
+
|
|
182
|
+
// eslint-disable-next-line no-restricted-globals
|
|
183
|
+
fetch(url.toString())
|
|
184
|
+
}
|
|
185
|
+
|
|
120
186
|
private async extractLicenseKey(licenseKey: string): Promise<LicenseInfo> {
|
|
121
187
|
const [data, signature] = licenseKey.split('.')
|
|
122
188
|
const [prefix, encodedData] = data.split('/')
|
|
@@ -203,6 +269,9 @@ export class LicenseManager {
|
|
|
203
269
|
const isAnnualLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.ANNUAL_LICENSE)
|
|
204
270
|
const isPerpetualLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.PERPETUAL_LICENSE)
|
|
205
271
|
|
|
272
|
+
const isEvaluationLicense = this.isFlagEnabled(licenseInfo.flags, FLAGS.EVALUATION_LICENSE)
|
|
273
|
+
const daysSinceExpiry = this.getDaysSinceExpiry(expiryDate)
|
|
274
|
+
|
|
206
275
|
const result: ValidLicenseKeyResult = {
|
|
207
276
|
license: licenseInfo,
|
|
208
277
|
isLicenseParseable: true,
|
|
@@ -214,7 +283,12 @@ export class LicenseManager {
|
|
|
214
283
|
isPerpetualLicense,
|
|
215
284
|
isPerpetualLicenseExpired: isPerpetualLicense && this.isPerpetualLicenseExpired(expiryDate),
|
|
216
285
|
isInternalLicense: this.isFlagEnabled(licenseInfo.flags, FLAGS.INTERNAL_LICENSE),
|
|
286
|
+
isNativeLicense: this.isNativeLicense(licenseInfo),
|
|
217
287
|
isLicensedWithWatermark: this.isFlagEnabled(licenseInfo.flags, FLAGS.WITH_WATERMARK),
|
|
288
|
+
isEvaluationLicense,
|
|
289
|
+
isEvaluationLicenseExpired:
|
|
290
|
+
isEvaluationLicense && this.isEvaluationLicenseExpired(expiryDate),
|
|
291
|
+
daysSinceExpiry,
|
|
218
292
|
}
|
|
219
293
|
this.outputLicenseInfoIfNeeded(result)
|
|
220
294
|
|
|
@@ -231,6 +305,7 @@ export class LicenseManager {
|
|
|
231
305
|
|
|
232
306
|
return licenseInfo.hosts.some((host) => {
|
|
233
307
|
const normalizedHost = host.toLowerCase().trim()
|
|
308
|
+
const maybeProtocol = normalizedHost.endsWith(':') ? normalizedHost : undefined
|
|
234
309
|
|
|
235
310
|
// Allow the domain if listed and www variations, 'example.com' allows 'example.com' and 'www.example.com'
|
|
236
311
|
if (
|
|
@@ -262,10 +337,20 @@ export class LicenseManager {
|
|
|
262
337
|
}
|
|
263
338
|
}
|
|
264
339
|
|
|
340
|
+
// Native license support
|
|
341
|
+
// In this case, `normalizedHost` is actually a protocol, e.g. `app-bundle:`
|
|
342
|
+
if (this.isNativeLicense(licenseInfo) && window.location.protocol === maybeProtocol) {
|
|
343
|
+
return true
|
|
344
|
+
}
|
|
345
|
+
|
|
265
346
|
return false
|
|
266
347
|
})
|
|
267
348
|
}
|
|
268
349
|
|
|
350
|
+
private isNativeLicense(licenseInfo: LicenseInfo) {
|
|
351
|
+
return this.isFlagEnabled(licenseInfo.flags, FLAGS.NATIVE_LICENSE)
|
|
352
|
+
}
|
|
353
|
+
|
|
269
354
|
private getExpirationDateWithoutGracePeriod(expiryDate: Date) {
|
|
270
355
|
return new Date(expiryDate.getFullYear(), expiryDate.getMonth(), expiryDate.getDate())
|
|
271
356
|
}
|
|
@@ -280,15 +365,7 @@ export class LicenseManager {
|
|
|
280
365
|
|
|
281
366
|
private isAnnualLicenseExpired(expiryDate: Date) {
|
|
282
367
|
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
|
|
368
|
+
return new Date() >= expiration
|
|
292
369
|
}
|
|
293
370
|
|
|
294
371
|
private isPerpetualLicenseExpired(expiryDate: Date) {
|
|
@@ -301,6 +378,21 @@ export class LicenseManager {
|
|
|
301
378
|
return dates.major >= expiration || dates.minor >= expiration
|
|
302
379
|
}
|
|
303
380
|
|
|
381
|
+
private getDaysSinceExpiry(expiryDate: Date): number {
|
|
382
|
+
const now = new Date()
|
|
383
|
+
const expiration = this.getExpirationDateWithoutGracePeriod(expiryDate)
|
|
384
|
+
const diffTime = now.getTime() - expiration.getTime()
|
|
385
|
+
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
|
|
386
|
+
return Math.max(0, diffDays)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private isEvaluationLicenseExpired(expiryDate: Date): boolean {
|
|
390
|
+
// Evaluation licenses have no grace period - they expire immediately
|
|
391
|
+
const now = new Date()
|
|
392
|
+
const expiration = this.getExpirationDateWithoutGracePeriod(expiryDate)
|
|
393
|
+
return now >= expiration
|
|
394
|
+
}
|
|
395
|
+
|
|
304
396
|
private isFlagEnabled(flags: number, flag: number) {
|
|
305
397
|
return (flags & flag) === flag
|
|
306
398
|
}
|
|
@@ -318,19 +410,6 @@ export class LicenseManager {
|
|
|
318
410
|
}
|
|
319
411
|
|
|
320
412
|
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
413
|
// If we added a new flag it will be twice the value of the currently highest flag.
|
|
335
414
|
// 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
415
|
if (result.license.flags >= HIGHEST_FLAG * 2) {
|
|
@@ -367,15 +446,80 @@ export class LicenseManager {
|
|
|
367
446
|
static className = 'tl-watermark_SEE-LICENSE'
|
|
368
447
|
}
|
|
369
448
|
|
|
370
|
-
export function
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
449
|
+
export function getLicenseState(
|
|
450
|
+
result: LicenseFromKeyResult,
|
|
451
|
+
outputMessages: (messages: string[]) => void,
|
|
452
|
+
isDevelopment: boolean
|
|
453
|
+
): LicenseState {
|
|
454
|
+
if (!result.isLicenseParseable) {
|
|
455
|
+
if (isDevelopment) {
|
|
456
|
+
return 'unlicensed'
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// All unlicensed scenarios should not work in production
|
|
460
|
+
if (result.reason === 'no-key-provided') {
|
|
461
|
+
outputMessages([
|
|
462
|
+
'No tldraw license key provided!',
|
|
463
|
+
'A license is required for production deployments.',
|
|
464
|
+
`Please reach out to ${LICENSE_EMAIL} to purchase a license.`,
|
|
465
|
+
])
|
|
466
|
+
} else {
|
|
467
|
+
outputMessages([
|
|
468
|
+
'Invalid license key. tldraw requires a valid license for production use.',
|
|
469
|
+
`Please reach out to ${LICENSE_EMAIL} to purchase a license.`,
|
|
470
|
+
])
|
|
471
|
+
}
|
|
472
|
+
return 'unlicensed-production'
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (!result.isDomainValid && !result.isDevelopment) {
|
|
476
|
+
outputMessages([
|
|
477
|
+
'License key is not valid for this domain.',
|
|
478
|
+
'A license is required for production deployments.',
|
|
479
|
+
`Please reach out to ${LICENSE_EMAIL} to purchase a license.`,
|
|
480
|
+
])
|
|
481
|
+
return 'unlicensed-production'
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Handle evaluation licenses - they expire immediately with no grace period
|
|
485
|
+
if (result.isEvaluationLicense) {
|
|
486
|
+
if (result.isEvaluationLicenseExpired) {
|
|
487
|
+
outputMessages([
|
|
488
|
+
'Your tldraw evaluation license has expired!',
|
|
489
|
+
`Please reach out to ${LICENSE_EMAIL} to purchase a full license.`,
|
|
490
|
+
])
|
|
491
|
+
return 'expired'
|
|
492
|
+
} else {
|
|
493
|
+
// Valid evaluation license - tracked but no watermark shown
|
|
494
|
+
return 'licensed'
|
|
376
495
|
}
|
|
377
|
-
return true
|
|
378
496
|
}
|
|
379
497
|
|
|
380
|
-
|
|
498
|
+
// Handle expired regular licenses (both annual and perpetual)
|
|
499
|
+
if (result.isPerpetualLicenseExpired || result.isAnnualLicenseExpired) {
|
|
500
|
+
outputMessages([
|
|
501
|
+
'Your tldraw license has been expired for more than 30 days!',
|
|
502
|
+
`Please reach out to ${LICENSE_EMAIL} to renew your license.`,
|
|
503
|
+
])
|
|
504
|
+
return 'expired'
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Check if license is past expiry date but within grace period
|
|
508
|
+
const daysSinceExpiry = result.daysSinceExpiry
|
|
509
|
+
if (daysSinceExpiry > 0 && !result.isEvaluationLicense) {
|
|
510
|
+
outputMessages([
|
|
511
|
+
'Your tldraw license has expired.',
|
|
512
|
+
`License expired ${daysSinceExpiry} days ago.`,
|
|
513
|
+
`Please reach out to ${LICENSE_EMAIL} to renew your license.`,
|
|
514
|
+
])
|
|
515
|
+
// Within 30-day grace period: still licensed (no watermark)
|
|
516
|
+
return 'licensed'
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// License is valid, determine if it has watermark
|
|
520
|
+
if (result.isLicensedWithWatermark) {
|
|
521
|
+
return 'licensed-with-watermark'
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return 'licensed'
|
|
381
525
|
}
|
|
@@ -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,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
|
-
|
|
8
|
+
vi.mock('./useLicenseManagerState', () => ({
|
|
8
9
|
useLicenseManagerState: () => mockLicenseState,
|
|
9
10
|
}))
|
|
10
11
|
|
|
@@ -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
|
}
|
package/src/lib/options.ts
CHANGED
|
@@ -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: [
|