Calling Twilio Voice API with Blazor - c#

I am working on a project with blazor server and I am trying to make a voice call using Twilio.
I followed the documentation online from twilio for this to the letter but getting this error:
System.Net.Http.HttpRequestException: Response status code does not indicate success: 404 (Not Found).
at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
at QUBeMyGuest.Pages.GuestArrivals.EmergencyContact.GetClientToken() in C:\Pages\GuestArrivals\EmergencyContact.razor:line 98
at QUBeMyGuest.Pages.GuestArrivals.EmergencyContact.OnAfterRenderAsync(Boolean firstRender) in C:\Users\\Pages\GuestArrivals\EmergencyContact.razor:line 58
at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle)
The documentation stated that I should open a new asp.net core api project within my solution and add this class to the controller folder:
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Twilio.Jwt;
using Twilio.Jwt.Client;
using Twilio.TwiML;
using Twilio.Types;
using System.Net.Http;
namespace api.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TwilioBackEndController : ControllerBase
{
public readonly string AccountSid = "xxxxxxxxxx";
public readonly string AuthToken = "xxxxxxxx";
public readonly string AppSid = "xxxxxxxx";
public readonly string PhoneNumber = "xxxxxxxx";
[HttpGet("token")]
public async Task<IActionResult> GetToken()
{
var scopes = new HashSet<IScope>
{
new OutgoingClientScope(AppSid),
new IncomingClientScope("tester")
};
var capability = new ClientCapability(AccountSid, AuthToken, scopes: scopes);
return await Task.FromResult(Content(capability.ToJwt(), "application/jwt"));
}
[HttpPost("voice")]
public async Task<IActionResult> PostVoiceRequest([FromForm] string phone)
{
var destination = !phone.StartsWith('+') ? $"+{phone}" : phone;
var response = new VoiceResponse();
var dial = new Twilio.TwiML.Voice.Dial
{
CallerId = PhoneNumber
};
dial.Number(new PhoneNumber(destination));
response.Append(dial);
return await Task.FromResult(Content(response.ToString(), "application/xml"));
}
}
}
I then set up the page to make the call with my blazor project
#page "/guest/emergencycall"
#using System.ComponentModel.DataAnnotations
#inject HttpClient httpClient
#using Microsoft.Extensions.DependencyInjection
#using System.Net.Http
<EditForm Model="Input" OnValidSubmit="InitiatePhoneCall">
<DataAnnotationsValidator />
<ValidationSummary />
<p>
<label for="phoneNumber">Enter Phone Number:</label>
<InputText id="phoneNumber" #bind-Value="Input.PhoneNumber"></InputText>
<button type="submit" class="btn btn-primary" disabled="#IsDialDisabled">DIAL</button>
<button type="button" id="endBtn" class="btn btn-primary" disabled="#IsEndDisabled" #onclick="EndPhoneCall">END</button>
<button type="button" id="clearBtn" class="btn btn-primary" disabled="#IsClearDisabled" #onclick="ClearPhoneNumber">CLEAR</button>
</p>
</EditForm>
<hr />
#if (Logs.Count == 0)
{
<p>No Logs available yet</p>
}
else
{
<ul>
#foreach (var log in Logs)
{
<li>#log</li>
}
</ul>
}
#code {
private string _tokenUrl = "https://xxxxxxxxxxxxxxx";
private bool appSetupRun = false;
protected bool IsDialDisabled { get; set; } = false;
protected bool IsEndDisabled { get { return !IsDialDisabled; } }
protected bool IsClearDisabled { get { return string.IsNullOrEmpty(Input.PhoneNumber); } }
protected List<string> Logs { get; set; } = new List<string>();
protected InputModel Input { get; set; } = new InputModel();
[Inject]
protected IJSRuntime JSRuntime { get; set; }
[Inject]
protected IHttpClientFactory HttpClientFactory { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && !appSetupRun)
{
var token = await GetClientToken();
await JSRuntime.InvokeVoidAsync("appFunctions.setup", token);
appSetupRun = true;
}
}
protected async Task InitiatePhoneCall()
{
IsDialDisabled = true;
await LogMessage($"Calling the number {Input.PhoneNumber}");
await JSRuntime.InvokeVoidAsync("appFunctions.placeCall", Input.PhoneNumber);
await LogMessage($"Called the number {Input.PhoneNumber}");
StateHasChanged();
}
protected async Task EndPhoneCall()
{
IsDialDisabled = false;
await LogMessage($"Ending the call to {Input.PhoneNumber}");
await JSRuntime.InvokeVoidAsync("appFunctions.endCall");
await LogMessage($"Ended the call to {Input.PhoneNumber}");
StateHasChanged();
}
protected async Task ClearPhoneNumber()
{
await LogMessage("Clearing the phone number entry");
Input.PhoneNumber = string.Empty;
await LogMessage("Cleared the phone number entry");
StateHasChanged();
}
private async Task<string> GetClientToken()
{
var uri = new Uri(_tokenUrl);
using var client = HttpClientFactory.CreateClient();
var response = await client.GetAsync(uri);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
[JSInvokable]
public async Task LogMessage(string message)
{
Logs.Add($"{DateTimeOffset.Now} - {message}");
await Task.CompletedTask;
}
public class InputModel
{
[Required]
[Phone(ErrorMessage = "Please enter your phone number in a proper format")]
public string PhoneNumber { get; set; }
}
}
and then added this JS function:
window.appFunctions = {
setup: function (token) {
console.log('Getting connected');
// Setup Twilio Device
Twilio.Device.setup(token);
Twilio.Device.ready(() => {
console.log('We are connected and ready to do the thing');
});
Twilio.Device.error((err) => {
console.error('This should not have been reached. We need to do something here');
console.error(err);
});
},
placeCall: function (destination) {
console.log(`Calling ${destination}`);
Twilio.Device.connect({ phone: destination });
console.log(`Successfully called ${destination}`);
},
endCall: function () {
console.log('Ending the call');
Twilio.Device.disconnectAll();
console.log('Successfully ended the call');
}
};
in my startup file I then added this, but doesn't seem to have made a difference:
services.AddHttpClient();
if (!services.Any(x => x.ServiceType == typeof(HttpClient)))
{
services.AddScoped<HttpClient>(s =>
{
var uriHelper = s.GetRequiredService<NavigationManager>();
return new HttpClient
{
BaseAddress = new Uri(uriHelper.BaseUri)
};
});
}
Any suggestions on what I could be doing wrong?

