## Consumer API documentation for Foxy Live V2
### Steps to start a call
0. Check if at least one artist is available to take a call
```javascript=
function ensureAtleastOneArtist() {
let noArtistsURL = `/live/plugin/no_artists/?brand_id=${localStorage.getItem('brand_id')}&u=${params.get('u')}&referrer=${localStorage.getItem('referrer')}`;
let artistAvailabilityURL = `${window.live_api_host}/v1/artist_availabilities/?store_id=${localStorage.getItem('selected-store') || ''}&brand_id=${localStorage.getItem('brand_id') || ''}&artist_id=${params.get('artist_id') || ''}`;
return new Promise((resolve, reject) => {
fetch(artistAvailabilityURL, {
method: 'GET',
headers: {
'x-auth-token': localStorage.getItem('foxy_token')
}
})
.then((response) => response.json())
.then((response) => {
if (!response.artists_available) {
logEvent('artist_availability_check_failed');
window.location = noArtistsURL;
} else {
logEvent('artist_availability_check_succeeded');
resolve(this);
}
})
.catch((error) => {
logEvent('artist_availability_check_errored', {
error: error
});
window.location = noArtistsURL;
})
});
}
```
0.1 Search for a store and select one either according to brand, or location.
```javascript=
fetch(`${window.live_api_host}/v1/beauty_stores/?coords=${coords || ''}&brand_id=${localStorage.getItem('brand_id') || ''}`)
.then(response => response.json())
.then(response => {
const body = $('body');
const detectPrompt = body.find('#dev-detect-prompt');
logEvent('search_api_success');
// save for later use
localStorage.setItem('store_search_results', JSON.stringify(response));
// render results
detectPrompt.hide();
if (!initiator || initiator === 'google') {
renderSearchResults(response)
} else if (initiator === 'auto') {
let selectedStore = response.data[0];
if (selectedStore) {
selectStore(selectedStore.id, false);
}
}
})
.catch(error => {
console.error(error);
logEvent('search_api_success', {
error: error
});
})
```
1. Create a One On One Session.
```javascript=
let response = await fetch(`${Constants.api_host}/v1/one_on_one_sessions`, {
method: 'POST',
headers: Constants.default_headers,
body: JSON.stringify({
one_on_one_session: {
beauty_store_id: encodedData.beauty_store_id,
coords: encodedData.coords,
primary_artist_ids: encodedData.primary_artist_ids ? encodedData.primary_artist_ids : null,
secondary_artist_ids: encodedData.secondary_artist_ids ? encodedData.secondary_artist_ids : null,
invite_id: encodedData.invite_id,
...Constants.utm_params
}
})
});
```
primary and secondary artists are only specified if you want to skip routing and call a particular artist. Invite ID is only present if the call originated from an invite link. UTM params are always present.
2. Use the client settings received in response to create and join an Agora RTC room and Agora RTM room.
```json=
{
"data": {
"id": "1816",
"type": "oneOnOneSession",
"attributes": {
"status": "created",
"callAcceptedAtInWords": null,
"createdAtInWords": "less than a minute ago",
"isGuestSession": false,
"duration": null,
"createdAt": "1604042228",
"clientSettingsForCustomer": {
"mode": "rtc",
"codec": "vp8",
"appId": "907a2273d4e344a997ca5153eb92d9b5",
"channel": "88aefccd-8f2b-47a3-a7e9-208a5f85a957",
"rtc_token": "006907a2273d4e344a997ca5153eb92d9b5IACaBKcQ4kc1eYuVcpTKjjYxNoDEhMW9dmBH/6UgJXPpioOxVL4RAGHAIgAgesipdA+dXwQAAQD0YJ5fAgD0YJ5fAwD0YJ5fBAD0YJ5f",
"rtm_token": "006907a2273d4e344a997ca5153eb92d9b5IABXaxDFc2N8OSIlf+OyWkV4PwqHjJyIx2VELSYPUUrdKBEAYcAAAAAAEACq616CdA+dXwEA6AP0YJ5f",
"host_uid": null,
"customer_uid": "P3hgfU",
"created_at": "1604042228"
},
"clientSettingsForHost": {
"mode": "rtc",
"codec": "vp8",
"appId": "907a2273d4e344a997ca5153eb92d9b5",
"channel": "88aefccd-8f2b-47a3-a7e9-208a5f85a957",
"rtc_token": null,
"rtm_token": null,
"host_uid": null,
"customer_uid": "P3hgfU"
},
"callAcceptedAt": null,
"calEndedAt": null
},
"relationships": {
"user": {
"data": {
"id": "2962",
"type": "user"
}
},
"streamSession": {
"data": {
"id": "1816",
"type": "streamSession"
}
},
"agoraRoom": {
"data": {
"id": "984",
"type": "agoraRoom"
}
}
}
},
"included": [
{
"id": "984",
"type": "agoraRoom",
"attributes": {
"channelName": "88aefccd-8f2b-47a3-a7e9-208a5f85a957"
}
},
{
"id": "1816",
"type": "streamSession"
}
]
}
```
3. Create a websocket connection with CallsChannel
```javascript=
import Constants from "../constants/index.js.erb";
import encodedData from "../utils/encodedData";
import logEvent from "../utils/events";
class CallsChannel {
constructor(onCallStatusChange) {
this.createConsumer();
this.onCallStatusChange = onCallStatusChange
}
createConsumer = () => {
logEvent('websocket_create_consumer', {channel: 'CallsChannel'});
this.socket = new WebSocket(`${Constants.action_cable.host}?auth_token=${encodedData.auth_token}`);
this.socket.onopen = this.subscribe;
this.socket.onclose = this.onClose;
this.socket.onmessage = this.onMessage;
this.socket.onerror = this.onError;
};
subscribe = () => {
logEvent('websocket_subscribe', {channel: 'CallsChannel'});
if (!encodedData || !encodedData.auth_token) {
console.error('Skipping websocket connection, no auth token available');
return;
}
const message = {
command: 'subscribe',
identifier: JSON.stringify({
channel: 'CallsChannel'
}),
};
this.socket.send(JSON.stringify(message))
};
onClose = () => {
logEvent('websocket_disconnected', {channel: 'CallsChannel'});
window.setTimeout(() => {
logEvent('websocket_reconnect_attempt', {channel: 'CallsChannel'});
this.createConsumer();
}, 5000);
};
onMessage = (message) => {
let data = JSON.parse(message.data);
// default rails payload have a type field,
// but everything we send goes inside message.
let type;
// ignore everything but message types
if (!Object.keys(data).includes('message') || (['ping', 'welcome', 'confirm_subscription'].includes(data.type))) {
return
}
logEvent('websocket_message', {channel: 'CallsChannel', message: JSON.stringify(data)});
if (data.message.type === 'status') {
if (data.message.id === parseInt(localStorage.getItem('one_on_one_session_id'))) {
this.onCallStatusChange(data.message);
localStorage.setItem('one_on_one_session_status', data.message.status);
}
};
if (data.message.type === 'accepted_artist') {
if (data.message.id === parseInt(localStorage.getItem('one_on_one_session_id'))) {
localStorage.setItem('accepted_artist', JSON.stringify(data.message))
}
};
if (data.message.type === 'timestamps') {
localStorage.setItem('call_started_at', data.message.call_started_at);
localStorage.setItem('call_created_at', data.message.created_at);
}
};
onError = (data) => {
console.error(data);
}
}
export default CallsChannel;
```
4. If all went well, make a API call to start ringing artists.
```javascript=
const url = `/v1/one_on_one_sessions/${localStorage.getItem('one_on_one_session_id')}/start_ringing/`;
let response = await fetch(Constants.api_host + url, {
method: 'POST',
headers: Constants.default_headers
});
```
4. On receiving a message of `type===status` and `status===streaming`, start the call. ( Hide the call preview screen, open the call screen)
5. On receiving a message of `type===status` and `status===failed_assignment`, go to the "no advisor found" screen.
6. After receving a message of `status===streaming`, expect a message of `type===accepted_artist` with details of the advisor that accepted the call.
7. Also expect a message of `type===timestamps` with details of call duration, when call started, etc.
Scenarios:
1. Customer hangs up when the call was in the `ringing_primary` or `ringing_secondary` state: make a hangup API call and exit the Agora rooms.
```javascript=
let response = await fetch(`${Constants.api_host}/v1/one_on_one_sessions/${oo_id}/`, {
method: 'PATCH',
headers: Constants.default_headers,
body: JSON.stringify({
call_action: 'hangup'
})
});
```
2. customer hangs up when teh call was in the `streaming` state: same as above. The only difference is that in the first case, the call goes to 'customer_abandoned' state. In the second, it goes to 'completed'state.
3. The advisor leaves the call, and a `ParticipantLeftRoom` event is received from Agora: Make an API call to get the status of the call:
```javascript=
let response = await fetch(`${Constants.api_host}/v1/one_on_one_sessions/${localStorage.getItem('one_on_one_session_id')}/`, {
method: 'GET',
headers: Constants.default_headers
});
```
Unless the status is `completed`, show a message to the customer telling them the advisor will be back. If the status is `completed`, go to the feedback screen.
4. A `TokenWillExpire` event is receieved from Agora: refresh token and pass it to Agora Client.
```javascript=
let response = await fetch(`${Constants.api_host}/v1/one_on_one_sessions/${localStorage.getItem('one_on_one_session_id')}/renew_agora_token`, {
method: 'POST',
headers: Constants.default_headers
});
```
5. If the app crashes, or something unexpected happens, make an API call for ongoing calls that are not stale and attempt to rejoin. ( Postman )
### feedback screen
1. Make an API call after collecting feedback to save it
```javascript=
let response = await fetch(`${Constants.api_host}/v1/one_on_one_session_ratings/`, {
method: 'POST',
headers: Constants.default_headers,
body: JSON.stringify({
one_on_one_session_rating: {
one_on_one_session_id: oneOnOneSessionID,
advisorRating: advisorRating,
callRating: callRating,
comments: comments || 'No Comments'
}
})
});
```