Skip to content

refactor(license): one license and sync presentation on every surface - #4005

Merged
datlechin merged 2 commits into
mainfrom
refactor/license-sync
Nov 3, 2026
Merged

datlechin merged 2 commits into
mainfrom
refactor/license-sync

Conversation

@datlechin

@datlechin datlechin commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Summary

  • One license presentation, LicensePresentation, under every license surface. Each state has one title, one message and one action (LicenseNoticeAction: Activate License…, Renew License, Buy a License, Upgrade Plan, Try Again).
    • gate(for:access:): no license names the plan and offers Activate License… (the dialog carries the purchase link); a plan the license lacks offers Upgrade Plan; expired, suspended and unverified reuse the License pane's own notice, so a paying customer is never asked to buy.
    • ProFeatureAccess gains .suspended, which used to fold into .unlicensed.
    • Rendered by the gate overlay (Query Insights, Result Charts), the Compare & Sync and Data Rewind alerts (LicenseGateAlert), a LicenseGateRow in Settings (Sync, Linked Folders, Data Rewind) and the connection export sheet, the License pane, and Welcome.
    • The orange PRO capsule (ProBadge) is gone.
  • A failed license check now shows. LicenseManager.checkFailure holds a check that reached no verdict (offline, server error); every Try Again shows a spinner while it runs and the reason under it after; Refresh shows it under the plan line. Checks coalesce, and an answer for a license deactivated or replaced during the check is dropped.
  • Welcome:
    • the license line renders the License pane notice as a link, expiring soon included; renew opens the pricing page, Could Not Check This License opens Settings > License and checks again, suspended and removed open the pane; no license keeps Activate License;
    • a sync blocker (storage full, signed out, data removed, update required, restricted, unavailable) shows its title as a link to Settings > Sync;
    • the Black Friday line is unchanged and still shows only to people without a working license.
  • One sync presentation, SyncStatusPresentation in TableProSyncTransport, used by both apps: titles, symbols and actions (SyncStatusAction). Each app keeps only its sentences (this Mac, this device) and handlers; iOS drops the three actions it has no public way to perform.
  • Sync pane (macOS):
    • a blocker's notice carries its Try Again, and Sync Now leaves the status section while a blocker stands, so restricted, update required and unavailable no longer offer a retry that cannot help;
    • license states render the gate row under the switch instead of a "Sync Paused" notice;
    • SyncCoordinator.evaluateStatus reads the switch before the license, so sync that was never turned on is off by choice.
  • Doc blocks cleared in the files touched.
Defect on main Fixed by
Gates told an unverified (offline 30 days) or suspended customer to buy: "Compare & Sync requires the Pro plan", "Data Rewind needs a Pro license / Purchase License", PRO capsule to pricing, "needs a Pro license" in Data Rewind settings gate(for:access:), .suspended access
Each gate wrote its own text; the overlay said "License validation failed / Retry Validation", the pane "Could Not Check This License / Try Again" One presentation
Welcome offered Activate License to expired, suspended and removed licenses, showed an orange tier name with only a tooltip for an unverified one, and never showed License Expiring Soon welcomeLine
A failed license check showed nothing; Try Again, Check Again, Retry Validation and Refresh re-enabled with no change offline checkFailure, LicenseCheckFailureLine
Try Again and Retry Validation could start a second check while one ran; an answer arriving after Deactivate restored the license Coalesced checks, adoptsValidation
Storage full, signed out, data removed and update required showed only in Settings > Sync on macOS Welcome sync line
macOS and iOS sync copies drifted: signed out offered Open System Settings vs Try Again; not ready, restricted and unavailable had different symbols; Turn Off Sync vs Turn Off iCloud Sync Shared presentation
Sync Now offered for restricted, update required and unavailable offersSyncNow
A suspended license showed only the PRO capsule in the Sync pane; an expired one showed "Sync Paused" even with sync never turned on; the license branches of statusTitle were unreachable Gate row, switch read first, license branches removed
The gate overlay did not observe the license manager @ObservedObject

Decisions taken without a design review:

  • Welcome performs the notice's action when its outcome is visible (sheet, browser) and opens Settings > License otherwise, since Welcome has no room for a check's result.
  • A gate with no action (suspended) opens Settings > License instead of an alert that only informs.
  • Settings rows use a plain form row (title, secondary message, trailing action), not a badge.
  • Sync symbols follow the macOS set; "Turn Off iCloud Sync" names the feature on both apps.
  • iCloud Is Not Ready stays off Welcome: it clears itself.