When seeing a 404, the first thing that should come to mind is an incorrect URL. Since the 404 is coming from GetClientToken in your razor file. That means your _tokenUrl is probably incorrect. Looking at your controller, your URL should look something like https://{host}/token/. It's possible that you hardcoded the link in the documentation, meanwhile, it's supposed to be based on your controller.
PS: HTTP 404 means that the requested resource cannot be found. Get familiar with other HTTP response codes too, might save you a ton of debugging.

Related

How can I directly call the service method in index.js

I would like to know if I can call the service method in index.js directly. Instead of going thru index.razor page. Because the content I am transferring from index.razor to index.js is huge in size (high MBs) and takes a long time to go between these pages. I would rather like to skip index.razor and directly call service method in index.js. Sample code below:
FileRepository:
public async Task<Block> GetFile(string fileName)
{
....
res.Byte = data;
return res;
}
FileService:
public async Task<Block> GetFile(string fileName)
{
var response = await_httpClient.GetAsync($"api/data/{fileName}");
var result = await response.Content.ReadFromJsonAsync<Block>();
return result
}
Passing the result contentStr to index.js
Index.razor:
private Block result;
private async Task open(string fileName)
{
contentStr = "";
do
{
result = await FileService.GetFile(fileName);
contentStr = contentStr + System.Text.Encoding.Default.GetString(result.Byte);
await JSRuntime.InvokeVoidAsync("Chart", contentStr); // calls `chart `method in index.js
} while (contentLength == something);
}
I would like to directly call FileService.GetFile below in index.js, instead of contentStr is being transferred from index.razor.
Index.js:
window.Chart = (contentStr) => {
var data = contentStr
}
Edited:
public class Block
{
public byte[] Byte { get; set; }
public long contentLength { get; set; }
}
Following this document and I had code below in my .razor component.
By the way, document mentioned The .NET method must be public, static, and have the [JSInvokable] attribute.
#inject IJSRuntime JS
<div>Current Content: #currentContent</div>
<button class="btn btn-primary" #onclick="addAppend">Click me</button>
#code {
private string currentContent = "hello";
private async Task addAppend()
{
currentContent = await JS.InvokeAsync<string>("changeText", currentContent);
await JS.InvokeVoidAsync("logContent");
}
[JSInvokable]
public static string getContent()
{
return "Hello world";
}
}
And I had this script in index.html page.
<script>
window.changeText = (strData) => {
var temp = "+ append Content";
var res = strData + temp;
return res;
};
window.logContent = () => {
DotNet.invokeMethodAsync('BlazorWsamAad', 'getContent')
.then(data => {
console.log(data);
alert(data);
});
};
</script>
And this is the test result.

