@player-ui/storybook 0.0.1-next.2
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/dist/index.cjs.js +659 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.esm.js +643 -0
- package/package.json +37 -0
- package/register.js +1 -0
- package/src/addons/appetize/index.tsx +72 -0
- package/src/addons/constants.ts +10 -0
- package/src/addons/editor/index.tsx +85 -0
- package/src/addons/events/events.css +7 -0
- package/src/addons/events/index.tsx +100 -0
- package/src/addons/index.tsx +46 -0
- package/src/addons/refresh/index.tsx +28 -0
- package/src/decorator/index.tsx +46 -0
- package/src/index.ts +3 -0
- package/src/player/Appetize.tsx +121 -0
- package/src/player/PlayerFlowSummary.tsx +36 -0
- package/src/player/PlayerStory.tsx +198 -0
- package/src/player/hooks.ts +29 -0
- package/src/player/index.ts +1 -0
- package/src/player/storybookWebPlayerPlugin.ts +113 -0
- package/src/state/events.ts +62 -0
- package/src/state/hooks.ts +152 -0
- package/src/state/index.ts +2 -0
- package/src/types.ts +41 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { WebPlayerPlugin, PlayerFlowStatus, Flow } from '@player-ui/react';
|
|
3
|
+
import { WebPlayer } from '@player-ui/react';
|
|
4
|
+
import { ChakraProvider, Spinner } from '@chakra-ui/react';
|
|
5
|
+
import { makeFlow } from '@player-ui/make-flow';
|
|
6
|
+
import addons from '@storybook/addons';
|
|
7
|
+
import type { AsyncImportFactory, RenderTarget } from '../types';
|
|
8
|
+
import { useEditorFlow } from './hooks';
|
|
9
|
+
import { Appetize } from './Appetize';
|
|
10
|
+
import { StorybookPlayerPlugin } from './storybookWebPlayerPlugin';
|
|
11
|
+
import { useStateActions, subscribe } from '../state/hooks';
|
|
12
|
+
import { PlayerFlowSummary } from './PlayerFlowSummary';
|
|
13
|
+
|
|
14
|
+
interface LocalPlayerStory {
|
|
15
|
+
/** the mock to load */
|
|
16
|
+
flow: Flow;
|
|
17
|
+
|
|
18
|
+
/** plugins to the player */
|
|
19
|
+
webPlugins?: Array<WebPlayerPlugin>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const WebPlayerPluginContext = React.createContext<{
|
|
23
|
+
/** Plugins to use for the player */
|
|
24
|
+
plugins?: Array<WebPlayerPlugin>;
|
|
25
|
+
}>({ plugins: [] });
|
|
26
|
+
|
|
27
|
+
export const PlayerRenderContext = React.createContext<RenderTarget>({
|
|
28
|
+
platform: 'web',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const StorybookControlsContext = React.createContext<{
|
|
32
|
+
/** any storybook controls to include */
|
|
33
|
+
controls?: Flow['data'];
|
|
34
|
+
}>({
|
|
35
|
+
controls: {},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/** A component to render a player + flow */
|
|
39
|
+
const LocalPlayerStory = (props: LocalPlayerStory) => {
|
|
40
|
+
let flow = useEditorFlow(props.flow);
|
|
41
|
+
|
|
42
|
+
const renderContext = React.useContext(PlayerRenderContext);
|
|
43
|
+
const pluginContext = React.useContext(WebPlayerPluginContext);
|
|
44
|
+
const controlsContext = React.useContext(StorybookControlsContext);
|
|
45
|
+
const stateActions = useStateActions(addons.getChannel());
|
|
46
|
+
const plugins = props.webPlugins ?? pluginContext?.plugins ?? [];
|
|
47
|
+
const [playerState, setPlayerState] =
|
|
48
|
+
React.useState<PlayerFlowStatus>('not-started');
|
|
49
|
+
|
|
50
|
+
const wp = React.useMemo(() => {
|
|
51
|
+
return new WebPlayer({
|
|
52
|
+
plugins: [new StorybookPlayerPlugin(stateActions), ...plugins],
|
|
53
|
+
});
|
|
54
|
+
}, [plugins]);
|
|
55
|
+
|
|
56
|
+
/** A callback to start the flow */
|
|
57
|
+
const startFlow = () => {
|
|
58
|
+
setPlayerState('in-progress');
|
|
59
|
+
wp.start(flow)
|
|
60
|
+
.then(() => {
|
|
61
|
+
setPlayerState('completed');
|
|
62
|
+
})
|
|
63
|
+
.catch((e) => {
|
|
64
|
+
console.error('Error starting flow', e);
|
|
65
|
+
setPlayerState('error');
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
React.useEffect(() => {
|
|
70
|
+
startFlow();
|
|
71
|
+
}, [wp, flow]);
|
|
72
|
+
|
|
73
|
+
React.useEffect(() => {
|
|
74
|
+
// merge new data from storybook controls
|
|
75
|
+
if (controlsContext) {
|
|
76
|
+
flow = {
|
|
77
|
+
...flow,
|
|
78
|
+
data: {
|
|
79
|
+
...(flow.data ?? {}),
|
|
80
|
+
...controlsContext,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
stateActions.setFlow(flow);
|
|
84
|
+
}
|
|
85
|
+
}, [controlsContext]);
|
|
86
|
+
|
|
87
|
+
React.useEffect(() => {
|
|
88
|
+
return subscribe(addons.getChannel(), '@@player/flow/reset', () => {
|
|
89
|
+
startFlow();
|
|
90
|
+
});
|
|
91
|
+
}, [wp, flow]);
|
|
92
|
+
|
|
93
|
+
if (renderContext.platform !== 'web' && renderContext.token) {
|
|
94
|
+
return (
|
|
95
|
+
<Appetize
|
|
96
|
+
flow={flow}
|
|
97
|
+
platform={renderContext.platform}
|
|
98
|
+
token={renderContext.token}
|
|
99
|
+
baseUrl={renderContext.baseUrl}
|
|
100
|
+
osVersions={renderContext.appetizeVersions}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const currentState = wp.player.getState();
|
|
106
|
+
|
|
107
|
+
if (playerState === 'completed' && currentState.status === 'completed') {
|
|
108
|
+
return (
|
|
109
|
+
<PlayerFlowSummary
|
|
110
|
+
reset={startFlow}
|
|
111
|
+
outcome={currentState.endState.outcome}
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (playerState === 'error' && currentState.status === 'error') {
|
|
117
|
+
return <PlayerFlowSummary reset={startFlow} error={currentState.error} />;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return <wp.Component />;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
type Mock = Record<string, unknown>;
|
|
124
|
+
type MockFactory = () => Mock;
|
|
125
|
+
type MockFactoryOrPromise = MockFactory | Mock;
|
|
126
|
+
|
|
127
|
+
/** Wrap the component in a lazy type to trigger suspense */
|
|
128
|
+
function wrapInLazy(
|
|
129
|
+
/** The component to load */
|
|
130
|
+
Component: React.ComponentType<{
|
|
131
|
+
/** the flow */
|
|
132
|
+
flow: Flow;
|
|
133
|
+
}>,
|
|
134
|
+
|
|
135
|
+
/** A mock or a promise that resolve to a mock */
|
|
136
|
+
flowFactory: AsyncImportFactory<MockFactoryOrPromise> | MockFactoryOrPromise,
|
|
137
|
+
|
|
138
|
+
/** Any other props to pass */
|
|
139
|
+
other?: any
|
|
140
|
+
) {
|
|
141
|
+
/** an async loader to wrap the mock as a player component */
|
|
142
|
+
const asPlayer = async () => {
|
|
143
|
+
const mock =
|
|
144
|
+
typeof flowFactory === 'function' ? await flowFactory() : flowFactory;
|
|
145
|
+
|
|
146
|
+
/** The component to load */
|
|
147
|
+
const Comp = () => {
|
|
148
|
+
const flow = {
|
|
149
|
+
...makeFlow('default' in mock ? mock.default : mock),
|
|
150
|
+
...(other ?? {}),
|
|
151
|
+
};
|
|
152
|
+
return <Component flow={flow} />;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
default: Comp,
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return React.lazy(asPlayer);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface PlayerStoryProps {
|
|
164
|
+
/** The mock to load */
|
|
165
|
+
flow: AsyncImportFactory<MockFactoryOrPromise> | MockFactoryOrPromise;
|
|
166
|
+
/** Custom data to use in the flow */
|
|
167
|
+
data?: Flow['data'];
|
|
168
|
+
/** props from storybook controls */
|
|
169
|
+
storybookControls?: Flow['data'];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Takes an initial flow and renders it as a story in storybook.
|
|
174
|
+
* This handles all of the wiring of the mock into the flow editor, events, etc
|
|
175
|
+
*/
|
|
176
|
+
export const PlayerStory = (props: PlayerStoryProps) => {
|
|
177
|
+
const { flow, storybookControls, ...other } = props;
|
|
178
|
+
const MockComp = React.useMemo(
|
|
179
|
+
() => wrapInLazy(LocalPlayerStory, flow, other),
|
|
180
|
+
[]
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<div>
|
|
185
|
+
<ChakraProvider>
|
|
186
|
+
<React.Suspense fallback={<Spinner />}>
|
|
187
|
+
<StorybookControlsContext.Provider
|
|
188
|
+
value={{
|
|
189
|
+
...storybookControls,
|
|
190
|
+
}}
|
|
191
|
+
>
|
|
192
|
+
<MockComp />
|
|
193
|
+
</StorybookControlsContext.Provider>
|
|
194
|
+
</React.Suspense>
|
|
195
|
+
</ChakraProvider>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Flow } from '@player-ui/player';
|
|
3
|
+
import addons from '@storybook/addons';
|
|
4
|
+
import { DocsContext } from '@storybook/addon-docs';
|
|
5
|
+
import { useStateActions, useFlowState } from '../state/hooks';
|
|
6
|
+
|
|
7
|
+
/** Use the flow from the editor or the original one */
|
|
8
|
+
export function useEditorFlow(initialFlow: Flow) {
|
|
9
|
+
const stateActions = useStateActions(addons.getChannel());
|
|
10
|
+
const flow = useFlowState(addons.getChannel());
|
|
11
|
+
|
|
12
|
+
const docsContext = React.useContext(DocsContext);
|
|
13
|
+
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
stateActions.setFlow(initialFlow);
|
|
16
|
+
}, [initialFlow]);
|
|
17
|
+
|
|
18
|
+
React.useEffect(() => {
|
|
19
|
+
if (!flow) {
|
|
20
|
+
stateActions.setFlow(initialFlow);
|
|
21
|
+
}
|
|
22
|
+
}, [flow]);
|
|
23
|
+
|
|
24
|
+
if (docsContext.id) {
|
|
25
|
+
return initialFlow;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return flow ?? initialFlow;
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './PlayerStory';
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { WebPlayer, Player, WebPlayerPlugin } from '@player-ui/react';
|
|
2
|
+
import type { Timing } from '@player-ui/metrics-plugin-react';
|
|
3
|
+
import { MetricsPlugin } from '@player-ui/metrics-plugin-react';
|
|
4
|
+
import type {
|
|
5
|
+
StateActions,
|
|
6
|
+
DataChangeEventType,
|
|
7
|
+
LogEventType,
|
|
8
|
+
StateChangeEventType,
|
|
9
|
+
MetricChangeEventType,
|
|
10
|
+
} from '../state';
|
|
11
|
+
import { createEvent } from '../state';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
*
|
|
15
|
+
* A web plugin for interacting with storybook
|
|
16
|
+
*/
|
|
17
|
+
export class StorybookPlayerPlugin implements WebPlayerPlugin {
|
|
18
|
+
public readonly name = 'Storybook';
|
|
19
|
+
|
|
20
|
+
private actions: StateActions;
|
|
21
|
+
private metricsPlugin: MetricsPlugin;
|
|
22
|
+
|
|
23
|
+
constructor(actions: StateActions) {
|
|
24
|
+
this.actions = actions;
|
|
25
|
+
this.metricsPlugin = new MetricsPlugin({
|
|
26
|
+
onUpdate: (metrics) => {
|
|
27
|
+
actions.setMetrics(metrics);
|
|
28
|
+
},
|
|
29
|
+
onRenderEnd: (timing) => {
|
|
30
|
+
this.onMetricChange(timing, 'render');
|
|
31
|
+
},
|
|
32
|
+
onUpdateEnd: (timing) => {
|
|
33
|
+
this.onMetricChange(timing, 'update');
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
apply(player: Player) {
|
|
39
|
+
player.registerPlugin(this.metricsPlugin);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
applyWeb(wp: WebPlayer) {
|
|
43
|
+
wp.registerPlugin(this.metricsPlugin);
|
|
44
|
+
|
|
45
|
+
wp.player.hooks.dataController.tap(this.name, (dc) => {
|
|
46
|
+
this.actions.clearEvents();
|
|
47
|
+
|
|
48
|
+
dc.hooks.onUpdate.tap(this.name, (dataUpdates) => {
|
|
49
|
+
const events: Array<DataChangeEventType> = dataUpdates.map(
|
|
50
|
+
(dataUpdate) =>
|
|
51
|
+
createEvent({
|
|
52
|
+
type: 'dataChange',
|
|
53
|
+
binding: dataUpdate.binding.asString(),
|
|
54
|
+
from: dataUpdate.oldValue,
|
|
55
|
+
to: dataUpdate.newValue,
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
this.actions.addEvents(events);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
wp.player.logger.hooks.log.tap(this.name, (severity, data) => {
|
|
63
|
+
this.actions.addEvents([
|
|
64
|
+
createEvent<LogEventType>({
|
|
65
|
+
type: 'log',
|
|
66
|
+
message: data,
|
|
67
|
+
severity,
|
|
68
|
+
}),
|
|
69
|
+
]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
wp.player.hooks.state.tap(this.name, (newState) => {
|
|
73
|
+
if ('error' in newState) {
|
|
74
|
+
this.actions.addEvents([
|
|
75
|
+
createEvent<StateChangeEventType>({
|
|
76
|
+
type: 'stateChange',
|
|
77
|
+
state: newState.status,
|
|
78
|
+
error: newState.error.message,
|
|
79
|
+
}),
|
|
80
|
+
]);
|
|
81
|
+
} else if (newState.status === 'completed') {
|
|
82
|
+
this.actions.addEvents([
|
|
83
|
+
createEvent<StateChangeEventType>({
|
|
84
|
+
type: 'stateChange',
|
|
85
|
+
state: newState.status,
|
|
86
|
+
outcome: newState.endState.outcome,
|
|
87
|
+
}),
|
|
88
|
+
]);
|
|
89
|
+
} else {
|
|
90
|
+
this.actions.addEvents([
|
|
91
|
+
createEvent<StateChangeEventType>({
|
|
92
|
+
type: 'stateChange',
|
|
93
|
+
state: newState.status,
|
|
94
|
+
}),
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
onMetricChange(timing: Timing, metricType: string) {
|
|
101
|
+
if (!timing.completed) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.actions.addEvents([
|
|
106
|
+
createEvent<MetricChangeEventType>({
|
|
107
|
+
type: 'metric',
|
|
108
|
+
metricType,
|
|
109
|
+
message: `Duration: ${timing.duration.toFixed(0)} ms`,
|
|
110
|
+
}),
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { v4 as uuid } from 'uuid';
|
|
2
|
+
import type { PlayerFlowStatus } from '@player-ui/player';
|
|
3
|
+
import type { Severity } from '@player-ui/player';
|
|
4
|
+
|
|
5
|
+
interface BaseEventType<T extends string> {
|
|
6
|
+
/** A timestamp for the event */
|
|
7
|
+
time: number;
|
|
8
|
+
/** the event id */
|
|
9
|
+
id: string;
|
|
10
|
+
/** the event type */
|
|
11
|
+
type: T;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface LogEventType extends BaseEventType<'log'> {
|
|
15
|
+
/** the message */
|
|
16
|
+
message: Array<unknown>;
|
|
17
|
+
/** the log severity */
|
|
18
|
+
severity: Severity;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DataChangeEventType extends BaseEventType<'dataChange'> {
|
|
22
|
+
/** The binding */
|
|
23
|
+
binding: string;
|
|
24
|
+
/** the old value */
|
|
25
|
+
from: unknown;
|
|
26
|
+
/** the new value */
|
|
27
|
+
to: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface StateChangeEventType extends BaseEventType<'stateChange'> {
|
|
31
|
+
/** The state type */
|
|
32
|
+
state: PlayerFlowStatus;
|
|
33
|
+
/** The error message */
|
|
34
|
+
error?: string;
|
|
35
|
+
/** The outcome */
|
|
36
|
+
outcome?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface MetricChangeEventType extends BaseEventType<'metric'> {
|
|
40
|
+
/** The metric type */
|
|
41
|
+
metricType: string;
|
|
42
|
+
|
|
43
|
+
/** The message for the metrics change type */
|
|
44
|
+
message: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type EventType =
|
|
48
|
+
| LogEventType
|
|
49
|
+
| DataChangeEventType
|
|
50
|
+
| StateChangeEventType
|
|
51
|
+
| MetricChangeEventType;
|
|
52
|
+
|
|
53
|
+
/** Create an event for the timeline */
|
|
54
|
+
export function createEvent<T extends EventType>(
|
|
55
|
+
base: Omit<T, 'id' | 'time'>
|
|
56
|
+
): T {
|
|
57
|
+
return {
|
|
58
|
+
id: uuid(),
|
|
59
|
+
time: Date.now(),
|
|
60
|
+
...base,
|
|
61
|
+
} as T;
|
|
62
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Channel } from '@storybook/channels';
|
|
3
|
+
import type { Flow } from '@player-ui/react';
|
|
4
|
+
import type { PlayerFlowMetrics } from '@player-ui/metrics-plugin-react';
|
|
5
|
+
import type { EventType } from './events';
|
|
6
|
+
import type { RenderTarget } from '../types';
|
|
7
|
+
|
|
8
|
+
export interface StateActions {
|
|
9
|
+
addEvents(events: Array<EventType>): void;
|
|
10
|
+
clearEvents(): void;
|
|
11
|
+
setFlow(flow: Flow): void;
|
|
12
|
+
setMetrics(metrics: PlayerFlowMetrics): void;
|
|
13
|
+
resetFlow(): void;
|
|
14
|
+
setPlatform(platform: RenderTarget['platform']): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface BasePublishedEvent<T extends string> {
|
|
18
|
+
/** The base event type */
|
|
19
|
+
type: T;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type EventClearType = BasePublishedEvent<'@@player/event/clear'>;
|
|
23
|
+
|
|
24
|
+
interface EventAddType extends BasePublishedEvent<'@@player/event/add'> {
|
|
25
|
+
/** The events to append */
|
|
26
|
+
events: Array<EventType>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface FlowSetType extends BasePublishedEvent<'@@player/flow/set'> {
|
|
30
|
+
/** the flow to use */
|
|
31
|
+
flow: Flow;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type FlowResetType = BasePublishedEvent<'@@player/flow/reset'>;
|
|
35
|
+
|
|
36
|
+
interface MetricsSetEventType
|
|
37
|
+
extends BasePublishedEvent<'@@player/metrics/set'> {
|
|
38
|
+
/** the metrics data */
|
|
39
|
+
metrics: PlayerFlowMetrics;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface PlatformSetType
|
|
43
|
+
extends BasePublishedEvent<'@@player/platform/set'> {
|
|
44
|
+
/** The platform to render on */
|
|
45
|
+
platform: RenderTarget['platform'];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type PlayerEventType =
|
|
49
|
+
| EventClearType
|
|
50
|
+
| EventAddType
|
|
51
|
+
| FlowSetType
|
|
52
|
+
| MetricsSetEventType
|
|
53
|
+
| FlowResetType
|
|
54
|
+
| PlatformSetType;
|
|
55
|
+
|
|
56
|
+
/** Subscribe to player events in storybook */
|
|
57
|
+
export function subscribe<T extends PlayerEventType>(
|
|
58
|
+
chan: Channel,
|
|
59
|
+
eventName: T['type'],
|
|
60
|
+
callback: (evt: T) => void
|
|
61
|
+
): () => void {
|
|
62
|
+
/** The handler to call */
|
|
63
|
+
const handler = (payload: string) => {
|
|
64
|
+
callback(JSON.parse(payload));
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
chan.addListener(eventName, handler);
|
|
68
|
+
|
|
69
|
+
return () => {
|
|
70
|
+
chan.removeListener(eventName, handler);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** publish an event to storybook */
|
|
75
|
+
export function publish(chan: Channel, event: PlayerEventType) {
|
|
76
|
+
chan.emit(event.type, JSON.stringify(event));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** wrapper to emit events */
|
|
80
|
+
export function useStateActions(chan: Channel): StateActions {
|
|
81
|
+
return React.useMemo<StateActions>(
|
|
82
|
+
() => ({
|
|
83
|
+
addEvents: (events) => {
|
|
84
|
+
publish(chan, {
|
|
85
|
+
type: '@@player/event/add',
|
|
86
|
+
events,
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
setMetrics: (metrics) => {
|
|
90
|
+
publish(chan, {
|
|
91
|
+
type: '@@player/metrics/set',
|
|
92
|
+
metrics,
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
clearEvents: () => {
|
|
96
|
+
publish(chan, {
|
|
97
|
+
type: '@@player/event/clear',
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
setFlow: (flow) => {
|
|
101
|
+
publish(chan, {
|
|
102
|
+
type: '@@player/flow/set',
|
|
103
|
+
flow,
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
resetFlow: () => {
|
|
107
|
+
publish(chan, { type: '@@player/flow/reset' });
|
|
108
|
+
},
|
|
109
|
+
setPlatform: (platform) => {
|
|
110
|
+
publish(chan, { type: '@@player/platform/set', platform });
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
113
|
+
[chan]
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** react hook to subscribe to events */
|
|
118
|
+
export function useEventState(chan: Channel) {
|
|
119
|
+
const [events, setEvents] = React.useState<Array<EventType>>([]);
|
|
120
|
+
|
|
121
|
+
React.useEffect(() => {
|
|
122
|
+
const unsubAdd = subscribe<EventAddType>(
|
|
123
|
+
chan,
|
|
124
|
+
'@@player/event/add',
|
|
125
|
+
(evt) => setEvents((old) => [...old, ...evt.events])
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const unsubClear = subscribe(chan, '@@player/event/clear', () => {
|
|
129
|
+
setEvents([]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return () => {
|
|
133
|
+
unsubAdd();
|
|
134
|
+
unsubClear();
|
|
135
|
+
};
|
|
136
|
+
}, [chan]);
|
|
137
|
+
|
|
138
|
+
return events;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** hook to subscribe to flow events */
|
|
142
|
+
export function useFlowState(chan: Channel) {
|
|
143
|
+
const [flow, setFlow] = React.useState<Flow | undefined>();
|
|
144
|
+
|
|
145
|
+
React.useEffect(() => {
|
|
146
|
+
return subscribe<FlowSetType>(chan, '@@player/flow/set', (evt) => {
|
|
147
|
+
setFlow(evt.flow);
|
|
148
|
+
});
|
|
149
|
+
}, [chan]);
|
|
150
|
+
|
|
151
|
+
return flow;
|
|
152
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { WebPlayerPlugin } from '@player-ui/react';
|
|
2
|
+
import type { AppetizeVersions } from './player/Appetize';
|
|
3
|
+
|
|
4
|
+
export interface PlayerParametersType {
|
|
5
|
+
/** plugins to use for any loaded player */
|
|
6
|
+
webplayerPlugins?: Array<WebPlayerPlugin>;
|
|
7
|
+
|
|
8
|
+
/** Appetize tokens to use */
|
|
9
|
+
appetizeTokens?: {
|
|
10
|
+
/** the ios token */
|
|
11
|
+
ios?: string;
|
|
12
|
+
|
|
13
|
+
/** the android token */
|
|
14
|
+
android?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** The base URL to use for appetize if it is not the default */
|
|
18
|
+
appetizeBaseUrl?: string;
|
|
19
|
+
|
|
20
|
+
/** The OS Versions to use for appetize */
|
|
21
|
+
appetizeVersions?: AppetizeVersions;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type AsyncImportFactory<T> = () => Promise<{
|
|
25
|
+
/** default export of the module */
|
|
26
|
+
default: T;
|
|
27
|
+
}>;
|
|
28
|
+
|
|
29
|
+
export type RenderTarget = {
|
|
30
|
+
/** platform to render on */
|
|
31
|
+
platform: 'ios' | 'android' | 'web';
|
|
32
|
+
|
|
33
|
+
/** the token to use if applicable */
|
|
34
|
+
token?: string;
|
|
35
|
+
|
|
36
|
+
/** A different base URL for appetize if necessary */
|
|
37
|
+
baseUrl?: string;
|
|
38
|
+
|
|
39
|
+
/** The OS Versions to use for appetize */
|
|
40
|
+
appetizeVersions?: AppetizeVersions;
|
|
41
|
+
};
|