@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.
- package/LICENSE +21 -0
- package/README.md +88 -2
- package/package.json +47 -3
- package/skills/ads-and-raf/SKILL.md +231 -0
- package/skills/ads-and-raf/references/integration-flows.md +332 -0
- package/skills/app-packaging/SKILL.md +107 -0
- package/skills/app-performance/SKILL.md +133 -0
- package/skills/cli-dev-loop/SKILL.md +217 -0
- package/skills/cli-dev-loop/references/logs-and-crashes.md +114 -0
- package/skills/cpu-and-heap-profiling/SKILL.md +108 -0
- package/skills/cpu-and-heap-profiling/references/reading-profiles.md +52 -0
- package/skills/data-entry-keyboards/SKILL.md +160 -0
- package/skills/device-management/SKILL.md +158 -0
- package/skills/dialogs-and-modals/SKILL.md +210 -0
- package/skills/effects/SKILL.md +183 -0
- package/skills/fetch-and-data-loading/SKILL.md +164 -0
- package/skills/fetch-and-data-loading/references/https-and-certificates.md +79 -0
- package/skills/flex-grid/SKILL.md +236 -0
- package/skills/flex-grid/references/focus-integration.md +355 -0
- package/skills/flex-grid/references/plugins.md +201 -0
- package/skills/focus-management/SKILL.md +210 -0
- package/skills/focus-management/references/focus-recipes.md +290 -0
- package/skills/fonts-and-glyphs/SKILL.md +231 -0
- package/skills/fonts-and-glyphs/references/glyph-tables.md +128 -0
- package/skills/logging-and-errors/SKILL.md +227 -0
- package/skills/logging-and-errors/references/uncaught-exceptions.md +137 -0
- package/skills/mcp-ai-dev-loop/SKILL.md +189 -0
- package/skills/mcp-ai-dev-loop/references/crash-and-console.md +68 -0
- package/skills/mcp-ai-dev-loop/references/live-debugging.md +129 -0
- package/skills/mcp-setup/SKILL.md +131 -0
- package/skills/project-setup/SKILL.md +210 -0
- package/skills/project-setup/references/roku-config.md +120 -0
- package/skills/screen-navigation/SKILL.md +166 -0
- package/skills/sdk-components/SKILL.md +213 -0
- package/skills/sdk-components/references/dialogs.md +148 -0
- package/skills/sdk-components/references/scenegraph-node-spelling.md +123 -0
- package/skills/sdk-installation/SKILL.md +129 -0
- package/skills/slideshow/SKILL.md +157 -0
- package/skills/slideshow/references/focus-and-handoff.md +187 -0
- package/skills/solid-on-roku/SKILL.md +181 -0
- package/skills/solid-on-roku/references/hermes-and-exports.md +130 -0
- package/skills/testing/SKILL.md +147 -0
- package/skills/testing/references/async-and-timing.md +97 -0
- package/skills/user-accounts-and-purchases/SKILL.md +164 -0
- package/skills-manifest.json +316 -0
- package/skills-manifest.schema.json +62 -0
- package/src/__tests__/manifest.test.ts +267 -0
- package/src/__tests__/snippets.test.ts +221 -0
- package/src/__tests__/trigger-routing.test.ts +77 -0
- package/src/index.ts +36 -0
- package/src/manifest.ts +215 -0
- package/src/router.ts +147 -0
- 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.
|