How do I instantiate a C# service and use its data in a Blazor component

I am using Blazor and I am a beginner.
Currently I have several Blazor components that all need the same JSON data (data fetched from one URL, e.g., http://localhost/todo)
Instead of fetching the same URL inside all my components and duplicating my code, I decided to create a Service that fetchs the URL and share this service's output accross my components.
Here is the service, and all it does fetch a URL and return a JSON object (at least that is what I am trying to do)
using TodoApp.Models.TodoTestModel;
using Newtonsoft.Json;
using System.Net.Http.Headers;
namespace Todo.Services
{
public class Todo
{
public string TodoURL { get; set; }
public object result { get; set; }
async public Task<object> TodoTypes()
{
using (HttpClient client = new HttpClient())
{
// HTTP header stuff
HttpResponseMessage response = client.GetAsync(TodoURL).Result;
response.EnsureSuccessStatusCode();
string responseData = await response.Content.ReadAsStringAsync();
result = JsonConvert.DeserializeObject<TodoTestModel>(responseData);
return result;
}
}
}
}
}
The idea is call this service and give my components the output as
<TodoComponent ToDoResult="#result"> </ToDoComponent>
But I am having a problem when instanciating the object, e.g,:
#page "/"
#using TodoApp.Services;
#inject TodoApp.Services.Todo Todo;
<h5> List of todos </h5>
#code {
Todo td = new Todo();
td.TodoURL = ".." // does not work
}
In short I am trying to do the following:
Instanciate the class
Provide it a URL
Get the JSON data (result) to pass it into my TodoComponent
Thanks for the help
vaeon you can directly use client.GetJsonAsync and avoid the deserialization step in Vikram's example.The returned result set is not matching with the expected typeof List.
var response = await client.GetFromJsonAsync(TodoURL);
return response;
Program.cs
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using BlazorApp1;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped(sp => new TodoService());
await builder.Build().RunAsync();
TodoService.cs
namespace BlazorApp1;
public class TodoService
{
public TodoService()
{
}
public List<string> GetTodos()
{
//write your code here that fetches the todos using the TodosUrl
throw new NotImplementedException();
}
public string TodosUrl { get; set; } = "https://google.com";
}
TodosComponent
#inject TodoService TodoService
<ul>
#foreach (var todo in Todos)
{
<li>#todo</li>
}
</ul>
#code {
public List<string> Todos { get; set; }
protected override void OnInitialized()
{
TodoService.TodosUrl = "https://stackoverflow.com";
Todos = TodoService.GetTodos();
base.OnInitialized();
}
}
Add below code before await builder.Build().RunAsync(); in the Program.cs file.
Program.cs:
...
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new
Uri(builder.HostEnvironment.BaseAddress) });
// Register services
builder.Services.AddScoped<TodoService>(); // <----- Add this line
await builder.Build().RunAsync();
TodoService.cs:
public class TodoService
{
public string TodoURL { get; set; }
public async Task<List<TodoTestModel>> GetTodoTypesAsync()
{
using (HttpClient client = new HttpClient())
{
// HTTP header stuff
HttpResponseMessage response = await client.GetAsync(TodoURL);
response.EnsureSuccessStatusCode();
string responseData = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<List<TodoTestModel>>(responseData);
}
}
}
In TodoComponent.razor file, add the below code:
#page "/"
#using TodoApp.Services;
#inject TodoService TodoService;
<h5> List of todos </h5>
#if(todoList is not null && todoList.Any())
{
<ul>
#foreach(var item in todoList)
{
<li>#item.Name</li>
}
</ul>
}
#code {
private List<TodoTestModel> todoList;
protected override async Task OnInitializedAsync()
{
TodoServices.TodoURL = ""; // TODO: set the API url
todoList = await TodoService.GetTodoTypesAsync();
await base.OnInitializedAsync();
}
}