Tests changed on purpose, because they pinned the old shape:

  • LicenseTierTests.inactiveStatusesReportUnlicensed (suspended moved to suspendedReportsSuspended);
  • LicensePresentationTests.lapsedLicenseMarksNothing (suspended access);
  • macOS SyncStatusPresentationTests.storageFullAction, removedDataOffersAChoice (Try Again in the notice, Sync Now hidden for blockers);
  • iOS SyncStatusPresentationTests (mobileActions).

Test plan

  • New suites and cases:
    • package SyncStatusPresentationTests (7);
    • macOS: gate, Welcome line and check failure cases in LicensePresentationTests, LicenseManagerDecisionTests (checkFailureIsOnlyANonVerdict, validationAnswersOnlyForTheCheckedLicense), LicenseTierTests.suspendedReportsSuspended, SyncCoordinatorArmingTests.offWithoutALicenseIsOffByChoice, SyncStatusPresentationTests (Sync Now, license states, Welcome);
    • iOS SyncStatusPresentationTests.macOnlyActionsAreDropped.
  • Package TableProSyncTests sync suites: 17 of 17.
  • macOS license and sync suites (18 suites, every type touched): 204 executed; the one failure was lapsedLicenseMarksNothing, updated above; rerun of the license suites after the race fix: 104 of 104.
  • iOS: SyncStatusPresentationTests 6 of 6, ConnectionListStateTests and IOSSyncCoordinatorTests 69 of 69.
  • macOS Unit plan on this branch (main had not moved): 28,417 passed, 0 failed, 2 skipped.
  • Lint on 30 changed Swift files: 0 violations. verify.sh l10n: pass on both catalogs. The iOS catalog change is inserted in place (66 lines), not a re-sort.
  • Codex: an adversarial round found overlapping checks could restore a deactivated license (fixed: coalesced checks, stale answers dropped); a second round against origin/main found nothing.
  • Not verified:
    • a real license server answer; the check outcomes are covered by decision tests and seeded screenshots;
    • a live CloudKit blocker; the Welcome and Sync pane shots use a seeded status;
    • macOS 13 to 15 and iOS 18 to 25.

Screenshots

States other than "no license" are seeded through a temporary patch that is not in this PR: a signed-looking license issued 63 days ago (so it resolves to not checked), a failed check, and a forced storage full status with sync on. Before is origin/main at ef25ea7 with the same patch.

Light Dark
Before: Welcome, license not checked in 30 days, orange tier name, tooltip only Before, Welcome, license not checked in 30 days Before, Welcome, license not checked in 30 days, dark
After: link to Settings > License that checks again After, Welcome, license not checked in 30 days After, Welcome, license not checked in 30 days, dark
Before: Settings > Sync, license not checked in 30 days, PRO capsule to pricing Before, Settings > Sync, license not checked in 30 days Before, Settings > Sync, license not checked in 30 days, dark
After: row with Try Again and the failed check After, Settings > Sync, license not checked in 30 days After, Settings > Sync, license not checked in 30 days, dark
Before: Settings > Data, Data Rewind, license not checked, "needs a Pro license" Before, Settings > Data, Data Rewind, license not checked Before, Settings > Data, Data Rewind, license not checked, dark
After: the same row After, Settings > Data, Data Rewind, license not checked After, Settings > Data, Data Rewind, license not checked, dark
Before: Compare & Sync, license not checked, "requires the Pro plan" / View License Before, Compare & Sync, license not checked Before, Compare & Sync, license not checked, dark
After: the license notice / Try Again After, Compare & Sync, license not checked After, Compare & Sync, license not checked, dark
Before: Query Insights overlay, license not checked, "License validation failed" / Retry Validation Before, Query Insights overlay, license not checked Before, Query Insights overlay, license not checked, dark
After: the license notice, Try Again and the failed check After, Query Insights overlay, license not checked After, Query Insights overlay, license not checked, dark
Before: Settings > Sync, no license, PRO capsule Before, Settings > Sync, no license Before, Settings > Sync, no license, dark
After: row naming the plan with Activate License… After, Settings > Sync, no license After, Settings > Sync, no license, dark
Before: Welcome, iCloud storage full, nothing Before, Welcome, iCloud storage full Before, Welcome, iCloud storage full, dark
After: link to Settings > Sync After, Welcome, iCloud storage full After, Welcome, iCloud storage full, dark
Before: Settings > Sync, iCloud storage full, Manage Storage… in the notice, Sync Now below Before, Settings > Sync, iCloud storage full Before, Settings > Sync, iCloud storage full, dark
After: Manage Storage… and Try Again in the notice, no Sync Now After, Settings > Sync, iCloud storage full After, Settings > Sync, iCloud storage full, dark

iOS 26 (iPhone 17 Pro simulator; the real views in a window)

