fix(ui): onboarding design fixes (#1993)

* fix(ui): onboarding design fixes

* more stuff

* some assist details and options
This commit is contained in:
Delirium 2024-03-25 16:27:51 +01:00 committed by GitHub
parent 21c312f98b
commit 8b52432a9a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 410 additions and 249 deletions

View file

@ -43,8 +43,36 @@ function MyApp() {
<div className="font-bold my-2">Options</div>
<Highlight className="js">
{`trackerAssist({
confirmText: string;
})`}
onAgentConnect: StartEndCallback;
onCallStart: StartEndCallback;
onRemoteControlStart: StartEndCallback;
onRecordingRequest?: (agentInfo: Record<string, any>) => any;
onCallDeny?: () => any;
onRemoteControlDeny?: (agentInfo: Record<string, any>) => any;
onRecordingDeny?: (agentInfo: Record<string, any>) => any;
session_calling_peer_key: string;
session_control_peer_key: string;
callConfirm: ConfirmOptions;
controlConfirm: ConfirmOptions;
recordingConfirm: ConfirmOptions;
socketHost?: string;
config: RTCConfiguration;
serverURL: string
callUITemplate?: string;
})
type ConfirmOptions = {
text?:string,
style?: StyleObject, // style object (i.e {color: 'red', borderRadius: '10px'})
confirmBtn?: ButtonOptions,
declineBtn?: ButtonOptions
}
type ButtonOptions = HTMLButtonElement | string | {
innerHTML?: string, // to pass an svg string or text
style?: StyleObject, // style object (i.e {color: 'red', borderRadius: '10px'})
}
`}
</Highlight>
</div>
);

View file

@ -1,17 +1,20 @@
import React, { useState, useEffect, ChangeEvent, FormEvent } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { Form, Input, Button, Icon, SegmentSelection } from 'UI';
import { save, edit, update, fetchList, remove } from 'Duck/site';
import { pushNewSite } from 'Duck/user';
import { setSiteId } from 'Duck/site';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import styles from './siteForm.module.css';
import { confirm } from 'UI';
import { clearSearch } from 'Duck/search';
import { clearSearch as clearSearchLive } from 'Duck/liveSearch';
import { withStore } from 'App/mstore';
import { Segmented } from 'antd';
import React, { ChangeEvent, FormEvent, useEffect, useState } from 'react';
import { ConnectedProps, connect } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { toast } from 'react-toastify';
import { withStore } from 'App/mstore';
import { clearSearch as clearSearchLive } from 'Duck/liveSearch';
import { clearSearch } from 'Duck/search';
import { edit, fetchList, remove, save, update } from 'Duck/site';
import { setSiteId } from 'Duck/site';
import { pushNewSite } from 'Duck/user';
import { Button, Form, Icon, Input, SegmentSelection } from 'UI';
import { confirm } from 'UI';
import styles from './siteForm.module.css';
type OwnProps = {
onClose: (arg: any) => void;
mstore: any;
@ -81,8 +84,10 @@ const NewSiteForm = ({
const handleRemove = async () => {
if (
await confirm({
header: 'Projects',
confirmation: `Are you sure you want to delete this Project? We won't be able to record anymore sessions.`,
header: 'Project Deletion Alert',
confirmation: `Are you sure you want to delete this project? Deleting it will permanently remove the project, along with all associated sessions and data.`,
confirmButton: 'Yes, delete',
cancelButton: 'Cancel',
})
) {
remove(site.id).then(() => {
@ -94,15 +99,25 @@ const NewSiteForm = ({
}
};
const handleEdit = ({ target: { name, value } }: ChangeEvent<HTMLInputElement>) => {
const handleEdit = ({
target: { name, value },
}: ChangeEvent<HTMLInputElement>) => {
setExistsError(false);
edit({ [name]: value });
};
return (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<h3 className="p-5 text-2xl">{site.exists() ? 'Edit Project' : 'New Project'}</h3>
<Form className={styles.formWrapper} onSubmit={site.validate() && onSubmit}>
<div
className="bg-white h-screen overflow-y-auto"
style={{ width: '350px' }}
>
<h3 className="p-5 text-2xl">
{site.exists() ? 'Edit Project' : 'New Project'}
</h3>
<Form
className={styles.formWrapper}
onSubmit={site.validate() && onSubmit}
>
<div className={styles.content}>
<Form.Field>
<label>{'Name'}</label>
@ -117,17 +132,24 @@ const NewSiteForm = ({
</Form.Field>
<Form.Field>
<label>Project Type</label>
<SegmentSelection
name={"platform"}
value={{ name: site.platform, value: site.platform }}
list={[
{ name: 'Web', value: 'web' },
{ name: 'iOS', value: 'ios' },
]}
onSelect={(_, { value }) => {
edit({ platform: value });
}}
/>
<div>
<Segmented
options={[
{
value: 'web',
label: 'Web',
},
{
value: 'ios',
label: 'iOS',
},
]}
value={site.platform}
onChange={(value) => {
edit({ platform: value });
}}
/>
</div>
</Form.Field>
<div className="mt-6 flex justify-between">
<Button
@ -140,12 +162,21 @@ const NewSiteForm = ({
{site.exists() ? 'Update' : 'Add'}
</Button>
{site.exists() && (
<Button variant="text" type="button" onClick={handleRemove} disabled={!canDelete}>
<Button
variant="text"
type="button"
onClick={handleRemove}
disabled={!canDelete}
>
<Icon name="trash" size="16" />
</Button>
)}
</div>
{existsError && <div className={styles.errorMessage}>{'Project exists already.'}</div>}
{existsError && (
<div className={styles.errorMessage}>
{'Project exists already.'}
</div>
)}
</div>
</Form>
</div>
@ -156,7 +187,9 @@ const mapStateToProps = (state: any) => ({
activeSiteId: state.getIn(['site', 'active', 'id']),
site: state.getIn(['site', 'instance']),
siteList: state.getIn(['site', 'list']),
loading: state.getIn(['site', 'save', 'loading']) || state.getIn(['site', 'remove', 'loading']),
loading:
state.getIn(['site', 'save', 'loading']) ||
state.getIn(['site', 'remove', 'loading']),
canDelete: state.getIn(['site', 'list']).size > 1,
});

View file

@ -11,7 +11,7 @@ interface Props {
function InsightItem(props: Props) {
const { item, onClick = () => {} } = props;
const className =
'whitespace-nowrap flex items-center py-4 hover:bg-active-blue -mx-4 px-4 border-b last:border-transparent cursor-pointer';
'flex items-start flex-wrap py-4 hover:bg-active-blue -mx-4 px-4 border-b last:border-transparent cursor-pointer';
switch (item.category) {
case IssueCategory.RAGE:

View file

@ -34,7 +34,7 @@ function DashboardList() {
<div className="grid grid-cols-12 py-2 font-medium px-6">
<div className="col-span-8">Title</div>
<div className="col-span-2">Visibility</div>
<div className="col-span-2 text-right">Creation Date</div>
<div className="col-span-2 text-right">Last Modified</div>
</div>
{sliceListPerPage(list, dashboardStore.page - 1, dashboardStore.pageSize).map(

View file

@ -7,6 +7,7 @@ import DocCard from 'Shared/DocCard/DocCard';
import withOnboarding, { WithOnboardingProps } from '../withOnboarding';
import { OB_TABS } from 'App/routes';
import withPageTitle from 'App/components/hocs/withPageTitle';
import { Button as AntButton } from 'antd'
interface Props extends WithOnboardingProps {
platforms: Array<{
@ -41,13 +42,14 @@ function IdentifyUsersTab(props: Props) {
href={`https://docs.openreplay.com/en/installation/identify-user${platform.value === "web" ? "/#with-npm" : "/#with-ios-app"}`}
target="_blank"
>
<Button variant="text-primary" icon="question-circle" className="ml-2">
See Documentation
</Button>
<AntButton size={'small'} type={'text'} className="ml-2 flex items-center gap-2">
<Icon name={'question-circle'} />
<div className={'text-main'}>See Documentation</div>
</AntButton>
</a>
</h1>
<div className="p-4 flex gap-2 items-center">
<span className="font-medium">Your platform</span>
<span className="font-medium">Project Type</span>
<Segmented
options={platforms}
value={platform.value}
@ -69,11 +71,11 @@ function IdentifyUsersTab(props: Props) {
{platform.value === 'web' ? (
<HighlightCode className="js" text={`tracker.setUserID('john@doe.com');`} />
) : (
<HighlightCode
className="swift"
text={`OpenReplay.shared.setUserID('john@doe.com');`}
/>
)}
<HighlightCode
className="swift"
text={`OpenReplay.shared.setUserID('john@doe.com');`}
/>
)}
{platform.value === 'web' ? (
<div className="flex items-center my-2">
<Icon name="info-circle" color="gray-darkest" />
@ -121,11 +123,11 @@ function IdentifyUsersTab(props: Props) {
{platform.value === 'web' ? (
<HighlightCode className="js" text={`tracker.setMetadata('plan', 'premium');`} />
) : (
<HighlightCode
className="swift"
text={`OpenReplay.shared.setMetadata('plan', 'premium');`}
/>
)}
<HighlightCode
className="swift"
text={`OpenReplay.shared.setMetadata('plan', 'premium');`}
/>
)}
</div>
</div>
</div>

View file

@ -8,6 +8,7 @@ import { WithOnboardingProps } from '../withOnboarding';
import { OB_TABS } from 'App/routes';
import withPageTitle from 'App/components/hocs/withPageTitle';
import { Segmented } from 'antd';
import { Button as AntButton } from 'antd'
interface Props extends WithOnboardingProps {
platforms: Array<{
@ -39,14 +40,15 @@ function InstallOpenReplayTab(props: Props) {
<ProjectFormButton />
</div>
</div>
<a href={platform.value === 'web' ? 'https://docs.openreplay.com/en/sdk/' : 'https://docs.openreplay.com/en/ios-sdk/'} target="_blank">
<Button variant="text-primary" icon="question-circle" className="ml-2">
See Documentation
</Button>
<a href={"https://docs.openreplay.com/en/using-or/"} target="_blank">
<AntButton size={"small"} type={"text"} className="ml-2 flex items-center gap-2">
<Icon name={"question-circle"} />
<div className={"text-main"}>See Documentation</div>
</AntButton>
</a>
</h1>
<div className="p-4 flex gap-2 items-center">
<span className="font-medium">Your platform</span>
<span className="font-medium">Project Type</span>
<Segmented
options={platforms}
value={platform.value}

View file

@ -1,8 +1,11 @@
import { Button as AntButton } from 'antd';
import React from 'react';
import { Button } from 'UI';
import Integrations from 'App/components/Client/Integrations/Integrations';
import withOnboarding, { WithOnboardingProps } from '../withOnboarding';
import withPageTitle from 'App/components/hocs/withPageTitle';
import { Button, Icon } from 'UI';
import withOnboarding, { WithOnboardingProps } from '../withOnboarding';
interface Props extends WithOnboardingProps {}
function IntegrationsTab(props: Props) {
@ -14,15 +17,27 @@ function IntegrationsTab(props: Props) {
<div className="ml-3">Integrations</div>
</div>
<a href="https://docs.openreplay.com/en/v1.10.0/integrations/" target="_blank">
<Button variant="text-primary" icon="question-circle" className="ml-2">
See Documentation
</Button>
<a
href="https://docs.openreplay.com/en/integrations/"
target="_blank"
>
<AntButton
size={'small'}
type={'text'}
className="ml-2 flex items-center gap-2"
>
<Icon name={'question-circle'} />
<div className={'text-main'}>See Documentation</div>
</AntButton>
</a>
</h1>
<Integrations hideHeader={true} />
<div className="border-t px-4 py-3 flex justify-end">
<Button variant="primary" className="" onClick={() => (props.skip ? props.skip() : null)}>
<Button
variant="primary"
className=""
onClick={() => (props.skip ? props.skip() : null)}
>
Complete Setup
</Button>
</div>
@ -30,4 +45,6 @@ function IntegrationsTab(props: Props) {
);
}
export default withOnboarding(withPageTitle("Integrations - OpenReplay")(IntegrationsTab));
export default withOnboarding(
withPageTitle('Integrations - OpenReplay')(IntegrationsTab)
);

View file

@ -5,6 +5,7 @@ import { Button, Icon } from 'UI';
import withOnboarding, { WithOnboardingProps } from '../withOnboarding';
import { OB_TABS } from 'App/routes';
import withPageTitle from 'App/components/hocs/withPageTitle';
import { Button as AntButton } from 'antd'
interface Props extends WithOnboardingProps {}
@ -21,9 +22,10 @@ function ManageUsersTab(props: Props) {
href="https://docs.openreplay.com/en/tutorials/adding-users/"
target="_blank"
>
<Button variant="text-primary" icon="question-circle" className="ml-2">
See Documentation
</Button>
<AntButton size={'small'} type={'text'} className="ml-2 flex items-center gap-2">
<Icon name={'question-circle'} />
<div className={'text-main'}>See Documentation</div>
</AntButton>
</a>
</h1>
<div className="grid grid-cols-6 gap-4 p-4">

View file

@ -36,7 +36,7 @@ function InstallDocs({ site }) {
const [isSpa, setIsSpa] = useState(true);
return (
<div>
<div className="mb-8">
<div className="mb-6">
<div className="font-semibold mb-2 flex items-center">
<CircleNumber text="1" />
Install the npm package.
@ -48,7 +48,7 @@ function InstallDocs({ site }) {
<Highlight className="cli">{installationCommand}</Highlight>
</div>
</div>
<div>
<div className={'mb-6'}>
<div className="font-semibold mb-2 flex items-center">
<CircleNumber text="2" />
Continue with one of the following options.
@ -87,7 +87,9 @@ function InstallDocs({ site }) {
<div>
<div className="mb-2 text-sm">
Otherwise, if your web app is <strong>Server-Side-Rendered (SSR)</strong> (i.e.
NextJS, NuxtJS) use this snippet:
NextJS, NuxtJS),{' '}
<a className={'text-main'} href={'https://docs.openreplay.com/en/using-or/next/'}>consider async imports</a>
or cjs version of the library:
</div>
<div className={cn(stl.snippetWrapper)}>
<div className="absolute mt-1 mr-2 right-0">
@ -100,6 +102,43 @@ function InstallDocs({ site }) {
</div>
</div>
</div>
<div>
<div className="font-semibold mb-2 flex items-center">
<CircleNumber text="3" />
Enable Assist (Optional)
</div>
<div className="flex ml-10 mt-4">
<div className="w-full">
<div>
<div className="mb-2">
Install the plugin via npm:
</div>
<div className={cn(stl.snippetWrapper)}>
<div className="absolute mt-1 mr-2 right-0">
<CopyButton content={`npm i @openreplay/tracker-assist`} />
</div>
<Highlight className="js">{`$ npm i @openreplay/tracker-assist`}</Highlight>
</div>
</div>
<div>
<div className={'mb-2'}>
Then enable it with your tracker:
</div>
<div className={cn(stl.snippetWrapper)}>
<div className="absolute mt-1 mr-2 right-0">
<CopyButton content={`tracker.use(trackerAssist(options));`} />
</div>
<Highlight className="js">{`tracker.use(trackerAssist(options));`}</Highlight>
</div>
<div className={'text-sm'}>Read more about available options <a
className={'text-main'}
href={'https://github.com/openreplay/openreplay/blob/main/tracker/tracker-assist/README.md'}>here</a>.
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View file

@ -1,8 +1,8 @@
import React from 'react';
import { Button} from 'UI';
import { confirmable } from 'react-confirm';
import { Modal } from 'UI'
import { Button } from 'antd'
const Confirmation = ({
show,
proceed,
@ -31,7 +31,7 @@ const Confirmation = ({
<Modal.Footer>
<Button
onClick={() => proceed(true)}
variant="primary"
type={'primary'}
className="mr-2"
>
{confirmButton}

View file

@ -1,184 +1,212 @@
import { makeAutoObservable, observable, action } from "mobx"
import User from "./types/user";
import { userService } from "App/services";
import { toast } from 'react-toastify';
import copy from 'copy-to-clipboard';
import { action, makeAutoObservable, observable } from 'mobx';
import { toast } from 'react-toastify';
import { userService } from 'App/services';
import User from './types/user';
export default class UserStore {
list: User[] = [];
instance: User|null = null;
page: number = 1;
pageSize: number = 10;
searchQuery: string = "";
modifiedCount: number = 0;
list: User[] = [];
instance: User | null = null;
page: number = 1;
pageSize: number = 10;
searchQuery: string = '';
modifiedCount: number = 0;
loading: boolean = false;
saving: boolean = false;
limits: any = {};
initialDataFetched: boolean = false;
loading: boolean = false;
saving: boolean = false;
limits: any = {};
initialDataFetched: boolean = false;
constructor() {
makeAutoObservable(this, {
instance: observable,
updateUser: action,
updateKey: action,
initUser: action,
setLimits: action,
constructor() {
makeAutoObservable(this, {
instance: observable,
updateUser: action,
updateKey: action,
initUser: action,
setLimits: action,
});
}
fetchLimits(): Promise<any> {
return new Promise((resolve, reject) => {
userService
.getLimits()
.then((response: any) => {
this.setLimits(response);
resolve(response);
})
}
fetchLimits(): Promise<any> {
return new Promise((resolve, reject) => {
userService.getLimits()
.then((response: any) => {
this.setLimits(response);
resolve(response);
}).catch((error: any) => {
reject(error);
});
.catch((error: any) => {
reject(error);
});
}
});
}
setLimits(limits: any) {
this.limits = limits;
}
setLimits(limits: any) {
this.limits = limits;
}
initUser(user?: any ): Promise<void> {
return new Promise((resolve) => {
if (user) {
this.instance = new User().fromJson(user.toJson());
} else {
this.instance = new User();
}
resolve();
initUser(user?: any): Promise<void> {
return new Promise((resolve) => {
if (user) {
this.instance = new User().fromJson(user.toJson());
} else {
this.instance = new User();
}
resolve();
});
}
updateKey(key: keyof this, value: any) {
this[key] = value;
if (key === 'searchQuery') {
this.page = 1;
}
}
updateUser(user: User) {
const index = this.list.findIndex((u) => u.userId === user.userId);
if (index > -1) {
this.list[index] = user;
}
}
fetchUser(userId: string): Promise<any> {
this.loading = true;
return new Promise((resolve, reject) => {
userService
.one(userId)
.then((response) => {
this.instance = new User().fromJson(response.data);
resolve(response);
})
}
updateKey(key: keyof this, value: any) {
this[key] = value
if (key === 'searchQuery') {
this.page = 1
}
}
updateUser(user: User) {
const index = this.list.findIndex(u => u.userId === user.userId);
if (index > -1) {
this.list[index] = user;
}
}
fetchUser(userId: string): Promise<any> {
this.loading = true;
return new Promise((resolve, reject) => {
userService.one(userId)
.then(response => {
this.instance = new User().fromJson(response.data);
resolve(response);
}).catch(error => {
this.loading = false;
reject(error);
}).finally(() => {
this.loading = false;
});
});
}
fetchUsers(): Promise<any> {
this.loading = true;
return new Promise((resolve, reject) => {
userService.all()
.then(response => {
this.list = response.map(user => new User().fromJson(user));
resolve(response);
}).catch(error => {
this.loading = false;
reject(error);
}).finally(() => {
this.loading = false;
});
});
}
saveUser(user: User): Promise<any> {
this.saving = true;
const wasCreating = !user.userId;
return new Promise((resolve, reject) => {
userService.save(user).then(response => {
const newUser = new User().fromJson(response);
if (wasCreating) {
this.modifiedCount -= 1;
this.list.push(newUser);
toast.success('User created successfully');
} else {
this.updateUser(newUser);
toast.success('User updated successfully');
}
resolve(response);
}).catch(async (e) => {
const err = await e.response?.json();
this.saving = false;
toast.error(err.errors[0] ? err.errors[0] : 'Error saving user');
reject(e);
}).finally(() => {
this.saving = false;
});
});
}
deleteUser(userId: string): Promise<any> {
this.saving = true;
return new Promise((resolve, reject) => {
userService.delete(userId)
.then(response => {
this.modifiedCount += 1;
this.list = this.list.filter(user => user.userId !== userId);
resolve(response);
}).catch(error => {
this.saving = false;
toast.error('Error deleting user');
reject(error);
}).finally(() => {
this.saving = false;
});
});
}
copyInviteCode(userId: string): void {
const content = this.list.find(u => u.userId === userId)?.invitationLink;
if (content) {
copy(content);
toast.success('Invite code copied successfully');
} else {
toast.error('Invite code not found');
}
}
generateInviteCode(userId: string): Promise<any> {
this.saving = true;
const promise = new Promise((resolve, reject) => {
userService.generateInviteCode(userId)
.then(response => {
const index = this.list.findIndex(u => u.userId === userId);
if (index > -1) {
this.list[index].updateKey('isExpiredInvite', false);
this.list[index].updateKey('invitationLink', response.invitationLink);
}
resolve(response);
}).catch(error => {
this.saving = false;
reject(error);
}).finally(() => {
this.saving = false;
});
});
toast.promise(promise, {
pending: 'Generating an invite code...',
success: 'Invite code generated successfully',
.catch((error) => {
this.loading = false;
reject(error);
})
.finally(() => {
this.loading = false;
});
});
}
return promise;
fetchUsers(): Promise<any> {
this.loading = true;
return new Promise((resolve, reject) => {
userService
.all()
.then((response) => {
this.list = response.map((user) => new User().fromJson(user));
resolve(response);
})
.catch((error) => {
this.loading = false;
reject(error);
})
.finally(() => {
this.loading = false;
});
});
}
saveUser(user: User): Promise<any> {
this.saving = true;
const wasCreating = !user.userId;
return new Promise((resolve, reject) => {
userService
.save(user)
.then((response) => {
const newUser = new User().fromJson(response);
if (wasCreating) {
this.modifiedCount -= 1;
this.list.push(newUser);
toast.success('User created successfully');
} else {
this.updateUser(newUser);
toast.success('User updated successfully');
}
resolve(response);
})
.catch(async (e) => {
const err = await e.response?.json();
this.saving = false;
const errStr = err.errors[0]
? err.errors[0].includes('already exists')
? `This email is already linked to an account or team on OpenReplay and can't be used again.`
: err.errors[0]
: 'Error saving user';
toast.error(errStr);
reject(e);
})
.finally(() => {
this.saving = false;
});
});
}
deleteUser(userId: string): Promise<any> {
this.saving = true;
return new Promise((resolve, reject) => {
userService
.delete(userId)
.then((response) => {
this.modifiedCount += 1;
this.list = this.list.filter((user) => user.userId !== userId);
resolve(response);
})
.catch((error) => {
this.saving = false;
toast.error('Error deleting user');
reject(error);
})
.finally(() => {
this.saving = false;
});
});
}
copyInviteCode(userId: string): void {
const content = this.list.find((u) => u.userId === userId)?.invitationLink;
if (content) {
copy(content);
toast.success('Invite code copied successfully');
} else {
toast.error('Invite code not found');
}
}
generateInviteCode(userId: string): Promise<any> {
this.saving = true;
const promise = new Promise((resolve, reject) => {
userService
.generateInviteCode(userId)
.then((response) => {
const index = this.list.findIndex((u) => u.userId === userId);
if (index > -1) {
this.list[index].updateKey('isExpiredInvite', false);
this.list[index].updateKey(
'invitationLink',
response.invitationLink
);
}
resolve(response);
})
.catch((error) => {
this.saving = false;
reject(error);
})
.finally(() => {
this.saving = false;
});
});
toast.promise(promise, {
pending: 'Generating an invite code...',
success: 'Invite code generated successfully',
});
return promise;
}
}

View file

@ -61,14 +61,24 @@ function MyApp() {
#### Options
```js
```ts
trackerAssist({
callConfirm?: string|ConfirmOptions;
controlConfirm?: string|ConfirmOptions;
config?: object;
onAgentConnect?: () => (()=>void | void);
onCallStart?: () => (()=>void | void);
onRemoteControlStart?: () => (()=>void | void);
onAgentConnect: StartEndCallback;
onCallStart: StartEndCallback;
onRemoteControlStart: StartEndCallback;
onRecordingRequest?: (agentInfo: Record<string, any>) => any;
onCallDeny?: () => any;
onRemoteControlDeny?: (agentInfo: Record<string, any>) => any;
onRecordingDeny?: (agentInfo: Record<string, any>) => any;
session_calling_peer_key: string;
session_control_peer_key: string;
callConfirm: ConfirmOptions;
controlConfirm: ConfirmOptions;
recordingConfirm: ConfirmOptions;
socketHost?: string;
config: RTCConfiguration;
serverURL: string
callUITemplate?: string;
})
```