Building an API service can be complex and time-intensive, but a few well-positioned API services and open source libraries can reduce developer frustration and accelerate your time-to-market. In this post, you’ll create a complete API service plus a site to consume it and then drop in a few clever additions to smooth your way.
When you’re done with the example API service you’ll be able to:
- Allow user registration, with Stormpath as middleware
- Store user account data securely with Stormpath
- Create API Keys for the user with Stormpath
- Handle credit card payments with Stripe
- Bill users on per-API call basis with Stripe
- Find Bitcoin exchange rates with Bitcoin Charts
- Send SMS messages with the Bitcoin exchange rate with Twilio
You’ll begin with Stormpath Evangelist Lee Brandt’s AngularJS + ASP.NET Core starter project, and add code to keep track of user account data such as API keys, total API queries, and balance. Finally, you’ll create a form to allow users to add credit to their accounts. As long as they have credit, they will be able to use your API service at a rate of $0.02/request.
You can find the code that backs this project on GitHub.
Now, let’s get started!
Start with ASP.NET Core + AngularJS
Lee wrote a simple and useful tutorial about creating an application with User Authentication in AngularJS and ASP.NET Core. The application is a to-do CRUD app with authentication and authorization. Clone the finished
branch or download the finished project.
The Stormpath ASP.NET Core Quickstart shows how to create an API key; here’s the abridged version:
From the Home tab of the Admin Console select Manage API Keys under the Developer Tools heading. Click the Create API Key button to trigger a download of a apiKey-{API_KEY}.properties file. Open the file in Notepad.
Using the Command Prompt or Powershell, run these commands:
1 2 |
setx STORMPATH_CLIENT_APIKEY_ID your_id_here setx STORMPATH_CLIENT_APIKEY_SECRET your_secret_here |
Once you get your Stormpath API keys and run the finished application, you should see something like this:
Set Up Twilio
Twilio is an API service that helps your application send and receive calls and SMS messages. In this article, we’re going to explore just the SMS functionality.
Create your free Twilio trial account and get your own dedicated phone number.
You will need a verified caller ID to receive SMS messages from your Twilio account. For this article’s purpose, I am going to use a public free SMS number.
Twilio will send you a verification code and you will have to enter it on the site to confirm your number. The SMS message should be something like this:
1 2 |
Your Twilio verification code is: 7999 |
Now, go to your Twilio Account Page and copy your API key credentials. Twilio gives you two API tokens: an Account SID and an Auth Token. Keep them safe and don’t share them with anyone!
Now that you have your sender (your Twilio trial number) and receiver (which must be verified by Twilio) numbers and your API keys, write them down somewhere handy, we’ll use them shortly.
Create the Backend Logic to Support Sending SMS Messages
Twilio’s CSharp library is not yet compatible with ASP.NET Core but there is a workaround via the Twilio REST API.
To store your sensitive Twilio data, you’ll want to use the ASP.NET Core User Secrets Manager and the IOptions
pattern. If you want to learn more about different approaches to storing sensitive data check out my post on how to store and protect sensitive data.
Let’s set that up now. Create a class for Twilio Settings:
1 2 3 4 5 6 7 8 9 |
public class SmsSettings { public string Sid { get; set; } public string Token { get; set; } public string BaseUri { get; set; } public string RequestUri { get; set; } public string From { get; set; } } |
Right-click on the project and select Manage User Secrets. Edit the secrets.json
file and add these configuration settings:
1 2 3 4 5 6 7 8 9 10 |
{ "SMSSettings": { "Sid": "Your_Twilio_LIVE_Account_Sid", "Token": "Your_Twilio_LIVE_Auth_Token", "BaseUri": "https://api.twilio.com", "RequestUri": "/2010-04-01/Accounts/Your_LIVE_Account_Sid/Messages", // Paste your Account SID "From": "+Your_Twilio_Trial_Phone_Number" } } |
In “From” set your trial Twilio number, it should be something like: 12022022022
. As you can see, you have to use your LIVE secrets but with your trial you should be fine to do this tutorial for free.
In the Startup
class, add the AddUserSecrets
method in the constructor to load your settings from the secrets.json
file.
1 2 3 4 5 |
if (env.IsDevelopment()) { builder.AddUserSecrets(); } |
To bind the SmsSettings
class to your application you will need to add it to the ConfigureServices
method of the Startup
class.
1 2 3 4 5 6 7 8 9 10 |
public void ConfigureServices(IServiceCollection services) { services.AddStormpath(); services.Configure<SmsSettings>(Configuration.GetSection("SMSSettings")); // Add framework services. services.AddMvc(); } |
Create a Services
folder and add a new class called SmsService
, this class will be responsible for sending your application’s SMS messages.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
public class SmsService { private readonly SmsSettings _smsSettings; public SmsService(IOptions<SmsSettings> smsSettings) { _smsSettings = smsSettings.Value; } public async Task SendSms(string message, string phoneNumber) { using (var client = new HttpClient { BaseAddress = new Uri(_smsSettings.BaseUri) }) { phoneNumber = phoneNumber.Trim(); if (phoneNumber.StartsWith("+")) { phoneNumber = phoneNumber.Substring(1); } var basicHeaderValue = $"{_smsSettings.Sid}:{_smsSettings.Token}"; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes(basicHeaderValue))); var content = new FormUrlEncodedContent(new Dictionary<string, string> { ["To"] = $"+{phoneNumber}", ["From"] = _smsSettings.From, ["Body"] = message }); var response = await client.PostAsync(_smsSettings.RequestUri, content); if (!response.IsSuccessStatusCode) { throw new Exception("An error occurred while sending the SMS"); } } } } |
Don’t forget to add this service in the ConfigureService
method to also be injected by the framework later:
1 2 3 4 5 6 7 8 9 10 11 |
public void ConfigureServices(IServiceCollection services) { services.AddStormpath(); services.AddTransient<SmsService>(); services.Configure<SmsSettings>(Configuration.GetSection("SMSSettings")); // Add framework services. services.AddMvc(); } |
Thus far, you have all the logic needed to send SMS messages, but you will see this in action in the following steps. Now, let’s continue with Stripe to manage payments.
Set up Stripe
Visit Stripe, and create your Stripe account if you don’t have one already. Once you’re logged in, go to Account – API and copy your test API keys to be used later on. You’ll need both the public (publishable) and private (secret) key.
Create the Backend Logic to Support Credit Card Payments
You’ll need to store the Stripe API keys securely, simply replicate the approach applied above for Twilio’s sensitive data.
Create a PaymentSettings
class to store your Stripe API Keys.
1 2 3 4 5 6 |
public class PaymentSettings { public string StripePublicKey { get; set; } public string StripePrivateKey { get; set; } } |
Open the secrets.json
file and add your Stripe API keys next to your Twilio settings:
1 2 3 4 5 |
"PaymentSettings": { "StripePublicKey": "your_stripe_TEST_public_key", "StripePrivateKey": "your_stripe_TEST_private_key" } |
Then, bind the PaymentSettings
with your secrets by adding the following code in the ConfigureServices
method of the Startup
class:
1 2 |
services.Configure<PaymentSettings>(Configuration.GetSection("PaymentSettings")); |
Install Stripe.NET using the NuGet Package Manager, or from the Package Manager Console:
1 2 |
PM> Install-Package Stripe.net |
Then, add a new service class called PaymentService
to your “Services” folder. This class will be responsible for handling and processing the payment:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
public class PaymentService { public static readonly int DepositAmount = 2000; public static readonly int CostPerQuery = 2; private readonly PaymentSettings _paymentSettings; private readonly StripeChargeService _stripeChargeService; public PaymentService(IOptions<PaymentSettings> paymentSettings, StripeChargeService stripeChargeService) { _paymentSettings = paymentSettings.Value; _stripeChargeService = stripeChargeService; } public bool ProcessPayment(string token, int amount) { var myCharge = new StripeChargeCreateOptions { Amount = amount, Currency = "usd", Description = "Bitcoin API Call", SourceTokenOrExistingSourceId = token, Capture = true }; _stripeChargeService.ApiKey = _paymentSettings.StripePrivateKey; var stripeCharge = _stripeChargeService.Create(myCharge); var success = string.IsNullOrEmpty(stripeCharge.FailureCode) && string.IsNullOrEmpty(stripeCharge.FailureMessage); return success; } } |
Register the PaymentService
and the StripeChargeService
in the Startup class to be injected later on by the framework:
1 2 3 4 5 6 7 8 9 10 11 12 |
public void ConfigureServices(IServiceCollection services) { // … services.AddTransient<StripeChargeService>(); services.AddTransient<PaymentService>(); services.Configure<PaymentSettings>(Configuration.GetSection("PaymentSettings")); // Add framework services. services.AddMvc(); } |
Create the Backend Logic to Manage the User Account
Create a class UserAccountInfo
in the Models
folder to store the user account data:
1 2 3 4 5 6 7 8 |
public class UserAccountInfo { public string ApiKeyId { get; set; } public string ApiKeySecret { get; set; } public decimal Balance { get; set; } public int TotalQueries { get; set; } } |
Then, create a service in the Services folder called AccountService
. This service will be responsible for handling the user account data:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
public class AccountService { private readonly IAccount _account; public static readonly string BalanceKey = "Balance"; public static readonly string TotalQueriesKey = "TotalQueries"; public AccountService(IAccount account) { _account = account; } public async Task<UserAccountInfo> GetUserAccountInfo() { var userAccountInfo = new UserAccountInfo(); var apiKeys = await _account.GetApiKeys().FirstAsync(); var accountCustomData = await _account.GetCustomDataAsync(); userAccountInfo.ApiKeyId = apiKeys.Id; userAccountInfo.ApiKeySecret = apiKeys.Secret; userAccountInfo.Balance = decimal.Parse(accountCustomData[BalanceKey].ToString()); userAccountInfo.TotalQueries = int.Parse(accountCustomData[TotalQueriesKey].ToString()); return userAccountInfo; } public async Task UpdateUserBalance(decimal amount) { var customData = await _account.GetCustomDataAsync(); var oldValue = decimal.Parse(customData[BalanceKey].ToString()); customData[BalanceKey] = oldValue + amount; await customData.SaveAsync(); } public async Task UpdateUserTotalQueries(int totalQueries) { var customData = await _account.GetCustomDataAsync(); var oldValue = int.Parse(customData[TotalQueriesKey].ToString()); customData[TotalQueriesKey] = oldValue + totalQueries; await customData.SaveAsync(); } } |
Then, replace the AddStormpath()
configuration line in the ConfigureServices
of the Startup
class by the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddStormpath(new StormpathOptions() { PostRegistrationHandler = async (ctx, ct) => { // Set the initial balance and query count ctx.Account.CustomData[AccountService.BalanceKey] = 0; ctx.Account.CustomData[AccountService.TotalQueriesKey] = 0; await ctx.Account.SaveAsync(ct); // Create an API key for the user await ctx.Account.CreateApiKeyAsync(ct); } }); ... } |
Here, you added a handler to be executed after user registration and initialize the user’s custom data fields. These fields will hold the user’s API call volume (their total queries) and the money that the user has in their balance.
Create the Bitcoin Backend Logic
Let’s do a little recap: Thus far, you have built the backend services to send SMS messages via Twilio and process payments with Stripe. Also, your backend is capable of managing the user account, including reading and updating its custom data, where you will store the user balance and the number of API calls.
You will now create the service to retrieve the Bitcoin exchange rate:
Create a BtcExchangeRateResponse
and a BtcExchangeRateCurrency
class in the Models folder:
1 2 3 4 5 6 7 8 9 10 11 |
public class BtcExchangeRateResponse { public BtcExchangeRateCurrency Usd { get; set; } } public class BtcExchangeRateCurrency { [JsonProperty("24h")] public decimal? Last24H { get; set; } } |
These classes will hold the Bitcoin data.
Create a BitcoinExchangeRateService
in the Services folder:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class BitcoinExchangeRateService { public async Task<decimal> GetBitcoinExchangeRate() { using (var client = new HttpClient()) using (var response = await client.GetAsync("http://api.bitcoincharts.com/v1/weighted_prices.json")) { if (response.StatusCode != HttpStatusCode.OK) { throw new Exception("Failed to retrieve BTC exchange rates"); } var responseBody = JsonConvert.DeserializeObject<BtcExchangeRateResponse>(await response.Content.ReadAsStringAsync()); if (responseBody.Usd?.Last24H == null) { throw new Exception("Failed to retrieve BTC exchange rates"); } return responseBody.Usd.Last24H.Value; } } } |
The bitcoincharts site provides a publicly available API that lets you grab the current Bitcoin exchange rates. Here, you’ll be using the bitcoincharts API to get the current Bitcoin exchange rate.
Again, you’ll have to configure this service in the Startup.cs
so it can be injected into the controller:
1 2 3 4 5 6 |
public void ConfigureServices(IServiceCollection services) { // ... services.AddTransient<BitcoinExchangeRateService>(); } |
Create the Controllers
Now that you have all the services that you need, it’s time to create the controllers to expose the endpoints!
Create a MeController
in the Controllers folder. This controller will allow you to grab the user account info.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
[Authorize] [Route("api/[controller]")] public class MeController : Controller { private readonly AccountService _accountService; public MeController(AccountService accountService) { _accountService = accountService; } [HttpGet] public async Task<IActionResult> Get() { var userAccountInfo = await _accountService.GetUserAccountInfo(); return Ok(userAccountInfo); } } |
Then, create the PaymentController
in the Controllers folder, this controller will allow you to update the user’s balance.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
[Authorize] [Route("api/[controller]")] public class PaymentController : Controller { private readonly PaymentService _paymentService; private readonly AccountService _accountService; public PaymentController(PaymentService paymentService, AccountService accountService) { _paymentService = paymentService; _accountService = accountService; } [HttpPost] public async Task<IActionResult> Post([FromBody] PaymentFormData formData) { if (!_paymentService.ProcessPayment(formData.Token, PaymentService.DepositAmount)) { return BadRequest(); } await _accountService.UpdateUserBalance(PaymentService.DepositAmount); var updatedAccountInfo = await _accountService.GetUserAccountInfo(); return Ok(updatedAccountInfo); } } |
As you can see, the Post
method receives PaymentFormData
as parameter. This object is a wrapper to encapsulate the Stripe response token.
Create the PaymentFormData
class in the Models
folder:
1 2 3 4 5 |
public class PaymentFormData { public string Token { get; set; } } |
Lastly, create a MessageController
in the Controllers folder:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
[Authorize] [Route("api/[controller]")] public class MessageController : Controller { private readonly AccountService _accountService; private readonly SmsService _smsService; private readonly BitcoinExchangeRateService _bitcoinExchangeRateService; public MessageController( AccountService accountService, SmsService smsService, BitcoinExchangeRateService bitcoinExchangeRateService) { _accountService = accountService; _smsService = smsService; _bitcoinExchangeRateService = bitcoinExchangeRateService; } [HttpPost] public async Task<IActionResult> Post([FromBody] SendSmsRequest payload) { if(string.IsNullOrEmpty(payload.PhoneNumber)) { return BadRequest("Invalid phone number"); } var userAccountInfo = await _accountService.GetUserAccountInfo(); if (userAccountInfo.Balance == 0) { return StatusCode((int)HttpStatusCode.PaymentRequired); } try { var btcExchangeRate = await _bitcoinExchangeRateService.GetBitcoinExchangeRate(); var message = $"1 Bitcoin is currently worth ${btcExchangeRate} USD."; await _smsService.SendSms(message, payload.PhoneNumber); await _accountService.UpdateUserTotalQueries(1); await _accountService.UpdateUserBalance(-PaymentService.CostPerQuery); userAccountInfo = await _accountService.GetUserAccountInfo(); return Ok(userAccountInfo); } catch(Exception ex) { return StatusCode((int)HttpStatusCode.InternalServerError); } } } |
The [Authorize]
attribute ensures that only authenticated users have access.
The MessageController
uses the AccountService
to grab and update the user’s balance and query count. It also uses BitcoinExchangeRateService
to get the Bitcoin rate, and sends the SMS message to the provided phone number through the SmsService
.
Whew! You now have a full-fledged API service! But wait! There’s more! Keep going to see your service in action.
Create the Dashboard View and Files
Thus far, you have all the backend logic ready. Now, is time to build the frontend and integrate it with your API.
In this section, I will show you how to build all the stuff related to the front-end, such as: dashboard and payment views, controllers and services.
You will have now to update the index.html
so that your web application has a Dashboard option in the nav bar. This option will be available for authenticated users, and once they enter to this section, they will see all their account information.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
<html class="no-js" ng-app="ToDoApp"> <!--<![endif]--> <head> <base href="/"> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>WebAPI Angular Storm</title> <meta name="description" content=""> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- Latest compiled and minified CSS --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <!-- Optional theme --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous"> <link rel="stylesheet" href="main.css"> <!-- Angular Material style sheet --> <link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/angular_material/1.1.0/angular-material.min.css"> </head> <body> <!--[if lt IE 7]> <p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="#">upgrade your browser</a> to improve your experience.</p> <![endif]--> <div class="navbar navbar-inverse navbar-fixed-top"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a ui-sref="home" class="navbar-brand">BTC </a> </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li><a ui-sref="home">Home</a></li> <li><a ui-sref="todo" if-user>ToDos</a></li> <li><a ui-sref="dashboard" if-user>Dashboard</a></li> <li><a ui-sref="login" if-not-user>Login</a></li> <li><a ui-sref="register" if-not-user>Register</a></li> <li><a ui-sref="home" sp-logout if-user>Logout</a></li> </ul> </div> </div> </div> <section class="container"> <div class="jumbotron"> <h1>Bitcoin Exchange Rate Application</h1> <h3>.NET Core Web API, AngularJS 1.x and Stormpath</h3> </div> <div ui-view></div> </section> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script> <!-- Latest compiled and minified JavaScript --> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular-cookies.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular-resource.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular-sanitize.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.3.1/angular-ui-router.min.js"></script> <script src="//cdn.rawgit.com/stormpath/stormpath-sdk-angularjs/1.0.0/dist/stormpath-sdk-angularjs.min.js"></script> <script src="//cdn.rawgit.com/stormpath/stormpath-sdk-angularjs/1.0.0/dist/stormpath-sdk-angularjs.tpls.min.js"></script> <!-- Angular Material requires Angular.js Libraries --> <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script> <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-animate.min.js"></script> <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-aria.min.js"></script> <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-messages.min.js"></script> <!-- Angular Material Library --> <script src="http://ajax.googleapis.com/ajax/libs/angular_material/1.1.0/angular-material.min.js"></script> <!-- angular-payments library - you probably want to install it through either bower or npm --> <script src="http://cdn.rawgit.com/laurihy/angular-payments/2472bc9befa256780d106a8e53a9dea12b7341ed/lib/angular-payments.js"></script> <script src="https://js.stripe.com/v2/"></script> <script src="app/app.js"></script> <script src="app/auth/controllers/login.controller.js"></script> <script src="app/auth/controllers/register.controller.js"></script> <script src="app/home/controllers/home.controller.js"></script> <script src="app/todo/controllers/todo.controller.js"></script> <script src="app/todo/services/todo.service.js"></script> <script src="app/dashboard/services/dashboard.service.js"></script> <script src="app/dashboard/controllers/dashboard.controller.js"></script> </body> </html> |
As you can see, the Index.html
includes a couple of new script tags, such as Angular Payments, which will help you with the Stripe payment form and Angular Material js files to improve the app user experience.
The dashboard
and auth
services, view and controllers are added too. Below, I will show you how to create each of them.
Create a folder called dashboard
under the wwwroot > app folder. Then, under dashboard
create three new folders: controllers
, services
and views
. These will be for the dashboard.controller.js
, dashboard.service.js
and the dashboard.view.html
files, respectively.
Now, create the dashboard.service
to encapsulate the dashboard
api calls and logic:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
(function () { 'use strict'; function dashboardService($http) { var addCredit = function (payload) { return $http.post('/api/payment', payload); }; var loadData = function () { return $http.get('/api/me'); }; var sendBitcoinRate = function (payload) { return $http.post('/api/message', payload); }; return { AddCredit: addCredit, LoadData: loadData, SendBitcoinRate: sendBitcoinRate }; } angular.module('ToDoApp') .factory('DashboardService', ['$http', dashboardService]) }()) |
Create the dashboard.controller
to encapsulate the logic to interact with the view:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
(function () { 'use strict'; function dashboardController(DashboardService, $mdDialog, $scope, $timeout) { var vm = this; vm.loadData = function () { DashboardService.LoadData().then( function (results) { vm.totalQueries = results.data.totalQueries; vm.balance = results.data.balance; vm.apiKeyId = results.data.apiKeyId; vm.apiKeySecret = results.data.apiKeySecret; }, errorHandler); }; vm.showPaymentForm = function (ev) { $mdDialog.show({ controller: paymentFormController, templateUrl: 'app/dashboard/templates/payment-form.tmpl.html', parent: angular.element(document.body), targetEvent: ev, clickOutsideToClose: true, fullscreen: false }) }; vm.sendBitcoinExchangeRate = function () { vm.sendingSMS = true DashboardService.SendBitcoinRate({ phoneNumber: vm.phoneNumber }).then( function (response) { vm.sendingSMS = false vm.totalQueries = response.data.totalQueries; vm.balance = response.data.balance; vm.phoneNumber = ''; $mdDialog.show( $mdDialog.alert() .parent(angular.element(document.querySelector('body'))) .clickOutsideToClose(true) .title('Success!') .textContent('SMS sent successfully.') .ariaLabel('SMS Success') .ok('Ok') ); }, function (error) { vm.sendingSMS = false $mdDialog.show( $mdDialog.alert() .parent(angular.element(document.querySelector('body'))) .clickOutsideToClose(true) .title('Error') .textContent('An error occurred while sending the SMS. Please try again.') .ariaLabel('SMS Error') .ok('Ok') ); } ); }; function paymentFormController($scope) { $scope.value = 'VALUE'; $scope.closePaymentFormModal = function () { $mdDialog.hide(); } $scope.stripeCallback = function (code, result) { if (result.error) { $mdDialog.show( $mdDialog.alert() .parent(angular.element(document.querySelector('body'))) .clickOutsideToClose(true) .title('Error') .textContent('An error occurred while retrieving the payment token from Stripe. Please try again.') .ariaLabel('Payment Error') .ok('Ok') ); } else { $scope.processingPayment = true; DashboardService.AddCredit({ Token: result.id }) .then(function (response) { $scope.processingPayment = false; vm.totalQueries = response.data.totalQueries; vm.balance = response.data.balance; $mdDialog.hide(); }, function (error) { $scope.processingPayment = false; $mdDialog.show( $mdDialog.alert() .parent(angular.element(document.querySelector('body'))) .clickOutsideToClose(true) .title('Error') .textContent('An error occurred while processing your payment. Please try again.') .ariaLabel('Payment Error') .ok('Ok') ); }) } }; } return vm; } function errorHandler(error) { console.error(error); } angular.module('ToDoApp') .controller('DashboardController', ['DashboardService', '$mdDialog', '$scope', '$timeout', dashboardController]); }()) |
Create the dashboard.view.html
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
<section ng-controller="DashboardController as vm" ng-init="vm.loadData()" ng-cloak> <div class="panel panel-default header-panel"> <div class="api-key"> <strong>API Key:</strong> <span>{{ vm.apiKeyId }}</span> </div> <div class="api-secret"> <strong>API Secret:</strong> <span>{{ vm.apiKeySecret }}</span> </div> </div> <div class="dashboard-container"> <div class="left panel panel-primary"> <div class="panel-heading text-center"> <h4>Analytics</h4> </div> <div class="panel-body text-center"> <p class="total-queries">{{ vm.totalQueries }}</p> <p><i>* Total queries</i></p> </div> </div> <div class="right panel panel-primary"> <div class="panel-heading text-center"> <h4>Billing</h4> </div> <div class="panel-body text-center"> <p class="balance">{{ (vm.balance / 100) | currency }}</p> <p><i>* Current Account Balance</i></p> <button class="btn btn-default btn-primary" ng-click="vm.showPaymentForm()">Pay with Card</button> </div> </div> </div> <form ng-if="vm.balance > 0" class="form-inline send-sms-form" ng-submit="vm.sendBitcoinExchangeRate()"> <div class="form-group"> <label for="phone-number">Phone number:</label> <input type="text" class="form-control" id="phone-number" name="phone-number" placeholder="ex. +15551234567" value="" ng-model="vm.phoneNumber" /> </div> <button type="submit" class="btn btn-default btn-primary">Send BitCoin Rate by SMS</button> <div ng-if="vm.sendingSMS" class="sending-sms-spinner"> <span>Sending SMS</span> <md-icon md-svg-src="img/ellipsis.svg" aria-label="Close dialog"></md-icon> </div> </form> </section> |
Create a template
folder to include a template for the payment form view:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
<md-dialog aria-label="Payment Form"> <md-toolbar> <div class="md-toolbar-tools"> <h2>Payment Form</h2> <span flex></span> <md-button class="md-icon-button" ng-click="closePaymentFormModal()"> <md-icon md-svg-src="img/icons/ic_close_24px.svg" aria-label="Close dialog"></md-icon> </md-button> </div> </md-toolbar> <md-dialog-content> <div class="md-dialog-content"> <form class="form-horizontal payment-form" stripe-form="stripeCallback" name="form"> <div class="form-group" ng-class="{ 'has-error': form.card.$dirty && form.card.$invalid }"> <label for="card" class="control-label col-sm-4">Card number</label> <div class="col-sm-8"> <input ng-model="number" placeholder="Card Number" payments-format="card" class="form-control" payments-validate="card" id="card" name="card" ng-required="true" /> </div> </div> <div class="form-group" ng-class="{ 'has-error': form.expiry.$dirty && form.expiry.$invalid }"> <label for="expiry" class="control-label col-sm-4">Expiry Date</label> <div class="col-sm-8"> <input ng-model="expiry" placeholder="Expiration" payments-format="expiry" class="form-control" payments-validate="expiry" id="expiry" name="expiry" ng-required="true" /> </div> </div> <div class="form-group" ng-class="{ 'has-error': form.cvc.$dirty && form.cvc.$invalid }"> <label for="cvc" class="control-label col-sm-4">CVC</label> <div class="col-sm-8"> <input ng-model="cvc" placeholder="CVC" payments-format="cvc" class="form-control" payments-validate="cvc" id="cvc" name="cvc" ng-required="true" /> </div> </div> <div class="form-group btn-container"> <button class="btn btn-default btn-primary" type="submit" ng-disabled="!form.$valid || processingPayment">Pay $20.00</button> <md-icon ng-if="processingPayment" md-svg-src="img/ellipsis.svg" aria-label="Close dialog"></md-icon> </div> </form> <div class="errors-container"> <div ng-if="form.card.$dirty && form.card.$invalid" class="panel panel-default form-error"> <i>Error: Invalid card number</i> </div> <div ng-if="form.expiry.$dirty && form.expiry.$invalid" class="panel panel-default form-error"> <i>Error: Invalid expiration date</i> </div> <div ng-if="form.cvc.$dirty && form.cvc.$invalid" class="panel panel-default form-error"> <i>Error: Invalid CVC</i> </div> </div> </div> </md-dialog-content> </md-dialog> |
Finally, modify the app.js
to include the dashboard
url in the routes. Make sure you paste in your Stripe public (publishable) key!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
(function(){ 'use strict'; function config($stateProvider, $urlRouterProvider, $locationProvider, STORMPATH_CONFIG) { window.Stripe.setPublishableKey('YOUR_STRIPE_PUBLIC_KEY'); $locationProvider.html5Mode(true); STORMPATH_CONFIG.FORM_CONTENT_TYPE = 'application/json'; $stateProvider .state('home', { url: '/', templateUrl: '/app/home/views/home.view.html' }) .state('register', { url: '/register', templateUrl: '/app/auth/views/register.view.html' }) .state('login', { url: '/login', templateUrl: '/app/auth/views/login.view.html' }) .state('dashboard', { url: '/dashboard', templateUrl: '/app/dashboard/views/dashboard.view.html', sp: { authenticate: true } }) .state('todo', { url: '/todo', templateUrl: '/app/todo/views/todo.view.html', sp: { authenticate: true } }); $urlRouterProvider.otherwise('/'); } function initializer($stormpath, $rootScope, $state) { // Finally, configure the login state and the default state after login $stormpath.uiRouter({ loginState: 'login', defaultPostLoginState: 'todo' }); // Bind the logout event $rootScope.$on('$sessionEnd', function () { $state.transitionTo('login'); }); } angular.module('ToDoApp', ['ngCookies', 'ngResource', 'ngSanitize', 'ui.router', 'stormpath', 'stormpath.templates', 'ngMaterial', 'angularPayments']) .config(['$stateProvider', '$urlRouterProvider', '$locationProvider', 'STORMPATH_CONFIG', config]) .run(['$stormpath', '$rootScope', '$state', initializer]); }()) |
Note: Be sure to have the img
folder just as it is on the GitHub repository and set your Stripe public key in the placeholder!
Now you’ll want to modify the main.css
stylesheet to make sure your application looks polished.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
.todos li{ width: 50%; } .todo span{ padding: .5rem; display: inline-block; width: 90%; } .todo span.completed{ color: lightgray; text-decoration: line-through; } .new-todo{ margin-top: .5rem; } .new-todo input { width: 50%; } /* Dashboard */ .header-panel { display: flex; display: -webkit-flex; justify-content: space-between; padding: 1em 2em; } .dashboard-container { display: flex; justify-content: space-between; margin: 4em 0; } .dashboard-container .left, .dashboard-container .right { width: 45%; } .total-queries, .balance { font-size: 2em; } .send-sms-form { margin: 4em 0; } .send-sms-form button { margin-left: 2em; } .sending-sms-spinner { display: inline-block; margin-left: 2em; } /* End Dashboard */ /* Payment Form */ .payment-form { min-width: 400px; } .payment-form .btn-container { margin-top: 3em; text-align: center; } .errors-container { height: 50px; } .errors-container .form-error { padding: 1em 2em; background: #FDBABA; border-color: #A94442; color: #A94442; } .errors-container div { display: none; } .errors-container div:first-child { display: block; } /* End Payment Form */ |
Modify the applicationUrl
property in the launchSettings.json
and set the default route. In my case:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
{ "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:49941/", "sslPort": 0 } }, "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } |
And… that’s it! You just have built an API in ASP.NET Core with Stormpath, Stripe and Twilio.
I hope you have found this post useful and that it inspires you to apply new ideas to your projects. There’s much more to learn about, continue exploring and check out some of our other interesting posts!