Skip to content

4.19.1 - #609

Merged
yusuftor merged 77 commits into
masterfrom
develop
Nov 6, 2026
Merged

4.19.1#609
yusuftor merged 77 commits into
masterfrom
develop

Conversation

@yusuftor

@yusuftor yusuftor commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

Changes in this pull request

Releases 4.19.1 from develop to master.

Enhancements

  • Adds watchOS 8+ and tvOS 15+ support through Swift Package Manager, for purchases, subscription status, entitlements, user attributes and events. Paywalls can't be shown there, so when one would show, register runs non-gated features and returns an error for gated ones. (Build on watchOS and tvOS without paywalls #594)

Fixes

Also in this release

The version is 4.19.1 in Constants.swift, SuperwallKit.podspec and CHANGELOG.md. Merging starts the Release workflow: the unit tests and, because of the uitests-required label, the UI tests run on the merge commit. Only if they pass does it tag 4.19.1, publish the GitHub Release and push to CocoaPods. SPM users get it once the tag exists.

Checklist

  • All unit tests pass.
  • All UI tests pass.
  • Demo project builds and runs on iOS.
  • Demo project builds and runs on Mac Catalyst.
  • Demo project builds and runs on visionOS.
  • I added/updated tests or detailed why my change isn't tested.
  • I added an entry to the CHANGELOG.md for any breaking changes, enhancements, or bug fixes.
  • I have run swiftlint in the main directory and fixed any issues.
  • I have updated the SDK documentation as well as the online docs.
  • I have reviewed the contributing guide

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

The reviewed changes appear safe to merge.

Summary

Releases 4.19.1 with watchOS and tvOS support and fixes across identity, configuration, purchases, and paywall presentation.

  • Apps can use Superwall purchases and placements on watchOS and tvOS.
  • User changes keep the new identity and its attributes together.
  • Config refreshes apply in order and report when loading fails.
  • Enrichment stops using user and device data when tracking is off.
  • Standard Superwall event names are rejected as placements.
  • Paywall product slots keep their own billing plan.
  • Customer-info calls return when configuration cannot load.
  • Paywalls keep the app's rotation rules and Mac previews honor their links.
  • Release announcements explain failures and whether the version shipped.
  • Release notes and project guidance describe the updated SDK.

No actionable issues were identified. Apple-platform builds and tests were not run in this Linux environment.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[App registers placement] --> B[Wait for config and identity]
  B --> C[Evaluate audience]
  C -->|Skipped| D[Run feature]
  C -->|Paywall matches| E{WebKit available?}
  E -->|Yes| F[Load and present paywall]
  E -->|No| G[Fetch paywall access rule]
  G -->|Non-gated| D
  G -->|Gated or fetch fails| H[Report error]
Loading

Reviews (1) · Last reviewed commit: "Merge pull request #568 from superwall/b..." · Reviewed by Greptile

anglinb and others added 30 commits October 26, 2026 16:30
The SDK presents a paywall in a window of its own whose root was a plain
UIViewController, so the paywall allowed every orientation regardless of
the app. Apps that lock orientation in a view controller rather than
Info.plist could rotate while a paywall was up, and stayed rotated after
it closed. Unity games hit this: Unity's app controller asks each
window's root which orientations it allows, so Screen.autorotateTo* was
never consulted for the paywall's window.

The window's root now takes its orientations from the top view
controller of the app's window it covers, and PaywallViewController from
its presenter. Tearing the window down asks the app to check its
orientations again.

🌸 Shipped with Kanna — https://kanna.sh

