@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
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@player-ui/storybook",
|
|
3
|
+
"version": "0.0.1-next.2",
|
|
4
|
+
"private": false,
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"registry": "https://registry.npmjs.org"
|
|
7
|
+
},
|
|
8
|
+
"peerDependencies": {
|
|
9
|
+
"@storybook/react": "^6.4.15",
|
|
10
|
+
"@storybook/addons": "^6.4.15",
|
|
11
|
+
"@storybook/addon-docs": "^6.4.15",
|
|
12
|
+
"react": "^17.0.2",
|
|
13
|
+
"@types/react": "^17.0.25",
|
|
14
|
+
"@player-ui/binding-grammar": "0.0.1-next.2",
|
|
15
|
+
"@player-ui/types": "0.0.1-next.2"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@monaco-editor/react": "^4.3.1",
|
|
19
|
+
"bowser": "^2.11.0",
|
|
20
|
+
"storybook-dark-mode": "^1.0.8",
|
|
21
|
+
"@devtools-ds/table": "^1.1.2",
|
|
22
|
+
"lz-string": "^1.4.4",
|
|
23
|
+
"@types/lz-string": "^1.3.34",
|
|
24
|
+
"@chakra-ui/react": "^1.7.3",
|
|
25
|
+
"monaco-editor": "^0.31.1",
|
|
26
|
+
"react-redux": "^7.2.6",
|
|
27
|
+
"redux": "^4.1.2",
|
|
28
|
+
"dequal": "^2.0.2",
|
|
29
|
+
"ts-debounce": "^4.0.0",
|
|
30
|
+
"uuid": "^8.3.2",
|
|
31
|
+
"@player-ui/binding-grammar": "0.0.1-next.2",
|
|
32
|
+
"@babel/runtime": "7.15.4"
|
|
33
|
+
},
|
|
34
|
+
"main": "dist/index.cjs.js",
|
|
35
|
+
"module": "dist/index.esm.js",
|
|
36
|
+
"typings": "dist/index.d.ts"
|
|
37
|
+
}
|
package/register.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require('./dist/index.esm').register();
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { API } from '@storybook/api';
|
|
3
|
+
import { useParameter } from '@storybook/api';
|
|
4
|
+
import { STORY_CHANGED } from '@storybook/core-events';
|
|
5
|
+
import {
|
|
6
|
+
IconButton,
|
|
7
|
+
Icons,
|
|
8
|
+
WithTooltip,
|
|
9
|
+
TooltipLinkList,
|
|
10
|
+
} from '@storybook/components';
|
|
11
|
+
import type { RenderTarget } from '../../types';
|
|
12
|
+
import { useStateActions } from '../../state';
|
|
13
|
+
|
|
14
|
+
interface RenderSelectionProps {
|
|
15
|
+
/** storybook api */
|
|
16
|
+
api: API;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Component to show the appetize dropdown */
|
|
20
|
+
export const RenderSelection = ({ api }: RenderSelectionProps) => {
|
|
21
|
+
const params = useParameter('appetizeTokens', {});
|
|
22
|
+
const actions = useStateActions(api.getChannel());
|
|
23
|
+
const [selectedPlatform, setPlatform] =
|
|
24
|
+
React.useState<RenderTarget['platform']>('web');
|
|
25
|
+
|
|
26
|
+
React.useEffect(() => {
|
|
27
|
+
/** callback for the subscribe listener */
|
|
28
|
+
const listener = () => {
|
|
29
|
+
setPlatform('web');
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
api.getChannel().addListener(STORY_CHANGED, listener);
|
|
33
|
+
|
|
34
|
+
return () => {
|
|
35
|
+
api.getChannel().removeListener(STORY_CHANGED, listener);
|
|
36
|
+
};
|
|
37
|
+
}, [api]);
|
|
38
|
+
|
|
39
|
+
const mobilePlatforms = Object.keys(params) as Array<'ios' | 'android'>;
|
|
40
|
+
|
|
41
|
+
if (mobilePlatforms.length === 0) {
|
|
42
|
+
// No keys set so don't show
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<WithTooltip
|
|
48
|
+
closeOnClick
|
|
49
|
+
placement="top"
|
|
50
|
+
trigger="click"
|
|
51
|
+
tooltip={({ onHide }) => (
|
|
52
|
+
<TooltipLinkList
|
|
53
|
+
links={(['web', ...mobilePlatforms] as const).map((platform) => ({
|
|
54
|
+
id: platform,
|
|
55
|
+
title: platform,
|
|
56
|
+
onClick: () => {
|
|
57
|
+
setPlatform(platform);
|
|
58
|
+
actions.setPlatform(platform);
|
|
59
|
+
onHide();
|
|
60
|
+
},
|
|
61
|
+
value: platform,
|
|
62
|
+
active: platform === selectedPlatform,
|
|
63
|
+
}))}
|
|
64
|
+
/>
|
|
65
|
+
)}
|
|
66
|
+
>
|
|
67
|
+
<IconButton title="Change the render target">
|
|
68
|
+
<Icons icon={selectedPlatform === 'web' ? 'browser' : 'mobile'} />
|
|
69
|
+
</IconButton>
|
|
70
|
+
</WithTooltip>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const PARAM_KEY = 'web-player';
|
|
2
|
+
export const ADDON_ID = 'web-player/addon';
|
|
3
|
+
|
|
4
|
+
export const FLOW_PANEL_ID = `${ADDON_ID}/flow-editor-panel`;
|
|
5
|
+
export const EVENT_PANEL_ID = `${ADDON_ID}/events-panel`;
|
|
6
|
+
export const DOCS_PANEL_ID = `${ADDON_ID}/asset-docs-panel`;
|
|
7
|
+
export const MOBILE_TOOL_ID = `${ADDON_ID}/mobile-tool`;
|
|
8
|
+
export const FLOW_REFRESH_TOOL_ID = `${ADDON_ID}/flow-refresh-tool`;
|
|
9
|
+
export const PLAYER_RENDERER_TOOL_ID = `${ADDON_ID}/player-renderer-tool`;
|
|
10
|
+
export const RENDER_SELECT_TOOL_ID = `${ADDON_ID}/render-select-tool`;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { API } from '@storybook/api';
|
|
3
|
+
import { useDarkMode } from 'storybook-dark-mode';
|
|
4
|
+
import { dequal } from 'dequal';
|
|
5
|
+
import Editor from '@monaco-editor/react';
|
|
6
|
+
import { useFlowState, useStateActions } from '../../state';
|
|
7
|
+
|
|
8
|
+
interface EditorPanelProps {
|
|
9
|
+
/** if the panel is shown */
|
|
10
|
+
active: boolean;
|
|
11
|
+
/** storybook api */
|
|
12
|
+
api: API;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** the panel for the flow editor */
|
|
16
|
+
export const EditorPanel = (props: EditorPanelProps) => {
|
|
17
|
+
const { active } = props;
|
|
18
|
+
const darkMode = useDarkMode();
|
|
19
|
+
const flow = useFlowState(props.api.getChannel());
|
|
20
|
+
const actions = useStateActions(props.api.getChannel());
|
|
21
|
+
const [editorValue, setEditorValue] = React.useState(
|
|
22
|
+
flow ? JSON.stringify(flow, null, 2) : '{}'
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const updateTimerRef = React.useRef<NodeJS.Timeout | undefined>(undefined);
|
|
26
|
+
|
|
27
|
+
/** remove any pending saves */
|
|
28
|
+
function clearPending() {
|
|
29
|
+
if (updateTimerRef.current) {
|
|
30
|
+
clearTimeout(updateTimerRef.current);
|
|
31
|
+
updateTimerRef.current = undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
if (!active) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
if (editorValue) {
|
|
42
|
+
const parsed = JSON.parse(editorValue);
|
|
43
|
+
if (dequal(flow, parsed)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch (e) {}
|
|
48
|
+
|
|
49
|
+
setEditorValue(JSON.stringify(flow, null, 2));
|
|
50
|
+
}, [flow, active]);
|
|
51
|
+
|
|
52
|
+
if (!active) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** handler for changes to the content */
|
|
57
|
+
const onChange = (val: string | undefined) => {
|
|
58
|
+
clearPending();
|
|
59
|
+
setEditorValue(val ?? '');
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
if (val) {
|
|
63
|
+
const parsed = JSON.parse(val);
|
|
64
|
+
if (!dequal(parsed, flow)) {
|
|
65
|
+
updateTimerRef.current = setTimeout(() => {
|
|
66
|
+
if (active) {
|
|
67
|
+
actions.setFlow(parsed);
|
|
68
|
+
}
|
|
69
|
+
}, 1000);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div>
|
|
77
|
+
<Editor
|
|
78
|
+
theme={darkMode ? 'vs-dark' : 'light'}
|
|
79
|
+
value={editorValue}
|
|
80
|
+
language="json"
|
|
81
|
+
onChange={onChange}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Table, Head, HeadCell, Cell, Body, Row } from '@devtools-ds/table';
|
|
3
|
+
import makeClass from 'clsx';
|
|
4
|
+
import { useDarkMode } from 'storybook-dark-mode';
|
|
5
|
+
import type { API } from '@storybook/api';
|
|
6
|
+
import { useEventState } from '../../state/hooks';
|
|
7
|
+
import type { EventType } from '../../state';
|
|
8
|
+
import styles from './events.css';
|
|
9
|
+
|
|
10
|
+
interface EventsPanelProps {
|
|
11
|
+
/** if the panel is shown */
|
|
12
|
+
active: boolean;
|
|
13
|
+
/** storybook api */
|
|
14
|
+
api: API;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Pad the cells to give room */
|
|
18
|
+
const ExtraCells = (event: EventType) => {
|
|
19
|
+
if (event.type === 'log') {
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
<td>{event.severity}</td>
|
|
23
|
+
<td>{event.message.map((a) => JSON.stringify(a)).join(' ')}</td>
|
|
24
|
+
</>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (event.type === 'dataChange') {
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<td>{event.binding}</td>
|
|
32
|
+
<td>{`${JSON.stringify(event.from)} ➜ ${JSON.stringify(event.to)}`}</td>
|
|
33
|
+
</>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (event.type === 'stateChange') {
|
|
38
|
+
let name: string = event.state;
|
|
39
|
+
|
|
40
|
+
if (event.state === 'completed') {
|
|
41
|
+
name = `${name} (${event.error ? 'error' : 'success'})`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
<td>{name}</td>
|
|
47
|
+
<td>{event.outcome ?? event.error ?? ''}</td>
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (event.type === 'metric') {
|
|
53
|
+
return (
|
|
54
|
+
<>
|
|
55
|
+
<td>{event.metricType}</td>
|
|
56
|
+
<td>{event.message}</td>
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** The panel to show events */
|
|
65
|
+
export const EventsPanel = (props: EventsPanelProps) => {
|
|
66
|
+
const events = useEventState(props.api.getChannel());
|
|
67
|
+
const darkMode = useDarkMode();
|
|
68
|
+
|
|
69
|
+
if (!props.active) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
className={makeClass(styles.wrapper, {
|
|
76
|
+
[styles.dark]: darkMode,
|
|
77
|
+
})}
|
|
78
|
+
>
|
|
79
|
+
<Table colorScheme={darkMode ? 'dark' : 'light'}>
|
|
80
|
+
<Head className={styles.header}>
|
|
81
|
+
<Row>
|
|
82
|
+
<HeadCell>Time</HeadCell>
|
|
83
|
+
<HeadCell>Type</HeadCell>
|
|
84
|
+
<HeadCell />
|
|
85
|
+
<HeadCell />
|
|
86
|
+
</Row>
|
|
87
|
+
</Head>
|
|
88
|
+
<Body className={styles.body}>
|
|
89
|
+
{events.map((evt) => (
|
|
90
|
+
<Row key={evt.id}>
|
|
91
|
+
<Cell>{evt.time}</Cell>
|
|
92
|
+
<Cell>{evt.type}</Cell>
|
|
93
|
+
<ExtraCells {...evt} />
|
|
94
|
+
</Row>
|
|
95
|
+
))}
|
|
96
|
+
</Body>
|
|
97
|
+
</Table>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import addons, { types } from '@storybook/addons';
|
|
3
|
+
import {
|
|
4
|
+
ADDON_ID,
|
|
5
|
+
EVENT_PANEL_ID,
|
|
6
|
+
FLOW_PANEL_ID,
|
|
7
|
+
FLOW_REFRESH_TOOL_ID,
|
|
8
|
+
RENDER_SELECT_TOOL_ID,
|
|
9
|
+
} from './constants';
|
|
10
|
+
import { EditorPanel } from './editor';
|
|
11
|
+
import { EventsPanel } from './events';
|
|
12
|
+
import { FlowRefresh } from './refresh';
|
|
13
|
+
import { RenderSelection } from './appetize';
|
|
14
|
+
|
|
15
|
+
/** register all the storybook addons */
|
|
16
|
+
export function register() {
|
|
17
|
+
addons.register(ADDON_ID, (api) => {
|
|
18
|
+
addons.addPanel(EVENT_PANEL_ID, {
|
|
19
|
+
title: 'Events',
|
|
20
|
+
render: ({ active, key }) => (
|
|
21
|
+
<EventsPanel key={key} api={api} active={Boolean(active)} />
|
|
22
|
+
),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
addons.addPanel(FLOW_PANEL_ID, {
|
|
26
|
+
title: 'Flow',
|
|
27
|
+
render: ({ active, key }) => (
|
|
28
|
+
<EditorPanel key={key} api={api} active={Boolean(active)} />
|
|
29
|
+
),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Tools show up in the top panel
|
|
33
|
+
|
|
34
|
+
addons.add(FLOW_REFRESH_TOOL_ID, {
|
|
35
|
+
title: 'Refresh Flow',
|
|
36
|
+
type: types.TOOL,
|
|
37
|
+
render: () => <FlowRefresh api={api} />,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
addons.add(RENDER_SELECT_TOOL_ID, {
|
|
41
|
+
title: 'Render Selection',
|
|
42
|
+
type: types.TOOL,
|
|
43
|
+
render: () => <RenderSelection api={api} />,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { API } from '@storybook/api';
|
|
3
|
+
import { IconButton, Icons, Separator } from '@storybook/components';
|
|
4
|
+
import { useStateActions } from '../../state';
|
|
5
|
+
|
|
6
|
+
interface FlowRefreshProps {
|
|
7
|
+
/** storybook api */
|
|
8
|
+
api: API;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** BUtton to refresh the current player flow */
|
|
12
|
+
export const FlowRefresh = ({ api }: FlowRefreshProps) => {
|
|
13
|
+
const actions = useStateActions(api.getChannel());
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<>
|
|
17
|
+
<Separator />
|
|
18
|
+
<IconButton
|
|
19
|
+
title="Reset the current flow"
|
|
20
|
+
onClick={() => {
|
|
21
|
+
actions.resetFlow();
|
|
22
|
+
}}
|
|
23
|
+
>
|
|
24
|
+
<Icons icon="sync" />
|
|
25
|
+
</IconButton>
|
|
26
|
+
</>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { DecoratorFn } from '@storybook/react';
|
|
3
|
+
import addons from '@storybook/addons';
|
|
4
|
+
import type { PlatformSetType } from '../state/hooks';
|
|
5
|
+
import { subscribe } from '../state/hooks';
|
|
6
|
+
import { WebPlayerPluginContext, PlayerRenderContext } from '../player';
|
|
7
|
+
import type { PlayerParametersType, RenderTarget } from '../types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A story decorator for rendering player content
|
|
11
|
+
*/
|
|
12
|
+
export const PlayerDecorator: DecoratorFn = (story, ctx) => {
|
|
13
|
+
const playerParams = ctx.parameters as PlayerParametersType;
|
|
14
|
+
const [selectedPlatform, setPlatform] =
|
|
15
|
+
React.useState<RenderTarget['platform']>('web');
|
|
16
|
+
|
|
17
|
+
React.useEffect(() => {
|
|
18
|
+
return subscribe<PlatformSetType>(
|
|
19
|
+
addons.getChannel(),
|
|
20
|
+
'@@player/platform/set',
|
|
21
|
+
(evt) => {
|
|
22
|
+
setPlatform(evt.platform);
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<PlayerRenderContext.Provider
|
|
29
|
+
value={{
|
|
30
|
+
platform: selectedPlatform,
|
|
31
|
+
token:
|
|
32
|
+
selectedPlatform === 'web'
|
|
33
|
+
? undefined
|
|
34
|
+
: playerParams?.appetizeTokens?.[selectedPlatform],
|
|
35
|
+
baseUrl: playerParams.appetizeBaseUrl,
|
|
36
|
+
appetizeVersions: playerParams.appetizeVersions,
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
<WebPlayerPluginContext.Provider
|
|
40
|
+
value={{ plugins: playerParams.webplayerPlugins }}
|
|
41
|
+
>
|
|
42
|
+
{story()}
|
|
43
|
+
</WebPlayerPluginContext.Provider>
|
|
44
|
+
</PlayerRenderContext.Provider>
|
|
45
|
+
);
|
|
46
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Flow } from '@player-ui/player';
|
|
3
|
+
import { compressToEncodedURIComponent } from 'lz-string';
|
|
4
|
+
|
|
5
|
+
export interface AppetizeVersions {
|
|
6
|
+
/** The iOS version to load */
|
|
7
|
+
ios: string;
|
|
8
|
+
/** The Android version to load */
|
|
9
|
+
android: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface AppetizeProps {
|
|
13
|
+
/** the platform to load */
|
|
14
|
+
platform: 'ios' | 'android';
|
|
15
|
+
|
|
16
|
+
/** the token for the build */
|
|
17
|
+
token: string;
|
|
18
|
+
|
|
19
|
+
/** the flow to load */
|
|
20
|
+
flow: Flow;
|
|
21
|
+
|
|
22
|
+
/** The versions to use for each platform */
|
|
23
|
+
osVersions?: AppetizeVersions;
|
|
24
|
+
|
|
25
|
+
/** The base URL to use for appetize */
|
|
26
|
+
baseUrl?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const AppetizePhones = [
|
|
30
|
+
// 'iphone4s',
|
|
31
|
+
// 'iphone5s',
|
|
32
|
+
// 'iphone6',
|
|
33
|
+
// 'iphone6plus',
|
|
34
|
+
// 'iphone6s',
|
|
35
|
+
// 'iphone6splus',
|
|
36
|
+
// 'iphone7',
|
|
37
|
+
// 'iphone7plus',
|
|
38
|
+
// 'iphone8',
|
|
39
|
+
// 'iphone8plus',
|
|
40
|
+
// 'iphonex',
|
|
41
|
+
'iphonexs',
|
|
42
|
+
// 'iphonexsmax',
|
|
43
|
+
// 'iphone11pro',
|
|
44
|
+
// 'iphone11promax',
|
|
45
|
+
// 'ipadair',
|
|
46
|
+
// 'ipadair2'
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
export type AppetizePhone = typeof AppetizePhones[number];
|
|
50
|
+
|
|
51
|
+
const DEVICE_HEIGHT: Record<AppetizePhone, number> = {
|
|
52
|
+
iphonexs: 845,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
interface AppetizeParams {
|
|
56
|
+
/** if the device should auto-start */
|
|
57
|
+
autoplay: boolean;
|
|
58
|
+
/** the device type */
|
|
59
|
+
device: AppetizePhone;
|
|
60
|
+
/** color */
|
|
61
|
+
deviceColor: 'black' | 'white';
|
|
62
|
+
|
|
63
|
+
/** render scale */
|
|
64
|
+
scale: number;
|
|
65
|
+
|
|
66
|
+
/** The operating system version to use */
|
|
67
|
+
osVersion: string;
|
|
68
|
+
|
|
69
|
+
/** other stuff to pass */
|
|
70
|
+
params: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Create the url w/ params */
|
|
74
|
+
export const toAppetizeUrl = (
|
|
75
|
+
baseUrl: string,
|
|
76
|
+
key: string,
|
|
77
|
+
params: AppetizeParams
|
|
78
|
+
) =>
|
|
79
|
+
`https://${baseUrl}/embed/${key}?${Object.entries(params)
|
|
80
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
81
|
+
.join('&')}`;
|
|
82
|
+
|
|
83
|
+
/** A component to render something using appetize */
|
|
84
|
+
export const Appetize = (props: AppetizeProps) => {
|
|
85
|
+
const device: AppetizePhone = 'iphonexs';
|
|
86
|
+
const height = DEVICE_HEIGHT[device];
|
|
87
|
+
|
|
88
|
+
const defaultVersions: AppetizeVersions = {
|
|
89
|
+
ios: '13.7',
|
|
90
|
+
android: '8.1',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const {
|
|
94
|
+
baseUrl = 'appetize.io',
|
|
95
|
+
token,
|
|
96
|
+
flow,
|
|
97
|
+
osVersions = defaultVersions,
|
|
98
|
+
platform,
|
|
99
|
+
} = props;
|
|
100
|
+
|
|
101
|
+
const osVersion = osVersions[platform];
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<iframe
|
|
105
|
+
title="native app"
|
|
106
|
+
style={{ height: `${height}px`, border: 'none', width: '100%' }}
|
|
107
|
+
src={toAppetizeUrl(baseUrl, token, {
|
|
108
|
+
autoplay: true,
|
|
109
|
+
device: 'iphonexs',
|
|
110
|
+
deviceColor: 'black',
|
|
111
|
+
scale: 100,
|
|
112
|
+
osVersion,
|
|
113
|
+
params: encodeURIComponent(
|
|
114
|
+
JSON.stringify({
|
|
115
|
+
json: compressToEncodedURIComponent(JSON.stringify(flow)),
|
|
116
|
+
})
|
|
117
|
+
),
|
|
118
|
+
})}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { VStack, Code, Heading, Button, Text } from '@chakra-ui/react';
|
|
3
|
+
|
|
4
|
+
export type PlayerFlowSummaryProps = {
|
|
5
|
+
/** Reset the flow */
|
|
6
|
+
reset: () => void;
|
|
7
|
+
/** The outcome of the flow */
|
|
8
|
+
outcome?: string;
|
|
9
|
+
/** any error */
|
|
10
|
+
error?: Error;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** A component to show at the end of a flow */
|
|
14
|
+
export const PlayerFlowSummary = (props: PlayerFlowSummaryProps) => {
|
|
15
|
+
return (
|
|
16
|
+
<VStack gap="10">
|
|
17
|
+
<Heading>Flow Completed {props.error ? 'with Error' : ''}</Heading>
|
|
18
|
+
|
|
19
|
+
{props.outcome && (
|
|
20
|
+
<Code>
|
|
21
|
+
Outcome: <Text as="strong">{props.outcome}</Text>
|
|
22
|
+
</Code>
|
|
23
|
+
)}
|
|
24
|
+
|
|
25
|
+
{props.error && (
|
|
26
|
+
<Code colorScheme="red">
|
|
27
|
+
<pre>{props.error?.message}</pre>
|
|
28
|
+
</Code>
|
|
29
|
+
)}
|
|
30
|
+
|
|
31
|
+
<Button variant="solid" onClick={props.reset}>
|
|
32
|
+
Reset
|
|
33
|
+
</Button>
|
|
34
|
+
</VStack>
|
|
35
|
+
);
|
|
36
|
+
};
|