Onboarding Stripe Connect Express accounts in ASP.NET Core
Tags: stripe, dotnet, webdev, aspnetcore
Published: 2024-02-20 18:12:02

## Preface
In this series of blog posts, we’ll explore some key highlights for a modest implementation of a Stripe integration that brings together the capabilities of Stripe Connect and Stripe Billing for a fictitious business, Oasis Hubs. 

This "Airbnb for co-working spaces" business provides customers with access to unique private and commercial workspaces that can be conveniently booked for a given number of hours. Customers will be able to choose between different subscription tiers that will give them access to various workspace listings (hubs) which have been provided by the service vendors (hosts). Hosts will sign up to the platform to provide details of their available workspaces, and get paid monthly based on the number of hours booked for their workspace. 

Throughout this series, we will cover topics such as setting up Stripe Connect Onboarding, managing Stripe events, implementing metered billing, and validating subscription logic with test clocks. You can find a link to the demo code for this series in the reference [links below](#references).


## Get Connect(ed) 
Before hosts can begin posting their workspace listings and getting paid for bookings, they first need to get signed up for an Oasis Hubs account and then go through host onboarding. For sign ups, we’ll see how to make use of [ASP.NET Core Identity](https://learn.microsoft.com/aspnet/core/security/authentication/identity) with a custom model to store some additional data. 

What about onboarding though? Hosts need to be able to connect their business to Oasis Hubs in a compliant way before they can start getting paid for the usage of their workspaces. Since Oasis Hubs is already using Stripe to handle payments, we’ll use Stripe Connect to connect (see what I did there) the hosts with Oasis Hubs’ Stripe account so that payments from customers and to hosts can be handled in the same platform.

If you are unfamiliar with [Stripe Connect](https://stripe.com/docs/connect), you can think of it as a solution that allows businesses to process payments and route those payments between multiple providers. This makes Stripe Connect an interesting choice for businesses that operate marketplaces or offer franchising opportunities. For our use case, the hosts will be the service providers for Oasis Hubs.  As payments are processed by the main business, various amounts can be distributed between the respective providers.

There are a few things that need to happen first before Oasis Hubs, or even your own business, can start their Stripe Connect integration. Through the Stripe dashboard:

- Make sure the Stripe account has been [activated](https://dashboard.stripe.com/account/onboarding)
- [Register](https://dashboard.stripe.com/connect/tasklist) the Stripe account for Connect
- Optionally, configure a [payout schedule](https://stripe.com/docs/connect/explore-connect-guide#pay-users)

A host will have to be associated with one of three Stripe Connect account types; Standard, Express and Custom. [Choosing between one account type versus another](https://stripe.com/docs/connect/accounts) really depends on the experience you want your users to have and also the available engineering resources to execute the integration. As you can probably tell from the title, this article and the rest of the series will focus on the usage of Express accounts. With this option, your integration effort is fairly low and Stripe handles all of the onboarding, account management, and identity verification for Oasis Hubs. This allows Stripe to gather any KYC (Know Your Customer) requirements enforced by global financial regulators. However, this choice also means that Oasis Hubs will have to be responsible for handling refunds, disputes, and providing customer support. If you’re interested in learning more about the other account types, I recommend taking a look at some of the references linked at the end of this article.

## Signing up
For some background, Oasis Hubs is built on .NET 7 using a combination of ASP.NET Core [Razor Pages](https://learn.microsoft.com/en-us/aspnet/core/razor-pages/), [Entity Framework Core](https://learn.microsoft.com/en-us/ef/core/), [Tailwind UI](https://tailwindcss.com/) and some other interesting components that will be explored across this series. As mentioned earlier, ASP.NET Core Identity needs to be set up to handle the customer sign ups so the following NuGet packages will need to be added to the project.

- Microsoft.AspNetCore.Identity.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.SqlServer
- Stripe .NET

Once the packages are installed, we will move forward with creating the custom model for the user.  ASP.NET Core Identity comes bundled with an IdentityUser class  that has some predetermined fields like UserName, Email, PhoneNumber, etc. In Oasis Hubs, the same user type will be used for both hosts and customers. In the image below, OasisHubUser extends IdentityUser with fields that will be used later on.

```csharp

public class OasisHubsUser : IdentityUser {
   public string StripeCustomerId { get; set; }
   public string StripeAccountId { get; set; }
   public string ActiveSubscriptionId { get; set; }
   public bool IsHost { get; set; }
   public bool HasSubscriptionActive { get; set; }
   public bool IsEnabled { get; set; }
   public virtual ICollection<IdentityUserClaim<string>> Claims { get; set; }
}

```

Here is a listing of the custom properties that will need to be added to the model. Their usage will be covered as we progress through the series.

- StripeCustomerId - Id for the Stripe Customer object associated with this user
- StripeAccountId - Id of the Stripe Express account for a host. Shouldn’t be set for a regular user
- ActiveSubscriptionId - Id of the most recent Stripe Subscription for a user
- HasSubscriptionActive - Set to true if user has an active subscription
- IsHost - Set to true if the user is a host, false if they’re a regular user.
- IsEnabled - Set to true if a user is in good standing and allowed to log in

ASP.NET Core Identity (Identity) has support for using Entity Framework Core (EF Core) for data storage through the IdentityDbContext type. To make the EF Core and Identity aware of the OasisHubUser model, IdentityDbContext needs to be extended as well.

```csharp
public class OasisHubsDbContext : IdentityDbContext<OasisHubsUser> {
   public OasisHubsDbContext(DbContextOptions<OasisHubsDbContext> options)
       : base(options) {
   }
}
```

Now that this is done, Identity and the customer EF Core class needs to be registered in the service collection for ASP.NET Core.

```csharp
services.AddPooledDbContextFactory<OasisHubsDbContext>(options =>
 options.UseSqlServer(config.GetConnectionString("OasisHubsSQLServer"),
    opts => opts.EnableRetryOnFailure()));

services.AddIdentity<OasisHubsUser, IdentityRole>(options => {
    options.User.RequireUniqueEmail = true;
  })
 .AddDefaultTokenProviders()
 .AddEntityFrameworkStores<OasisHubsDbContext>();
```

The code above adds the custom IdentityDbContext and wires up Identity with ASP.NET Core with some custom password settings. While we’re here, why not register the Stripe client as well.

```csharp
StripeConfiguration.ApiKey = config.GetValue<string>("SecretKey");

var appInfo = new AppInfo { Name = "Oasis Hubs", Version = "0.1.0" };
StripeConfiguration.AppInfo = appInfo;

services.AddHttpClient("Stripe");
services.AddTransient<IStripeClient, StripeClient>(s => {
     var clientFactory = s.GetRequiredService<IHttpClientFactory>();
     
     var sysHttpClient = new SystemNetHttpClient(
        httpClient: clientFactory.CreateClient("Stripe"),
        maxNetworkRetries: StripeConfiguration.MaxNetworkRetries,
        appInfo: appInfo,
        enableTelemetry: StripeConfiguration.EnableTelemetry);

     return new StripeClient(apiKey: StripeConfiguration.ApiKey, httpClient: sysHttpClient);
});
```

> The code above isn’t absolutely necessary but it does provide a more elegant way of injecting an instance of StripeClient that works alongside HttpClientFactory.  

With all this in place, we can move on to creating the sign up page. For the sake of brevity, we’ll focus on the Razor Page PageModel and not the html form markup. 


![Account Sign up](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xw1uz6fw2tbvc3tb232e.png)
The sign up form is fairly simple; three fields and a button.
```csharp

public class SignUpModel : PageModel {
   private readonly UserManager<OasisHubsUser> _userManager;
   private readonly SignInManager<OasisHubsUser> _signInManager;
   private readonly IStripeClient _stripeClient;
   private readonly ILogger<SignUpModel> _logger;

   [BindProperty] [Required] public string Name { get; set; }

   [BindProperty]
   [Required]
   [EmailAddress]
   public string Email { get; set; } 

   [BindProperty]
   [Required]
   [DataType(DataType.Password)]
   public string Password { get; set; }

   [TempData] public string ErrorMessage { get; set; }

   public SignUpModel(UserManager<OasisHubsUser> userManager,
      SignInManager<OasisHubsUser> signInManager,
      IStripeClient stripeClient,
      ILogger<SignUpModel> logger) {
      this._userManager = userManager;
      this._signInManager = signInManager;
      this._stripeClient = stripeClient;
      this._logger = logger;
   }

```

The PageModel that serves the sign up page is set up to bind to the html form properties, and it also gets injected with some of the services that we registered earlier like the StripeClient and Identity services.

```csharp
   public async Task<IActionResult> OnPostAsync() {
      if (ModelState.IsValid) {
         var customerService = new CustomerService(_stripeClient);
         var customers = await customerService.ListAsync(new() { Email = Email });

         if (customers.Any()) {
            ErrorMessage = "An account with that email already exists.";
            return Page();
         }

         var options = new CustomerCreateOptions { Name = Name, Email = Email };

         var newCustomer = await customerService.CreateAsync(options);
         var newUser = new OasisHubsUser {
            UserName = Email,
            Email = Email,
            EmailConfirmed = true,
            StripeCustomerId = newCustomer.Id
         };

         var createResult = await _userManager.CreateAsync(newUser, Password);
         if (createResult.Succeeded) {
            await _userManager.AddClaimAsync(newUser, new Claim(ClaimsConstants.OASIS_USER_TYPE, "customer"));
            await _signInManager.SignInAsync(newUser, isPersistent: false);
            return RedirectToPage("/Index");
         }

         this._logger.LogWarning("Unable to create user");
         foreach (var error in createResult.Errors) {
            ModelState.AddModelError(string.Empty, error.Description);
         }
      }

      return Page();
   }
```
When the form is submitted and the post handler fires, a few things happen on the backend. 

First, the form fields get validated. Once that is successful, an instance of Stripe’s CustomerService is created and is passed the injected StripeClient. 

The CustomerService is used to create a customer object in the Oasis Hub Stripe account for the newly registered user. Every Oasis Hub user will have a Stripe customer object created for them with the Oasis Hubs Stripe account regardless if they are a host or not. This is so that we can take account of the participants on both sides of a payment. After the customer object is created, the Id is attached to the OasisHubsUser which is then stored in the Identity database and the user is signed in. 

If you’re interested in seeing the sign in/out page implementations, take a look at the linked Github repository below.

## Host Onboarding
At this point, users are able to register and sign in to Oasis Hubs, but they still need to be onboarded before they can become hosts.

![Host sign up page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mpyr1vasmsnjs2jb5uwr.png)

Once an authenticated user navigates to the host sign up page and clicks the “Become a Host” button, the OnPostAsync handler for the HostSignUpModel PageModel model gets invoked. There’s a few things going on here so let’s break it up.

```csharp
var acOptions = new AccountCreateOptions {
   Country = "US",
   Type = "express",
   Email = currentUser.Email,
   Company =
     new AccountCompanyOptions {
        Name = companyName,
        Structure =
          "single_member_llc"
     },
   Capabilities =
     new AccountCapabilitiesOptions {
        UsBankAccountAchPayments =
          new AccountCapabilitiesUsBankAccountAchPaymentsOptions { Requested =
                                                                     true },
        LinkPayments =
          new AccountCapabilitiesLinkPaymentsOptions { Requested = true },
        CardPayments =
          new AccountCapabilitiesCardPaymentsOptions { Requested = true },
        Transfers = new AccountCapabilitiesTransfersOptions { Requested = true }
     },
   BusinessType = "company",
   BusinessProfile =
     new AccountBusinessProfileOptions {
        Name = companyName,
        Mcc = "6513", // https://stripe.com/docs/connect/setting-mcc#list
        ProductDescription = "Remote work rental space",
        SupportEmail = currentUser.Email
     },
   TosAcceptance =
     new AccountTosAcceptanceOptions { ServiceAgreement = "full" },
   Metadata = new Dictionary<string, string> { ["owner.customer.id"] =
                                                 currentUser.StripeCustomerId }
};
var accountService = new AccountService(this._stripeClient);
var newExpressAccount = await accountService.CreateAsync(acOptions);
```

First, the user record is retrieved and then an Express account is created using the Stripe AccountService class. The express account represents the host’s business that they wish to connect to the primary Oasis Hubs business. In Stripe terminology, we call the primary business the “platform" account and the host account would be known as a “connected account”.


The majority of the code here is prefilling some account information to help speed up the onboarding process for the user. For example, it sets the account type, country, business type, support email, and some metadata. It even requests that the account have certain capabilities such as the ability to take payments from US bank accounts, credit cards, or [Link](https://stripe.com/payments/link).


> As we request more capabilities, Stripe will ensure that the hosts provide all of the KYC information required to enable each one.

```csharp
// update Stripe customer with express account Id
var cuOptions = new CustomerUpdateOptions {
   Metadata = new Dictionary<string, string> { ["host.account.id"] =
                                                 newExpressAccount.Id }
};
var customerService = new CustomerService(this._stripeClient);
await customerService.UpdateAsync(currentUser.StripeCustomerId, cuOptions);

// Link account to platform
var basePageUri = _linkGenerator.GetUriByPage(this.HttpContext, "/Index");
var alcOptions =
  new AccountLinkCreateOptions { Account = newExpressAccount.Id,
                                 RefreshUrl = $"{basePageUri}/hosts/refresh",
                                 ReturnUrl = $"{basePageUri}/hosts/complete",
                                 Type = "account_onboarding",
                                 Collect = "eventually_due" };

var accountLinkService = new AccountLinkService(_stripeClient);
var acLink = await accountLinkService.CreateAsync(alcOptions);
return Redirect(acLink.Url);
```

All that’s left now is to redirect the user to Stripe’s hosted onboarding for the Express account that was just created. To do that, we have to make use of another service. Using the AccountLinkService, we provide the Id for the Express account, set the Type to “account_onboarding”, and also set Collect to “eventually_due” to collect as much company information as necessary during onboarding. Also notice that the RefreshUrl and ReturnUrl properties are set all well. These create links back to the Oasis Hubs site to either refresh the onboarding session or redirect the user once onboarding is successfully completed.

The create account link will contain a URL property that can be used to redirect the user to the hosted [onboarding experience](https://stripe.com/docs/connect/required-verification-information).


![Stripe hosted onboarding](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2o26bpe2ll9s7rrmkgea.png)

After the user completes the onboarding process, they should be redirected to the Oasis Hubs main site. In the background, Stripe will process and verify the collected information. Throughout the onboarding and verification process, Stripe will fire webhook events associated with the Oasis Hubs account that should be monitored to check the current status. For onboarding, the account.updated event will tell us if and when the business was verified and can start accepting pay outs. Now we can move forward to handle other concerns like invoicing, handling webhook events and testing.

## Summary
Hopefully at this point, you have a better understanding of the considerations that need to be made when integrating Stripe Connect into an ASP.NET Core application. We've covered everything from custom Identity models, creating various stripe objects, and working with Express connect accounts.

In the next post in this series, we will discuss handling webhooks for connected accounts and take a look at some key events that we should pay attention to.

## References
- [Oasis Hubs GitHub Repository](https://github.com/stripe-samples/oasis-hubs-dotnet)
- [Stripe Connect](https://stripe.com/docs/connect) 
- [Stripe Connect guide](https://stripe.com/docs/connect/explore-connect-guide)
- [Choose your Connect account type](https://stripe.com/docs/connect/accounts)
- [Know Your Customer (KYC)](https://support.stripe.com/questions/know-your-customer-obligations)

## Stay connected with Stripe
You can also stay up-to-date with Stripe developer updates on the following platforms:
- 📣 Follow [@StripeDev](https://twitter.com/stripedev) on Twitter.
- 📺 Subscribe to our [YouTube channel](https://www.youtube.com/StripeDevelopers).
- 💬 Join the official [Discord server](https://discord.com/invite/RuJnSBXrQn).
- 📧 Sign up for the [Developer Digest](https://go.stripe.global/dev-digest).