Blazor Server - Local Storage after login

I'm working on a Blazor Server project using the default Microsoft Identity Platform.
My goal is to get/create a user in my db and save it to local storage after microsoft login is completed.
In the startup I'm sucessfully able to use the OnTokenValidated event to do some action after login. However it's not possible to write to local storage in this stage since the page isn't rendered yet.
I'd like to do something like this which is possible with Webassembly.
<RemoteAuthenticatorView Action="#Action" OnLogInSucceeded="SomeCode" />
Does anyone know a way to do this without using a solution like adding OnAfterRenderAsync in the MainLayout, which will fire on each page reload. I'd like to call a method after the Identity login redirects back to my site in a state where LocalStorage is accessible.
You do need to put some code in OnAfterRenderAsync, but in this demo I've put it in App and check if the component has already rendered.
Here's a demo and some code to interact with LocalStorage. You should be able to adapt it to fit your needs.
First a service to encapsulate getting and setting to Local Storage
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
public class LocalStorageService
{
private readonly ProtectedLocalStorage _storage;
public LocalStorageService(ProtectedLocalStorage storage)
=> _storage = storage;
public async ValueTask<CommandResult> SaveAsync<TRecord>(CommandRequest<TRecord> request)
{
if (request.Record is not null)
await _storage.SetAsync(request.StorageName, request.Record);
// No return so we return success!
return CommandResult.Success();
}
public async ValueTask<RecordQueryResult<TRecord>> ReadAsync<TRecord>(RecordQueryRequest<TRecord> request)
{
// We need to cover the situation were the component calling this is in the initial page
// and Blazor server is trying to statically render the page
try
{
var result = await _storage.GetAsync<TRecord>(request.StorageName);
return new RecordQueryResult<TRecord> { Successful = result.Success, Record = result.Value, Message = $"Failed to retrieve a value for {request.StorageName}" };
}
catch
{
return new RecordQueryResult<TRecord> { Successful = false, Message = $"Failed to retrieve a value for {request.StorageName}" };
}
}
}
The CQS Request and Result objects:
public record CommandRequest<TRecord>(string StorageName, TRecord Record);
public record RecordQueryRequest<TRecord>(string StorageName);
public record CommandResult
{
public bool Successful { get; init; }
public string Message { get; init; } = string.Empty;
public static CommandResult Success()
=> new CommandResult { Successful = true };
public static CommandResult Failure(string message)
=> new CommandResult { Successful = false };
}
public record RecordQueryResult<TRecord>
{
public TRecord? Record { get; init; }
public bool Successful { get; init; }
public string Message { get; init; } = string.Empty;
public static RecordQueryResult<TRecord> Success(TRecord record)
=> new RecordQueryResult<TRecord> { Record = record, Successful = true };
public static RecordQueryResult<TRecord> Failure(string message)
=> new RecordQueryResult<TRecord> { Successful = false };
}
Registered like this:
builder.Services.AddScoped<LocalStorageService>();
My simple Data:
public record TestData( string LastSaved);
Add code to App to set as if you are getting data after login. This implements a custom after render handler.
#inject LocalStorageService Service
#implements IHandleAfterRender
<CascadingAuthenticationState>
<Router AppAssembly="#typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="#routeData" DefaultLayout="#typeof(MainLayout)" />
<FocusOnNavigate RouteData="#routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="#typeof(MainLayout)">
<p role="alert">Sorry, theres nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
#code {
private bool _hasCalledOnAfterRender;
// implements a custom IHandleAfterRender handler
async Task IHandleAfterRender.OnAfterRenderAsync()
{
// Only do if first render and the data in local storage is empty
if (!_hasCalledOnAfterRender && !await GetData())
{
var newData = new TestData($"Saved at {DateTime.Now.ToLongTimeString()}");
var result = await this.Service.SaveAsync<TestData>(new CommandRequest<TestData>("TestData", newData));
_hasCalledOnAfterRender = true;
}
}
private async Task<bool> GetData()
{
var result = await this.Service.ReadAsync<TestData>(new RecordQueryRequest<TestData>("TestData"));
return result?.Successful ?? false;
}
}
And my test route/page to display the data.
#page "/"
#inject LocalStorageService Service
#implements IDisposable
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
<div class="bg-black text-white m-3">
Test Data Last Saved at : #this.data.LastSaved
</div>
<div class="m-3">
<button class="btn btn-primary" #onclick=SaveToLocal>Save Data to Local</button>
</div>
#code {
private TestData data = new TestData(string.Empty);
protected override async Task OnInitializedAsync()
{
await this.GetData();
this.Service.StorageChanged += this.DataChanged;
}
private async void DataChanged(object? sender, EventArgs e)
{
await this.GetData();
await this.InvokeAsync(StateHasChanged);
}
private async Task<bool> GetData()
{
var result = await this.Service.ReadAsync<TestData>(new RecordQueryRequest<TestData>("TestData"));
data = result?.Record ?? new TestData(string.Empty);
return result?.Successful ?? false;
}
private async Task SaveToLocal()
{
var newData = new TestData($"Saved at {DateTime.Now.ToLongTimeString()}");
var result = await this.Service.SaveAsync<TestData>(new CommandRequest<TestData>("TestData", newData));
await this.GetData();
}
public void Dispose()
=> this.Service.StorageChanged -= this.DataChanged;
}

