In this tutorial, I'm going to show you how to build a Software-as-a-Service (SaaS) minimum viable product (MVP). To keep things simple, the software is going to allow our customers to save a list of notes.
I am going to offer three subscription plans: the Basic plan will have a limit of 100 notes per user, the Professional plan will allow customers to save up to 10,000 notes, and the Business plan will allow a million notes. The plans are going to cost $10, $20 and $30 per month respectively. In order to receive payment from our customers, I'm going to use Stripe as a payment gateway, and the website is going to be deployed to Azure.
In a very short time Stripe has become a very well known Payment Gateway, mainly because of their developer-friendly approach, with simple and well-documented APIs. Their pricing is also very clear: 2.9% per transaction + 30 cents. No setup fees or hidden charges.
Credit card data is also very sensitive data, and in order to be allowed to receive and store that data in my server, I need to be PCI compliant. Because that's not an easy or quick task for most small companies, the approach that many payment gateways take is: You display the order details, and when the customer agrees to purchase, you redirect the customer to a page hosted by the payment gateway (bank, PayPal, etc), and then they redirect the customer back.
Stripe has a nicer approach to this problem. They offer a JavaScript API, so we can send the credit card number directly from the front-end to Stripe's servers. They return a one-time use token that we can save to our database. Now, we only need an SSL certificate for our website that we can quickly purchase from about $5 per year.
Now, sign up for a Stripe account, as you'll need it to charge your customers.
As a developer I don't want to be dealing with dev-ops tasks and managing servers if I don't have to. Azure websites is my choice for hosting, because it's a fully managed Platform-as-a-Service. It allows me to deploy from Visual Studio or Git, I can scale it easily if my service is successful, and I can focus on improving my application. They offer $200 to spend on all Azure services in the first month to new customers. That's enough to pay for the services that I am using for this MVP. Sign up for Azure.
Sending emails from our application might not seem like a very complex task, but I would like to monitor how many emails are delivered successfully, and also design responsive templates easily. This is what Mandrill offers, and they also let us send up to 12,000 emails per month for free. Mandrill is built by MailChimp, so they know about the business of sending emails. Also, we can create our templates from MailChimp, export them to Mandrill, and send emails from our app using our templates. Sign up for Mandrill, and sign up for MailChimp.
Last but not least, we need Visual Studio to write our application. This edition, which was launched only a few months ago, is completely free and is pretty much equivalent to Visual Studio Professional. You can download it here, and this is all we need, so now we can focus on the development.
The first thing that we need to do is open Visual Studio 2013. Create a new ASP.NET Web Application:
This project creates an application where a user can login by registering an account with the website. The website is styled using Bootstrap, and I'll continue building the rest of the app with Bootstrap. If you hit F5 in Visual Studio to run the application, this is what you will see:
This is the default landing page, and this page is one of the most important steps to convert our visitors into customers. We need to explain the product, show the price for each plan, and offer them the chance to sign up for a free trial. For this application I am creating three different subscription plans:
For some help creating a landing page, you can visit ThemeForest and purchase a template. For this sample, I am using a free template, and you can see the final result in the photo below.
In the website that we created in the previous step, we also get a Registration form template. From the landing page, when you navigate to Prices, and click on Free Trial, you navigate to the registration page. This is the default design:
We only need one extra field here to identify the subscription plan that the user is joining. If you can see in the navigation bar of the photo, I am passing that as a GET parameter. In order to do that, I generate the markup for the links in the landing page using this line of code:
<a href="@Url.Action("Register", "Account", new { plan = "business" })"> Free Trial </a>
To bind the Subscription Plan to the back-end, I need to modify the class RegisterViewModel
and add the new property.
public class RegisterViewModel { [Required] [EmailAddress] [Display(Name = "Email")] public string Email { get; set; } [Required] [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] [DataType(DataType.Password)] [Display(Name = "Password")] public string Password { get; set; } [DataType(DataType.Password)] [Display(Name = "Confirm password")] [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] public string ConfirmPassword { get; set; } public string SubscriptionPlan { get; set; } }
I also have to edit AccountController.cs, and modify the Action Register to receive the plan:
[AllowAnonymous] public ActionResult Register(string plan) { return View(new RegisterViewModel { SubscriptionPlan = plan }); }
Now, I have to render the Plan Identifier in a hidden field in the Register form:
@Html.HiddenFor(m => m.SubscriptionPlan)
The last step will be to subscribe the user to the plan, but we'll get to that a bit later. I also update the design of the registration form.
In the template we also get a login page and action controllers implemented. The only thing I need to do is to make it look prettier.
Take a second look at the previous screenshot, and you'll notice that I added a "Forgot your Password?" link. This is already implemented in the template, but it's commented out by default. I don't like the default behaviour, where the user needs to have the email address confirmed to be able to reset the password. Let's remove that restriction. In the file AccountController.cs edit the action ForgotPassword
:
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model) { if (ModelState.IsValid) { var user = await UserManager.FindByNameAsync(model.Email); if (user == null) { // Don't reveal that the user does not exist or is not confirmed return View("ForgotPasswordConfirmation"); } // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771 // Send an email with this link // string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id); // var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme); // await UserManager.SendEmailAsync(user.Id, "Reset Password", "Please reset your password by clicking <a href=\"" + callbackUrl + "\">here</a>"); // return RedirectToAction("ForgotPasswordConfirmation", "Account"); } // If we got this far, something failed, redisplay form return View(model); }
The code to send the email with the link to reset the password is commented out. I'll show how to implement that part a bit later. The only thing left for now is to update the design of the pages:
ASP.NET Identity is a fairly new library that has been built based on the assumption that users will no longer log in by using only a username and password. OAuth integration to allow users to log in through social channels such as Facebook, Twitter, and others is very easy now. Also, this library can be used with Web API, and SignalR.
On the other hand, the persistence layer can be replaced, and it's easy to plug in different storage mechanisms such as NoSQL databases. For the purposes of this application, I will use Entity Framework and SQL Server.
The project that we just created contains the following three NuGet packages for ASP.NET Identity:
The main configuration for Identity is in App_Start/IdentityConfig.cs. This is the code that initializes Identity.
public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context) { var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>())); // Configure validation logic for usernames manager.UserValidator = new UserValidator<ApplicationUser>(manager) { AllowOnlyAlphanumericUserNames = false, RequireUniqueEmail = true }; // Configure validation logic for passwords manager.PasswordValidator = new PasswordValidator { RequiredLength = 6, RequireNonLetterOrDigit = true, RequireDigit = true, RequireLowercase = true, RequireUppercase = true, }; // Configure user lockout defaults manager.UserLockoutEnabledByDefault = true; manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5); manager.MaxFailedAccessAttemptsBeforeLockout = 5; // Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user // You can write your own provider and plug it in here. manager.RegisterTwoFactorProvider("Phone Code", new PhoneNumberTokenProvider<ApplicationUser> { MessageFormat = "Your security code is {0}" }); manager.RegisterTwoFactorProvider("Email Code", new EmailTokenProvider<ApplicationUser> { Subject = "Security Code", BodyFormat = "Your security code is {0}" }); manager.EmailService = new EmailService(); manager.SmsService = new SmsService(); var dataProtectionProvider = options.DataProtectionProvider; if (dataProtectionProvider != null) { manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity")); } return manager; }
As you can see in the code, it's pretty easy to configure users' validators and password validators, and two factor authentication can also be enabled. For this application, I use cookie-based authentication. The cookie is generated by the framework and is encrypted. This way, we can scale horizontally, adding more servers if our application needs it.
You can use MailChimp to design email templates, and Mandrill to send emails from your application. In the first place you need to link your Mandrill account to your MailChimp account:
Navigate to Templates in MailChimp, and click on Create Template.
Now, select one of the templates offered by MailChimp. I selected the first one:
In the template editor, we modify the content as we like. One thing to note, as you can see below, is that we can use variables. The format is *|VARIABLE_NAME|*
. From the code, we'll set those for each customer. When you are ready, click on Save and Exit at the bottom right.
In the Templates list, click on Edit, on the right side, and select Send To Mandrill. After a few seconds you will get a confirmation message.
To confirm that the template has been exported, navigate to Mandrill and log in. Select Outbound from the left menu, and then Templates from the top menu. In the image below you can see that the template has been exported.
If you click on the name of the template, you'll see more information about the template. The field "Template Slug" is the text identifier that we will use in our application to let Mandrill API know which template we want to use for the email that we are sending.
I leave it as an exercise for you to create a "Reset Password" template.
In the first place, install Mandrill from NuGet. After that, add your Mandrill API Key to Web.config App Settings. Now, open App_Start/IdentityConfig.cs and you'll see the class EmailService
skeleton pending implementation:
public class EmailService : IIdentityMessageService { public Task SendAsync(IdentityMessage message) { // Plug in your email service here to send an email. return Task.FromResult(0); } }
Although this class has only the method SendAsync
, because we have two different templates (Welcome Email Template and Reset Password Template), we will implement new methods. The final implementation will look like this.
public class EmailService : IIdentityMessageService { private readonly MandrillApi _mandrill; private const string EmailFromAddress = "no-reply@mynotes.com"; private const string EmailFromName = "My Notes"; public EmailService() { _mandrill = new MandrillApi(ConfigurationManager.AppSettings["MandrillApiKey"]); } public Task SendAsync(IdentityMessage message) { var task = _mandrill.SendMessageAsync(new EmailMessage { from_email = EmailFromAddress, from_name = EmailFromName, subject = message.Subject, to = new List<Mandrill.EmailAddress> { new EmailAddress(message.Destination) }, html = message.Body }); return task; } public Task SendWelcomeEmail(string firstName, string email) { const string subject = "Welcome to My Notes"; var emailMessage = new EmailMessage { from_email = EmailFromAddress, from_name = EmailFromName, subject = subject, to = new List<Mandrill.EmailAddress> { new EmailAddress(email) }, merge = true, }; emailMessage.AddGlobalVariable("subject", subject); emailMessage.AddGlobalVariable("first_name", firstName); var task = _mandrill.SendMessageAsync(emailMessage, "welcome-my-notes-saas", null); task.Wait(); return task; } public Task SendResetPasswordEmail(string firstName, string email, string resetLink) { const string subject = "Reset My Notes Password Request"; var emailMessage = new EmailMessage { from_email = EmailFromAddress, from_name = EmailFromName, subject = subject, to = new List<Mandrill.EmailAddress> { new EmailAddress(email) } }; emailMessage.AddGlobalVariable("subject", subject); emailMessage.AddGlobalVariable("FIRST_NAME", firstName); emailMessage.AddGlobalVariable("RESET_PASSWORD_LINK", resetLink); var task = _mandrill.SendMessageAsync(emailMessage, "reset-password-my-notes-saas", null); return task; } }
To send an email through Mandrill API:
In AccountController -> Register action, this is the code snippet to send the welcome email:
await _userManager.EmailService.SendWelcomeEmail(user.UserName, user.Email);
In AccountController -> ForgotPassword action, this is the code to send the email:
// Send an email to reset password string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id); var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme); await UserManager.EmailService.SendResetPasswordEmail(user.UserName, user.Email, callbackUrl);
One important thing in SAAS applications is billing. We need to have a way to charge our customers periodically, monthly in this example. Because this part is something that requires a lot of work, but doesn't add anything valuable to the product that we are selling, we are going to use the open source library SAAS Ecom that was created for this purpose.
SAAS Ecom has a dependency on Entity Framework Code First. For those of you that are not familiar with it, Entity Framework Code First allows you to focus on creating C# POCO classes, letting Entity Framework map the classes to database tables. It follows the idea of convention over configuration, but you can still specify mappings, foreign keys and so on, if needed.
To add SAAS Ecom to our project, just install the dependency using NuGet. The library is split in two packages: SaasEcom.Core that contains the business logic, and SaasEcom.FrontEnd that contains some view helpers to use in an MVC application. Go ahead and install SaasEcom.FrontEnd.
You can see that some files have been added to your solution:
There are still a few steps left to integrate SAAS Ecom, so get your Stripe API Keys and add them to Web.config.
<appSettings> <add key="StripeApiSecretKey" value="your_key_here" /> <add key="StripeApiPublishableKey" value="your_key_here" /> </appSettings>
If you try to compile, you'll see errors:
Open the file Models/IdentityModels.cs, and then make the class ApplicationUser inherit from SaasEcomUser.
ApplicationUser : SaasEcomUser { /* your class methods*/ }
Open the file Models/IdentityModels.cs, and then your class ApplicationDbContext should inherit from SaasEcomDbContext<ApplicationUser>.
ApplicationDbContext : SaasEcomDbContext<ApplicationUser> { /* Your Db context properties */ }
Because ApplicationUser
is inheriting from SaasEcomUser
, the default behaviour for Entity Framework would be to create two tables in the database. Because we don't need that in this case, we need to add this method to the class ApplicationDbContext
to specify that it should use only one table:
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<ApplicationUser>().Map(m => m.MapInheritedProperties()); base.OnModelCreating(modelBuilder); }
As we just updated the DbContext
, to make it inherit from SaasEcomDbContext
, the database has to be updated too. In order to do that, enable code migrations and update the database opening NuGet Package Manager from the menu Tools > NuGet Package Manager > Package Manager Console:
PM > enable-migrations PM > add-migration Initial PM > update-database
If you get an error when you run update-database
, the database (SQL Compact) is inside your AppData folder, so open the database, delete all the tables in it, and then run update-database
again.
The next step in the project is to integrate Stripe to charge our customers monthly, and for that we need to create the subscription plans and pricing in Stripe. So sign in to your Stripe dashboard, and create your subscription plans as you can see in the pictures.
Once we have created the Subscription Plans in Stripe, let's add them to the database. We do this so that we don't have to query Stripe API each time that we need any information related to subscription plans.
Also, we can store specific properties related to each plan. In this example, I'm saving as a property of each plan the number of notes that a user can save: 100 notes for the basic plan, 10,000 for the professional, and 1 million for the business plan. We add that information to the Seed method that is executed each time that the database is updated when we run update-database
from NuGet Package Manager console.
Open the file Migrations/Configuration.cs and add this method:
protected override void Seed(MyNotes.Models.ApplicationDbContext context) { // This method will be called after migrating to the latest version. var basicMonthly = new SubscriptionPlan { Id = "basic_monthly", Name = "Basic", Interval = SubscriptionPlan.SubscriptionInterval.Monthly, TrialPeriodInDays = 30, Price = 10.00, Currency = "USD" }; basicMonthly.Properties.Add(new SubscriptionPlanProperty { Key = "MaxNotes", Value = "100" }); var professionalMonthly = new SubscriptionPlan { Id = "professional_monthly", Name = "Professional", Interval = SubscriptionPlan.SubscriptionInterval.Monthly, TrialPeriodInDays = 30, Price = 20.00, Currency = "USD" }; professionalMonthly.Properties.Add(new SubscriptionPlanProperty { Key = "MaxNotes", Value = "10000" }); var businessMonthly = new SubscriptionPlan { Id = "business_monthly", Name = "Business", Interval = SubscriptionPlan.SubscriptionInterval.Monthly, TrialPeriodInDays = 30, Price = 30.00, Currency = "USD" }; businessMonthly.Properties.Add(new SubscriptionPlanProperty { Key = "MaxNotes", Value = "1000000" }); context.SubscriptionPlans.AddOrUpdate( sp => sp.Id, basicMonthly, professionalMonthly, businessMonthly); }
The next thing that we need to do is to ensure that each time a user registers for our app, we also create the user in Stripe using their API. To do that we use SAAS Ecom API, and we just need to edit the action Register on AccountController and add these lines after creating the user in the database:
// Create Stripe user await SubscriptionsFacade.SubscribeUserAsync(user, model.SubscriptionPlan); await UserManager.UpdateAsync(user);
The method SubscribeUserAsync
subscribes the user to the plan in Stripe, and if the user doesn't exist already in Stripe it is created too. This is useful if you have a freemium SAAS and you only create users in Stripe once they are on a paid plan. Another small change in the Register
action from AccountController
is to save the RegistrationDate
and LastLoginTime
when you create the user:
var user = new ApplicationUser { UserName = model.Email, Email = model.Email, RegistrationDate = DateTime.UtcNow, LastLoginTime = DateTime.UtcNow }; var result = await UserManager.CreateAsync(user, model.Password);
As we need the dependency SubscriptionsFacade from SAAS Ecom, add it as a property to Account Controller:
private SubscriptionsFacade _subscriptionsFacade; private SubscriptionsFacade SubscriptionsFacade { get { return _subscriptionsFacade ?? (_subscriptionsFacade = new SubscriptionsFacade( new SubscriptionDataService<ApplicationDbContext, ApplicationUser> (HttpContext.GetOwinContext().Get<ApplicationDbContext>()), new SubscriptionProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"]), new CardProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"], new CardDataService<ApplicationDbContext, ApplicationUser>(Request.GetOwinContext().Get<ApplicationDbContext>())), new CardDataService<ApplicationDbContext, ApplicationUser>(Request.GetOwinContext().Get<ApplicationDbContext>()), new CustomerProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"]), new SubscriptionPlanDataService<ApplicationDbContext, ApplicationUser>(Request.GetOwinContext().Get<ApplicationDbContext>()), new ChargeProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"]))); } }
You can simplify the way that this is instantiated using dependency injection, but this is something that can be covered in another article.
When we added SAAS Ecom to the project, some view partials were added too. They use the main _Layout.cshtml, but that layout is the one being used by the landing page. We need to add a different layout for the web application area or customer dashboard.
I have created a very similar version to the _Layout.cshtml that is created when you add a new MVC project in Visual Studio—you can see the _DashboardLayout.cshtml in GitHub.
The main differences are that I have added font-awesome and an area to display Bootstrap notifications if they're present:
<div id="bootstrap_alerts"> @if (TempData.ContainsKey("flash")) { @Html.Partial("_Alert", TempData["flash"]); } </div>
For the views in the folder Views/Billing, set the layout to _DashboardLayout, otherwise it would use the default one that is _Layout.cshtml. Do the same thing for views on the folder Views/Manage:
Layout = "~/Views/Shared/_DashboardLayout.cshtml";
I have slightly modified "DashboardLayout" to use some styles from the main website, and it looks like this after signing up and navigating to the Billing section:
In the billing area a customer can Cancel or Upgrade / Downgrade a subscription. Add payment details, using Stripe JavaScript API, so we don't need to be PCI compliant and only need SSL in the server to take payments from our customers.
To properly test your new application, you can use several credit card numbers provided by Stripe.
The last thing that you might want to do is set up Stripe Webhooks. This is used to let Stripe notify you about events that happen in your billing, like payment successful, payment overdue, trial about to expire, and so on—you can get a full list from the Stripe documentation. The Stripe event is sent as JSON to a public facing URL. To test this locally you probably want to use Ngrok.
When SAAS Ecom was installed, a new controller was added to handle the webhooks from Stripe: StripeWebhooksController.cs. You can see there how the invoice created event is handled:
case "invoice.payment_succeeded": // Occurs whenever an invoice attempts to be paid, and the payment succeeds. StripeInvoice stripeInvoice = Stripe.Mapper<StripeInvoice>.MapFromJson(stripeEvent.Data.Object.ToString()); Invoice invoice = SaasEcom.Core.Infrastructure.Mappers.Map(stripeInvoice); if (invoice != null && invoice.Total > 0) { // TODO get the customer billing address, we still have to instantiate the address on the invoice invoice.BillingAddress = new BillingAddress(); await InvoiceDataService.CreateOrUpdateAsync(invoice); // TODO: Send invoice by email } break;
You can implement as many events in the controller as you need.
The most important part of this SAAS application is to allow our customers to save notes. In order to create this functionality, let's start by creating the Note
class:
public class Note { public int Id { get; set; } [Required] [MaxLength(250)] public string Title { get; set; } [Required] public string Text { get; set; } public DateTime CreatedAt { get; set; } }
Add a One to Many relationship from ApplicationUser
to Note
:
public virtual ICollection<Note> Notes { get; set; }
Because the DbContext has changed, we need to add a new database Migration, so open Nuget Package Manager console and run:
PM> add-migration NotesAddedToModel
This is the generated code:
public partial class NotesAddedToModel : DbMigration { public override void Up() { CreateTable( "dbo.Notes", c => new { Id = c.Int(nullable: false, identity: true), Title = c.String(nullable: false, maxLength: 250), Text = c.String(nullable: false), CreatedAt = c.DateTime(nullable: false), ApplicationUser_Id = c.String(maxLength: 128), }) .PrimaryKey(t => t.Id) .ForeignKey("dbo.AspNetUsers", t => t.ApplicationUser_Id) .Index(t => t.ApplicationUser_Id); } public override void Down() { DropForeignKey("dbo.Notes", "ApplicationUser_Id", "dbo.AspNetUsers"); DropIndex("dbo.Notes", new[] { "ApplicationUser_Id" }); DropTable("dbo.Notes"); } }
The next thing we need is the Controller MyNotes. As we already have the model class Notes, we use the scaffold to create the controller class to have create, read, update and delete methods using Entity Framework. We also use the scaffold to generate the views.
At this point, after a user is registered successfully on My Notes, redirect the user to the Index
action of NotesController
:
TempData["flash"] = new FlashSuccessViewModel("Congratulations! Your account has been created."); return RedirectToAction("Index", "Notes");
So far, we have created a CRUD (Create / Read / Update / Delete) interface for Notes. We still need to check when users try to add notes, to make sure that they have enough space in their subscriptions.
Empty list of notes:
Create new note:
List of notes:
Note detail:
Edit note:
Confirm note deletion:
I'm going to edit slightly the default markup:
CreatedAt
field, and set the value in the controller.CreatedAt
to be a hidden field so that it's not editable.When we generated the Notes controller using Entity Framework, the list of notes was listing all the notes in the database, not only the notes for the logged-in user. For security we need to check that users can only see, modify or delete the notes that belong to them.
We also need to check how many notes a user has before allowing him or her to create a new one, to check that the subscription plan limits are met. Here is the new code for NotesController:
public class NotesController : Controller { private readonly ApplicationDbContext _db = new ApplicationDbContext(); private SubscriptionsFacade _subscriptionsFacade; private SubscriptionsFacade SubscriptionsFacade { get { return _subscriptionsFacade ?? (_subscriptionsFacade = new SubscriptionsFacade( new SubscriptionDataService<ApplicationDbContext, ApplicationUser> (HttpContext.GetOwinContext().Get<ApplicationDbContext>()), new SubscriptionProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"]), new CardProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"], new CardDataService<ApplicationDbContext, ApplicationUser>(Request.GetOwinContext().Get<ApplicationDbContext>())), new CardDataService<ApplicationDbContext, ApplicationUser>(Request.GetOwinContext().Get<ApplicationDbContext>()), new CustomerProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"]), new SubscriptionPlanDataService<ApplicationDbContext, ApplicationUser>(Request.GetOwinContext().Get<ApplicationDbContext>()), new ChargeProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"]))); } } // GET: Notes public async Task<ActionResult> Index() { var userId = User.Identity.GetUserId(); var userNotes = await _db.Users.Where(u => u.Id == userId) .Include(u => u.Notes) .SelectMany(u => u.Notes) .ToListAsync(); return View(userNotes); } // GET: Notes/Details/5 public async Task<ActionResult> Details(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } var userId = User.Identity.GetUserId(); ICollection<Note> userNotes = ( await _db.Users.Where(u => u.Id == userId) .Include(u => u.Notes).Select(u => u.Notes) .FirstOrDefaultAsync()); if (userNotes == null) { return HttpNotFound(); } Note note = userNotes.FirstOrDefault(n => n.Id == id); if (note == null) { return HttpNotFound(); } return View(note); } // GET: Notes/Create public ActionResult Create() { return View(); } // POST: Notes/Create // To protect from overposting attacks, please enable the specific properties you want to bind to, for // more details see http://go.microsoft.com/fwlink/?LinkId=317598. [HttpPost] [ValidateAntiForgeryToken] public async Task<ActionResult> Create([Bind(Include = "Id,Title,Text,CreatedAt")] Note note) { if (ModelState.IsValid) { if (await UserHasEnoughSpace(User.Identity.GetUserId())) { note.CreatedAt = DateTime.UtcNow; // The note is added to the user object so the Foreign Key is saved too var userId = User.Identity.GetUserId(); var user = await this._db.Users.Where(u => u.Id == userId).FirstOrDefaultAsync(); user.Notes.Add(note); await _db.SaveChangesAsync(); return RedirectToAction("Index"); } else { TempData.Add("flash", new FlashWarningViewModel("You can not add more notes, upgrade your subscription plan or delete some notes.")); } } return View(note); } private async Task<bool> UserHasEnoughSpace(string userId) { var subscription = (await SubscriptionsFacade.UserActiveSubscriptionsAsync(userId)).FirstOrDefault(); if (subscription == null) { return false; } var userNotes = await _db.Users.Where(u => u.Id == userId).Include(u => u.Notes).Select(u => u.Notes).CountAsync(); return subscription.SubscriptionPlan.GetPropertyInt("MaxNotes") > userNotes; } // GET: Notes/Edit/5 public async Task<ActionResult> Edit(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } if (!await NoteBelongToUser(User.Identity.GetUserId(), noteId: id.Value)) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Note note = await _db.Notes.FindAsync(id); if (note == null) { return HttpNotFound(); } return View(note); } // POST: Notes/Edit/5 // To protect from overposting attacks, please enable the specific properties you want to bind to, for // more details see http://go.microsoft.com/fwlink/?LinkId=317598. [HttpPost] [ValidateAntiForgeryToken] public async Task<ActionResult> Edit([Bind(Include = "Id,Title,Text,CreatedAt")] Note note) { if (ModelState.IsValid && await NoteBelongToUser(User.Identity.GetUserId(), note.Id)) { _db.Entry(note).State = EntityState.Modified; await _db.SaveChangesAsync(); return RedirectToAction("Index"); } return View(note); } // GET: Notes/Delete/5 public async Task<ActionResult> Delete(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } if (!await NoteBelongToUser(User.Identity.GetUserId(), noteId: id.Value)) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Note note = await _db.Notes.FindAsync(id); if (note == null) { return HttpNotFound(); } return View(note); } // POST: Notes/Delete/5 [HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] public async Task<ActionResult> DeleteConfirmed(int id) { if (!await NoteBelongToUser(User.Identity.GetUserId(), noteId: id)) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Note note = await _db.Notes.FindAsync(id); _db.Notes.Remove(note); await _db.SaveChangesAsync(); return RedirectToAction("Index"); } private async Task<bool> NoteBelongToUser(string userId, int noteId) { return await _db.Users.Where(u => u.Id == userId).Where(u => u.Notes.Any(n => n.Id == noteId)).AnyAsync(); } protected override void Dispose(bool disposing) { if (disposing) { _db.Dispose(); } base.Dispose(disposing); } }
This is it—we have the core functionality for our SAAS application.
At the beginning of this year the legislation in the European Union for VAT for business supplying digital services to private consumers changed. The main difference is that businesses have to charge VAT to private customers, not business customers with a valid VAT number, according to the country in the EU in which they are based. To validate in which country they're based we need to keep a record of at least two of these forms:
For this reason we are going to geo-locate the user IP address, to save it along with the billing address and credit card country.
For geo-location, I am going to use Maxmind GeoLite2. It's a free database that gives us the country where an IP is located.
Download, and add the database to App_Data, as you can see in the photo:
Create Extensions/GeoLocationHelper.cs.
public static class GeoLocationHelper { // ReSharper disable once InconsistentNaming /// <summary> /// Gets the country ISO code from IP. /// </summary> /// <param name="ipAddress">The ip address.</param> /// <returns></returns> public static string GetCountryFromIP(string ipAddress) { string country; try { using ( var reader = new DatabaseReader(HttpContext.Current.Server.MapPath("~/App_Data/GeoLite2-Country.mmdb"))) { var response = reader.Country(ipAddress); country = response.Country.IsoCode; } } catch (Exception ex) { country = null; } return country; } /// <summary> /// Selects the list countries. /// </summary> /// <param name="country">The country.</param> /// <returns></returns> public static List<SelectListItem> SelectListCountries(string country) { var getCultureInfo = CultureInfo.GetCultures(CultureTypes.SpecificCultures); var countries = getCultureInfo.Select(cultureInfo => new RegionInfo(cultureInfo.LCID)) .Select(getRegionInfo => new SelectListItem { Text = getRegionInfo.EnglishName, Value = getRegionInfo.TwoLetterISORegionName, Selected = country == getRegionInfo.TwoLetterISORegionName }).OrderBy(c => c.Text).DistinctBy(i => i.Text).ToList(); return countries; } public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) { var seenKeys = new HashSet<TKey>(); return source.Where(element => seenKeys.Add(keySelector(element))); } }
There are two methods implemented in this static class:
GetCountryFromIP
: Returns the country ISO Code given an IP Address.SelectListCountries
: Returns a list of countries to use in a drop-down field. It has the country ISO Code as a value for each country and the country name to be displayed.In the action Register
from AccountController
, when creating the user, save the IP and the country the IP belongs to:
var userIP = GeoLocation.GetUserIP(Request); var user = new ApplicationUser { UserName = model.Email, Email = model.Email, RegistrationDate = DateTime.UtcNow, LastLoginTime = DateTime.UtcNow, IPAddress = userIP, IPAddressCountry = GeoLocationHelper.GetCountryFromIP(userIP), };
Also, when we create the subscription in Stripe, we need to pass the Tax Percentage for this customer. We do that a few lines after creating the user:
// Create Stripe user var taxPercent = EuropeanVat.Countries.ContainsKey(user.IPAddressCountry) ? EuropeanVat.Countries[user.IPAddressCountry] : 0; await SubscriptionsFacade.SubscribeUserAsync(user, model.SubscriptionPlan, taxPercent: taxPercent);
By default, if a user is based in the European Union, I'm setting the tax percentage to that subscription. The rules are a bit more complex than that, but summarizing:
At the moment we are not allowing to our customers to save a Billing Address, and their VAT number if they are an EU VAT registered business. In that case, we need to change their tax percentage to 0.
SAAS Ecom provides the BillingAddress
class, but it's not attached to any entity of the model. The main reason for this is that in some SAAS applications it might make sense to assign this to an Organization class if multiple users have access to the same account. If this is not the case, as in our sample, we can safely add that relationship to the ApplicationUser
class:
public class ApplicationUser : SaasEcomUser { public virtual ICollection<Note> Notes { get; set; } public virtual BillingAddress BillingAddress { get; set; } public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager) { // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie); // Add custom user claims here return userIdentity; } }
As each time that we modify the model, we need to add a database migration, open Tools > NuGet Package Manager > Package Manager Console:
PM> add-migration BillingAddressAddedToUser
And this is the migration class that we get:
public partial class BillingAddressAddedToUser : DbMigration { public override void Up() { AddColumn("dbo.AspNetUsers", "BillingAddress_Name", c => c.String()); AddColumn("dbo.AspNetUsers", "BillingAddress_AddressLine1", c => c.String()); AddColumn("dbo.AspNetUsers", "BillingAddress_AddressLine2", c => c.String()); AddColumn("dbo.AspNetUsers", "BillingAddress_City", c => c.String()); AddColumn("dbo.AspNetUsers", "BillingAddress_State", c => c.String()); AddColumn("dbo.AspNetUsers", "BillingAddress_ZipCode", c => c.String()); AddColumn("dbo.AspNetUsers", "BillingAddress_Country", c => c.String()); AddColumn("dbo.AspNetUsers", "BillingAddress_Vat", c => c.String()); } public override void Down() { DropColumn("dbo.AspNetUsers", "BillingAddress_Vat"); DropColumn("dbo.AspNetUsers", "BillingAddress_Country"); DropColumn("dbo.AspNetUsers", "BillingAddress_ZipCode"); DropColumn("dbo.AspNetUsers", "BillingAddress_State"); DropColumn("dbo.AspNetUsers", "BillingAddress_City"); DropColumn("dbo.AspNetUsers", "BillingAddress_AddressLine2"); DropColumn("dbo.AspNetUsers", "BillingAddress_AddressLine1"); DropColumn("dbo.AspNetUsers", "BillingAddress_Name"); } }
To create these changes in the database, we execute in the Package Manager Console:
PM> update-database
One more detail that we need to fix is that in AccountController > Register, we need to set a default billing address as it's a non-nullable field.
var user = new ApplicationUser { UserName = model.Email, Email = model.Email, RegistrationDate = DateTime.UtcNow, LastLoginTime = DateTime.UtcNow, IPAddress = userIP, IPAddressCountry = GeoLocationHelper.GetCountryFromIP(userIP), BillingAddress = new BillingAddress() };
In the billing page, we need to display the Billing Address for the customer if it has been added, and also allow our customers to edit it. First, we need to modify the action Index
from BillingController
to pass the billing address to the view:
public async Task<ViewResult> Index() { var userId = User.Identity.GetUserId(); ViewBag.Subscriptions = await SubscriptionsFacade.UserActiveSubscriptionsAsync(userId); ViewBag.PaymentDetails = await SubscriptionsFacade.DefaultCreditCard(userId); ViewBag.Invoices = await InvoiceDataService.UserInvoicesAsync(userId); ViewBag.BillingAddress = (await UserManager.FindByIdAsync(userId)).BillingAddress; return View(); }
To display the address, we just need to edit the view "Billing/Index.cshtml", and add the view partial provided by SAAS Ecom for that:
<h2>Billing</h2> <br /> @Html.Partial("_Subscriptions", (List<Subscription>)ViewBag.Subscriptions) <br/> @Html.Partial("_PaymentDetails", (CreditCard)ViewBag.PaymentDetails) <br /> @Html.Partial("_BillingAddress", (BillingAddress)ViewBag.BillingAddress) <br /> @Html.Partial("_Invoices", (List<Invoice>)ViewBag.Invoices)
Now, if we navigate to Billing we can see the new section:
The next step is on the BillingController > BillingAddress action, we need to pass the Billing address to the view. Because we need to get the user's two-letter ISO country code, I've added a dropdown to select the country, which defaults to the country that the user IP belongs to:
public async Task<ViewResult> BillingAddress() { var model = (await UserManager.FindByIdAsync(User.Identity.GetUserId())).BillingAddress; // List for dropdown country select var userCountry = (await UserManager.FindByIdAsync(User.Identity.GetUserId())).IPAddressCountry; ViewBag.Countries = GeoLocationHelper.SelectListCountries(userCountry); return View(model); }
When the user submits the form, we need to save the billing address and update the tax percent if it's needed:
[HttpPost] public async Task<ActionResult> BillingAddress(BillingAddress model) { if (ModelState.IsValid) { var userId = User.Identity.GetUserId(); // Call your service to save the billing address var user = await UserManager.FindByIdAsync(userId); user.BillingAddress = model; await UserManager.UpdateAsync(user); // Model Country has to be 2 letter ISO Code if (!string.IsNullOrEmpty(model.Vat) && !string.IsNullOrEmpty(model.Country) && EuropeanVat.Countries.ContainsKey(model.Country)) { await UpdateSubscriptionTax(userId, 0); } else if (!string.IsNullOrEmpty(model.Country) && EuropeanVat.Countries.ContainsKey(model.Country)) { await UpdateSubscriptionTax(userId, EuropeanVat.Countries[model.Country]); } TempData.Add("flash", new FlashSuccessViewModel("Your billing address has been saved.")); return RedirectToAction("Index"); } return View(model); } private async Task UpdateSubscriptionTax(string userId, decimal tax) { var user = await UserManager.FindByIdAsync(userId); var subscription = (await SubscriptionsFacade.UserActiveSubscriptionsAsync(userId)).FirstOrDefault(); if (subscription != null && subscription.TaxPercent != tax) { await SubscriptionsFacade.UpdateSubscriptionTax(user, subscription.StripeId, tax); } }
This is what the form to add or edit a billing address looks like:
After adding the address, I get redirected back to the billing area:
As you can see in the screenshot above, because I set my country to United Kingdom, and I didn't enter a VAT number, 20% VAT is added to the monthly price. The code showed here is assuming that you are a non-EU-based company. If that's the case, you need to handle the case where your customer is in your country, and regardless of whether they have VAT or not, you'll have to charge VAT.
Create Modern Vue Apps Using Create-Vue and Vite
/Pros and Cons of Using WordPress
/How to Fix the “There Has Been a Critical Error in Your Website” Error in WordPress
/How To Fix The “There Has Been A Critical Error in Your Website” Error in WordPress
/How to Create a Privacy Policy Page in WordPress
/How Long Does It Take to Learn JavaScript?
/The Best Way to Deep Copy an Object in JavaScript
/Adding and Removing Elements From Arrays in JavaScript
/Create a JavaScript AJAX Post Request: With and Without jQuery
/5 Real-Life Uses for the JavaScript reduce() Method
/How to Enable or Disable a Button With JavaScript: jQuery vs. Vanilla
/How to Enable or Disable a Button With JavaScript: jQuery vs Vanilla
/Confirm Yes or No With JavaScript
/How to Change the URL in JavaScript: Redirecting
/15+ Best WordPress Twitter Widgets
/27 Best Tab and Accordion Widget Plugins for WordPress (Free & Premium)
/21 Best Tab and Accordion Widget Plugins for WordPress (Free & Premium)
/30 HTML Best Practices for Beginners
/31 Best WordPress Calendar Plugins and Widgets (With 5 Free Plugins)
/25 Ridiculously Impressive HTML5 Canvas Experiments
/How to Implement Email Verification for New Members
/How to Create a Simple Web-Based Chat Application
/30 Popular WordPress User Interface Elements
/Top 18 Best Practices for Writing Super Readable Code
/Best Affiliate WooCommerce Plugins Compared
/18 Best WordPress Star Rating Plugins
/10+ Best WordPress Twitter Widgets
/20+ Best WordPress Booking and Reservation Plugins
/Working With Tables in React: Part Two
/Best CSS Animations and Effects on CodeCanyon
/30 CSS Best Practices for Beginners
/How to Create a Custom WordPress Plugin From Scratch
/10 Best Responsive HTML5 Sliders for Images and Text… and 3 Free Options
/16 Best Tab and Accordion Widget Plugins for WordPress
/18 Best WordPress Membership Plugins and 5 Free Plugins
/25 Best WooCommerce Plugins for Products, Pricing, Payments and More
/10 Best WordPress Twitter Widgets
1 /12 Best Contact Form PHP Scripts for 2020
/20 Popular WordPress User Interface Elements
/10 Best WordPress Star Rating Plugins
/12 Best CSS Animations on CodeCanyon
/12 Best WordPress Booking and Reservation Plugins
/12 Elegant CSS Pricing Tables for Your Latest Web Project
/24 Best WordPress Form Plugins for 2020
/14 Best PHP Event Calendar and Booking Scripts
/Getting Started With Django: Newly Updated Course
/Create a Blog for Each Category or Department in Your WooCommerce Store
/8 Best WordPress Booking and Reservation Plugins
/Best Exit Popups for WordPress Compared
/Best Exit Popups for WordPress Compared
/11 Best Tab & Accordion WordPress Widgets & Plugins
/12 Best Tab & Accordion WordPress Widgets & Plugins
1 /New Course: Practical React Fundamentals
/Preview Our New Course on Angular Material
/Build Your Own CAPTCHA and Contact Form in PHP
/Object-Oriented PHP With Classes and Objects
/Best Practices for ARIA Implementation
/Accessible Apps: Barriers to Access and Getting Started With Accessibility
/Dramatically Speed Up Your React Front-End App Using Lazy Loading
/15 Best Modern JavaScript Admin Templates for React, Angular, and Vue.js
/15 Best Modern JavaScript Admin Templates for React, Angular and Vue.js
/19 Best JavaScript Admin Templates for React, Angular, and Vue.js
/New Course: Build an App With JavaScript and the MEAN Stack
/10 Best WordPress Facebook Widgets
13 /Hands-on With ARIA: Accessibility for eCommerce
/New eBooks Available for Subscribers
/Hands-on With ARIA: Homepage Elements and Standard Navigation
/Site Accessibility: Getting Started With ARIA
/How Secure Are Your JavaScript Open-Source Dependencies?
/New Course: Secure Your WordPress Site With SSL
/Testing Components in React Using Jest and Enzyme
/Testing Components in React Using Jest: The Basics
/15 Best PHP Event Calendar and Booking Scripts
/Create Interactive Gradient Animations Using Granim.js
/How to Build Complex, Large-Scale Vue.js Apps With Vuex
1 /Examples of Dependency Injection in PHP With Symfony Components
/Set Up Routing in PHP Applications Using the Symfony Routing Component
1 /A Beginner’s Guide to Regular Expressions in JavaScript
/Introduction to Popmotion: Custom Animation Scrubber
/Introduction to Popmotion: Pointers and Physics
/New Course: Connect to a Database With Laravel’s Eloquent ORM
/How to Create a Custom Settings Panel in WooCommerce
/Building the DOM faster: speculative parsing, async, defer and preload
1 /20 Useful PHP Scripts Available on CodeCanyon
3 /How to Find and Fix Poor Page Load Times With Raygun
/Introduction to the Stimulus Framework
/Single-Page React Applications With the React-Router and React-Transition-Group Modules
12 Best Contact Form PHP Scripts
1 /Getting Started With the Mojs Animation Library: The ShapeSwirl and Stagger Modules
/Getting Started With the Mojs Animation Library: The Shape Module
/Getting Started With the Mojs Animation Library: The HTML Module
/Project Management Considerations for Your WordPress Project
/8 Things That Make Jest the Best React Testing Framework
/Creating an Image Editor Using CamanJS: Layers, Blend Modes, and Events
/New Short Course: Code a Front-End App With GraphQL and React
/Creating an Image Editor Using CamanJS: Applying Basic Filters
/Creating an Image Editor Using CamanJS: Creating Custom Filters and Blend Modes
/Modern Web Scraping With BeautifulSoup and Selenium
/Challenge: Create a To-Do List in React
1 /Deploy PHP Web Applications Using Laravel Forge
/Getting Started With the Mojs Animation Library: The Burst Module
/10 Things Men Can Do to Support Women in Tech
/A Gentle Introduction to Higher-Order Components in React: Best Practices
/Challenge: Build a React Component
/A Gentle Introduction to HOC in React: Learn by Example
/A Gentle Introduction to Higher-Order Components in React
/Creating Pretty Popup Messages Using SweetAlert2
/Creating Stylish and Responsive Progress Bars Using ProgressBar.js
/18 Best Contact Form PHP Scripts for 2022
/How to Make a Real-Time Sports Application Using Node.js
/Creating a Blogging App Using Angular & MongoDB: Delete Post
/Set Up an OAuth2 Server Using Passport in Laravel
/Creating a Blogging App Using Angular & MongoDB: Edit Post
/Creating a Blogging App Using Angular & MongoDB: Add Post
/Introduction to Mocking in Python
/Creating a Blogging App Using Angular & MongoDB: Show Post
/Creating a Blogging App Using Angular & MongoDB: Home
/Creating a Blogging App Using Angular & MongoDB: Login
/Creating Your First Angular App: Implement Routing
/Persisted WordPress Admin Notices: Part 4
/Creating Your First Angular App: Components, Part 2
/Persisted WordPress Admin Notices: Part 3
/Creating Your First Angular App: Components, Part 1
/How Laravel Broadcasting Works
/Persisted WordPress Admin Notices: Part 2
/Create Your First Angular App: Storing and Accessing Data
/Persisted WordPress Admin Notices: Part 1
/Error and Performance Monitoring for Web & Mobile Apps Using Raygun
/Using Luxon for Date and Time in JavaScript
7 /How to Create an Audio Oscillator With the Web Audio API
/How to Cache Using Redis in Django Applications
/20 Essential WordPress Utilities to Manage Your Site
/Beginner’s Guide to Angular 4: HTTP
/Rapid Web Deployment for Laravel With GitHub, Linode, and RunCloud.io
/Beginners Guide to Angular 4: Routing
/Beginner’s Guide to Angular 4: Services
/Beginner’s Guide to Angular 4: Components
/Creating a Drop-Down Menu for Mobile Pages
/Introduction to Forms in Angular 4: Writing Custom Form Validators
/10 Best WordPress Booking & Reservation Plugins
/Getting Started With Redux: Connecting Redux With React
/Getting Started With Redux: Learn by Example
/Getting Started With Redux: Why Redux?
/Understanding Recursion With JavaScript
/How to Auto Update WordPress Salts
/How to Download Files in Python
/Eloquent Mutators and Accessors in Laravel
1 /10 Best HTML5 Sliders for Images and Text
/Creating a Task Manager App Using Ionic: Part 2
/Creating a Task Manager App Using Ionic: Part 1
/Introduction to Forms in Angular 4: Reactive Forms
/Introduction to Forms in Angular 4: Template-Driven Forms
/24 Essential WordPress Utilities to Manage Your Site
/25 Essential WordPress Utilities to Manage Your Site
/Get Rid of Bugs Quickly Using BugReplay
1 /Manipulating HTML5 Canvas Using Konva: Part 1, Getting Started
/10 Must-See Easy Digital Downloads Extensions for Your WordPress Site
/22 Best WordPress Booking and Reservation Plugins
/Understanding ExpressJS Routing
/15 Best WordPress Star Rating Plugins
/Creating Your First Angular App: Basics
/Inheritance and Extending Objects With JavaScript
/Introduction to the CSS Grid Layout With Examples
1Performant Animations Using KUTE.js: Part 5, Easing Functions and Attributes
Performant Animations Using KUTE.js: Part 4, Animating Text
/Performant Animations Using KUTE.js: Part 3, Animating SVG
/New Course: Code a Quiz App With Vue.js
/Performant Animations Using KUTE.js: Part 2, Animating CSS Properties
Performant Animations Using KUTE.js: Part 1, Getting Started
/10 Best Responsive HTML5 Sliders for Images and Text (Plus 3 Free Options)
/Single-Page Applications With ngRoute and ngAnimate in AngularJS
/Deferring Tasks in Laravel Using Queues
/Site Authentication in Node.js: User Signup and Login
/Working With Tables in React, Part Two
/Working With Tables in React, Part One
/How to Set Up a Scalable, E-Commerce-Ready WordPress Site Using ClusterCS
/New Course on WordPress Conditional Tags
/TypeScript for Beginners, Part 5: Generics
/Building With Vue.js 2 and Firebase
6 /Essential JavaScript Libraries and Frameworks You Should Know About
/Vue.js Crash Course: Create a Simple Blog Using Vue.js
/Build a React App With a Laravel RESTful Back End: Part 1, Laravel 5.5 API
/API Authentication With Node.js
/Beginner’s Guide to Angular: Routing
/Beginners Guide to Angular: Routing
/Beginner’s Guide to Angular: Services
/Beginner’s Guide to Angular: Components
/How to Create a Custom Authentication Guard in Laravel
/Learn Computer Science With JavaScript: Part 3, Loops
/Build Web Applications Using Node.js
/Learn Computer Science With JavaScript: Part 4, Functions
/Learn Computer Science With JavaScript: Part 2, Conditionals
/Create Interactive Charts Using Plotly.js, Part 5: Pie and Gauge Charts
/Create Interactive Charts Using Plotly.js, Part 4: Bubble and Dot Charts
/Create Interactive Charts Using Plotly.js, Part 3: Bar Charts
/Awesome JavaScript Libraries and Frameworks You Should Know About
/Create Interactive Charts Using Plotly.js, Part 2: Line Charts
/Bulk Import a CSV File Into MongoDB Using Mongoose With Node.js
/Build a To-Do API With Node, Express, and MongoDB
/Getting Started With End-to-End Testing in Angular Using Protractor
/TypeScript for Beginners, Part 4: Classes
/Object-Oriented Programming With JavaScript
/10 Best Affiliate WooCommerce Plugins Compared
/Stateful vs. Stateless Functional Components in React
/Make Your JavaScript Code Robust With Flow
/Build a To-Do API With Node and Restify
/Testing Components in Angular Using Jasmine: Part 2, Services
/Testing Components in Angular Using Jasmine: Part 1
/Creating a Blogging App Using React, Part 6: Tags
/React Crash Course for Beginners, Part 3
/React Crash Course for Beginners, Part 2
/React Crash Course for Beginners, Part 1
/Set Up a React Environment, Part 4
1 /Set Up a React Environment, Part 3
/New Course: Get Started With Phoenix
/Set Up a React Environment, Part 2
/Set Up a React Environment, Part 1
/Command Line Basics and Useful Tricks With the Terminal
/How to Create a Real-Time Feed Using Phoenix and React
/Build a React App With a Laravel Back End: Part 2, React
/Build a React App With a Laravel RESTful Back End: Part 1, Laravel 9 API
/Creating a Blogging App Using React, Part 5: Profile Page
/Pagination in CodeIgniter: The Complete Guide
/JavaScript-Based Animations Using Anime.js, Part 4: Callbacks, Easings, and SVG
/JavaScript-Based Animations Using Anime.js, Part 3: Values, Timeline, and Playback
/Learn to Code With JavaScript: Part 1, The Basics
/10 Elegant CSS Pricing Tables for Your Latest Web Project
/Getting Started With the Flux Architecture in React
/Getting Started With Matter.js: The Composites and Composite Modules
Getting Started With Matter.js: The Engine and World Modules
/10 More Popular HTML5 Projects for You to Use and Study
/Understand the Basics of Laravel Middleware
/Iterating Fast With Django & Heroku
/Creating a Blogging App Using React, Part 4: Update & Delete Posts
/Creating a jQuery Plugin for Long Shadow Design
/How to Register & Use Laravel Service Providers
2 /Unit Testing in React: Shallow vs. Static Testing
/Creating a Blogging App Using React, Part 3: Add & Display Post
/Creating a Blogging App Using React, Part 2: User Sign-Up
20 /Creating a Blogging App Using React, Part 1: User Sign-In
/Creating a Grocery List Manager Using Angular, Part 2: Managing Items
/9 Elegant CSS Pricing Tables for Your Latest Web Project
/Dynamic Page Templates in WordPress, Part 3
/Angular vs. React: 7 Key Features Compared
/Creating a Grocery List Manager Using Angular, Part 1: Add & Display Items
New eBooks Available for Subscribers in June 2017
/Create Interactive Charts Using Plotly.js, Part 1: Getting Started
/The 5 Best IDEs for WordPress Development (And Why)
/33 Popular WordPress User Interface Elements
/New Course: How to Hack Your Own App
/How to Install Yii on Windows or a Mac
/What Is a JavaScript Operator?
/How to Register and Use Laravel Service Providers
/
waly Good blog post. I absolutely love this…