Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
531ba2e
Basic A2UI working
KetanReddy May 12, 2026
a3a49da
core/react A2UI basic catalog implementations
KetanReddy May 12, 2026
09c1a0f
storybook (and react player) changes
KetanReddy May 12, 2026
3146154
add std lib functions
KetanReddy May 12, 2026
c16ce9b
Fix binding issues
KetanReddy May 12, 2026
582512f
Use actual binding regex for text asset transform
KetanReddy May 12, 2026
440e4f6
Fix tabs and modal
KetanReddy May 12, 2026
0186f1b
Add more fallbacks for text handling binding vs static text
KetanReddy May 12, 2026
1771510
Pull out input format translation into a hook, move out A2UI translat…
KetanReddy May 22, 2026
8db542e
Clean up mocks
KetanReddy May 22, 2026
56d3375
minor types cleanup
KetanReddy May 22, 2026
8014781
Merge branch 'main' into feture/a2ui
KetanReddy May 22, 2026
da2143a
iOS A2UI Support
KetanReddy Jun 16, 2026
dc165ef
JVM/Android A2UI Support
KetanReddy Jun 16, 2026
e78a3da
Merge branch 'feture/a2ui' of https://github.com/player-ui/player int…
KetanReddy Jun 16, 2026
6799e5b
Merge branch 'main' into feture/a2ui
KetanReddy Jun 16, 2026
626ba4b
Prepackaged A2UI Players for iOS and Android
KetanReddy Jun 16, 2026
e64af40
Merge branch 'feture/a2ui' of https://github.com/player-ui/player int…
KetanReddy Jun 16, 2026
3cd95e2
Fix missing A2UI mocks on iOS
KetanReddy Jun 16, 2026
2fe172f
Remove common types from A2UI prepackaging
KetanReddy Jun 16, 2026
157f7f8
Fix failing tests
KetanReddy Jun 16, 2026
78c2bd9
Merge branch 'main' of https://github.com/player-ui/player into fetur…
KetanReddy Jun 24, 2026
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 .bazelignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,4 @@ tools/storybook/node_modules
tools/components/node_modules
tools/mocks/node_modules
docs/site/node_modules
docs/astro/node_modules
docs/astro/node_modules
3 changes: 1 addition & 2 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,7 @@ npm.npm_translate_lock(
],
npm_package_target_name = "{dirname}",
npmrc = "//:.npmrc",
pnpm_lock = "//:pnpm-lock.yaml",
verify_node_modules_ignored = "//:.bazelignore",
pnpm_lock = "//:pnpm-lock.yaml"
)
use_repo(npm, "npm")

