@roku-sdk/ai 0.0.0-stage → 2.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -2
  3. package/package.json +47 -3
  4. package/skills/ads-and-raf/SKILL.md +231 -0
  5. package/skills/ads-and-raf/references/integration-flows.md +332 -0
  6. package/skills/app-packaging/SKILL.md +107 -0
  7. package/skills/app-performance/SKILL.md +133 -0
  8. package/skills/cli-dev-loop/SKILL.md +217 -0
  9. package/skills/cli-dev-loop/references/logs-and-crashes.md +114 -0
  10. package/skills/cpu-and-heap-profiling/SKILL.md +108 -0
  11. package/skills/cpu-and-heap-profiling/references/reading-profiles.md +52 -0
  12. package/skills/data-entry-keyboards/SKILL.md +160 -0
  13. package/skills/device-management/SKILL.md +158 -0
  14. package/skills/dialogs-and-modals/SKILL.md +210 -0
  15. package/skills/effects/SKILL.md +183 -0
  16. package/skills/fetch-and-data-loading/SKILL.md +164 -0
  17. package/skills/fetch-and-data-loading/references/https-and-certificates.md +79 -0
  18. package/skills/flex-grid/SKILL.md +236 -0
  19. package/skills/flex-grid/references/focus-integration.md +355 -0
  20. package/skills/flex-grid/references/plugins.md +201 -0
  21. package/skills/focus-management/SKILL.md +210 -0
  22. package/skills/focus-management/references/focus-recipes.md +290 -0
  23. package/skills/fonts-and-glyphs/SKILL.md +231 -0
  24. package/skills/fonts-and-glyphs/references/glyph-tables.md +128 -0
  25. package/skills/logging-and-errors/SKILL.md +227 -0
  26. package/skills/logging-and-errors/references/uncaught-exceptions.md +137 -0
  27. package/skills/mcp-ai-dev-loop/SKILL.md +189 -0
  28. package/skills/mcp-ai-dev-loop/references/crash-and-console.md +68 -0
  29. package/skills/mcp-ai-dev-loop/references/live-debugging.md +129 -0
  30. package/skills/mcp-setup/SKILL.md +131 -0
  31. package/skills/project-setup/SKILL.md +210 -0
  32. package/skills/project-setup/references/roku-config.md +120 -0
  33. package/skills/screen-navigation/SKILL.md +166 -0
  34. package/skills/sdk-components/SKILL.md +213 -0
  35. package/skills/sdk-components/references/dialogs.md +148 -0
  36. package/skills/sdk-components/references/scenegraph-node-spelling.md +123 -0
  37. package/skills/sdk-installation/SKILL.md +129 -0
  38. package/skills/slideshow/SKILL.md +157 -0
  39. package/skills/slideshow/references/focus-and-handoff.md +187 -0
  40. package/skills/solid-on-roku/SKILL.md +181 -0
  41. package/skills/solid-on-roku/references/hermes-and-exports.md +130 -0
  42. package/skills/testing/SKILL.md +147 -0
  43. package/skills/testing/references/async-and-timing.md +97 -0
  44. package/skills/user-accounts-and-purchases/SKILL.md +164 -0
  45. package/skills-manifest.json +316 -0
  46. package/skills-manifest.schema.json +62 -0
  47. package/src/__tests__/manifest.test.ts +267 -0
  48. package/src/__tests__/snippets.test.ts +221 -0
  49. package/src/__tests__/trigger-routing.test.ts +77 -0
  50. package/src/index.ts +36 -0
  51. package/src/manifest.ts +215 -0
  52. package/src/router.ts +147 -0
  53. package/src/snippets.ts +212 -0