ChatMessages.Add(message) is not hit

I'm getting started with simple signalR application. When user puts his "name" and "room". It is sent to my hub
//This is Index page
public async Task<ActionResult> OnPost()
{
UserConnection userConnection = new()
{
Name = UserInput,
Room = JoinRoomInput
};
await _hubConnection.SendAsync("JoinRoom", userConnection);
return RedirectToPage("ChatGroup");
}
my chat looks like this
public class ChatHub : Hub<IChatClient>
{
private readonly string _botUser;
public ChatHub()
{
_botUser = "MyChat Bot";
}
public async Task JoinRoom(UserConnection userConnection)
{
await Groups.AddToGroupAsync(Context.ConnectionId, userConnection.Room);
await Clients.Group(userConnection.Room).ReceiveMessage(_botUser, $"{userConnection.Name} has joined {userConnection.Room}");
}
public async Task SendMessage(UserConnection userConnection, string message)
{
await Clients.Group(userConnection.Room).ReceiveMessage($"{userConnection.Name} : ", message);
}
}
So then, the use posts his details and hit joinroom. He is redirected to another razor page called Chatgroup.In this page, He should get the message as defined in "JoinRoom" in ChatHub.
//This is Chatgroup page
private readonly HubConnection _hubConnection;
public List<string> ChatMessages { get; set; }
public ChatGroupModel(HubConnection hubConnection)
{
_hubConnection = hubConnection;
}
public void OnGet()
{
_hubConnection.On<string>("ReceiveMessage", (message) => //Breakpoint hits here
{
ChatMessages.Add(message); // When placed a breakpoint here, It is skipped.
});
}
I'm getting ChatMessages is Null error.
So my question is how will my client side code _hubConnection.On<> get the response from chathub?
I think this is the issue.
I've registered my signalr in startup class like this
services.AddTransient<HubConnection>((ChatClient) => {
var hubConnection = new HubConnectionBuilder().WithUrl("https://localhost:44389/chathub").Build();
hubConnection.Closed += async (error) =>
{
await Task.Delay(new Random().Next(0, 5) * 1000);
await hubConnection.StartAsync();
};
hubConnection.StartAsync();
return hubConnection;
});
I get a null exception error in my razor page. I think it's because _hubConnection.On<>... skips chatmessage.add.
#foreach(var messages in Model.ChatMessages) //ChatMessages is Null
{
<div>
#messages
</div>
}

