Player 1.0
Player 1.0.0 is releasing across the entire ecosystem. This is not a conventional “1.0”: Player has been running in production for years and this isn’t a first declaration of stability. Rather, it marks the point where Player and its surrounding ecosystem have converged into a cohesive whole we’re ready to treat as the foundation for future development, with a set of breaking changes consolidated here so subsequent work can proceed from a consistent base. With more breaking changes already in the pipeline, 1.0.0 gives us a defined starting point from which future majors can be planned, announced, and rolled out predictably.
Breaking Changes (Player)
Section titled “Breaking Changes (Player)”Below is a detailed guide explaining all the breaking changes we have made as part of this 1.0 release. If a change pertains to only one platform, that section will be prefaced with “(Core)”, “(iOS)”, or “(Android)”. E.g. (iOS) .anyArray and .anyDictionary use AnyType.
(iOS) .anyArray and .anyDictionary use AnyType
Section titled “(iOS) .anyArray and .anyDictionary use AnyType”The AnyType enum now conforms to Sendable protocol and is concurrency-safe. This required removing all APIs that depended on the Any type:
// Beforecase anyDictionary(data: [String: Any])case anyArray(data: [Any])
// Aftercase anyDictionary(data: [String: AnyType])case anyArray(data: [AnyType])Migration
Section titled “Migration”Use AnyType cases directly instead of converting from/to Any:
// Old (removed)let anyType = AnyType.anyDictionary(data: ["key": "value"])let anyArray = AnyType.anyArray(data: ["key", 2])
// New (Sendable-safe)let anyType = AnyType.anyDictionary(data: ["key": .string(data: "value")])let anyArray = AnyType.anyArray(data: [.string(data: "key"), .number(data: 2)])(iOS) AnyType will only decode .unknownData with AnyTypeDecodingContext
Section titled “(iOS) AnyType will only decode .unknownData with AnyTypeDecodingContext”Users are required to provide AnyTypeDecodingContext when attempting to decode anyArray or anyDictionary. The decoder will throw if we need the context but one is not provided. As a side effect of this, .unknownData will only be returned when a context is provided. For example:
// Before, decoding data = "null"let anyType = try? JSONDecoder() // no context .decode(AnyType.self, from: data) // This will return .unknownData
// After, decoding data = "null"let anyType = try? JSONDecoder() // no context .decode(AnyType.self, from: data) // This will throw an error
let anyType = try? AnyTypeDecodingContext(rawData: data) // with context .inject(to: JSONDecoder()) .decode(AnyType.self, from: data) // This will return .unknownData(iOS) AnyTypeDecodingContext.objectFor is no longer public.
Section titled “(iOS) AnyTypeDecodingContext.objectFor is no longer public.”All decoding of AnyType is now handled internally. So AnyTypeDecodingContext.objectFor(path: [CodingKey]) throws -> Any is no longer exposed to users.
FlowManager alignment
Section titled “FlowManager alignment”1. All platforms have same FlowManager
Section titled “1. All platforms have same FlowManager”All platforms now have a user-facing FlowManager with an aligned API.
Android previously aliased AsyncIterationManager to FlowManager. However, Android’s actual FlowManager equivalent was AsyncIterator. AsyncIterator with appropriate types is now aliased as FlowManager to match the other platforms.
Migration: In Kotlin, use the FlowManager type with PlayerViewModel instead of any AsyncIterator. E.g.
// BeforePlayerViewModel(flows = someAsyncFlowIterator)
// AfterPlayerViewModel(manager: someFlowManager)2. next(...)
Section titled “2. next(...)”- The parameter of
nextis now namedresultacross all platforms. - Swift and Kotlin now both return the simpler
String?(null/nil = done); React returns an object object to fit the JSIteratorprotocol
| Platform | Before | After |
|---|---|---|
| React | | Migration: Change the function signature to match. |
| Swift | | Migration: Instead of |
| Kotlin | | Unchanged |
3. terminate(...)
Section titled “3. terminate(...)”This API is now the same across all platforms. It accepts an optional InProgressState.
| Platform | Before | After |
|---|---|---|
| React | | Migration: Call terminate with the
|
| Swift | | Unchanged |
| Kotlin | (on | (on Migration: Call terminate with the
|
4. (Android) AsyncIterator takes 3 types
Section titled “4. (Android) AsyncIterator takes 3 types”The generic AsyncIterator now accepts 3 types instead of 2:
public interface AsyncIterator<Item : Any, Result : Any, Data : Any>The 3rd type defines the parameter for terminate:
public suspend fun terminate(data: Data? = null) {}Migration: Provide the Data type appropriate for your use case. If terminate will always be called with nill, use Nothing.
(iOS/Android) CommonTypesPlugin, CommonExpressionsPlugin, ComputedPropertiesPlugin, and StageRevertDataPlugin removed
Section titled “(iOS/Android) CommonTypesPlugin, CommonExpressionsPlugin, ComputedPropertiesPlugin, and StageRevertDataPlugin removed”These plugins are now core-only. They operate at the shared JS core layer and should not be registered per-platform.
Migration
Section titled “Migration”Remove any direct registration of these plugins in your iOS or Android code:
// Before — no longer neededplugins: [ CommonTypesPlugin(), CommonExpressionsPlugin(), ComputedPropertiesPlugin(), StageRevertDataPlugin(),]// Before — no longer neededoverride val plugins = listOf( CommonTypesPlugin(), ReferenceAssetsPlugin(),)Instead, register these plugins once in the shared core layer of your Player plugin, following the process described in the corresponding docs:
(iOS) Dropped CocoaPods Support
Section titled “(iOS) Dropped CocoaPods Support”Since CocoaPods is in maintenance mode, we are dropping support in favour of using Swift Package Manager (SPM) only. Please leverage the Swift Package, e.g.
// In your Package.swift.dependencies: [ .package(url: "https://github.com/player-ui/playerui-swift-package.git", from: "1.0.0"),],targets: [ .target( name: "SomeExampleTarget", dependencies: [ .product(name: "PlayerUI", package: "playerui-swift-package"), ], ),](iOS) ErrorState.error is now JSValueError instead of String
Section titled “(iOS) ErrorState.error is now JSValueError instead of String”ErrorState.error previously held the raw error message as a String. It is now a JSValueError struct that exposes the full error metadata from the JS layer:
// Beforelet message: String = errorState.error
// Afterlet message: String = errorState.error.messagelet type: String = errorState.error.typelet severity: ErrorSeverity? = errorState.error.severitylet metadata: [String: Any]? = errorState.error.metadataMigration: Replace direct string usage of errorState.error with errorState.error.message.
ExternalActionPlugin renamed to ExternalStatePlugin with new handler API
Section titled “ExternalActionPlugin renamed to ExternalStatePlugin with new handler API”The plugin has been renamed from ExternalActionPlugin to ExternalStatePlugin.
The name ExternalState better reflects what the plugin does — it handles EXTERNAL
navigation states — and is now used consistently across all platforms.
The handler API has changed.
Instead of a single function that receives all external states, you now provide an array
of handlers. Each handler requires a ref (matching the external state’s ref field)
and a handlerFunction. An optional match object can be provided to match on
additional state properties beyond ref:
// Beforenew ExternalActionPlugin((state, options) => { if (state.ref === "my-action") { return "Next"; }});
// Afternew ExternalStatePlugin([ { ref: "my-action", handlerFunction: (state, options) => "Next" }, { ref: "other-action", handlerFunction: (state, options) => "Prev" },]);// Before — ExternalActionPlugin// Single handler receives all external states; ref checked manually insideval plugin = ExternalActionPlugin { state, options, transition -> if (state.ref == "my-action") { transition("Next") }}
// After — ExternalStatePlugin// Separate handler per ref; matched automaticallyval plugin = ExternalStatePlugin( ExternalStateHandler(ref = "my-action") { state, options, transition -> transition("Next") })// Before — ExternalActionPlugin// Single handler receives all external states; ref checked manually insidelet plugin = ExternalActionPlugin { state, options, transition in guard state.ref == "my-action" else { return } transition("Next")}
// After — ExternalStatePlugin// Separate handler per ref; matched automaticallylet plugin = ExternalStatePlugin(handlers: [ ExternalStateHandler(ref: "my-action") { state, options, transition in transition("Next") }])
// Before — ExternalActionViewModifierPluginlet plugin = ExternalActionViewModifierPlugin<ExternalStateSheetModifier> { state, options, transition in return AnyView(Text("External State"))}
// After — ExternalStateViewModifierPluginlet plugin = ExternalStateViewModifierPlugin<ExternalStateSheetModifier>(handlers: [ ExternalStateViewModifierHandler(ref: "my-action") { state, options, transition in return AnyView(Text("External State")) }])Handler matching and specificity
Section titled “Handler matching and specificity”Handlers are matched against the external state using partial object matching on ref
and any additional properties in match. When multiple handlers could match a state,
the most specific one wins (the one with the most matching properties).
This allows you to register a general handler for a ref and a more specific one for
a particular variant:
new ExternalStatePlugin([ // Matches any state with ref: "my-action" { ref: "my-action", handlerFunction: () => "default" }, // Takes precedence when type: "special" is also present { ref: "my-action", match: { type: "special" }, handlerFunction: () => "special", },]);Overriding handlers across plugins
Section titled “Overriding handlers across plugins”Overriding handlers is now supported. When multiple ExternalStatePlugin instances
register a handler for the same state,
the last registered handler wins. A debug log is emitted when a handler is
replaced, so you can tell when an override is happening.
To add more handlers after initial setup, register a new ExternalStatePlugin instance.
Plugins are typically grouped together and easy to find by name. This makes it straightforward to
determine the override order.
We considered allowing more handlers to be added to an ExternalStatePlugin after creation,
however, this would make it more difficult to figure out the override order. So we do not
support that.
Missing handlers
Section titled “Missing handlers”For now, If no handler matches an external state, the Player remains on that state and no
transition occurs. Ensure every EXTERNAL state in your flow has a registered handler.
Breaking Changes (Tooling)
Section titled “Breaking Changes (Tooling)”New Package Scopes/Names
Section titled “New Package Scopes/Names”The tooling ecosystem (previously all under the @player-tools scope) has been split up into separate scopes to better reflect the underlying capability groupings.
The table below maps each old @player-tools package to its new home:
| Old Package | New Package | Repo |
|---|---|---|
@player-tools/cli | @player-cli/cli | cli |
@player-tools/dsl | @player-lang/react-dsl | language |
@player-tools/fluent | @player-lang/functional-dsl | language |
player_tools_dsl | player_lang_dsl | language |
@player-tools/fluent-generator | @player-lang/functional-dsl-generator | language |
player_tools_dsl_generator | player_lang_dsl_generator | language |
@player-tools/json-language-server | @player-lang/json-language-server | language |
@player-tools/json-language-service | @player-lang/json-language-service | language |
@player-tools/typescript-expression-plugin | @player-lang/typescript-expression-plugin | language |
@player-tools/complexity-check-plugin | @player-lang/complexity-check-plugin | language |
@player-tools/metrics-output-plugin | @player-lang/metrics-output-plugin | language |
@player-tools/xlr | @xlr-lib/xlr | xlr |
@player-tools/xlr-sdk | @xlr-lib/xlr-sdk | xlr |
@player-tools/xlr-utils | @xlr-lib/xlr-utils | xlr |
@player-tools/xlr-converters | @xlr-lib/xlr-converters | xlr |
XLR Changes
Section titled “XLR Changes”@xlr-lib/xlr-utils
Section titled “@xlr-lib/xlr-utils”The following TypeScript-compiler-API-specific exports have been moved to the @xlr-lib/xlr-converters package and made private:
decorateNode, createDocString, createTSDocString, symbolDisplayToString, tsStripOptionalType, isExportedDeclaration, isNodeExported, getReferencedType, isTypeScriptLibType, getStringLiteralsFromUnion, buildTemplateRegex, isOptionalProperty, isGenericInterfaceDeclaration, isGenericTypeDeclaration, isTypeReferenceGeneric, TopLevelDeclaration, isTopLevelDeclaration, TopLevelNode, isTopLevelNode
This was done to make the utils package more portable and not pull in typescript as a dependency when used in web invocations of the SDK.
Language Changes
Section titled “Language Changes”No from last release in old report
- Updated to use Oclif 1.9
Devtools
Section titled “Devtools”TBA
Behavioral Changes & Bug Fixes
Section titled “Behavioral Changes & Bug Fixes”(Core) Partial Match Registry replaces exact matches, with log
Section titled “(Core) Partial Match Registry replaces exact matches, with log”The Registry.set() method now replaces existing entries with exactly matching keys/matches.
Whenever a replacement happens, a debug log will occur.
Example:
const registry = new Registry<string>();
// Add two entriesregistry.set({ foo: "bar" }, "exact-match");registry.set({ foo: "bar", baz: "qux" }, "more-specific-match");
// Before (old behavior):// Calling set() again would add a third entry, potentially causing unexpected behaviorregistry.set({ foo: "bar" }, "new-exact-match");// Result: All 3 entries exist in the registry
// After (new behavior):// Calling set() replaces the exact match onlyregistry.set({ foo: "bar" }, "new-exact-match");// Result: The { foo: "bar" } entry is replaced// The { foo: "bar", baz: "qux" } entry remains unchanged
registry.get({ foo: "bar" }); // Returns "new-exact-match"registry.get({ foo: "bar", baz: "qux" }); // Returns "more-specific-match"(Core) Partial Match Registry uses MOST SPECIFIC match
Section titled “(Core) Partial Match Registry uses MOST SPECIFIC match”The Registry.get() method now consistently returns the most specific matching entry, regardless of
the order entries were added. Previously, it would return the most recent matching entry.
Example:
const registry = new Registry<string>();
// Scenario: Add more specific match first, then less specificregistry.set({ foo: "bar", metaData: { role: "baz" } }, "more-specific");registry.set({ foo: "bar" }, "less-specific");
const query = { foo: "bar", metaData: { role: "baz" }, extra: "data" };
// Before: Result would be most recent matchregistry.get(query); // Returns "less-specific"
// After: Always returns the most specific matchregistry.get(query); // Always returns "more-specific"New Features
Section titled “New Features”(iOS) Convenience Methods for AnyType
Section titled “(iOS) Convenience Methods for AnyType”Added as<T>(_:) convenience method and AnyType (anyDictionary) subscripting for type-safe value extraction:
// Beforeif case let .anyDictionary(myDict) = anyType, let title = myDict["title"] as? String { // Do a thing}
// Afterlet title: String? = anyType["title"]?.as(String.self)ErrorController
Section titled “ErrorController”Player now includes a built-in ErrorController for structured error handling across the flow lifecycle. It is automatically instantiated when a flow starts and accessible via controllers.error in the InProgressState.
Key capabilities:
captureError(error)— captures anyErrorimplementingPlayerErrorMetadata(type, optionalseverity, optionalmetadata). Returnstrueif recovery succeeded viaerrorTransitions,falseif the flow was failed.onErrorhook — plugins can tap to observe errors; returntrueto take ownership and skiperrorStatenavigation.errorStatebinding — errors are automatically written toerrorStatein the data model, readable in views via{{errorState.message}},{{errorState.errorType}}, etc.errorTransitionsmap — flows can define per-state or flow-level error routing to navigate to dedicated error views instead of failing.
Render-time error recovery — All platforms provide the ability to recover from render-time errors by calling captureError(error)
See the Error Handling docs for the full API reference.