Co-Authored-By: Kanna <noreply@kanna.sh>
Kanna-Agent: claude/opus
The Swift counterpart of the Android SDK's SequentialActor. It holds a
piece of state that can be read synchronously, applies pure reducers
atomically, and runs async actions one at a time in dispatch order, even
across suspension points, which a plain Swift actor doesn't guarantee.
Calls made from inside an action run inline, flush() lets tests wait for
the queue without sleeping, and interceptors wrap updates and actions for
persistence, pending-work tracking and debug logging.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gAkpYE4U7gP8oNpsp8m
Covers launch (fresh and restored ids, attribute cleaning), configure with
and without assignments, identify (trimming, blank ids, repeat calls, seed
derivation, waiting for assignments when restoring), reset, attribute
merging and ordering, and switching users. They describe today's behavior
so the store-based rewrite can be checked against them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gAkpYE4U7gP8oNpsp8m
The identity now lives in an IdentityState on a SequentialStore instead of
underscored vars behind a DispatchQueue, a DispatchGroup and a manually
toggled subject. Reads are synchronous and never wait. identify, reset
and attribute merges are IdentityActions that run one at a time in call
order, and readiness is derived from the work still pending in the state.
Waiting on the config (for the seed) or on assignments happens outside the
queue, so it no longer holds up later changes.

This also fixes:
- the seed derived from the user id was written off the queue, racing
  reads, and could land on a user who had since been switched or reset
- after reset() the user attributes were left empty instead of carrying
  the new aliasId and seed
- a public reset() could have its new alias and seed wiped from disk by
  the storage reset that follows it
- a reset or switch could fire several user_attributes events, one per
  id that changed

The reducers and actions are covered by tests that run without the rest
of the SDK, using a fake IdentityContext.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gAkpYE4U7gP8oNpsp8m
The config state, triggers, retry count and initial purchases load now
live in a ConfigManagerState on a store and change through ConfigUpdate
reducers, replacing a hand-written queue and a property wrapper.
configState stays the publisher everything subscribes to, but it now
mirrors the store, so tests set the config through store.update().

Fetching and refreshing config are ConfigActions, so they run one at a
time in call order instead of being able to overlap and apply their
configs out of order. The refreshes a fetch schedules are queued rather
than started in a Task, which would have counted as part of the fetch.
A refresh called before there's any config stays a no-op, as before.

The identity tests now start from a published config: a newly identified
user isn't ready until the config says whether to derive the seed from
the user id, which held for the old IdentityManager too. The user switch
test waits for the outcome instead of for reads that used to block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gAkpYE4U7gP8oNpsp8m
IdentityManager held storage, the config manager and the web entitlement
redeemer unowned. Its side effects and queued actions can outlive them,
and then reading one crashed the process. They're weak now, so that work
does nothing instead.

The new identity and config tests let their harness, or the dependency
container behind it, go away while the manager was still in use, which
crashed the test process on CI. They keep it alive to the end now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gAkpYE4U7gP8oNpsp8m
The old IdentityManager marked the identity ready whenever no identity
work was running and a configure, identify or reset had finished, through
its DispatchGroup and didSetIdentity(). The rewrite left configuration
pending until configure() ran, so a reset or identify on an identity that
hadn't been configured never became ready, and a test that resets in
setUp waited on hasIdentity forever.

The state now records whether the identity has been established once,
and configuration is pending only while configure() runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gAkpYE4U7gP8oNpsp8m
The old IdentityManager queued writes and made reads wait for the queue,
so identify(userId:) followed by reading userId on the same thread
returned the new id, and the same for setUserAttributes and
userAttributes. Running identity actions on the store's async queue lost
that, and CI caught it in the attribution, test mode and CEL tests.

None of the identity actions wait on anything, so they're now
SyncStoreActions run with SequentialStore.dispatch(_:in:): on the calling
thread, one at a time in dispatch order, with an action dispatched from
inside another running once that one finishes. A dispatch made while
another thread is running actions is queued for that thread and returns
straight away. Blocking there could deadlock: an identify that switches
user resets the attribution fetcher with queue.sync, while that fetcher's
queue merges attributes into the identity. No lock is held while an
action runs.

