Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Custom AI slash commands now sync across your Macs with iCloud Sync, alongside your other settings.
- Join a team from the activation window: paste the invite code your team owner sent instead of a license key. Owners manage members and seats from their account on tablepro.app.
- Join a team by pasting the invite code your team owner sent where a license key goes, in Settings > Account or the activation window. Owners manage members and seats from their account on tablepro.app.
- Connections can take their password from 1Password (op://), HashiCorp Vault, or AWS Secrets Manager, resolved at connect time so the secret is never stored in TablePro.
- Team plan: publish connections to a shared folder your team reads from, without sending passwords. Right-click a connection, then Share > Publish to Team Catalog. Teammates add the folder under Settings > Linked Folders.
- Team Library: share connections and saved queries with your team through your account instead of a shared folder. Right-click a connection and choose Share > Publish to Team Library, or publish your saved queries from the Favorites sidebar. Teammates see shared connections in their connection list and shared queries in their sidebar, and you manage the library from your account on tablepro.app. Passwords are never included.
Expand Down
18 changes: 18 additions & 0 deletions TablePro/Core/Services/Licensing/LicenseManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,24 @@ final class LicenseManager {
// MARK: - Activation

/// Activate a license key on this machine
/// Activates from either a license key or a team invite code, auto-detecting which was entered.
/// Every activation entry point routes through here so the settings pane and the standalone sheet
/// behave identically.
func activate(codeOrKey: String) async throws {
let trimmed = codeOrKey.trimmingCharacters(in: .whitespacesAndNewlines)
if Self.isLicenseKey(trimmed) {
try await activate(licenseKey: trimmed)
} else {
try await activate(inviteCode: trimmed)
}
}

/// A license key looks like XXXXX-XXXXX-XXXXX-XXXXX-XXXXX; anything else is treated as an invite code.
nonisolated static func isLicenseKey(_ value: String) -> Bool {
let pattern = "^[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}$"
return value.uppercased().range(of: pattern, options: .regularExpression) != nil
}

func activate(licenseKey: String) async throws {
let trimmedKey = licenseKey.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
guard !trimmedKey.isEmpty else {
Expand Down
14 changes: 1 addition & 13 deletions TablePro/Views/Settings/LicenseActivationSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,23 +87,11 @@ struct LicenseActivationSheet: View {
isActivating = true
defer { isActivating = false }

let trimmed = licenseKeyInput.trimmingCharacters(in: .whitespacesAndNewlines)

do {
if Self.isLicenseKey(trimmed) {
try await LicenseManager.shared.activate(licenseKey: trimmed)
} else {
try await LicenseManager.shared.activate(inviteCode: trimmed)
}
try await LicenseManager.shared.activate(codeOrKey: licenseKeyInput)
dismiss()
} catch {
errorMessage = (error as? LicenseError)?.friendlyDescription ?? error.localizedDescription
}
}

/// A license key looks like XXXXX-XXXXX-XXXXX-XXXXX-XXXXX; anything else is treated as an invite code.
static func isLicenseKey(_ value: String) -> Bool {
let pattern = "^[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}$"
return value.uppercased().range(of: pattern, options: .regularExpression) != nil
}
}
2 changes: 1 addition & 1 deletion TablePro/Views/Settings/Sections/LicenseSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ struct LicenseSection: View {
defer { isActivating = false }

do {
try await licenseManager.activate(licenseKey: licenseKeyInput)
try await licenseManager.activate(codeOrKey: licenseKeyInput)
licenseKeyInput = ""
} catch {
AlertHelper.showErrorSheet(
Expand Down
10 changes: 5 additions & 5 deletions TableProTests/Models/LicenseTierTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,15 @@ struct LicenseTierTests {

@Test("A dashed license key is recognized as a license key, not an invite code")
func recognizesLicenseKeyFormat() {
#expect(LicenseActivationSheet.isLicenseKey("ABCDE-FGHIJ-KLMNO-PQRST-UVWXY") == true)
#expect(LicenseActivationSheet.isLicenseKey("abcde-fghij-klmno-pqrst-uvwxy") == true)
#expect(LicenseManager.isLicenseKey("ABCDE-FGHIJ-KLMNO-PQRST-UVWXY") == true)
#expect(LicenseManager.isLicenseKey("abcde-fghij-klmno-pqrst-uvwxy") == true)
}

@Test("A random invite token is not treated as a license key")
func recognizesInviteCode() {
#expect(LicenseActivationSheet.isLicenseKey("aB3xZ9qK7mN2pL5rT8wY1cV4dF6gH0jS") == false)
#expect(LicenseActivationSheet.isLicenseKey("ABCDE-FGHIJ") == false)
#expect(LicenseActivationSheet.isLicenseKey("") == false)
#expect(LicenseManager.isLicenseKey("aB3xZ9qK7mN2pL5rT8wY1cV4dF6gH0jS") == false)
#expect(LicenseManager.isLicenseKey("ABCDE-FGHIJ") == false)
#expect(LicenseManager.isLicenseKey("") == false)
}

@Test("Pro features require the starter tier; Team features require the team tier")
Expand Down
Loading