@tldraw/editor 5.3.0-internal.fba91ed55f6c → 5.3.0-next.4123e97459a9
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 +23 -6
- package/dist-cjs/index.js +1 -4
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +7 -2
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/license/LicenseManager.js +2 -48
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/license/useLicenseManagerState.js +0 -7
- package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +23 -6
- package/dist-esm/index.mjs +2 -5
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +7 -2
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseManager.mjs +3 -49
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/license/useLicenseManagerState.mjs +0 -7
- package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +7 -7
- package/src/index.ts +1 -3
- package/src/lib/editor/Editor.ts +14 -2
- package/src/lib/editor/shapes/ShapeUtil.ts +24 -0
- package/src/lib/license/LicenseManager.test.ts +0 -121
- package/src/lib/license/LicenseManager.ts +2 -91
- package/src/lib/license/useLicenseManagerState.ts +1 -17
- package/src/version.ts +3 -3
|
@@ -669,6 +669,30 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
|
|
|
669
669
|
*/
|
|
670
670
|
onBeforeCreate?(next: Shape): Shape | void
|
|
671
671
|
|
|
672
|
+
/**
|
|
673
|
+
* A callback called when a shape is reproduced from an existing shape, either by duplicating
|
|
674
|
+
* ({@link Editor.duplicateShapes}) or by pasting/putting content onto the page
|
|
675
|
+
* ({@link Editor.putContentOntoCurrentPage}). This provides a last chance to modify the copy
|
|
676
|
+
* before it's created — for example, to re-stamp attribution so the copy is credited to the
|
|
677
|
+
* current user rather than the original author. It is not called when content is put with
|
|
678
|
+
* `preserveIds` (e.g. {@link Editor.moveShapesToPage}), since the shape keeps its identity
|
|
679
|
+
* and no copy is made.
|
|
680
|
+
*
|
|
681
|
+
* @example
|
|
682
|
+
*
|
|
683
|
+
* ```ts
|
|
684
|
+
* onBeforeDuplicate = (source, duplicate) => {
|
|
685
|
+
* return { ...duplicate, props: { ...duplicate.props, editedBy: this.editor.getAttributionUserId() } }
|
|
686
|
+
* }
|
|
687
|
+
* ```
|
|
688
|
+
*
|
|
689
|
+
* @param source - The shape being copied from.
|
|
690
|
+
* @param duplicate - The new copy (with its own id), before it's created.
|
|
691
|
+
* @returns The next shape or void.
|
|
692
|
+
* @public
|
|
693
|
+
*/
|
|
694
|
+
onBeforeDuplicate?(source: Shape, duplicate: Shape): Shape | void
|
|
695
|
+
|
|
672
696
|
/**
|
|
673
697
|
* A callback called just before a shape is updated. This method provides a last chance to modify
|
|
674
698
|
* the updated shape.
|
|
@@ -4,7 +4,6 @@ import { publishDates } from '../../version'
|
|
|
4
4
|
import { str2ab } from '../utils/licensing'
|
|
5
5
|
import {
|
|
6
6
|
FLAGS,
|
|
7
|
-
getEnabledFeatures,
|
|
8
7
|
getLicenseState,
|
|
9
8
|
LicenseManager,
|
|
10
9
|
PROPERTIES,
|
|
@@ -509,62 +508,6 @@ describe('LicenseManager', () => {
|
|
|
509
508
|
expect(result.isEvaluationLicense).toBe(true)
|
|
510
509
|
expect(result.isEvaluationLicenseExpired).toBe(true)
|
|
511
510
|
})
|
|
512
|
-
|
|
513
|
-
it('Checks for the commenting feature flag', async () => {
|
|
514
|
-
const commentingLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
|
|
515
|
-
commentingLicenseInfo[PROPERTIES.FLAGS] |= FLAGS.FEAT_COMMENTING
|
|
516
|
-
const commentingLicenseKey = await generateLicenseKey(
|
|
517
|
-
JSON.stringify(commentingLicenseInfo),
|
|
518
|
-
keyPair
|
|
519
|
-
)
|
|
520
|
-
const result = (await licenseManager.getLicenseFromKey(
|
|
521
|
-
commentingLicenseKey
|
|
522
|
-
)) as ValidLicenseKeyResult
|
|
523
|
-
expect(result.isCommentingEnabled).toBe(true)
|
|
524
|
-
expect(result.isCollaborationEnabled).toBe(false)
|
|
525
|
-
})
|
|
526
|
-
|
|
527
|
-
it('Checks for the collaboration feature flag', async () => {
|
|
528
|
-
const collaborationLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
|
|
529
|
-
collaborationLicenseInfo[PROPERTIES.FLAGS] |= FLAGS.FEAT_COLLABORATION
|
|
530
|
-
const collaborationLicenseKey = await generateLicenseKey(
|
|
531
|
-
JSON.stringify(collaborationLicenseInfo),
|
|
532
|
-
keyPair
|
|
533
|
-
)
|
|
534
|
-
const result = (await licenseManager.getLicenseFromKey(
|
|
535
|
-
collaborationLicenseKey
|
|
536
|
-
)) as ValidLicenseKeyResult
|
|
537
|
-
expect(result.isCollaborationEnabled).toBe(true)
|
|
538
|
-
// The collaboration umbrella grants the commenting sub-feature.
|
|
539
|
-
expect(result.isCommentingEnabled).toBe(true)
|
|
540
|
-
})
|
|
541
|
-
|
|
542
|
-
it('Leaves feature flags off when no feature bits are set', async () => {
|
|
543
|
-
const standardLicenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
|
|
544
|
-
const result = (await licenseManager.getLicenseFromKey(
|
|
545
|
-
standardLicenseKey
|
|
546
|
-
)) as ValidLicenseKeyResult
|
|
547
|
-
expect(result.isCollaborationEnabled).toBe(false)
|
|
548
|
-
expect(result.isCommentingEnabled).toBe(false)
|
|
549
|
-
})
|
|
550
|
-
|
|
551
|
-
it('Enables features synchronously in development, before validation resolves', () => {
|
|
552
|
-
process.env.NODE_ENV = 'development'
|
|
553
|
-
// @ts-ignore
|
|
554
|
-
delete window.location
|
|
555
|
-
// @ts-ignore
|
|
556
|
-
window.location = new URL('https://www.example.com')
|
|
557
|
-
|
|
558
|
-
try {
|
|
559
|
-
// No await: the feature flags should already reflect development (all enabled) rather
|
|
560
|
-
// than the fail-closed default, which only lifts once async validation resolves.
|
|
561
|
-
const devLicenseManager = new LicenseManager('', keyPair.publicKey)
|
|
562
|
-
expect(devLicenseManager.isFeatureEnabled('commenting')).toBe(true)
|
|
563
|
-
expect(devLicenseManager.isFeatureEnabled('collaboration')).toBe(true)
|
|
564
|
-
} finally {
|
|
565
|
-
process.env.NODE_ENV = 'test'
|
|
566
|
-
}
|
|
567
|
-
})
|
|
568
511
|
})
|
|
569
512
|
|
|
570
513
|
describe('Expiry date timezone handling', () => {
|
|
@@ -850,8 +793,6 @@ function getDefaultLicenseResult(overrides: Partial<ValidLicenseKeyResult>): Val
|
|
|
850
793
|
isLicensedWithWatermark: false,
|
|
851
794
|
isEvaluationLicense: false,
|
|
852
795
|
isEvaluationLicenseExpired: false,
|
|
853
|
-
isCollaborationEnabled: false,
|
|
854
|
-
isCommentingEnabled: false,
|
|
855
796
|
daysSinceExpiry: 0,
|
|
856
797
|
// WatermarkManager does not check these fields, it relies on the calculated values like isAnnualLicenseExpired
|
|
857
798
|
license: {
|
|
@@ -1153,65 +1094,3 @@ describe('getLicenseState', () => {
|
|
|
1153
1094
|
})
|
|
1154
1095
|
})
|
|
1155
1096
|
})
|
|
1156
|
-
|
|
1157
|
-
describe('getEnabledFeatures', () => {
|
|
1158
|
-
it('enables every feature in development regardless of flags', () => {
|
|
1159
|
-
const result = getDefaultLicenseResult({
|
|
1160
|
-
isCollaborationEnabled: false,
|
|
1161
|
-
isCommentingEnabled: false,
|
|
1162
|
-
})
|
|
1163
|
-
expect(getEnabledFeatures(result, 'unlicensed', true)).toEqual({
|
|
1164
|
-
collaboration: true,
|
|
1165
|
-
commenting: true,
|
|
1166
|
-
})
|
|
1167
|
-
})
|
|
1168
|
-
|
|
1169
|
-
it('reflects the license flags for a valid licensed state', () => {
|
|
1170
|
-
const result = getDefaultLicenseResult({
|
|
1171
|
-
isCollaborationEnabled: false,
|
|
1172
|
-
isCommentingEnabled: true,
|
|
1173
|
-
})
|
|
1174
|
-
expect(getEnabledFeatures(result, 'licensed', false)).toEqual({
|
|
1175
|
-
collaboration: false,
|
|
1176
|
-
commenting: true,
|
|
1177
|
-
})
|
|
1178
|
-
})
|
|
1179
|
-
|
|
1180
|
-
it('still grants features while showing a watermark', () => {
|
|
1181
|
-
const result = getDefaultLicenseResult({
|
|
1182
|
-
isCollaborationEnabled: true,
|
|
1183
|
-
isCommentingEnabled: true,
|
|
1184
|
-
})
|
|
1185
|
-
expect(getEnabledFeatures(result, 'licensed-with-watermark', false)).toEqual({
|
|
1186
|
-
collaboration: true,
|
|
1187
|
-
commenting: true,
|
|
1188
|
-
})
|
|
1189
|
-
})
|
|
1190
|
-
|
|
1191
|
-
it.each(['unlicensed', 'unlicensed-production', 'expired', 'pending'] as const)(
|
|
1192
|
-
'disables all features for the %s state in production',
|
|
1193
|
-
(state) => {
|
|
1194
|
-
const result = getDefaultLicenseResult({
|
|
1195
|
-
isCollaborationEnabled: true,
|
|
1196
|
-
isCommentingEnabled: true,
|
|
1197
|
-
})
|
|
1198
|
-
expect(getEnabledFeatures(result, state, false)).toEqual({
|
|
1199
|
-
collaboration: false,
|
|
1200
|
-
commenting: false,
|
|
1201
|
-
})
|
|
1202
|
-
}
|
|
1203
|
-
)
|
|
1204
|
-
|
|
1205
|
-
it('disables all features when the license is not parseable', () => {
|
|
1206
|
-
expect(
|
|
1207
|
-
getEnabledFeatures(
|
|
1208
|
-
{ isLicenseParseable: false, reason: 'no-key-provided' },
|
|
1209
|
-
'licensed',
|
|
1210
|
-
false
|
|
1211
|
-
)
|
|
1212
|
-
).toEqual({
|
|
1213
|
-
collaboration: false,
|
|
1214
|
-
commenting: false,
|
|
1215
|
-
})
|
|
1216
|
-
})
|
|
1217
|
-
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { atom
|
|
1
|
+
import { atom } from '@tldraw/state'
|
|
2
2
|
import { publishDates, version } from '../../version'
|
|
3
3
|
import { getDefaultCdnBaseUrl } from '../utils/assets'
|
|
4
4
|
import { importPublicKey, str2ab } from '../utils/licensing'
|
|
@@ -22,12 +22,6 @@ export const FLAGS = {
|
|
|
22
22
|
// Native means the license is for native apps which switches
|
|
23
23
|
// on special-case logic.
|
|
24
24
|
NATIVE_LICENSE: 1 << 5,
|
|
25
|
-
|
|
26
|
-
// -- FEATURE FLAGS --
|
|
27
|
-
// Collaboration is the umbrella flag for collaboration features; it grants all sub-features.
|
|
28
|
-
FEAT_COLLABORATION: 1 << 6,
|
|
29
|
-
// Commenting is the first sub-feature of collaboration.
|
|
30
|
-
FEAT_COMMENTING: 1 << 7,
|
|
31
25
|
}
|
|
32
26
|
const HIGHEST_FLAG = Math.max(...Object.values(FLAGS))
|
|
33
27
|
|
|
@@ -51,23 +45,6 @@ export interface LicenseInfo {
|
|
|
51
45
|
expiryDate: string
|
|
52
46
|
}
|
|
53
47
|
|
|
54
|
-
/**
|
|
55
|
-
* Names of the licensable product features gated by the license. `collaboration` is an umbrella
|
|
56
|
-
* that also grants all of its sub-features (currently just `commenting`).
|
|
57
|
-
*
|
|
58
|
-
* @internal
|
|
59
|
-
*/
|
|
60
|
-
export type LicenseFeatureName = 'collaboration' | 'commenting'
|
|
61
|
-
|
|
62
|
-
const NO_FEATURES: Readonly<Record<LicenseFeatureName, boolean>> = {
|
|
63
|
-
collaboration: false,
|
|
64
|
-
commenting: false,
|
|
65
|
-
}
|
|
66
|
-
const ALL_FEATURES: Readonly<Record<LicenseFeatureName, boolean>> = {
|
|
67
|
-
collaboration: true,
|
|
68
|
-
commenting: true,
|
|
69
|
-
}
|
|
70
|
-
|
|
71
48
|
/** @internal */
|
|
72
49
|
export type LicenseState =
|
|
73
50
|
| 'pending' // License validation is in progress
|
|
@@ -107,8 +84,6 @@ export interface ValidLicenseKeyResult {
|
|
|
107
84
|
isLicensedWithWatermark: boolean
|
|
108
85
|
isEvaluationLicense: boolean
|
|
109
86
|
isEvaluationLicenseExpired: boolean
|
|
110
|
-
isCollaborationEnabled: boolean
|
|
111
|
-
isCommentingEnabled: boolean
|
|
112
87
|
daysSinceExpiry: number
|
|
113
88
|
}
|
|
114
89
|
|
|
@@ -123,9 +98,6 @@ export class LicenseManager {
|
|
|
123
98
|
public isTest: boolean
|
|
124
99
|
public isCryptoAvailable: boolean
|
|
125
100
|
state = atom<LicenseState>('license state', 'pending')
|
|
126
|
-
featureFlags = atom<Record<LicenseFeatureName, boolean>>('license feature flags', {
|
|
127
|
-
...NO_FEATURES,
|
|
128
|
-
})
|
|
129
101
|
public verbose = true
|
|
130
102
|
|
|
131
103
|
constructor(licenseKey: string | undefined, testPublicKey?: string) {
|
|
@@ -134,14 +106,6 @@ export class LicenseManager {
|
|
|
134
106
|
this.publicKey = testPublicKey || this.publicKey
|
|
135
107
|
this.isCryptoAvailable = !!crypto.subtle
|
|
136
108
|
|
|
137
|
-
// In development every feature is enabled (see `getEnabledFeatures`), and that doesn't depend
|
|
138
|
-
// on the async validation result. Reflect it eagerly so features aren't reported as disabled
|
|
139
|
-
// during the validation window — or left disabled if validation rejects (the `.catch` below
|
|
140
|
-
// never sets `featureFlags`). In production the fail-closed default stands until validation.
|
|
141
|
-
if (this.isDevelopment) {
|
|
142
|
-
this.featureFlags.set({ ...ALL_FEATURES })
|
|
143
|
-
}
|
|
144
|
-
|
|
145
109
|
this.getLicenseFromKey(licenseKey)
|
|
146
110
|
.then((result) => {
|
|
147
111
|
const licenseState = getLicenseState(
|
|
@@ -152,12 +116,7 @@ export class LicenseManager {
|
|
|
152
116
|
|
|
153
117
|
this.maybeTrack(result, licenseState)
|
|
154
118
|
|
|
155
|
-
|
|
156
|
-
// feature flags out of sync mid-update.
|
|
157
|
-
transact(() => {
|
|
158
|
-
this.state.set(licenseState)
|
|
159
|
-
this.featureFlags.set(getEnabledFeatures(result, licenseState, this.isDevelopment))
|
|
160
|
-
})
|
|
119
|
+
this.state.set(licenseState)
|
|
161
120
|
})
|
|
162
121
|
.catch((error) => {
|
|
163
122
|
console.error('License validation failed:', error)
|
|
@@ -165,14 +124,6 @@ export class LicenseManager {
|
|
|
165
124
|
})
|
|
166
125
|
}
|
|
167
126
|
|
|
168
|
-
/**
|
|
169
|
-
* Returns whether a given licensable feature is enabled. Reactive: reading this inside a signal
|
|
170
|
-
* recomputes when license validation resolves.
|
|
171
|
-
*/
|
|
172
|
-
isFeatureEnabled(feature: LicenseFeatureName): boolean {
|
|
173
|
-
return this.featureFlags.get()[feature]
|
|
174
|
-
}
|
|
175
|
-
|
|
176
127
|
private getIsDevelopment() {
|
|
177
128
|
// If we are using https on a non-loopback domain we assume it's a production env and a development one otherwise
|
|
178
129
|
return (
|
|
@@ -335,12 +286,6 @@ export class LicenseManager {
|
|
|
335
286
|
const isPerpetualLicenseExpired =
|
|
336
287
|
isPerpetualLicense && this.isPerpetualLicenseExpired(expiryDate)
|
|
337
288
|
|
|
338
|
-
// The collaboration umbrella grants all of its sub-features, so commenting is enabled
|
|
339
|
-
// by either the commenting flag or the collaboration flag.
|
|
340
|
-
const isCollaborationEnabled = this.isFlagEnabled(licenseInfo.flags, FLAGS.FEAT_COLLABORATION)
|
|
341
|
-
const isCommentingEnabled =
|
|
342
|
-
isCollaborationEnabled || this.isFlagEnabled(licenseInfo.flags, FLAGS.FEAT_COMMENTING)
|
|
343
|
-
|
|
344
289
|
// For perpetual licenses, the calendar expiry date only gates access to future
|
|
345
290
|
// major/minor releases; it does not "expire" the license itself. While the user
|
|
346
291
|
// is still on a covered version we report `daysSinceExpiry` as 0 so consumers
|
|
@@ -364,8 +309,6 @@ export class LicenseManager {
|
|
|
364
309
|
isEvaluationLicense,
|
|
365
310
|
isEvaluationLicenseExpired:
|
|
366
311
|
isEvaluationLicense && this.isEvaluationLicenseExpired(expiryDate),
|
|
367
|
-
isCollaborationEnabled,
|
|
368
|
-
isCommentingEnabled,
|
|
369
312
|
daysSinceExpiry,
|
|
370
313
|
}
|
|
371
314
|
this.outputLicenseInfoIfNeeded(result)
|
|
@@ -540,38 +483,6 @@ export class LicenseManager {
|
|
|
540
483
|
static className = 'tl-watermark_SEE-LICENSE'
|
|
541
484
|
}
|
|
542
485
|
|
|
543
|
-
/**
|
|
544
|
-
* Derives which licensable features are enabled from the parse result and the derived license
|
|
545
|
-
* state. In development every feature is enabled so SDK developers can build against them; in
|
|
546
|
-
* production a feature requires both an active, valid license and the corresponding flag.
|
|
547
|
-
*
|
|
548
|
-
* @internal
|
|
549
|
-
*/
|
|
550
|
-
export function getEnabledFeatures(
|
|
551
|
-
result: LicenseFromKeyResult,
|
|
552
|
-
licenseState: LicenseState,
|
|
553
|
-
isDevelopment: boolean
|
|
554
|
-
): Record<LicenseFeatureName, boolean> {
|
|
555
|
-
// Development gets all features so SDK developers can build against them.
|
|
556
|
-
if (isDevelopment) {
|
|
557
|
-
return { ...ALL_FEATURES }
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// Features require an active, valid license. Both the 30-day grace-period 'licensed' state and
|
|
561
|
-
// the watermark state count as valid; unlicensed/expired/pending do not.
|
|
562
|
-
if (
|
|
563
|
-
!result.isLicenseParseable ||
|
|
564
|
-
(licenseState !== 'licensed' && licenseState !== 'licensed-with-watermark')
|
|
565
|
-
) {
|
|
566
|
-
return { ...NO_FEATURES }
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
return {
|
|
570
|
-
collaboration: result.isCollaborationEnabled,
|
|
571
|
-
commenting: result.isCommentingEnabled,
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
486
|
export function getLicenseState(
|
|
576
487
|
result: LicenseFromKeyResult,
|
|
577
488
|
outputMessages: (messages: string[]) => void,
|
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
import { useValue } from '@tldraw/state-react'
|
|
2
|
-
import {
|
|
2
|
+
import { LicenseManager, LicenseState } from './LicenseManager'
|
|
3
3
|
|
|
4
4
|
/** @internal */
|
|
5
5
|
export function useLicenseManagerState(licenseManager: LicenseManager): LicenseState {
|
|
6
6
|
return useValue('watermarkState', () => licenseManager.state.get(), [licenseManager])
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Reactively reads whether a licensable feature is enabled for the current license. Re-renders when
|
|
11
|
-
* license validation resolves.
|
|
12
|
-
*
|
|
13
|
-
* @internal
|
|
14
|
-
*/
|
|
15
|
-
export function useLicenseFeatureFlag(
|
|
16
|
-
licenseManager: LicenseManager,
|
|
17
|
-
feature: LicenseFeatureName
|
|
18
|
-
): boolean {
|
|
19
|
-
return useValue('licenseFeature', () => licenseManager.isFeatureEnabled(feature), [
|
|
20
|
-
licenseManager,
|
|
21
|
-
feature,
|
|
22
|
-
])
|
|
23
|
-
}
|
package/src/version.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// This file is automatically generated by internal/scripts/refresh-assets.ts.
|
|
2
2
|
// Do not edit manually. Or do, I'm a comment, not a cop.
|
|
3
3
|
|
|
4
|
-
export const version = '5.3.0-
|
|
4
|
+
export const version = '5.3.0-next.4123e97459a9'
|
|
5
5
|
export const publishDates = {
|
|
6
6
|
major: '2026-05-06T16:28:18.473Z',
|
|
7
|
-
minor: '2026-07-
|
|
8
|
-
patch: '2026-07-
|
|
7
|
+
minor: '2026-07-23T10:43:39.737Z',
|
|
8
|
+
patch: '2026-07-23T10:43:39.737Z',
|
|
9
9
|
}
|