Identify and reset now apply before they return, so the pending markers
for them are gone. The identity is established by an explicit update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gAkpYE4U7gP8oNpsp8m
Keeps SequentialStore.swift within SwiftLint's file and type length
limits. SerialDispatcher holds the non-blocking drain behind dispatch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gAkpYE4U7gP8oNpsp8m
Left to inference, the closure parameter in
`attributes["key\($0)"] as? Int == $0` became `Int?`, so every key was
looked up as "keyOptional(0)" and the test failed although every merge
was applied. Types the index explicitly, and adds the same check against
the identity actions directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gAkpYE4U7gP8oNpsp8m
The Slack announcement now goes out once the release is on SPM and
CocoaPods and the wrappers have been told. Docs and Emerge failures are
still reported by report-failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Announce releases without waiting for docs and Emerge
The failure message said the release failed even when only a job after
tagging did, such as CocoaPods during its freeze. It now names the
failed jobs, and says the version is tagged and on SPM if it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
release pushes the tag before the release branch and the GitHub
Release, so it can fail with the version already out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sage

Say which release job failed, and whether the version is out
IdentityLogic.mergeAttributes renamed a `$key` to `key` and then looked
the value up under the new name. That found nothing, so it deleted any
attribute already stored as `key`. setUserAttributes documents `$` keys
as reserved and dropped, and filters them itself, so only internal
merges such as attributes set from a paywall reached this. They're now
skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gAkpYE4U7gP8oNpsp8m
The IdentityManager rewrite returned nil when the user id isn't a UUID,
dropping the old fallback to the UUID in the alias. Anonymous users and
users with non-UUID ids then purchased without an appAccountToken
(SK2) or applicationUsername (SK1), so their transactions couldn't be
linked back to them. The behavior test had pinned the regression; it
now checks the alias UUID.

Also included in this commit, from the same review: identify's reset for
a new user runs in store.callingOut so waiting dispatchers are released,
flush() waits for dispatched identity changes, and the test harness no
longer resets the shared instance on a user switch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gAkpYE4U7gP8oNpsp8m
- dispatch now waits for the caller's own action when another thread is
  running dispatched actions, so a read straight after identify,
  setUserAttributes or reset sees the change. The running action can
  release waiting callers with callingOut(_:), which identify uses around
  its reset for a new user: that reset takes other objects' queues, which
  a waiting caller may hold.
- The store recognizes its own action by the task running it rather than
  a task-local value. A Task started inside an action, or by a delegate
  callback it awaits, inherited the task-local and ran its own action
  inline, alongside the one still running, so two config refreshes could
  overlap again.
- Pending assignments are keyed by user id, like the seed, so restoring
  an earlier user's assignments can't make the next user ready early.
- An identify that switches user marks the new user's seed pending
  before the reset, so the anonymous user the reset leaves never counts
  as ready.
- The identity observer holds its store weakly; it formed a cycle.
- ConfigManager applies and publishes a config change under one lock, so
  configState sends changes in the order they were applied.
- CHANGELOG wording for user_attributes events and config refreshes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gAkpYE4U7gP8oNpsp8m
The fast path schedules a background config refresh. It's queued behind
the fetch now, so it can start after the 300ms the tests slept before
their container went away, and then read the container's freed objects
through ConfigManager's unowned references, crashing the test process.
The tests now wait for the store's queue to drain, bounded at 5s, before
the sleep for work the refresh starts in the background.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gAkpYE4U7gP8oNpsp8m
…l's action

- Pending seed and assignments work was keyed by user id. Identifying a
  user, resetting, and identifying the same user again before the first
  identify's work finished put both under one key, so the first to finish
  cleared the second's: with restorePaywallAssignments, paywalls could
  proceed before the current restore returned. Each identify now has its
  own IdentifyOperation, and resolves only its own work.
- immediateUntil didn't queue its action when the state already matched
  the predicate. It's now queued however the wait registers.

Both from Maple's review of #593.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gAkpYE4U7gP8oNpsp8m
identifyAnonymousUser checked that the seed was still pending right after
identify, but the fake answered the seed lookup straight away, so on a
busy runner the side effect could resolve it first. The test now holds
the lookup until after the check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gAkpYE4U7gP8oNpsp8m
Keeps develop's shorter 4.19.0 changelog wording and adds this branch's
fixes in the same style. Tests develop added that sent to
ConfigManager.configState now update its store, which configState mirrors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gAkpYE4U7gP8oNpsp8m
ConfigManagerState kept the error in a separate `failure` field beside
`configState`, so a state could hold an error together with a retrieved
config. The error is now the payload of `.failed`. Setting a failed state
directly completes configState with that error, as a failure from a fetch
does.