@@ -0,0 +1,147 @@
1
+ # Testing Components
2
+
3
+ ## Overview
4
+
5
+ `@roku-sdk/testing-library` renders a component through the same Solid renderer the app
6
+ uses, into an in-process tree of plain objects, and hands back queries over that tree. A
7
+ test therefore asserts on tags and prop values, never on pixels. `rk create` already
8
+ scaffolds `jest.config.mjs` and a passing example test, so writing a test is adding a
9
+ file — there is no harness to build first.
10
+
11
+ Children render for real. The renderer, the reactive graph and every nested component are
12
+ the app's own; only the platform edge beneath them is substituted. These are component
13
+ tests rather than isolated unit tests, which decides two things: do not mock a child to
14
+ get at its parent, and expect a broken child to fail its parent's test.
15
+
16
+ ## When to Use
17
+
18
+ - writing a test for a component, or reading one that is already there
19
+ - asserting that a node rendered, or that one of its props holds a particular value
20
+ - driving a D-pad press or any other key event through a component
21
+ - snapshotting the rendered tree, and updating that snapshot on purpose
22
+ - a test whose assertion reads a value the app gets right, because it ran before the
23
+ update it is asserting on
24
+
25
+ That last one has one cause and two fixes, both in `references/async-and-timing.md`. Read
26
+ it whenever a value is stale, whenever the component under test awaits anything, or before
27
+ reaching for a timer.
28
+
29
+ ### When not to use
30
+
31
+ | Situation | Use instead | Because |
32
+ |---|---|---|
33
+ | Proving behaviour on real hardware | The `mcp-ai-dev-loop` skill, or a `device-tests/` suite | Nothing here touches a Roku. The tree is a JavaScript object, so a passing test says what the component built, not what the screen showed |
34
+ | A React or web component test | That library's own documentation | Those render into a DOM and query it by text and role; these queries run over SceneGraph tags and props, and none of them transfer |
35
+ | A legacy BrightScript channel | Roku's own BrightScript unit-testing documentation | A different runtime and a different toolchain |
36
+ | Adding Jest itself, or a dependency the app is missing | The `project-setup` skill | That is project configuration; this skill starts once `pnpm test` runs |
37
+
38
+ ## Quick Reference
39
+
40
+ | Task | Do this |
41
+ |---|---|
42
+ | Render | `const view = await render(() => <TestApp />)` — `render` is async, and an un-awaited one hands back a promise with no queries on it |
43
+ | Get something renderable | `const TestApp = TestAnchorNode(App)` at module scope, where `App` is the tuple `defineAnchorNode` returned |
44
+ | One node by tag | `getByTag("label")` — tags are the lowercase SceneGraph names: `label`, `rectangle`, `group`, `poster`, `key-handler` |
45
+ | Every node by tag | `getAllByTag("label")` |
46
+ | One node by prop value | `getByProp("text", "Play")`, or `getByProp("id", "titleLabel")` |
47
+ | Tolerate absence | `queryByTag`, `queryAllByTag`, `queryByProp` — `null` or `[]` where the `get` forms throw. There is no `getAllByProp` |
48
+ | Read a found node | `props(node)`, `children(node)`, `tag(node)` |
49
+ | Press a key | `await fireEvent(getByTag("key-handler"), "onKeyEvent", { key: "right", press: true })` |
50
+ | Snapshot the tree | `expect(toString(2)).toMatchSnapshot()` |
51
+ | Let a pending update land | `await view.transaction()` |
52
+ | End the test | `dispose()` — synchronous, so there is nothing to await |
53
+ | Configure Jest | one line in `jest.config.mjs`: `export { default } from "@roku-sdk/dev-tools/jest-config/jest.config.mjs";` |
54
+
55
+ ## Instructions
56
+
57
+ 1. **Wrap the component once, at module scope.** `TestAnchorNode(App)` takes the tuple
58
+ `defineAnchorNode` returned — the component's default export — and returns something
59
+ `render` can mount. Building it inside each test rebuilds the component definition per
60
+ test for no benefit.
61
+
62
+ 2. **`await render(...)`.** It returns the queries, `fireEvent`, `transaction`, `toString`
63
+ and `dispose` in one object; destructure the ones the test uses. The await is not
64
+ ceremony: the first commit of the tree is queued and flushed inside `render`, so without
65
+ it there is nothing to query.
66
+
67
+ 3. **Let the `get` forms be the assertion.** `getByTag` and `getByProp` throw when nothing
68
+ matches, naming the tag or the prop they looked for, so `getByProp("text", "Play")` on
69
+ its own already fails the test if that label is absent. Wrapping it in an `expect` adds
70
+ a line and no information. Reach for `queryByProp` only when absence is the thing being
71
+ asserted, and then assert on the `null` it returns.
72
+
73
+ 4. **Drive input through the key handler node.** Focus and key routing in this SDK end at a
74
+ `key-handler` node, so `getByTag("key-handler")` is the entry point for a press, and the
75
+ payload is `{ key, press }` — `press: true` is the key going down, `false` its release.
76
+ Key names are Roku's: `up`, `down`, `left`, `right`, `OK`, `back`. `fireEvent` is async
77
+ and awaits the resulting commit, so `await` it and then assert directly.
78
+
79
+ 5. **Snapshot with `toString(2)`**, which serializes the tree with an indent of two and
80
+ omits internal node references. It is the whole rendered structure, so it is the right
81
+ tool for "this component still builds what it built" and the wrong one for a single
82
+ value — a snapshot that changes on every edit stops being read.
83
+
84
+ 6. **`dispose()` at the end of every test.** It tears the tree down and releases the
85
+ renderer. Tests that skip it leak state into the next one, and the failure shows up in
86
+ whichever test runs after, not in the one that caused it. It is Solid's disposer and
87
+ returns nothing, so awaiting it is a no-op that reads as though it were async.
88
+
89
+ ## Example
90
+
91
+ ```tsx rsg-skill=complete
92
+ /// <reference types="jest" />
93
+
94
+ import { Label, LayoutGroup } from "@roku-sdk/components";
95
+ import type { AnchorNodeDescriptor, AnchorNodeImplementation } from "@roku-sdk/rsg-ts";
96
+ import { defineAnchorNode } from "@roku-sdk/rsg-ts";
97
+ import { render, TestAnchorNode } from "@roku-sdk/testing-library";
98
+
99
+ const GreetingDescriptor: AnchorNodeDescriptor = {
100
+ componentName: "Greeting",
101
+ extends: "Group" as const,
102
+ inputFields: {},
103
+ outputFields: {}
104
+ };
105
+
106
+ const GreetingImpl: AnchorNodeImplementation<typeof GreetingDescriptor> = () => (
107
+ <layoutGroup layoutDirection="vert">
108
+ <label id="titleLabel" text="Hello" width={1920} height={100} />
109
+ </layoutGroup>
110
+ );
111
+
112
+ const Greeting = defineAnchorNode(GreetingDescriptor, GreetingImpl);
113
+ const TestGreeting = TestAnchorNode(Greeting);
114
+
115
+ describe("Greeting", () => {
116
+ test("renders the title", async () => {
117
+ const { getByProp, toString, dispose } = await render(() => <TestGreeting />);
118
+
119
+ // Throws when absent, so this line is the assertion.
120
+ getByProp("text", "Hello");
121
+ getByProp("id", "titleLabel");
122
+ expect(toString(2)).toMatchSnapshot();
123
+
124
+ dispose();
125
+ });
126
+
127
+ test("renders no subtitle", async () => {
128
+ const { queryByProp, dispose } = await render(() => <TestGreeting />);
129
+
130
+ // The tolerant form, because absence is what is being asserted.
131
+ expect(queryByProp("id", "subtitleLabel")).toBeNull();
132
+
133
+ dispose();
134
+ });
135
+ });
136
+ ```
137
+
138
+ ## Common Mistakes
139
+
140
+ | Symptom | Cause | Fix |
141
+ |---|---|---|
142
+ | `getByProp is not a function`, or every query is `undefined` | `render` was called without `await` | `const view = await render(...)` |
143
+ | `No node found with tag name: ...` on a node that is plainly in the JSX | The node's tag is the SceneGraph name, not the component's | Snapshot the tree with `toString(2)` once and read the tags off it |
144
+ | `getAllByProp is not a function` | It does not exist — the `All` forms are tag-only | Use `getAllByTag`, or `queryAllByTag` and filter on `props(node)` |
145
+ | An assertion reads the value from before the event | The commit had not been applied yet | See `references/async-and-timing.md` |
146
+ | A test passes alone and fails in the suite | A previous test never disposed | `dispose()` at the end of every test |
147
+ | The snapshot file churns on unrelated edits | A whole-tree snapshot is standing in for a value assertion | Snapshot once for structure; assert values with `getByProp` |
@@ -0,0 +1,97 @@
1
+ # Async and timing in a component test
2
+
3
+ The symptom is always the same: an assertion reads a prop, gets the value from before the
4
+ thing that was supposed to change it, and the app is demonstrably correct when run. The
5
+ cause is that renders are committed in a batch. A prop write does not land on the node the
6
+ moment the signal changes; it is queued, and the queue is flushed on a commit. Between
7
+ those two points the tree still holds the old value, and a query run there reads it.
8
+
9
+ `render` and `fireEvent` both await that flush for you, which is why most tests never meet
10
+ this. It surfaces where the change is triggered by something else.
11
+
12
+ ## The two fixes, in order of preference
13
+
14
+ **1. Await the call that caused the change.** Both entry points are already async and
15
+ already flush:
16
+
17
+ ```tsx
18
+ import { render, TestAnchorNode } from "@roku-sdk/testing-library";
19
+
20
+ import App from "./app";
21
+
22
+ const TestApp = TestAnchorNode(App);
23
+
24
+ const view = await render(() => <TestApp />);
25
+ await view.fireEvent(view.getByTag("key-handler"), "onKeyEvent", { key: "right", press: true });
26
+ // The tree is committed here. Assert directly.
27
+ view.getByProp("text", "Focused: 1");
28
+ ```
29
+
30
+ An un-awaited `fireEvent` is the single most common source of a stale read, and it is
31
+ silent: the call still fires, the assertion still runs, and it simply runs early.
32
+
33
+ **2. Flush explicitly with `transaction()`** when the change came from somewhere the test
34
+ did not await — a signal the test set directly, a callback the component invoked, a promise
35
+ the component under test kicked off:
36
+
37
+ ```tsx
38
+ // Same `view` as above.
39
+ setItems(["a", "b"]);
40
+ await view.transaction();
41
+ view.getByProp("text", "2 items");
42
+ ```
43
+
44
+ `transaction()` resolves when the queued commit has been applied. That is all it does. It
45
+ does not wait for a network call, a timer, or any promise the component is holding, so it
46
+ is the fix for a batching problem and never for a slow one.
47
+
48
+ ## There is no `waitFor` and no `findBy*`
49
+
50
+ This library ships neither, deliberately: both exist to paper over an unknown settling time,
51
+ and in a tree with an explicit commit there is not one to guess at. If an assertion needs a
52
+ retry loop, something in the component is genuinely asynchronous, and the test should await
53
+ that thing rather than poll the tree. Expose it — resolve a promise the test holds, or call
54
+ a callback the test passed in — and await that, then `transaction()`.
55
+
56
+ Where a component's own promise cannot be reached, yielding to the microtask queue before
57
+ flushing is the honest last resort, and it belongs in the one test that needs it rather
58
+ than in shared setup:
59
+
60
+ ```tsx
61
+ await Promise.resolve();
62
+ await view.transaction();
63
+ ```
64
+
65
+ ## Fake timers do not flush a commit
66
+
67
+ The commit is scheduled as a microtask, not on a timer, so `jest.advanceTimersByTime` does
68
+ not make a queued prop write land. A test with fake timers on still awaits the flush the
69
+ same way every other test does:
70
+
71
+ ```tsx
72
+ import { render, TestAnchorNode } from "@roku-sdk/testing-library";
73
+
74
+ import App from "./app";
75
+
76
+ const TestApp = TestAnchorNode(App);
77
+
78
+ jest.useFakeTimers();
79
+
80
+ const view = await render(() => <TestApp />);
81
+ jest.advanceTimersByTime(5000);
82
+ // The timer's callback ran; its effect on the tree has not landed yet.
83
+ await view.transaction();
84
+ view.getByProp("text", "Timed out");
85
+ ```
86
+
87
+ The shared Jest setup in `@roku-sdk/dev-tools` leaves real timers in place and restores them
88
+ after each test that turns them on, so a test opts in and does not have to clean up by hand.
89
+ Opt in only when the component reads the clock. For a value that is merely late, fake timers
90
+ are the wrong tool: the commit is not waiting on a timer, so advancing one changes nothing.
91
+
92
+ ## Disposal is part of timing
93
+
94
+ `dispose()` tears the tree down. It is Solid's disposer and returns nothing, so there is
95
+ nothing to await. Skipping it leaves a live tree behind, and the reactive work it is still
96
+ doing lands during whichever test runs next — which is why "passes alone, fails in the
97
+ suite" is nearly always a missing `dispose`, and why the failure names the wrong test.
@@ -0,0 +1,164 @@
1
+ # Viewer accounts and in-channel purchases
2
+
3
+ ## Overview
4
+
5
+ `ChannelStore` from `@roku-sdk/components` is the one component that reaches Roku's account
6
+ and commerce services. It covers three jobs: reading who the viewer is, remembering them
7
+ across their devices, and selling them something.
8
+
9
+ It is **not called like a function**. You render one, put the input on a data prop, register
10
+ the handler the answer will arrive on, and then set `command`. The result comes back
11
+ asynchronously on that handler, never as a return value. Everything below follows from that.
12
+
13
+ ## When to Use
14
+
15
+ | If the request is | Then |
16
+ |---|---|
17
+ | Reading the viewer's Roku account details to pre-fill a form | Here |
18
+ | Remembering a signed-in viewer so their next device does not ask again | Here |
19
+ | Proving to your own backend that a request came from a real Roku | Here |
20
+ | Listing, selling or confirming something inside the channel | Here |
21
+ | An auth gate in front of content, driven by whether a stored credential came back | Here |
22
+
23
+ ### When not to use
24
+
25
+ | Situation | Use instead | Because |
26
+ |---|---|---|
27
+ | Signing a build, uploading a version, or the beta channel | The `app-packaging` skill | Those are about shipping the app. This is about the viewer using it. The word "channel" appears on both sides and means different things |
28
+ | Installing SDK packages with `pnpm` | The `sdk-installation` skill | That brings dependencies into the app; it does not authenticate a viewer |
29
+ | A Roku's developer password, or which box a command reaches | The `device-management` skill | Also a developer credential. Nothing here reads or writes it |
30
+ | Calling your own partner API over the network | The `fetch-and-data-loading` skill | Use that for your backend, and this only for what Roku itself holds. The attestation token below is the bridge between them |
31
+ | How to spell the component, or why it keeps its backing asset | The `sdk-components` skill | That skill owns the spelling table and the tree-shaking rule |
32
+
33
+ **Viewer credentials and developer credentials are different things** and three skills own the
34
+ second kind. Nothing in this skill touches a signing password, a registry token or a box's
35
+ developer password.
36
+
37
+ ## Quick Reference
38
+
39
+ Eleven commands, plus `"none"`. Each result arrives on its own handler prop.
40
+
41
+ | Job | Command | Result arrives on |
42
+ |---|---|---|
43
+ | Who is this viewer | `getUserData` | `onUserDataObject` |
44
+ | Where are they | `getUserRegionData` | `onUserRegionDataObject` |
45
+ | Remember them | `storeChannelCredData` | `onStoreChannelCredDataStatusObject` |
46
+ | Are they already known | `getChannelCred` | `onChannelCredObject` |
47
+ | What can I sell | `getCatalog` | `onCatalogObject` |
48
+ | Sell it | `doOrder` | `onOrderStatusObject` |
49
+ | What do they own | `getPurchases` / `getAllPurchases` | `onPurchasesObject` — **and the status separately on `onPurchasesStatusObject`** |
50
+ | Partner-billed order | `requestPartnerOrder` / `confirmPartnerOrder` | `onRequestPartnerOrderStatusObject` / `onConfirmPartnerOrderStatusObject` |
51
+ | Prove the box is genuine | `getDeviceAttestationToken` | `onDeviceAttestationTokenObject` |
52
+ | Idle | `"none"` | — |
53
+
54
+ Input props that pair with them: `requestedUserData` (`"all"`, or a comma-separated list such
55
+ as `"email,firstname,lastname"`), `channelCredData`, `orderObject`, `deltaOrder`,
56
+ `requestPartnerOrderObject`, `confirmPartnerOrderObject`.
57
+
58
+ ## Instructions
59
+
60
+ 1. **Render one `ChannelStore` and drive `command` from a signal.** Start it at `"none"`. One
61
+ instance serves every command; there is no need for one per operation.
62
+
63
+ 2. **Set the data prop before you set the command.** The component reads its inputs when the
64
+ command changes, so a command set in the same breath as its data may act on the previous
65
+ value. Set the data signal, then the command signal.
66
+
67
+ 3. **Return `command` to `"none"` once a result has arrived.** It is the idle value, and it is
68
+ how the next request of the same kind is distinguishable from the last one. Left on the
69
+ previous command, a re-request can look like nothing happened.
70
+
71
+ 4. **Read the answer only on the matching handler.** Every result is
72
+ `(event: T) => void` — a plain callback, not a promise — so the code that reacts to a
73
+ command lives beside the render rather than after the call.
74
+
75
+ 5. **Ask for the narrowest `requestedUserData` you need.** `"all"` returns the whole profile.
76
+ A comma-separated list returns only those fields, which is both less to handle and less
77
+ to justify.
78
+
79
+ 6. **Adjust a basket with `deltaOrder`, not by rebuilding `orderObject`.** It applies one
80
+ change on its own, and a **negative quantity removes** rather than adding a negative line.
81
+
82
+ 7. **Read the result's `status`, and expect the credential as a string.** Every result
83
+ carries `status`, where **0 means success** — an arrived handler is not the same as a
84
+ successful command. Purchases are the one command that splits the two apart: the data
85
+ arrives on `onPurchasesObject` and the status on `onPurchasesStatusObject`, so a failed
86
+ request shows up as a status rather than as an empty list. And `getChannelCred` returns the credential as a **JSON string** in
87
+ `json`, not as structured fields, so it has to be parsed.
88
+
89
+ 8. **Treat `getChannelCred` as the auth gate.** On launch, ask; if a credential comes back,
90
+ the viewer is known and no sign-in screen is needed. If nothing does, show one, and store
91
+ the result with `storeChannelCredData` so the next device skips it.
92
+
93
+ 9. **Send the attestation token to your backend, not to your own logic.** It is a signed JWT
94
+ whose point is that a server can verify it. Checking it inside the app proves nothing,
95
+ because anything that can fake the request can fake the check.
96
+
97
+ ## Example
98
+
99
+ The auth gate: ask on launch, and store what a sign-in produced.
100
+
101
+ ```tsx rsg-skill=complete
102
+ import { ChannelStore, ChannelStoreCommand, IChannelCred, IStoreChannelCredDataStatus } from "@roku-sdk/components";
103
+ import { Component, createSignal, onMount } from "@roku-sdk/solid";
104
+
105
+ export const AuthGate: Component = () => {
106
+ const [command, setCommand] = createSignal<ChannelStoreCommand>("none");
107
+ const [credData, setCredData] = createSignal("");
108
+ const [known, setKnown] = createSignal<boolean | undefined>(undefined);
109
+
110
+ onMount(() => setCommand("getChannelCred"));
111
+
112
+ const onCred = (event: IChannelCred) => {
113
+ // `status` 0 is success. The credential itself is a JSON *string* in `json`.
114
+ setKnown(event.status === 0);
115
+ setCommand("none");
116
+ };
117
+
118
+ const onStored = (event: IStoreChannelCredDataStatus) => {
119
+ setKnown(event.status === 0);
120
+ setCommand("none");
121
+ };
122
+
123
+ return (
124
+ <group>
125
+ <ChannelStore
126
+ command={command()}
127
+ channelCredData={credData()}
128
+ onChannelCredObject={onCred}
129
+ onStoreChannelCredDataStatusObject={onStored}
130
+ />
131
+ <label
132
+ text={known() === undefined ? "checking" : known() ? "welcome back" : "please sign in"}
133
+ translation={[96, 96]}
134
+ />
135
+ <label text={credData() === "" ? "" : "credential pending"} translation={[96, 160]} />
136
+ </group>
137
+ );
138
+ };
139
+
140
+ /** Wire this to whatever your sign-in screen produces. */
141
+ export function rememberViewer(setCredData: (v: string) => void, setCommand: (c: ChannelStoreCommand) => void) {
142
+ return (token: string) => {
143
+ setCredData(token); // data first
144
+ setCommand("storeChannelCredData"); // then the command
145
+ };
146
+ }
147
+ ```
148
+
149
+ ## Common Mistakes
150
+
151
+ | Symptom | Cause | Fix |
152
+ |---|---|---|
153
+ | A command acts on the previous value | Data and command were set together, and the component read its inputs on the change | Set the data prop first, then the command |
154
+ | Re-issuing the same command appears to do nothing | `command` never returned to `"none"`, so there was no change to react to | Reset it to `"none"` in the handler |
155
+ | The handler never fires | The result was expected as a return value, or the wrong handler prop was wired | Each command has exactly one handler prop — match them from the table above |
156
+ | A basket ends up with a negative line | A removal was expressed by adding a negative entry to `orderObject` | A negative quantity on `deltaOrder` is the removal |
157
+ | The whole profile arrives when two fields were wanted | `requestedUserData` was left at `"all"` | Pass the comma-separated list instead |
158
+ | An attestation check passes for a forged request | The token was verified inside the app | Verify it on a server. In-app verification is not a check |
159
+ | A viewer is asked to sign in on every device | The credential was kept in app state rather than stored | `storeChannelCredData` puts it where the viewer's other boxes can read it |
160
+
161
+ `ChannelStore` is backed by a BrightScript XML extension rather than a native SceneGraph node,
162
+ which is why it behaves like a service rather than a view — it draws nothing. Every prop and
163
+ result type is documented with TSDoc in `@roku-sdk/components`; read a field's exact shape
164
+ there rather than inferring it from here.