Having an Issue when I use the function for a second time with Blazor Server side and Twilio

I have made a website with Blazor Server side. I am using Twilio Voice Api to enable the user to make an emergency call. I have been able to make the call successfully. When I leave the page and then go back to the page to make the call again I am receiving this error:
Error: Microsoft.JSInterop.JSException: Cannot read property 'setToken' of undefined
TypeError: Cannot read property 'setToken' of undefined
at a.register (https://media.twiliocdn.com/sdk/js/client/v1.3/twilio.min.js:39:420)
at Function.setup (https://media.twiliocdn.com/sdk/js/client/v1.3/twilio.min.js:47:126)
at Object.setup (https://localhost:44320/js/tw.js:6:23)
at https://localhost:44320/_framework/blazor.server.js:8:31619
at new Promise (<anonymous>)
at e.beginInvokeJSFromDotNet (https://localhost:44320/_framework/blazor.server.js:8:31587)
at https://localhost:44320/_framework/blazor.server.js:1:20052
at Array.forEach (<anonymous>)
at e.invokeClientMethod (https://localhost:44320/_framework/blazor.server.js:1:20022)
at e.processIncomingData (https://localhost:44320/_framework/blazor.server.js:1:18006)
at Microsoft.JSInterop.JSRuntime.InvokeWithDefaultCancellation[T](String identifier, Object[] args)
at Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(IJSRuntime jsRuntime, String identifier, Object[] args)
at QUBeMyGuest.Pages.GuestArrivals.EmergencyContact.OnAfterRenderAsync(Boolean firstRender) in C:Pages\GuestArrivals\EmergencyContact.razor:line 75
at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle)
My api is
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Twilio.Jwt;
using Twilio.Jwt.AccessToken;
using Twilio.Jwt.Client;
using Twilio.TwiML;
using Twilio.Types;
using System.Net.Http;
namespace api.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TwilioBackEndController : ControllerBase
{
public readonly string AccountSid = "xxxxxx";
public readonly string AuthToken = "xxxx";
public readonly string AppSid = "xxxxx";
public readonly string PhoneNumber = "xxxxx";
[HttpGet("token")]
public async Task<IActionResult> GetToken()
{
var scopes = new HashSet<IScope>
{
new OutgoingClientScope(AppSid),
new IncomingClientScope("tester")
};
var capability = new ClientCapability(AccountSid, AuthToken, scopes: scopes);
return await Task.FromResult(Content(capability.ToJwt(), "application/jwt"));
}
[HttpPost("voice")]
public async Task<IActionResult> PostVoiceRequest([FromForm] string phone)
{
var destination = !phone.StartsWith('+') ? $"+{phone}" : phone;
var response = new VoiceResponse();
var dial = new Twilio.TwiML.Voice.Dial
{
CallerId = PhoneNumber
};
dial.Number(new PhoneNumber(destination));
response.Append(dial);
return await Task.FromResult(Content(response.ToString(), "application/xml"));
}
}
}
My emergency contact page is:
#page "/guest/emergencycall"
#using System.ComponentModel.DataAnnotations
#inject HttpClient httpClient
#using Microsoft.Extensions.DependencyInjection
#using System.Net.Http
<EditForm Model="Input" OnValidSubmit="InitiatePhoneCall">
<DataAnnotationsValidator />
<ValidationSummary />
<p>
<label for="phoneNumber">Enter Phone Number:</label>
<InputText id="phoneNumber" #bind-Value="Input.PhoneNumber"></InputText>
<button type="submit" class="btn btn-primary" disabled="#IsDialDisabled">DIAL</button>
<button type="button" id="endBtn" class="btn btn-primary" disabled="#IsEndDisabled" #onclick="EndPhoneCall">END</button>
<button type="button" id="clearBtn" class="btn btn-primary" disabled="#IsClearDisabled" #onclick="ClearPhoneNumber">CLEAR</button>
</p>
</EditForm>
<hr />
#if (Logs.Count == 0)
{
<p>No Logs available yet</p>
}
else
{
<ul>
#foreach (var log in Logs)
{
<li>#log</li>
}
</ul>
}
#code {
private string _tokenUrl = "https://4b4cd1derdsb.ngrok.io/api/twiliobackend";
private bool appSetupRun = false;
protected bool IsDialDisabled { get; set; } = false;
protected bool IsEndDisabled { get { return !IsDialDisabled; } }
protected bool IsClearDisabled { get { return string.IsNullOrEmpty(Input.PhoneNumber); } }
protected List<string> Logs { get; set; } = new List<string>();
protected InputModel Input { get; set; } = new InputModel();
[Inject]
protected IJSRuntime JSRuntime { get; set; }
[Inject]
protected IHttpClientFactory HttpClientFactory { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && !appSetupRun)
{
var token = await GetClientToken();
await JSRuntime.InvokeVoidAsync("appFunctions.setup", token);
appSetupRun = true;
}
}
protected async Task InitiatePhoneCall()
{
IsDialDisabled = true;
await LogMessage($"Calling the number {Input.PhoneNumber}");
await JSRuntime.InvokeVoidAsync("appFunctions.placeCall", Input.PhoneNumber);
await LogMessage($"Called the number {Input.PhoneNumber}");
StateHasChanged();
}
protected async Task EndPhoneCall()
{
IsDialDisabled = false;
await LogMessage($"Ending the call to {Input.PhoneNumber}");
await JSRuntime.InvokeVoidAsync("appFunctions.endCall");
await LogMessage($"Ended the call to {Input.PhoneNumber}");
StateHasChanged();
}
protected async Task ClearPhoneNumber()
{
await LogMessage("Clearing the phone number entry");
Input.PhoneNumber = string.Empty;
await LogMessage("Cleared the phone number entry");
StateHasChanged();
}
private async Task<string> GetClientToken()
{
var uri = new Uri(_tokenUrl);
using var client = HttpClientFactory.CreateClient();
var response = await client.GetAsync(uri);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
[JSInvokable]
public async Task LogMessage(string message)
{
Logs.Add($"{DateTimeOffset.Now} - {message}");
await Task.CompletedTask;
}
public class InputModel
{
[Required]
[Phone(ErrorMessage = "Please enter your phone number in a proper format")]
public string PhoneNumber { get; set; }
}
}
and my javascript:
window.appFunctions = {
setup: function (token) {
console.log('Getting connected');
// Setup Twilio Device
Twilio.Device.setup(token);
Twilio.Device.ready(() => {
console.log('We are connected and ready to do the thing');
});
Twilio.Device.error((err) => {
console.error('This should not have been reached. We need to do something here');
console.error(err);
});
},
placeCall: function (destination) {
console.log(`Calling ${destination}`);
Twilio.Device.connect({ phone: destination });
console.log(`Successfully called ${destination}`);
},
endCall: function () {
console.log('Ending the call');
Twilio.Device.disconnectAll();
console.log('Successfully ended the call');
}
};
Any suggestions would be great
From my little experience
when Microsoft.JSInterop.JSException occur , it's often from when you call javascript
In your code please validate javascript functions which called from JSRuntime
[Inject]
protected IJSRuntime JSRuntime { get; set; }
...
await JSRuntime.InvokeVoidAsync("appFunctions.setup", token);
await JSRuntime.InvokeVoidAsync("appFunctions.placeCall", Input.PhoneNumber);
await JSRuntime.InvokeVoidAsync("appFunctions.endCall");
and becuase setToken keyword is always used for communication setup 's function name between web and api. I guess in javascript function appFunctions.setup , some object became null after you go back to the page so It can't initialize.
as you can see in
TypeError: Cannot read property 'setToken' of undefined
at a.register (https://media.twiliocdn.com/sdk/js/client/v1.3/twilio.min.js:39:420)
at Function.setup (https://media.twiliocdn.com/sdk/js/client/v1.3/twilio.min.js:47:126)
at Object.setup (https://localhost:44320/js/tw.js:6:23)
your javascript function in file tw.js(may be line 23?) is going to use some null object for set up communicate?

Categories

Resources