The triggers stay a separate field: they take effect when a config
arrives, before it's published, so events tracked while purchases load
can still trigger paywalls, and during a refresh they're the new config's
while the old one is still published. The state's doc comment says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gAkpYE4U7gP8oNpsp8m
- The identity observer read the state before and after an update and
  saved the difference with nothing held across those steps. Updates
  made outside dispatched actions, such as configure or a restore
  finishing in a side effect, run on other threads, so one could save a
  snapshot after a later update had saved its own: a configure finishing
  around an identify and a reset could write the logged-in user back
  over the anonymous one. One lock now covers the whole sequence.
- reset() became synchronous in this PR, so the identity never went
  unready while Superwall.reset() wiped storage and the paywall cache,
  and a paywall registered on another thread could be presented in the
  middle of that. The old queued reset kept it unready for that window.
  Superwall.reset() now marks a pending reset until its cleanup is done.

Both from Maple's review of #593 (F3, F4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gAkpYE4U7gP8oNpsp8m
Keeps IdentityManager.swift within SwiftLint's file length limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gAkpYE4U7gP8oNpsp8m
The package declared watchOS but never built there, and tvOS wasn't
declared at all. Both lack WebKit, so everything that needs a paywall
on screen is now behind `#if canImport(WebKit)`: the paywall view
controller and presentation, debugger, Test Mode, Customer Center,
permissions and the SDK's alerts. Purchases, observer mode, restores,
entitlements, customer info, user attributes, events and web checkout
redemption work as on iOS.

- `register` runs the audience filters, then skips (holdout, no match,
  placement not found) or sends a code 108 error when a paywall would
  show. Implicit triggers and paywall preloading do nothing.
- `getPresentationResult` returns the paywall the user would see.
- Test mode never turns on, since its pickers can't be shown.
- Superscript has no tvOS slice, so `Package.swift` drops it on tvOS
  and audience filters with an expression never match there.
- watchOS gets WatchKit-backed device info, lifecycle notifications and
  `PlatformApplication`. MMP install matching is off on both.
- Shared `@available` checks name matching watchOS/tvOS versions.
- CI builds watchOS and tvOS. CocoaPods stays iOS/macOS only.

Bumps to 4.20.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The guard only stopped the paywall downloads, so the preload entry points
still evaluated audiences, read occurrences from Core Data and could leave
a queued task sleeping for five seconds. Guard the entry points instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
register used to send its own error whenever a paywall would show, so
non-gated features never ran there. It now ends the way iOS does without
internet: it fetches the paywall, and declines it with
webViewFailedToLoad. Non-gated features run, gated ones get the existing
web view error, and a failed fetch sends its error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reusing the web view failure path called onDismiss for a paywall that was
never presented. register now passes its feature block down: a non-gated
paywall runs it with no present or dismiss callbacks, and a gated one sends
error 106.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in register

register already refused placements named after a standard Superwall
event, but did so silently: no error handler call, no feature block.
It now sends the error to the handler. getPaywall (and PaywallView,
which uses it) had no check at all, so a placement like "paywall_open"
was tracked as a real event and could match campaigns meant for the
standard one. It now throws the same error before tracking.

Bumps to 4.19.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude and others added 26 commits November 5, 2026 12:24
refreshWaitsForFetch ran a full launch fetch against a plain
DependencyContainer, whose real StoreKit purchase load never finished
on CI, so the test run hung. It now uses the early-publish tests'
harness, which mocks the device helper, receipts and products, and
every wait in it is bounded so a regression fails instead of hanging.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gAkpYE4U7gP8oNpsp8m
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stop getCustomerInfo hanging when config fails; fix Mac preview links
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in develop (getCustomerInfo no longer hangs when config fails;
Mac preview link fix) and the identity doc updates. Keeps both sets of
changelog entries, and switches the two new getCustomerInfo and
refreshCustomerInfo tests to change config through the store, like the
rest of the file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gAkpYE4U7gP8oNpsp8m
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A reset's cleanup wipes the user's storage once the identity is
unlocked. With user changes on two threads, a switch's cleanup could
run after another switch, or a login after reset(), had already
restored its user's assignments, and delete them.