Expand Down
24 changes: 24 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ let package = Package(
.playerPackage(name: "PlayerUI"),
.playerPackage(name: "PlayerUISwiftUI"),
.playerPackage(name: "PlayerUIReferenceAssets"),
.playerPackage(name: "PlayerUIA2UI"),
.playerPackage(name: "PlayerUIA2UIPreset"),
.playerPackage(name: "PlayerUILogger"),
.playerPackage(name: "PlayerUITestUtilities"),
.playerPackage(name: "PlayerUITestUtilitiesCore"),
Expand Down Expand Up @@ -99,6 +101,28 @@ let package = Package(
.process("Resources")
]
),
.target(
name: "PlayerUIA2UI",
dependencies: [
.product(name: "SwiftHooks", package: "swift-hooks"),
.target(name: "PlayerUI"),
.target(name: "PlayerUISwiftUI")
],
path: "plugins/a2ui/swiftui",
resources: [
.process("Resources")
]
),
.target(
name: "PlayerUIA2UIPreset",
dependencies: [
.target(name: "PlayerUI"),
.target(name: "PlayerUISwiftUI"),
.target(name: "PlayerUIA2UI"),
.target(name: "PlayerUICommonTypesPlugin")
],
path: "packages/a2ui/swiftui"
),
.target(
name: "PlayerUITestUtilitiesCore",
dependencies: [
Expand Down
1 change: 1 addition & 0 deletions REPO.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ignore_directories(["**/node_modules"])
2 changes: 2 additions & 0 deletions android/demo/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ main_deps = [
"//jvm/hermes:hermes-android",
"//jvm/utils",
"//plugins/reference-assets/android",
"//plugins/a2ui/android:a2ui-android",
"//plugins/common-types/jvm",
"//tools/mocks:jar",

artifact("androidx.navigation:navigation-ui-ktx"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.intuit.playerui.android.reference.demo.lifecycle

import com.intuit.playerui.android.AndroidPlayer
import com.intuit.playerui.android.AndroidPlayer.Config
import com.intuit.playerui.android.a2ui.A2UIPlugin
import com.intuit.playerui.android.asset.RenderableAsset.AsyncHydrationTrackerPlugin
import com.intuit.playerui.android.asset.asyncHydrationTrackerPlugin
import com.intuit.playerui.android.lifecycle.PlayerViewModel
Expand All @@ -19,6 +20,9 @@ class DemoPlayerViewModel(
) : PlayerViewModel(manager) {
override val plugins = listOf(
ReferenceAssetsPlugin(),
// A2UI assets coexist with the reference assets (PascalCase vs lowercase type
// namespaces). Start an A2UI snapshot with `androidPlayer.start(snapshot, "a2ui")`.
A2UIPlugin(),
PendingTransactionPlugin(),
AsyncHydrationTrackerPlugin(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ public class AndroidPlayer private constructor(

override fun start(flow: String): Completable<CompletedState> = player.start(flow)

override fun start(
flow: String,
format: String,
version: String?,
): Completable<CompletedState> = player.start(flow, format, version)

private val assetSerializer = RenderableAsset.Serializer(this)

/** [Registry] of [RenderableAsset] builders */
Expand Down
124 changes: 124 additions & 0 deletions core/player/src/__tests__/transform-content.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { describe, expect, test, vitest } from "vitest";
import { Player } from "..";
import type { Flow, PlayerPlugin, InProgressState } from "..";

/** Minimal Flow used to short-circuit setupFlow. */
const flowFor = (id: string): Flow => ({
id,
views: [{ id: "v1", type: "text", value: "hi" }],
data: {},
navigation: {
BEGIN: "F",
F: {
startState: "VIEW_1",
VIEW_1: {
state_type: "VIEW",
ref: "v1",
transitions: { "*": "END" },
},
END: { state_type: "END", outcome: "done" },
},
},
});

describe("transformContent hook", () => {
test("default format 'player' passes the payload through unchanged", async () => {
const player = new Player();
const flow = flowFor("plain");
player.start(flow);
const state = player.getState() as InProgressState;
expect(state.flow).toBe(flow);
});

test("a plugin tapping for a custom format transforms the payload", async () => {
const greetPlugin: PlayerPlugin = {
name: "greet",
apply(p) {
p.hooks.transformContent.tap("greet", (content, meta) => {
if (meta.format !== "greet") return content;
const { message } = content as { message: string };
return { ...flowFor("greet"), data: { message } };
});
},
};

const player = new Player({ plugins: [greetPlugin] });
player.start({ message: "Hello world" }, { format: "greet" });

const state = player.getState() as InProgressState;
await vitest.waitFor(() =>
expect(state.controllers.view.currentView?.lastUpdate).toBeDefined(),
);
expect(state.controllers.data.get("message")).toBe("Hello world");
});

test("fires before resolveFlowContent", async () => {
const order: string[] = [];

const orderPlugin: PlayerPlugin = {
name: "order",
apply(p) {
p.hooks.transformContent.tap("order", (c) => {
order.push("transformContent");
return c;
});
p.hooks.resolveFlowContent.tap("order", (c) => {
order.push("resolveFlowContent");
return c;
});
},
};

const player = new Player({ plugins: [orderPlugin] });
player.start(flowFor("order"));

expect(order).toEqual(["transformContent", "resolveFlowContent"]);
});

test("multiple format plugins coexist — only matching one transforms", async () => {
const aPlugin: PlayerPlugin = {
name: "a",
apply(p) {
p.hooks.transformContent.tap("a", (c, meta) =>
meta.format === "a" ? flowFor("from-a") : c,
);
},
};
const bPlugin: PlayerPlugin = {
name: "b",
apply(p) {
p.hooks.transformContent.tap("b", (c, meta) =>
meta.format === "b" ? flowFor("from-b") : c,
);
},
};

const player = new Player({ plugins: [aPlugin, bPlugin] });
player.start({ raw: true }, { format: "b" });

const state = player.getState() as InProgressState;
expect(state.flow.id).toBe("from-b");
});

test("version is plumbed from StartOptions to the hook's meta arg", () => {
const versions: Array<string | undefined> = [];
const probe: PlayerPlugin = {
name: "probe",
apply(p) {
p.hooks.transformContent.tap("probe", (c, meta) => {
versions.push(meta.version);
// For format !== "demo", pass through so default path runs.
if (meta.format !== "demo") return c;
return flowFor("demo");
});
},
};

const player = new Player({ plugins: [probe] });
player.start({ x: 1 }, { format: "demo", version: "2" });
player.start({ x: 1 }, { format: "demo", version: "1" });
player.start(flowFor("noversion"));

expect(versions).toEqual(["2", "1", undefined]);
});
});
2 changes: 1 addition & 1 deletion core/player/src/controllers/data/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export class ReadOnlyDataController
this.controller = controller;
}

get(binding: BindingLike, options?: DataModelOptions | undefined) {
get(binding: BindingLike, options?: DataModelOptions | undefined): any {
return this.controller.get(binding, options);
}
}
19 changes: 15 additions & 4 deletions core/player/src/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import type {
CompletedState,
ErrorState,
PlayerHooks,
ContentMeta,
StartOptions,
} from "./types";
import { NOT_STARTED_STATE } from "./types";

Expand Down Expand Up @@ -125,6 +127,7 @@ export class Player {
onStart: new SyncHook<[Flow]>(),
onEnd: new SyncHook<[]>(),
resolveFlowContent: new SyncWaterfallHook<[Flow]>(),
transformContent: new SyncWaterfallHook<[unknown, ContentMeta]>(),
};

constructor(config?: PlayerConfigOptions) {
Expand Down Expand Up @@ -513,8 +516,16 @@ export class Player {
};
}

public async start(payload: Flow): Promise<CompletedState> {
const ref = Symbol(payload?.id ?? "payload");
public async start(
payload: unknown,
options?: StartOptions,
): Promise<CompletedState> {
const meta: ContentMeta = {
format: options?.format ?? "player",
version: options?.version,
};
const flow = this.hooks.transformContent.call(payload, meta) as Flow;
const ref = Symbol(flow?.id ?? "payload");

/** A check to avoid updating the state for a flow that's not the current one */
const maybeUpdateState = <T extends PlayerFlowState>(newState: T) => {
Expand All @@ -537,7 +548,7 @@ export class Player {
});

try {
const { state, start } = this.setupFlow(payload);
const { state, start } = this.setupFlow(flow);
this.setState({
ref,
...state,
Expand All @@ -564,7 +575,7 @@ export class Player {
const errorState: ErrorState = {
status: "error",
ref,
flow: payload,
flow,
error,
};

Expand Down
41 changes: 41 additions & 0 deletions core/player/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,36 @@ import type { ReadOnlyDataController } from "./controllers/data/utils";
import { SyncHook, SyncWaterfallHook } from "tapable-ts";
import { ViewInstance } from "./view";

/**
* Metadata describing the incoming content to `Player.start()`. Passed
* alongside the raw payload through the `transformContent` hook so plugins
* can decide whether to claim/convert it.
*/
export interface ContentMeta {
/**
* Content format identifier. `"player"` (default) means the input is
* already a `Flow` and needs no conversion. Anything else is a free-form
* string a plugin can recognize.
*/
format: string;
/**
* Optional format version (e.g., `"0.9"`). Free-form — plugins decide the
* convention. Lets a single format plugin dispatch across major/minor
* versions without the caller picking a different `format` string.
*/
version?: string;
}

/**
* Options passed to `Player.start()` alongside the payload.
*/
export interface StartOptions {
/** Identifier of the input content's format. Default: `"player"`. */
format?: string;
/** Optional content-format version. See `ContentMeta.version`. */
version?: string;
}

/**
* Public Player Hooks
*/
Expand Down Expand Up @@ -47,6 +77,17 @@ export interface PlayerHooks {
[Flow<Asset<string>>],
Record<string, any>
>;
/**
* Transform raw input content into a Player `Flow` before any state is set
* up. Fires at the top of `Player.start()` — after plugins are applied,
* before `resolveFlowContent`. Plugins tap this and inspect `meta.format`
* (and optionally `meta.version`) to decide whether to convert. Plugins
* that don't handle the format pass the content through unchanged.
*/
transformContent: SyncWaterfallHook<
[unknown, ContentMeta],
Record<string, any>
>;
}

/** The status for a flow's execution state */
Expand Down
4 changes: 4 additions & 0 deletions docs/storybook/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Preview } from "@storybook/react-webpack5";
import { PlayerDecorator } from "@player-ui/storybook";
import { ReferenceAssetsPlugin } from "@player-ui/reference-assets-plugin-react";
import { A2UIPlugin } from "@player-ui/a2ui-plugin-react";
import { CommonTypesPlugin } from "@player-ui/common-types-plugin";
import { DataChangeListenerPlugin } from "@player-ui/data-change-listener-plugin";
import { ComputedPropertiesPlugin } from "@player-ui/computed-properties-plugin";
Expand All @@ -9,9 +10,11 @@ import * as dslRefComponents from "@player-ui/reference-assets-plugin-components
// @ts-expect-error referencing the pre-built output
import RefXLR from "@player-ui/reference-assets-plugin/dist/xlr/manifest.js";
import "@player-ui/reference-assets-plugin-react/dist/index.css";
import "@player-ui/a2ui-plugin-react/dist/index.css";

const reactPlayerPlugins = [
new ReferenceAssetsPlugin(),
new A2UIPlugin(),
new CommonTypesPlugin(),
new DataChangeListenerPlugin(),
new ComputedPropertiesPlugin(),
Expand All @@ -31,6 +34,7 @@ export const parameters = {
"Welcome",
"React Player",
"Reference Assets",
"A2UI",
["Docs", "Overview", "Intro"],
],
},
Expand Down
2 changes: 2 additions & 0 deletions docs/storybook/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ npm_link_all_packages(
)

deps = [
":node_modules/@player-ui/a2ui-plugin",
":node_modules/@player-ui/a2ui-plugin-react",
":node_modules/@player-ui/common-types-plugin",
":node_modules/@player-ui/make-flow",
":node_modules/@player-ui/reference-assets-plugin",
Expand Down
2 changes: 2 additions & 0 deletions docs/storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"private": true,
"version": "0.0.0-PLACEHOLDER",
"dependencies": {
"@player-ui/a2ui-plugin": "workspace:^",
"@player-ui/a2ui-plugin-react": "workspace:^",
"@player-ui/common-types-plugin": "workspace:^",
"@player-ui/make-flow": "workspace:^",
"@player-ui/reference-assets-plugin": "workspace:^",
Expand Down
Loading
Loading