Skip to content

Commit 48b3783

Browse files
authored
feat(templates): add voice-javascript-sdk template (#284)
* Create Voice JS SDK v2 quickstart * Linting * Prettier-ing * Add twilio.min.js to .eslintignore * Remove twilio.min.js, use jsDelivr * Fix poor copy/pasting * Putting the crucial script tag back into index.html :facepalm * Remove .min.js from .eslintignore
1 parent 77a422d commit 48b3783

28 files changed

+2817
-1
lines changed

.eslintignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
node_modules
2-
/docs
2+
/docs

templates.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,11 @@
160160
"name": "Voice Client JavaScript Sample Application",
161161
"description": "Set up a sample client-side Twilio web application"
162162
},
163+
{
164+
"id": "voice-javascript-sdk",
165+
"name": "Voice JavaScript SDK Sample Application",
166+
"description": "Set up a sample client-side Twilio web application with Twilio Voice JS SDK v2"
167+
},
163168
{
164169
"id": "conversations",
165170
"name": "conversations",

voice-javascript-sdk/.env

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# This application automatically initializes a server environment.
2+
# See: /admin/index.html
3+
# Be cautious adding values to this file as they will override the environment's values.
4+
5+
# description: Choose a name for your application
6+
# format: text
7+
# required: true
8+
APP_NAME=voice-javascript-sdk
9+
10+
# description: Set a password for your app. Users who want to use the admin interface will have to use this password to access it
11+
# format: text
12+
# required: true
13+
ADMIN_PASSWORD=default

voice-javascript-sdk/README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Twilio Voice JavaScript SDK
2+
3+
This is a collection of functions that will setup a sample environment for use with the Programmable Voice SDKs. These are specifically for use with **client side** implementations. This also provides an example web application to explore the Twilio Voice JavaScript SDK.
4+
5+
## Configuration
6+
7+
```bash
8+
twilio serverless:deploy
9+
```
10+
11+
Visit your deployed application admin page: https://YOUR-TWILIO-DOMAIN.twil.io/admin/index.html
12+
13+
The default password is `default`
14+
15+
## Environment variables
16+
17+
This Function expects the following environment variables set:
18+
19+
| Variable | Meaning | Required |
20+
| :----------- | :-------------------------------------------------------------------------------- | :------- |
21+
| `APP_NAME` | The name of this application | Yes |
22+
| `ADMIN_PASSWORD` | `/admin/index.html` is password protected, this is your password. | Yes |
23+
24+
## Development
25+
26+
In order to see your changes, you must deploy `twilio serverless:deploy`.
27+
28+
Variables set in [.env](./.env) will override environment variables set up in `/admin/index.html`
29+
30+
## Parameters
31+
32+
These functions don't expect any parameters passed.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
const assets = Runtime.getAssets();
2+
const { urlForSiblingPage } = require(assets['/admin/shared.js'].path);
3+
4+
class Actions {
5+
constructor(client, options) {
6+
this.client = client;
7+
this.options = options;
8+
}
9+
10+
async initialize() {
11+
let env = {};
12+
console.log('Creating TwiML Application');
13+
let results = await this.createTwimlApp(this.options);
14+
env = Object.assign(env, results);
15+
const voiceUrl = `https://${this.options.DOMAIN_NAME}${urlForSiblingPage(
16+
'voice-javascript-sdk-twiml-app',
17+
this.options.PATH,
18+
'..'
19+
)}`;
20+
console.log(
21+
`Wiring up TwiML Application ${env.TWIML_APPLICATION_SID} to the function: ${voiceUrl}`
22+
);
23+
await this.updateTwimlAppVoiceUrl({
24+
twimlApplicationSid: env.TWIML_APPLICATION_SID,
25+
voiceUrl,
26+
});
27+
console.log('Generating new REST API Key');
28+
results = await this.generateNewKey(this.options);
29+
env = Object.assign(env, results);
30+
const number = await this.chooseLogicalCallerId();
31+
results = await this.setCallerId({ number });
32+
env = Object.assign(env, results);
33+
env.INITIALIZED = 'voice-javascript-sdk-quickstart';
34+
env.INITIALIZATION_DATE = new Date().toISOString();
35+
return env;
36+
}
37+
38+
async createTwimlApp({ friendlyName }) {
39+
const result = await this.client.applications.create({ friendlyName });
40+
return {
41+
TWIML_APPLICATION_SID: result.sid,
42+
};
43+
}
44+
45+
async updateTwimlAppVoiceUrl({ twimlApplicationSid, voiceUrl }) {
46+
const app = await this.client
47+
.applications(twimlApplicationSid)
48+
.update({ voiceUrl });
49+
}
50+
51+
async generateNewKey({ friendlyName }) {
52+
const result = await this.client.newKeys.create({ friendlyName });
53+
return {
54+
API_KEY: result.sid,
55+
API_SECRET: result.secret,
56+
};
57+
}
58+
59+
async chooseLogicalCallerId() {
60+
const incomingNumbers = await this.client.incomingPhoneNumbers.list({
61+
limit: 1,
62+
});
63+
let number;
64+
if (incomingNumbers.length > 0) {
65+
number = incomingNumbers[0].phoneNumber;
66+
} else {
67+
const outgoingCallerIds = await this.client.outgoingCallerIds.list({
68+
limit: 1,
69+
});
70+
// Required right?
71+
number = outgoingCallerIds[0].phoneNumber;
72+
}
73+
return number;
74+
}
75+
76+
async setCallerId({ number }) {
77+
return {
78+
CALLER_ID: number,
79+
};
80+
}
81+
82+
async useExistingTwimlApp({ twimlApplicationSid }) {
83+
return {
84+
TWIML_APPLICATION_SID: twimlApplicationSid,
85+
};
86+
}
87+
}
88+
89+
module.exports = Actions;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
class AdminClient {
2+
constructor() {
3+
this.isReady = this.token !== null;
4+
}
5+
6+
async _handleResponse(response) {
7+
if (!response.ok) {
8+
if (response.status === 403) {
9+
console.warn('Invalid token, resetting client');
10+
this.token = null;
11+
this.isReady = false;
12+
}
13+
// Throw an error
14+
// eslint-disable-next-line no-throw-literal
15+
throw {
16+
statusCode: response.status,
17+
message: await response.text(),
18+
};
19+
}
20+
}
21+
22+
async _post(url, obj) {
23+
const response = await fetch(url, {
24+
method: 'POST',
25+
headers: {
26+
Accept: 'application/json',
27+
'Content-Type': 'application/json',
28+
},
29+
body: JSON.stringify(obj),
30+
});
31+
await this._handleResponse(response);
32+
return response.json();
33+
}
34+
35+
async login(password) {
36+
try {
37+
const result = await this._post('./login', { password });
38+
this.token = result.token;
39+
this.isReady = true;
40+
} catch (err) {
41+
console.error(`${err.statusCode}: ${err.message}`);
42+
}
43+
return this.token !== null;
44+
}
45+
46+
async postAction(action) {
47+
const bodyObj = {
48+
token: this.token,
49+
action,
50+
};
51+
return this._post('./perform-action', bodyObj);
52+
}
53+
54+
async fetchState() {
55+
const response = await fetch(
56+
`./check-status?token=${encodeURIComponent(this.token)}`
57+
);
58+
await this._handleResponse(response);
59+
return response.json();
60+
}
61+
62+
get token() {
63+
return localStorage.getItem('adminToken');
64+
}
65+
66+
set token(adminToken) {
67+
if (adminToken !== null && adminToken !== undefined) {
68+
localStorage.setItem('adminToken', adminToken);
69+
} else {
70+
localStorage.removeItem('adminToken');
71+
}
72+
}
73+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
body {
2+
background: #f9f9f9;
3+
color: #253856;
4+
font-weight: 300;
5+
font-size: 62.5%;
6+
padding: 0;
7+
margin: 0;
8+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
9+
Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
10+
}
11+
12+
#app {
13+
max-width: 720px;
14+
margin: 0 auto;
15+
font-size: 1.1rem;
16+
background: white;
17+
padding: 12px 24px 32px;
18+
border-radius: 4px;
19+
box-shadow: 0px 10px 25px rgba(0, 0, 0, 0.04);
20+
}
21+
22+
ul {
23+
list-style-type: none;
24+
margin: 0;
25+
padding: 0;
26+
}
27+
28+
li:not(:first-of-type) {
29+
margin-top: 40px;
30+
}
31+
32+
.title {
33+
background: #4c576f;
34+
color: white;
35+
border-radius: 4px;
36+
font-size: 1.1rem;
37+
line-height: 1.3;
38+
font-weight: 400;
39+
padding: 12px 16px;
40+
}
41+
42+
.title:before {
43+
float: left;
44+
margin: 0 10px 0 0;
45+
}
46+
47+
.title p {
48+
margin: 0;
49+
}
50+
51+
.invalid {
52+
background: #d2262f;
53+
}
54+
55+
.invalid:before {
56+
content: '✕';
57+
font-size: 1.2rem;
58+
line-height: 1.4rem;
59+
}
60+
61+
.valid {
62+
background: #2dae5b;
63+
}
64+
65+
.valid:before {
66+
content: '✓';
67+
font-size: 1.5rem;
68+
line-height: 1.3rem;
69+
}
70+
71+
code {
72+
background: rgba(0, 0, 0, 0.12);
73+
color: #3a4860;
74+
padding: 3px 6px;
75+
border-radius: 3px;
76+
vertical-align: 2px;
77+
font-weight: 400;
78+
display: inline-block;
79+
}
80+
81+
.title code {
82+
color: white;
83+
background: rgba(0, 0, 0, 0.2);
84+
}
85+
86+
button {
87+
font-size: 1.1rem;
88+
border-radius: 4px;
89+
padding: 8px 12px;
90+
cursor: pointer;
91+
}
92+
93+
p a {
94+
color: #2492f8;
95+
}

0 commit comments

Comments
 (0)