From ec7dbf348a63a4982303809e8352002357b2262c Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Wed, 8 Jul 2026 23:06:47 +0700 Subject: [PATCH] fix(settings): accept team invite codes in the license field, not just the activation sheet --- CHANGELOG.md | 2 +- .../Services/Licensing/LicenseManager.swift | 18 ++++++++++++++++++ .../Settings/LicenseActivationSheet.swift | 14 +------------- .../Settings/Sections/LicenseSection.swift | 2 +- TableProTests/Models/LicenseTierTests.swift | 10 +++++----- 5 files changed, 26 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71d44375d..88bd6696f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/TablePro/Core/Services/Licensing/LicenseManager.swift b/TablePro/Core/Services/Licensing/LicenseManager.swift index 1c470e228..2cae5b965 100644 --- a/TablePro/Core/Services/Licensing/LicenseManager.swift +++ b/TablePro/Core/Services/Licensing/LicenseManager.swift @@ -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 { diff --git a/TablePro/Views/Settings/LicenseActivationSheet.swift b/TablePro/Views/Settings/LicenseActivationSheet.swift index a24d76938..b28e6a4ef 100644 --- a/TablePro/Views/Settings/LicenseActivationSheet.swift +++ b/TablePro/Views/Settings/LicenseActivationSheet.swift @@ -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 - } } diff --git a/TablePro/Views/Settings/Sections/LicenseSection.swift b/TablePro/Views/Settings/Sections/LicenseSection.swift index 5c61541ac..3e9eebbfc 100644 --- a/TablePro/Views/Settings/Sections/LicenseSection.swift +++ b/TablePro/Views/Settings/Sections/LicenseSection.swift @@ -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( diff --git a/TableProTests/Models/LicenseTierTests.swift b/TableProTests/Models/LicenseTierTests.swift index 8029efa49..343ef0b68 100644 --- a/TableProTests/Models/LicenseTierTests.swift +++ b/TableProTests/Models/LicenseTierTests.swift @@ -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")