Light Dark
Before: Connection list, iCloud not ready, icloud.slash Before, Connection list, iCloud not ready Before, Connection list, iCloud not ready, dark
After: exclamationmark.icloud, as on the Mac After, Connection list, iCloud not ready After, Connection list, iCloud not ready, dark
Before: Empty list, iCloud restricted, icloud.slash Before, Empty list, iCloud restricted Before, Empty list, iCloud restricted, dark
After: xmark.icloud, as on the Mac After, Empty list, iCloud restricted After, Empty list, iCloud restricted, dark

@datlechin
datlechin enabled auto-merge (squash) November 3, 2026 05:02
@datlechin
datlechin merged commit 7e5f92a into main Nov 3, 2026
10 checks passed
@datlechin
datlechin deleted the refactor/license-sync branch November 3, 2026 12:57
datlechin added a commit that referenced this pull request Nov 3, 2026
Brings main's 27 commits, among them one license and sync presentation
(#4005), the Black Friday line (#3995), the update indicator and relaunch
(#3993, #3997), running work (#4009), native notices (#4007) and the
security merges for GHSA-3x6q-h487-r38w, GHSA-3xf7-86qj-4546,
GHSA-6g2g-65r6-v5wh, GHSA-q2hp-vxv3-87fw, GHSA-6p9q-hc7v-cxmx and
GHSA-vg8g-h6xp-6px5.

- #4005: ported onto FeatureAccess. LicensePresentation.gate reads
  AccountState: signed out names a paid plan and offers Sign In beside
  Purchase License, an account without the feature gets Upgrade Plan, the
  device limit opens Settings > Account, and a lapsed or failed check reads
  as Settings > Account words it, with Try Again, so a paying customer is
  never sent to the pricing page. The pane's notice text moved to
  AccountSettingsContent.Notice so both read it. LicenseGateRow replaces
  ProBadge in the Sync, Linked Folders, Data Rewind and connection export
  rows, LicenseGateAlert serves Compare & Sync and Data Rewind, and the
  overlay renders the same notice. Try Again shows a spinner and the
  failed check's reason; Refresh in Settings > Account shows the reason
  too. Welcome shows the pane's first notice as a link (a plan being
  canceled stays in the pane), else Sign In. Dropped: the .suspended
  access, since an entitlement carries no suspended state, and
  LicenseManager's coalesced checks and stale-answer guard, since
  AccountServices already runs one check at a time and drops a refresh
  that lands after sign-out.
- #3995: the Black Friday line shows when FeatureAccess.supportAudience is
  .prospect, the notion the day-30 ask reads.
- #3989: ported before this merge, in e39c309.
- #3993, #3997: merged as main has them; Welcome and the Sync pane keep
  UpdateCenter.
- #4007: a Team Library publish that goes through shows no info alert on
  Welcome or in Favorites; the section shows the items. The queued and
  failed messages stay.
- #4009: merged outside the Team Library. TeamLibraryStore's write
  generation, the shared in-flight pull, the refused second publish and
  the Team Library row in Settings > License are dropped: TeamLibrarySync
  runs syncs and publishes one at a time, bumps a generation on sign-out,
  and the Welcome and Favorites headers show the running sync, the last
  sync and a failure.
- 726a167 (security): LicenseAPIClient and LiveTeamLibraryAPIClient
  follow redirects only on their own origin. Ported by giving the account
  URLSession SameOriginRedirects as its delegate, which covers token
  requests, /v2 calls and the team library. The pulled library keeps a
  connection's sign-in choice for the consent prompt: ported to
  TeamLibraryChange.connection, and its test moved to TeamLibrarySyncTests.
  Publishing needs nothing, since portableSettings keeps the choice by
  default. EnvVarReferenceConsent reads FeatureAccess.
- DatabaseManager+Sessions: main's recovery guard and consent path, with
  Telemetry.connectionAttempted in place of MacAnalyticsProvider.
- project.yml: PostHog stays and zstd goes, as main moved Kafka's codecs
  into TableProCore; the app links main's new products, without
  TableProAnalytics.
- The storage rule, data-sync-storage.md: the paths main listed for the
  deleted TeamLibraryModels, TeamLibraryStore and MacAnalyticsProvider name
  TeamLibraryItem, TeamLibraryState and Telemetry, plus Account's stores.
- Localizable.xcstrings: merged key by key in this branch's order; no key
  changed on both sides. 271 keys come from main; the three SSH host keys
  main removed are gone, as nothing uses them.
- Tests deleted with the code this branch deleted: LicenseManagerDecisionTests,
  LicensePresentationTests and LicenseTierTests under Models,
  TeamLibraryModelsTests, TeamLibrarySyncCoordinatorTests, and the Macs
  link case in SupportLinksTests. New: LicensePresentationTests under
  Views/Settings, the sign-in choice case in TeamLibrarySyncTests and the
  redirect case in AccountConfigurationTests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant