@tldraw/editor 5.3.0-canary.fceaae5e9feb → 5.3.0-internal.fba91ed55f6c
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 +37 -2
- package/dist-cjs/index.js +4 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +6 -1
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +65 -13
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +9 -0
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +3 -0
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js +3 -0
- package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/types/emit-types.js.map +1 -1
- package/dist-cjs/lib/license/LicenseManager.js +62 -7
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/license/useLicenseManagerState.js +7 -0
- package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
- package/dist-cjs/lib/options.js +2 -1
- package/dist-cjs/lib/options.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 +37 -2
- package/dist-esm/index.mjs +5 -2
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +6 -1
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +65 -13
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +9 -0
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +3 -0
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs +3 -0
- package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseManager.mjs +63 -8
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/license/useLicenseManagerState.mjs +7 -0
- package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
- package/dist-esm/lib/options.mjs +2 -1
- package/dist-esm/lib/options.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +10 -0
- package/package.json +7 -7
- package/src/index.ts +3 -1
- package/src/lib/TldrawEditor.tsx +10 -1
- package/src/lib/editor/Editor.ts +97 -18
- package/src/lib/editor/managers/FontManager/FontManager.test.ts +66 -0
- package/src/lib/editor/managers/FontManager/FontManager.ts +14 -0
- package/src/lib/editor/managers/TextManager/TextManager.ts +3 -0
- package/src/lib/editor/shapes/BaseFrameLikeShapeUtil.tsx +6 -1
- package/src/lib/editor/types/emit-types.ts +1 -0
- package/src/lib/license/LicenseManager.test.ts +220 -29
- package/src/lib/license/LicenseManager.ts +107 -6
- package/src/lib/license/useLicenseManagerState.ts +17 -1
- package/src/lib/options.ts +3 -1
- package/src/version.ts +3 -3
|
@@ -4,6 +4,7 @@ import { publishDates } from '../../version'
|
|
|
4
4
|
import { str2ab } from '../utils/licensing'
|
|
5
5
|
import {
|
|
6
6
|
FLAGS,
|
|
7
|
+
getEnabledFeatures,
|
|
7
8
|
getLicenseState,
|
|
8
9
|
LicenseManager,
|
|
9
10
|
PROPERTIES,
|
|
@@ -508,6 +509,116 @@ describe('LicenseManager', () => {
|
|
|
508
509
|
expect(result.isEvaluationLicense).toBe(true)
|
|
509
510
|
expect(result.isEvaluationLicenseExpired).toBe(true)
|
|
510
511
|
})
|
|
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
|
+
})
|
|
569
|
+
|
|
570
|
+
describe('Expiry date timezone handling', () => {
|
|
571
|
+
afterEach(() => {
|
|
572
|
+
vi.useRealTimers()
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
// The named expiry date is minted as a date-only string and should mean "the license
|
|
576
|
+
// is fully usable through the end of that day in UTC", giving every user the same
|
|
577
|
+
// cutoff regardless of their local timezone.
|
|
578
|
+
async function makeEvaluationLicenseWithExpiry(expiry: string) {
|
|
579
|
+
const licenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
|
|
580
|
+
licenseInfo[PROPERTIES.FLAGS] = FLAGS.EVALUATION_LICENSE
|
|
581
|
+
licenseInfo[PROPERTIES.EXPIRY_DATE] = expiry
|
|
582
|
+
return generateLicenseKey(JSON.stringify(licenseInfo), keyPair)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
it('Keeps the license valid through the end of the named expiry day (UTC)', async () => {
|
|
586
|
+
const key = await makeEvaluationLicenseWithExpiry('2026-06-26')
|
|
587
|
+
|
|
588
|
+
// The final moment of the named day is still valid.
|
|
589
|
+
vi.useFakeTimers()
|
|
590
|
+
vi.setSystemTime(new Date('2026-06-26T23:59:59.000Z'))
|
|
591
|
+
expect(
|
|
592
|
+
((await licenseManager.getLicenseFromKey(key)) as ValidLicenseKeyResult)
|
|
593
|
+
.isEvaluationLicenseExpired
|
|
594
|
+
).toBe(false)
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
it('Expires the license at the start of the day after the named expiry day (UTC)', async () => {
|
|
598
|
+
const key = await makeEvaluationLicenseWithExpiry('2026-06-26')
|
|
599
|
+
|
|
600
|
+
vi.useFakeTimers()
|
|
601
|
+
vi.setSystemTime(new Date('2026-06-27T00:00:01.000Z'))
|
|
602
|
+
expect(
|
|
603
|
+
((await licenseManager.getLicenseFromKey(key)) as ValidLicenseKeyResult)
|
|
604
|
+
.isEvaluationLicenseExpired
|
|
605
|
+
).toBe(true)
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
it('Does not expire early for users west of UTC who are still on the named day locally', async () => {
|
|
609
|
+
const key = await makeEvaluationLicenseWithExpiry('2026-06-26')
|
|
610
|
+
|
|
611
|
+
// Under the old logic a user west of UTC parsed the date one calendar day early and
|
|
612
|
+
// expired at the *start* of that day, losing most of a day. With the uniform UTC
|
|
613
|
+
// cutoff the license stays valid all through June 26 for everyone, no matter their
|
|
614
|
+
// local offset. 1pm UTC on June 26 is well before the cutoff, so it is still valid.
|
|
615
|
+
vi.useFakeTimers()
|
|
616
|
+
vi.setSystemTime(new Date('2026-06-26T13:00:00.000Z'))
|
|
617
|
+
expect(
|
|
618
|
+
((await licenseManager.getLicenseFromKey(key)) as ValidLicenseKeyResult)
|
|
619
|
+
.isEvaluationLicenseExpired
|
|
620
|
+
).toBe(false)
|
|
621
|
+
})
|
|
511
622
|
})
|
|
512
623
|
|
|
513
624
|
describe('License expiry and grace period', () => {
|
|
@@ -540,38 +651,54 @@ describe('LicenseManager', () => {
|
|
|
540
651
|
})
|
|
541
652
|
|
|
542
653
|
it('Handles grace period correctly - 20 days expired should still be within grace period', async () => {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
654
|
+
// Pin the clock and use a date-only UTC expiry so the calculation is timezone
|
|
655
|
+
// independent. The named day is fully usable, so an expiry 20 calendar days before
|
|
656
|
+
// "now" has been past its usable window for 19 full days.
|
|
657
|
+
vi.useFakeTimers()
|
|
658
|
+
vi.setSystemTime(new Date('2026-06-26T12:00:00.000Z'))
|
|
659
|
+
try {
|
|
660
|
+
const expiredLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
|
|
661
|
+
expiredLicenseInfo[PROPERTIES.EXPIRY_DATE] = '2026-06-06' // 20 days before now
|
|
662
|
+
|
|
663
|
+
const expiredLicenseKey = await generateLicenseKey(
|
|
664
|
+
JSON.stringify(expiredLicenseInfo),
|
|
665
|
+
keyPair
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
// Test the getLicenseFromKey method to verify grace period calculation
|
|
669
|
+
const result = (await licenseManager.getLicenseFromKey(
|
|
670
|
+
expiredLicenseKey
|
|
671
|
+
)) as ValidLicenseKeyResult
|
|
672
|
+
expect(result.isAnnualLicense).toBe(true)
|
|
673
|
+
expect(result.isAnnualLicenseExpired).toBe(false) // Within 30-day grace period
|
|
674
|
+
expect(result.daysSinceExpiry).toBe(19)
|
|
675
|
+
} finally {
|
|
676
|
+
vi.useRealTimers()
|
|
677
|
+
}
|
|
559
678
|
})
|
|
560
679
|
|
|
561
680
|
it('Calculates days since expiry correctly', async () => {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
681
|
+
// Pin the clock and use a date-only UTC expiry so the calculation is timezone
|
|
682
|
+
// independent. The named day is fully usable, so an expiry 15 calendar days before
|
|
683
|
+
// "now" has been past its usable window for 14 full days.
|
|
684
|
+
vi.useFakeTimers()
|
|
685
|
+
vi.setSystemTime(new Date('2026-06-26T12:00:00.000Z'))
|
|
686
|
+
try {
|
|
687
|
+
const expiredLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
|
|
688
|
+
expiredLicenseInfo[PROPERTIES.EXPIRY_DATE] = '2026-06-11' // 15 days before now
|
|
689
|
+
|
|
690
|
+
const expiredLicenseKey = await generateLicenseKey(
|
|
691
|
+
JSON.stringify(expiredLicenseInfo),
|
|
692
|
+
keyPair
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
const result = (await licenseManager.getLicenseFromKey(
|
|
696
|
+
expiredLicenseKey
|
|
697
|
+
)) as ValidLicenseKeyResult
|
|
698
|
+
expect(result.daysSinceExpiry).toBe(14)
|
|
699
|
+
} finally {
|
|
700
|
+
vi.useRealTimers()
|
|
701
|
+
}
|
|
575
702
|
})
|
|
576
703
|
})
|
|
577
704
|
|
|
@@ -723,6 +850,8 @@ function getDefaultLicenseResult(overrides: Partial<ValidLicenseKeyResult>): Val
|
|
|
723
850
|
isLicensedWithWatermark: false,
|
|
724
851
|
isEvaluationLicense: false,
|
|
725
852
|
isEvaluationLicenseExpired: false,
|
|
853
|
+
isCollaborationEnabled: false,
|
|
854
|
+
isCommentingEnabled: false,
|
|
726
855
|
daysSinceExpiry: 0,
|
|
727
856
|
// WatermarkManager does not check these fields, it relies on the calculated values like isAnnualLicenseExpired
|
|
728
857
|
license: {
|
|
@@ -1024,3 +1153,65 @@ describe('getLicenseState', () => {
|
|
|
1024
1153
|
})
|
|
1025
1154
|
})
|
|
1026
1155
|
})
|
|
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 } from '@tldraw/state'
|
|
1
|
+
import { atom, transact } 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,6 +22,12 @@ 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,
|
|
25
31
|
}
|
|
26
32
|
const HIGHEST_FLAG = Math.max(...Object.values(FLAGS))
|
|
27
33
|
|
|
@@ -45,6 +51,23 @@ export interface LicenseInfo {
|
|
|
45
51
|
expiryDate: string
|
|
46
52
|
}
|
|
47
53
|
|
|
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
|
+
|
|
48
71
|
/** @internal */
|
|
49
72
|
export type LicenseState =
|
|
50
73
|
| 'pending' // License validation is in progress
|
|
@@ -84,6 +107,8 @@ export interface ValidLicenseKeyResult {
|
|
|
84
107
|
isLicensedWithWatermark: boolean
|
|
85
108
|
isEvaluationLicense: boolean
|
|
86
109
|
isEvaluationLicenseExpired: boolean
|
|
110
|
+
isCollaborationEnabled: boolean
|
|
111
|
+
isCommentingEnabled: boolean
|
|
87
112
|
daysSinceExpiry: number
|
|
88
113
|
}
|
|
89
114
|
|
|
@@ -98,6 +123,9 @@ export class LicenseManager {
|
|
|
98
123
|
public isTest: boolean
|
|
99
124
|
public isCryptoAvailable: boolean
|
|
100
125
|
state = atom<LicenseState>('license state', 'pending')
|
|
126
|
+
featureFlags = atom<Record<LicenseFeatureName, boolean>>('license feature flags', {
|
|
127
|
+
...NO_FEATURES,
|
|
128
|
+
})
|
|
101
129
|
public verbose = true
|
|
102
130
|
|
|
103
131
|
constructor(licenseKey: string | undefined, testPublicKey?: string) {
|
|
@@ -106,6 +134,14 @@ export class LicenseManager {
|
|
|
106
134
|
this.publicKey = testPublicKey || this.publicKey
|
|
107
135
|
this.isCryptoAvailable = !!crypto.subtle
|
|
108
136
|
|
|
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
|
+
|
|
109
145
|
this.getLicenseFromKey(licenseKey)
|
|
110
146
|
.then((result) => {
|
|
111
147
|
const licenseState = getLicenseState(
|
|
@@ -116,7 +152,12 @@ export class LicenseManager {
|
|
|
116
152
|
|
|
117
153
|
this.maybeTrack(result, licenseState)
|
|
118
154
|
|
|
119
|
-
|
|
155
|
+
// Update both atoms atomically so dependents never observe the license state and the
|
|
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
|
+
})
|
|
120
161
|
})
|
|
121
162
|
.catch((error) => {
|
|
122
163
|
console.error('License validation failed:', error)
|
|
@@ -124,6 +165,14 @@ export class LicenseManager {
|
|
|
124
165
|
})
|
|
125
166
|
}
|
|
126
167
|
|
|
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
|
+
|
|
127
176
|
private getIsDevelopment() {
|
|
128
177
|
// If we are using https on a non-loopback domain we assume it's a production env and a development one otherwise
|
|
129
178
|
return (
|
|
@@ -286,6 +335,12 @@ export class LicenseManager {
|
|
|
286
335
|
const isPerpetualLicenseExpired =
|
|
287
336
|
isPerpetualLicense && this.isPerpetualLicenseExpired(expiryDate)
|
|
288
337
|
|
|
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
|
+
|
|
289
344
|
// For perpetual licenses, the calendar expiry date only gates access to future
|
|
290
345
|
// major/minor releases; it does not "expire" the license itself. While the user
|
|
291
346
|
// is still on a covered version we report `daysSinceExpiry` as 0 so consumers
|
|
@@ -309,6 +364,8 @@ export class LicenseManager {
|
|
|
309
364
|
isEvaluationLicense,
|
|
310
365
|
isEvaluationLicenseExpired:
|
|
311
366
|
isEvaluationLicense && this.isEvaluationLicenseExpired(expiryDate),
|
|
367
|
+
isCollaborationEnabled,
|
|
368
|
+
isCommentingEnabled,
|
|
312
369
|
daysSinceExpiry,
|
|
313
370
|
}
|
|
314
371
|
this.outputLicenseInfoIfNeeded(result)
|
|
@@ -372,14 +429,26 @@ export class LicenseManager {
|
|
|
372
429
|
}
|
|
373
430
|
|
|
374
431
|
private getExpirationDateWithoutGracePeriod(expiryDate: Date) {
|
|
375
|
-
|
|
432
|
+
// The named expiry date is the last day the license is fully usable, so the license
|
|
433
|
+
// expires at the end of that day, i.e. the start of the following day. We work in UTC
|
|
434
|
+
// (the expiry date is minted and parsed as a UTC date-only string) so the cutoff is a
|
|
435
|
+
// single predictable instant for every user regardless of their local timezone.
|
|
436
|
+
return new Date(
|
|
437
|
+
Date.UTC(
|
|
438
|
+
expiryDate.getUTCFullYear(),
|
|
439
|
+
expiryDate.getUTCMonth(),
|
|
440
|
+
expiryDate.getUTCDate() + 1 // Add 1 day so the named date is fully usable
|
|
441
|
+
)
|
|
442
|
+
)
|
|
376
443
|
}
|
|
377
444
|
|
|
378
445
|
private getExpirationDateWithGracePeriod(expiryDate: Date) {
|
|
379
446
|
return new Date(
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
447
|
+
Date.UTC(
|
|
448
|
+
expiryDate.getUTCFullYear(),
|
|
449
|
+
expiryDate.getUTCMonth(),
|
|
450
|
+
expiryDate.getUTCDate() + GRACE_PERIOD_DAYS + 1 // Add 1 day to include the expiration day
|
|
451
|
+
)
|
|
383
452
|
)
|
|
384
453
|
}
|
|
385
454
|
|
|
@@ -471,6 +540,38 @@ export class LicenseManager {
|
|
|
471
540
|
static className = 'tl-watermark_SEE-LICENSE'
|
|
472
541
|
}
|
|
473
542
|
|
|
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
|
+
|
|
474
575
|
export function getLicenseState(
|
|
475
576
|
result: LicenseFromKeyResult,
|
|
476
577
|
outputMessages: (messages: string[]) => void,
|
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import { useValue } from '@tldraw/state-react'
|
|
2
|
-
import { LicenseManager, LicenseState } from './LicenseManager'
|
|
2
|
+
import { LicenseFeatureName, 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/lib/options.ts
CHANGED
|
@@ -86,6 +86,7 @@ export interface TldrawOptions {
|
|
|
86
86
|
readonly collaboratorCheckIntervalMs: number
|
|
87
87
|
readonly cameraMovingTimeoutMs: number
|
|
88
88
|
readonly hitTestMargin: number
|
|
89
|
+
readonly coarseHitTestMargin: number
|
|
89
90
|
readonly edgeScrollDelay: number
|
|
90
91
|
readonly edgeScrollEaseDuration: number
|
|
91
92
|
readonly edgeScrollSpeed: number
|
|
@@ -312,7 +313,8 @@ export const defaultTldrawOptions = {
|
|
|
312
313
|
collaboratorIdleTimeoutMs: 3000,
|
|
313
314
|
collaboratorCheckIntervalMs: 1200,
|
|
314
315
|
cameraMovingTimeoutMs: 64,
|
|
315
|
-
hitTestMargin:
|
|
316
|
+
hitTestMargin: 3,
|
|
317
|
+
coarseHitTestMargin: 4,
|
|
316
318
|
edgeScrollDelay: 200,
|
|
317
319
|
edgeScrollEaseDuration: 200,
|
|
318
320
|
edgeScrollSpeed: 25,
|
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-internal.fba91ed55f6c'
|
|
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-17T10:57:42.512Z',
|
|
8
|
+
patch: '2026-07-17T10:57:42.512Z',
|
|
9
9
|
}
|