An identify or reset now holds a user-change lock until its deferred
cleanup has finished. The lock is taken before the identity's and never
with it held. The cleanup only hops to private queues, so it can't wait
on the thread that's waiting for the lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CR7gAkpYE4U7gP8oNpsp8m
configState can briefly lag the store while another thread is still
publishing, so the IP collection gates and the paywall gate's quick
config check read ConfigManager.config instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebuild the identity on a locked state store with an Android-style reset
Move ConfigManager state onto StateStore and serialize fetch and refresh
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep each paywall slot's billing plan when slots share a product
The enrichment request posts the user and device attributes, so it now
follows eventTrackingBehavior = .none like the install-attribution match
does. It's checked again after the attributes are built, in case the app
opts out in between, and enrichment is fetched as soon as the app opts
back in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A request waiting for the foreground or a retry now checks tracking
before each attempt and stops without sending. A reply that lands after
opting out is dropped, and enrichment from before the opt-out, or
restored from disk, is left out of the device attributes while tracking
is off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing-off

Skip enrichment while event tracking is off
…-orientation

# Conflicts:
#	CHANGELOG.md
#	Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift
#	SuperwallKit.xcodeproj/project.pbxproj
UIKit only asks the root, or a view controller presented over the whole
window, which orientations a window allows. Following every presented view
controller meant a paywall opened from a sheet copied the sheet's default
mask instead of the locked screen under it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he store rules

dispatch now returns what the action returned, so an action can update the
state and report back (whether it claimed something, the state it saw) in one
locked step instead of the caller reading the state in a second call. read(_:)
reads part of the state with the store locked, so a copy of the whole state
can't outlive a removal.

The new State stores section in CLAUDE.md is the shape every store follows:
enum-shaped state, named reducers, closed actions, side effects on the Context
protocol, and a SerialTaskCoordinator for async work that must not overlap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On an iPhone in landscape a page sheet covers the whole window, so its
presentation controller says it presents full screen. UIKit still doesn't
ask a sheet about orientation, so a paywall opened from a sheet in a
landscape-only app rotated to portrait. Only full-screen and over-full-screen
presentations, and custom ones that fill the window, now count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
read held the lock without counting itself as an update or dispatch, so an
update or dispatch inside it ran its deferred work with the lock still held.
If that work waited on another thread that needed the store, they deadlocked.
read now goes through the same locked path as update and dispatch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Let StateStore actions return a value, and write down the store rules
Keep paywalls to the orientations the app allows
@yusuftor yusuftor added the uitests-required Run the Limrun UI tests before releasing (on the PR into master) label Nov 6, 2026
@maple-review-bot

maple-review-bot Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Maple review

🟢 Confidence 10/10 · safe to merge. Regression tests cover the changed state transitions and SDK behavior; all reported platform builds, unit tests and lint checks pass.
no findings · tests covered · risk high · 1/1 new units observable

Releases 4.19.1 with watchOS/tvOS headless support, serialized identity and configuration state, and fixes for billing-plan selection, tracking opt-out and presentation behavior. No confirmed defects require changes before merging.

  • StateStore serializes identity and configuration transitions.
  • finishWithoutPaywallUI handles registration without paywall UI.
  • Product.cacheKey distinguishes billing plans sharing an App Store product.
  • Network suppresses enrichment while event tracking is disabled.

1cde7db · What was checked, coverage and telemetry details are on the Maple / review check. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

@yusuftor

yusuftor commented Nov 6, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai

@yusuftor
yusuftor merged commit 035d7d0 into master Nov 6, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

uitests-required Run the Limrun UI tests before releasing (on the PR into master)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants