Mike  Kozey

Mike Kozey

1668602654

Django Stripe Subscriptions

This tutorial looks at how to handle subscription payments with Django and Stripe.

Need to accept one-time payments? Check out Django Stripe Tutorial.

Stripe Subscription Payment Options

There are multiple ways to implement and handle Stripe subscriptions, but the two most well-known are:

  1. Fixed-price subscriptions
  2. Future payments

In both cases, you can either use Stripe Checkout (which is a Stripe-hosted checkout page) or Stripe Elements (which are a set of custom UI components used to build payment forms). Use Stripe Checkout if you don't mind redirecting your users to a Stripe-hosted page and want Stripe to handle most of the payment process for you (e.g., customer and payment intent creation, etc.), otherwise use Stripe Elements.

The fixed-price approach is much easier to set up, but you don't have full control over the billing cycles and payments. By using this approach Stripe will automatically start charging your customers every billing cycle after a successful checkout.

Fixed-price steps:

  1. Redirect the user to Stripe Checkout (with mode=subscription)
  2. Create a webhook that listens for checkout.session.completed
  3. After the webhook is called, save relevant data to your database

The future payments approach is harder to set up, but this approach gives you full control over the subscriptions. You collect customer details and payment information in advance and charge your customers at a future date. This approach also allows you to sync billing cycles so that you can charge all of your customers on the same day.

Future payments steps:

  1. Redirect the user to Stripe Checkout (with mode=setup) to collect payment information
  2. Create a webhook that listens for checkout.session.completed
  3. After the webhook is called, save relevant data to your database
  4. From there, you can charge the payment method at a future date using the Payment Intents API

In this tutorial, we'll use the fixed-price approach with Stripe Checkout.

Billing Cycles

Before jumping in, it's worth noting that Stripe doesn't have a default billing frequency. Every Stripe subscription's billing date is determined by the following two factors:

  1. billing cycle anchor (timestamp of subscription's creation)
  2. recurring interval (daily, monthly, yearly, etc.)

For example, a customer with a monthly subscription set to cycle on the 2nd of the month will always be billed on the 2nd.

If a month doesn’t have the anchor day, the subscription will be billed on the last day of the month. For example, a subscription starting on January 31 bills on February 28 (or February 29 in a leap year), then March 31, April 30, and so on.

To learn more about billing cycles, refer to the Setting the subscription billing cycle date page from the Stripe documentation.

Project Setup

Let's start off by creating a new directory for our project. Inside the directory we'll create and activate a new virtual environment, install Django, and create a new Django project using django-admin:

$ mkdir django-stripe-subscriptions && cd django-stripe-subscriptions
$ python3.10 -m venv env
$ source env/bin/activate

(env)$ pip install django
(env)$ django-admin startproject djangostripe .

After that, create a new app called subscriptions:

(env)$ python manage.py startapp subscriptions

Register the app in djangostripe/settings.py under INSTALLED_APPS:

# djangostripe/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'subscriptions.apps.SubscriptionsConfig', # new
]

Create a new view called home, which will serve as our main index page:

# subscriptions/views.py

from django.shortcuts import render

def home(request):
    return render(request, 'home.html')

Assign a URL to the view by adding the following to subscriptions/urls.py:

# subscriptions/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('', views.home, name='subscriptions-home'),
]

Now, let's tell Django that the subscriptions app has its own URLs inside the main application:

# djangostripe/urls.py

from django.contrib import admin
from django.urls import path, include # new

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('subscriptions.urls')), # new
]

Finally, create a new template called home.html inside a new folder called "templates". Add the following HTML to the template:

<!-- templates/home.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Django + Stripe Subscriptions</title>
    <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
  </head>
  <body>
    <div class="container mt-5">
      <button type="submit" class="btn btn-primary" id="submitBtn">Subscribe</button>
    </div>
  </body>
</html>

Make sure to update the settings.py file so Django knows to look for a "templates" folder:

# djangostripe/settings.py

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': ['templates'], # new
        ...

Finally run migrate to sync the database and runserver to start Django's local web server.

(env)$ python manage.py migrate
(env)$ python manage.py runserver

Visit http://localhost:8000/ in your browser of choice. You should see the "Subscribe" button which we'll later use to redirect customers to the Stripe Checkout page.

Add Stripe

With the base project ready, let's add Stripe. Install the latest version:

(env)$ pip install stripe

Next, register for a Stipe account (if you haven't already done so) and navigate to the dashboard. Click on "Developers" and then from the list in the left sidebar click on "API keys":

Stripe Developers Key

Each Stripe account has four API keys: two for testing and two for production. Each pair has a "secret key" and a "publishable key". Do not reveal the secret key to anyone; the publishable key will be embedded in the JavaScript on the page that anyone can see.

Currently the toggle for "Viewing test data" in the upper right indicates that we're using the test keys now. That's what we want.

At the bottom of your settings.py file, add the following two lines including your own test secret and publishable keys. Make sure to include the '' characters around the actual keys.

# djangostripe/settings.py

STRIPE_PUBLISHABLE_KEY = '<enter your stripe publishable key>'
STRIPE_SECRET_KEY = '<enter your stripe secret key>'

Finally, you'll need to specify an "Account name" within your "Account settings" at https://dashboard.stripe.com/settings/account.

Create a Product

Next, let's create a subscription product to sell.

Click "Products" and then "Add product".

Add a product name and description, enter a price, and select "Recurring":

Stripe Add Product

Click "Save product".

Next, grab the API ID of the price:

Stripe Add Product

Save the ID in the settings.py file like so:

# djangostripe/settings.py

STRIPE_PRICE_ID = '<enter your stripe price id>'

Authentication

In order to associate Django users with Stripe customers and implement subscription management in the future, we'll need to enforce user authentication before allowing customers to subscribe to the service. We can achieve this by adding a @login_required decorator to all views that require authentication.

Let's first protect the home view:

# subscriptions/views.py

from django.contrib.auth.decorators import login_required  # new
from django.shortcuts import render

@login_required  # new
def home(request):
    return render(request, 'home.html')

Now, when non-authenticated users try to access the home view, they will be redirected to the LOGIN_REDIRECT_URL defined in settings.py.

If you have a preferred authentication system, set that up now and configure the LOGIN_REDIRECT_URL, otherwise hop to the next section to install django-allauth.

django-allauth (Optional)

django-allauth is one of the most popular Django packages for addressing authentication, registration, account management, and third-party account authentication. We'll use it to configure a simple register/login system.

First, install the package:

(env)$ pip install django-allauth

Update the INSTALLED_APPS in djangostripe/settings.py like so:

# djangostripe/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.sites', # new
    'allauth', # new
    'allauth.account', # new
    'allauth.socialaccount', # new
    'subscriptions.apps.SubscriptionsConfig',
]

Next, add the following django-allauth config to djangostripe/settings.py:

# djangostripe/settings.py

AUTHENTICATION_BACKENDS = [
    # Needed to login by username in Django admin, regardless of `allauth`
    'django.contrib.auth.backends.ModelBackend',

    # `allauth` specific authentication methods, such as login by e-mail
    'allauth.account.auth_backends.AuthenticationBackend',
]

# We have to set this variable, because we enabled 'django.contrib.sites'
SITE_ID = 1

# User will be redirected to this page after logging in
LOGIN_REDIRECT_URL = '/'

# If you don't have an email server running yet add this line to avoid any possible errors.
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

Register the allauth URLs:

# djangostripe/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('subscriptions.urls')),
    path('accounts/', include('allauth.urls')),  # new
]

Apply the migrations:

(env)$ python manage.py migrate

Test out auth by running the server and navigating to http://localhost.com:8000/. You should be redirected to the sign up page. Create an account and then log in.

Database Model

In order to handle customers and subscriptions correctly we'll need to store some information in our database. Let's create a new model called StripeCustomer which will store Stripe's customerId and subscriptionId and relate it back the Django auth user. This will allow us to fetch our customer and subscription data from Stripe.

We could theoretically fetch the customerId and subscriptionId from Stripe every time we need them, but that would greatly increase our chance of getting rate limited by Stripe.

Let's create our model inside subscriptions/models.py:

# subscriptions/models.py

from django.contrib.auth.models import User
from django.db import models


class StripeCustomer(models.Model):
    user = models.OneToOneField(to=User, on_delete=models.CASCADE)
    stripeCustomerId = models.CharField(max_length=255)
    stripeSubscriptionId = models.CharField(max_length=255)

    def __str__(self):
        return self.user.username

Register it with the admin in subscriptions/admin.py:

# subscriptions/admin.py

from django.contrib import admin
from subscriptions.models import StripeCustomer


admin.site.register(StripeCustomer)

Create and apply the migrations:

(env)$ python manage.py makemigrations && python manage.py migrate

Get Publishable Key

JavaScript Static File

Start by creating a new static file to hold all of our JavaScript:

(env)$ mkdir static
(env)$ touch static/main.js

Add a quick sanity check to the new main.js file:

// static/main.js

console.log("Sanity check!");

Then update the settings.py file so Django knows where to find static files:

# djangostripe/settings.py

STATIC_URL = 'static/'

# for django >= 3.1
STATICFILES_DIRS = [Path(BASE_DIR).joinpath('static')]  # new

# for django < 3.1
# STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]  # new

Add the static template tag along with the new script tag inside the HTML template:

<!-- templates/home.html -->

{% load static %} <!-- new -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Django + Stripe Subscriptions</title>
    <script src="https://js.stripe.com/v3/"></script>  <!-- new -->
    <script src="{% static 'main.js' %}"></script> <!-- new -->
    <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
  </head>
  <body>
    <div class="container mt-5">
      <button type="submit" class="btn btn-primary" id="submitBtn">Subscribe</button>
    </div>
  </body>
</html>

Run the development server again. Navigate to http://localhost:8000/, and open up the JavaScript console. You should see the sanity check inside your console.

View

Next, add a new view to subscriptions/views.py to handle the AJAX request:

# subscriptions/views.py

from django.conf import settings # new
from django.contrib.auth.decorators import login_required
from django.http.response import JsonResponse # new
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt # new


@login_required
def home(request):
    return render(request, 'home.html')


# new
@csrf_exempt
def stripe_config(request):
    if request.method == 'GET':
        stripe_config = {'publicKey': settings.STRIPE_PUBLISHABLE_KEY}
        return JsonResponse(stripe_config, safe=False)

Add a new URL as well:

# subscriptions/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('', views.home, name='subscriptions-home'),
    path('config/', views.stripe_config),  # new
]

AJAX Request

Next, use the Fetch API to make an AJAX request to the new /config/ endpoint in static/main.js:

// static/main.js

console.log("Sanity check!");

// new
// Get Stripe publishable key
fetch("/config/")
.then((result) => { return result.json(); })
.then((data) => {
  // Initialize Stripe.js
  const stripe = Stripe(data.publicKey);
});

The response from a fetch request is a ReadableStream. result.json() returns a promise, which we resolved to a JavaScript object -- i.e., data. We then used dot-notation to access the publicKey in order to obtain the publishable key.

Create Checkout Session

Moving on, we need to attach an event handler to the button's click event which will send another AJAX request to the server to generate a new Checkout Session ID.

View

First, add the new view:

# subscriptions/views.py

@csrf_exempt
def create_checkout_session(request):
    if request.method == 'GET':
        domain_url = 'http://localhost:8000/'
        stripe.api_key = settings.STRIPE_SECRET_KEY
        try:
            checkout_session = stripe.checkout.Session.create(
                client_reference_id=request.user.id if request.user.is_authenticated else None,
                success_url=domain_url + 'success?session_id={CHECKOUT_SESSION_ID}',
                cancel_url=domain_url + 'cancel/',
                payment_method_types=['card'],
                mode='subscription',
                line_items=[
                    {
                        'price': settings.STRIPE_PRICE_ID,
                        'quantity': 1,
                    }
                ]
            )
            return JsonResponse({'sessionId': checkout_session['id']})
        except Exception as e:
            return JsonResponse({'error': str(e)})

Here, if the request method is GET, we defined a domain_url, assigned the Stripe secret key to stripe.api_key (so it will be sent automatically when we make a request to create a new Checkout Session), created the Checkout Session, and sent the ID back in the response. Take note of the success_url and cancel_url. The user will be redirected back to those URLs in the event of a successful payment or cancellation, respectively. We'll set those views up shortly.

Don't forget the import:

import stripe

The full file should now look like this:

# subscriptions/views.py

import stripe
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.http.response import JsonResponse
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt


@login_required
def home(request):
    return render(request, 'home.html')


@csrf_exempt
def stripe_config(request):
    if request.method == 'GET':
        stripe_config = {'publicKey': settings.STRIPE_PUBLISHABLE_KEY}
        return JsonResponse(stripe_config, safe=False)


@csrf_exempt
def create_checkout_session(request):
    if request.method == 'GET':
        domain_url = 'http://localhost:8000/'
        stripe.api_key = settings.STRIPE_SECRET_KEY
        try:
            checkout_session = stripe.checkout.Session.create(
                client_reference_id=request.user.id if request.user.is_authenticated else None,
                success_url=domain_url + 'success?session_id={CHECKOUT_SESSION_ID}',
                cancel_url=domain_url + 'cancel/',
                payment_method_types=['card'],
                mode='subscription',
                line_items=[
                    {
                        'price': settings.STRIPE_PRICE_ID,
                        'quantity': 1,
                    }
                ]
            )
            return JsonResponse({'sessionId': checkout_session['id']})
        except Exception as e:
            return JsonResponse({'error': str(e)})

AJAX Request

Register the checkout session URL:

# subscriptions/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('', views.home, name='subscriptions-home'),
    path('config/', views.stripe_config),
    path('create-checkout-session/', views.create_checkout_session),  # new
]

Add the event handler and subsequent AJAX request to static/main.js:

// static/main.js

console.log("Sanity check!");

// Get Stripe publishable key
fetch("/config/")
.then((result) => { return result.json(); })
.then((data) => {
  // Initialize Stripe.js
  const stripe = Stripe(data.publicKey);

  // new
  // Event handler
  let submitBtn = document.querySelector("#submitBtn");
  if (submitBtn !== null) {
    submitBtn.addEventListener("click", () => {
    // Get Checkout Session ID
    fetch("/create-checkout-session/")
      .then((result) => { return result.json(); })
      .then((data) => {
        console.log(data);
        // Redirect to Stripe Checkout
        return stripe.redirectToCheckout({sessionId: data.sessionId})
      })
      .then((res) => {
        console.log(res);
      });
    });
  }
});

Here, after resolving the result.json() promise, we called redirectToCheckout with the Checkout Session ID from the resolved promise.

Navigate to http://localhost:8000/. On button click you should be redirected to an instance of Stripe Checkout (a Stripe-hosted page to securely collect payment information) with the subscription information:

Stripe Checkout

We can test the form by using one of the several test card numbers that Stripe provides. Let's use 4242 4242 4242 4242. Make sure the expiration date is in the future. Add any 3 numbers for the CVC and any 5 numbers for the postal code. Enter any email address and name. If all goes well, the payment should be processed and you should be subscribed, but the redirect will fail since we have not set up the /success/ URL yet.

User Redirect

Next, we'll create success and cancel views and redirect the user to the appropriate page after checkout.

Views:

# subscriptions/views.py

@login_required
def success(request):
    return render(request, 'success.html')


@login_required
def cancel(request):
    return render(request, 'cancel.html')

Create the success.html and cancel.html templates as well.

Success:

<!-- templates/success.html -->

{% load static %}

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Django + Stripe Subscriptions</title>
    <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
  </head>
  <body>
    <div class="container mt-5">
      <p>You have successfully subscribed!</p>
      <p><a href="{% url "subscriptions-home" %}">Return to the dashboard</a></p>
    </div>
  </body>
</html>

Cancel:

<!-- templates/cancel.html -->

{% load static %}

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Django + Stripe Subscriptions</title>
    <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
  </head>
  <body>
    <div class="container mt-5">
      <p>You have cancelled the checkout.</p>
      <p><a href="{% url "subscriptions-home" %}">Return to the dashboard</a></p>
    </div>
  </body>
</html>

Register the new views inside subscriptions/urls.py:

# subscriptions/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('', views.home, name='subscriptions-home'),
    path('config/', views.stripe_config),
    path('create-checkout-session/', views.create_checkout_session),
    path('success/', views.success),  # new
    path('cancel/', views.cancel),  # new
]

The user should now be redirected to /success if the payment goes through and cancel/ if the payment fails. Test this out.

Stripe Webhooks

Our app works well at this point, but we still can't programmatically confirm payments. We also haven't added a new customer to the StripeCustomer model when a customer subscribes successfully. We already redirect the user to the success page after they check out, but we can't rely on that page alone (as confirmation) since payment confirmation happens asynchronously.

There are two types of events in Stripe and programming in general. Synchronous events, which have an immediate effect and results (e.g., creating a customer), and asynchronous events, which don't have an immediate result (e.g., confirming payments). Because payment confirmation is done asynchronously, the user might get redirected to the success page before their payment is confirmed and before we receive their funds.

One of the easiest ways to get notified when the payment goes through is to use a callback or so-called Stripe webhook. We'll need to create a simple endpoint in our application, which Stripe will call whenever an event occurs (e.g., when a user subscribes). By using webhooks, we can be absolutely sure that the payment went through successfully.

In order to use webhooks, we need to:

  1. Set up the webhook endpoint
  2. Test the endpoint using the Stripe CLI
  3. Register the endpoint with Stripe

Endpoint

Create a new view called stripe_webhook which will create a new StripeCustomer every time someone subscribes to our service:

# subscriptions/views.py

@csrf_exempt
def stripe_webhook(request):
    stripe.api_key = settings.STRIPE_SECRET_KEY
    endpoint_secret = settings.STRIPE_ENDPOINT_SECRET
    payload = request.body
    sig_header = request.META['HTTP_STRIPE_SIGNATURE']
    event = None

    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, endpoint_secret
        )
    except ValueError as e:
        # Invalid payload
        return HttpResponse(status=400)
    except stripe.error.SignatureVerificationError as e:
        # Invalid signature
        return HttpResponse(status=400)

    # Handle the checkout.session.completed event
    if event['type'] == 'checkout.session.completed':
        session = event['data']['object']

        # Fetch all the required data from session
        client_reference_id = session.get('client_reference_id')
        stripe_customer_id = session.get('customer')
        stripe_subscription_id = session.get('subscription')

        # Get the user and create a new StripeCustomer
        user = User.objects.get(id=client_reference_id)
        StripeCustomer.objects.create(
            user=user,
            stripeCustomerId=stripe_customer_id,
            stripeSubscriptionId=stripe_subscription_id,
        )
        print(user.username + ' just subscribed.')

    return HttpResponse(status=200)

stripe_webhook now serves as our webhook endpoint. Here, we're only looking for checkout.session.completed events which are called whenever a checkout is successful, but you can use the same pattern for other Stripe events.

Make the following changes to the imports:

# subscriptions/views.py

import stripe
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User  # new
from django.http.response import JsonResponse, HttpResponse  # updated
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt

from subscriptions.models import StripeCustomer  # new

The only thing left do do to make the endpoint accessible is to register it in urls.py:

# subscriptions/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('', views.home, name='subscriptions-home'),
    path('config/', views.stripe_config),
    path('create-checkout-session/', views.create_checkout_session),
    path('success/', views.success),
    path('cancel/', views.cancel),
    path('webhook/', views.stripe_webhook),  # new
]

Testing the webhook

We'll use the Stripe CLI to test the webhook.

Once downloaded and installed, run the following command in a new terminal window to log in to your Stripe account:

$ stripe login

This command should generate a pairing code:

Your pairing code is: peach-loves-classy-cozy
This pairing code verifies your authentication with Stripe.
Press Enter to open the browser (^C to quit)

By pressing Enter, the CLI will open your default web browser and ask for permission to access your account information. Go ahead and allow access. Back in your terminal, you should see something similar to:

> Done! The Stripe CLI is configured for Django Test with account id acct_<ACCOUNT_ID>

Please note: this key will expire after 90 days, at which point you'll need to re-authenticate.

Next, we can start listening to Stripe events and forward them to our endpoint using the following command:

$ stripe listen --forward-to localhost:8000/webhook/

This will also generate a webhook signing secret:

> Ready! Your webhook signing secret is whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (^C to quit)

In order to initialize the endpoint, add the secret to the settings.py file:

# djangostribe/settings.py

STRIPE_ENDPOINT_SECRET = '<your webhook signing secret here>'

Stripe will now forward events to our endpoint. To test, run another test payment through with 4242 4242 4242 4242. In your terminal, you should see the <USERNAME> just subscribed. message.

Once done, stop the stripe listen --forward-to localhost:8000/webhook/ process.

Register the endpoint

Finally, after deploying your app, you can register the endpoint in the Stripe dashboard, under Developers > Webhooks. This will generate a webhook signing secret for use in your production app.

For example:

Django webhook

Fetch Subscription Data

Our app now allows users to subscribe to our service, but we still have no way to fetch their subscription data and display it.

Update the home view:

# subscriptions/views.py

@login_required
def home(request):
    try:
        # Retrieve the subscription & product
        stripe_customer = StripeCustomer.objects.get(user=request.user)
        stripe.api_key = settings.STRIPE_SECRET_KEY
        subscription = stripe.Subscription.retrieve(stripe_customer.stripeSubscriptionId)
        product = stripe.Product.retrieve(subscription.plan.product)

        # Feel free to fetch any additional data from 'subscription' or 'product'
        # https://stripe.com/docs/api/subscriptions/object
        # https://stripe.com/docs/api/products/object

        return render(request, 'home.html', {
            'subscription': subscription,
            'product': product,
        })

    except StripeCustomer.DoesNotExist:
        return render(request, 'home.html')

Here, if a StripeCustomer exists, we use the subscriptionId to fetch the customer's subscription and product info from the Stripe API.

Modify the home.html template to display the current plan to subscribed users:

<!-- templates/home.html -->

{% load static %}

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Django + Stripe Subscriptions</title>
    <script src="https://js.stripe.com/v3/"></script>
    <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
    <script src="{% static 'main.js' %}"></script>
  </head>
  <body>
    <div class="container mt-5">
      {% if subscription.status == "active" %}
        <h4>Your subscription:</h4>
        <div class="card" style="width: 18rem;">
          <div class="card-body">
            <h5 class="card-title">{{ product.name }}</h5>
            <p class="card-text">
              {{ product.description }}
            </p>
          </div>
        </div>
      {% else %}
        <button type="submit" class="btn btn-primary" id="submitBtn">Subscribe</button>
      {% endif %}
    </div>
  </body>
</html>

Our subscribed customers will now see their current subscription plan, while others will still see the subscribe button:

Subscribed View

Restricting user access

If you want to restrict access to specific views to only subscribed users, you can fetch the subscription as we did in the previous step and check if subscription.status == "active". By performing this check you will make sure the subscription is still active, which means that it has been paid and hasn't been cancelled.

Other possible subscription statuses are incomplete, incomplete_expired, trialing, active, past_due, canceled, or unpaid.

Conclusion

We have successfully created a Django web application that allows users to subscribe to our service and view their plan. Our customers will also be automatically billed every month.

This is just the basics. You'll still need to:

  • Allow users to manage/cancel their current plan
  • Handle future payment failures

You'll also want to use environment variables for the domain_url, API keys, and webhook signing secret rather than hardcoding them.

Grab the code from the django-stripe-subscriptions repo on GitHub.

Original article source at: https://testdriven.io

#django #stripe #subscriptions 

Django Stripe Subscriptions
Mike  Kozey

Mike Kozey

1653836280

Nami-flutter: Subscription and IAP marketing platform for Flutter

nami-flutter

Nami is the top Subscription and IAP Marketing platform for Flutter.

Project Structure

  • sdk folder contains the flutter plugin along with a basic example app
  • samples folder contains extra demo apps to help you get started with using the Nami ML platform

Installing

Use this package as a library

Depend on it

Run this command:

With Flutter:

 $ flutter pub add nami_flutter

This will add a line like this to your package's pubspec.yaml (and run an implicit flutter pub get):

dependencies:
  nami_flutter: ^1.0.0

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

Import it

Now in your Dart code, you can use:

import 'package:nami_flutter/analytics/nami_analytics_support.dart';
import 'package:nami_flutter/billing/nami_purchase.dart';
import 'package:nami_flutter/billing/nami_purchase_manager.dart';
import 'package:nami_flutter/channel.dart';
import 'package:nami_flutter/customer/nami_customer_manager.dart';
import 'package:nami_flutter/entitlement/nami_entitlement.dart';
import 'package:nami_flutter/entitlement/nami_entitlement_manager.dart';
import 'package:nami_flutter/entitlement/nami_entitlement_setter.dart';
import 'package:nami_flutter/ml/nami_ml_manager.dart';
import 'package:nami_flutter/nami.dart';
import 'package:nami_flutter/nami_configuration.dart';
import 'package:nami_flutter/nami_log_level.dart';
import 'package:nami_flutter/paywall/legal_citations.dart';
import 'package:nami_flutter/paywall/nami_locale_config.dart';
import 'package:nami_flutter/paywall/nami_paywall.dart';
import 'package:nami_flutter/paywall/nami_paywall_manager.dart';
import 'package:nami_flutter/paywall/nami_sku.dart';
import 'package:nami_flutter/paywall/paywall_display_options.dart';
import 'package:nami_flutter/paywall/paywall_style_data.dart';

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:nami_flutter/analytics/nami_analytics_support.dart';
import 'package:nami_flutter/billing/nami_purchase_manager.dart';
import 'package:nami_flutter/customer/nami_customer_manager.dart';
import 'package:nami_flutter/entitlement/nami_entitlement_manager.dart';
import 'package:nami_flutter/ml/nami_ml_manager.dart';
import 'package:nami_flutter/nami.dart';
import 'package:nami_flutter/nami_configuration.dart';
import 'package:nami_flutter/nami_log_level.dart';
import 'package:nami_flutter/paywall/nami_paywall_manager.dart';
import 'package:nami_flutter/paywall/nami_sku.dart';

import 'about.dart';

void main() {
  runApp(MaterialApp(home: MyApp()));
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
  static const _testExternalIdentifier = "9a9999a9-99aa-99a9-aa99-999a999999a8";
  static const _androidAppPlatformId = "3d062066-9d3c-430e-935d-855e2c56dd8e";
  static const _iosAppPlatformId = "002e2c49-7f66-4d22-a05c-1dc9f2b7f2af";

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        theme: ThemeData(
          buttonTheme: ButtonThemeData(
            buttonColor: Color.fromARGB(255, 65, 109, 124),
          ),
          primaryColor: Color.fromARGB(255, 65, 109, 124),
          accentColor: Color.fromARGB(255, 65, 109, 124),
        ),
        home: Scaffold(
            appBar: AppBar(
              centerTitle: true,
              title: SizedBox(
                  height: 24,
                  child: Image.asset("assets/images/nami_logo_white.png")),
            ),
            body: _buildMainPageBody()));
  }

  @override
  void initState() {
    super.initState();
    print('--------- initState ---------');
    WidgetsBinding.instance?.addObserver(this);
    initPlatformState();
    NamiCustomerManager.customerJourneyChangeEvents().listen((journeyState) {
      print("customerJourneyChange triggered");
      _handleCustomerJourneyChanged(journeyState);
    });
    NamiPaywallManager.signInEvents().listen((namiPaywall) {
      Nami.clearExternalIdentifier();
      Nami.setExternalIdentifier(
          _testExternalIdentifier, NamiExternalIdentifierType.uuid);
      print('--------- Sign In Clicked ---------');
    });
    _handleActiveEntitlementsFuture(
        NamiEntitlementManager.activeEntitlements());
    NamiEntitlementManager.entitlementChangeEvents()
        .listen((activeEntitlements) {
      print("EntitlementChangeListener triggered");
      _handleActiveEntitlements(activeEntitlements);
    });
    NamiPurchaseManager.purchaseChangeEvents()
        .listen((purchaseChangeEventData) {
      print("PurchasesChangedHandler triggered");
      _evaluateLastPurchaseEvent(purchaseChangeEventData);
    });
    NamiAnalyticsSupport.analyticsEvents().listen((analyticsData) {
      _printAnalyticsEventData(analyticsData);
    });
  }

  @override
  void dispose() {
    WidgetsBinding.instance?.removeObserver(this);
    super.dispose();
  }

  @override
  Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
    if (state == AppLifecycleState.resumed) {
      print('--------- ON RESUME ---------');
      _handleActiveEntitlementsFuture(
          NamiEntitlementManager.activeEntitlements());
    }
  }

  void _evaluateLastPurchaseEvent(
      PurchaseChangeEventData purchaseChangeEventData) {
    print('--------- Start ---------');
    print("Purchase State ${purchaseChangeEventData.purchaseState}");
    if (purchaseChangeEventData.purchaseState == NamiPurchaseState.purchased) {
      print("\nActive Purchases: ");
      purchaseChangeEventData.activePurchases.forEach((element) {
        print("\tSkuId: ${element.skuId}");
      });
    } else {
      print("Reason : ${purchaseChangeEventData.error}");
    }
    print('--------- End ---------');
  }

  void _handleActiveEntitlements(List<NamiEntitlement> activeEntitlements) {
    print('--------- Start ---------');
    if (activeEntitlements.isNotEmpty) {
      print("Active entitlements found!");
      activeEntitlements.forEach((element) {
        print(element.toString());
      });
    } else {
      print("No active entitlements");
    }
    print('--------- End ---------');
  }

  void _handleActiveEntitlementsFuture(
      Future<List<NamiEntitlement>> activeEntitlementsFuture) async {
    _handleActiveEntitlements(await activeEntitlementsFuture);
  }

  void _handleCustomerJourneyChanged(CustomerJourneyState state) async {
    print('--------- Start ---------');
    print("currentCustomerJourneyState");
    print("formerSubscriber ==> ${state.formerSubscriber}");
    print("inGracePeriod ==> ${state.inGracePeriod}");
    print("inIntroOfferPeriod ==> ${state.inIntroOfferPeriod}");
    print("inTrialPeriod ==> ${state.inTrialPeriod}");
    print("isCancelled ==> ${state.isCancelled}");
    print("inPause ==> ${state.inPause}");
    print("inAccountHold ==> ${state.inAccountHold}");
    print('--------- End ---------');
  }

  // Platform messages are asynchronous, so we initialize in an async method.
  Future<void> initPlatformState() async {
    var namiConfiguration = NamiConfiguration(
        appPlatformIDApple: _iosAppPlatformId,
        appPlatformIDGoogle: _androidAppPlatformId,
        namiLogLevel: NamiLogLevel.debug);
    Nami.configure(namiConfiguration);
    // If the widget was removed from the tree while the asynchronous platform
    // message was in flight, we want to discard the reply rather than calling
    // setState to update our non-existent appearance.
    if (!mounted) return;
  }

  SingleChildScrollView _buildMainPageBody() {
    return SingleChildScrollView(
        child: Padding(
            padding: EdgeInsets.all(16.0),
            child: Column(
                mainAxisSize: MainAxisSize.max,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: [
                  Text(
                    "Basic Flutter",
                    style: TextStyle(fontSize: 24, fontStyle: FontStyle.italic),
                  ),
                  Container(
                      margin: const EdgeInsets.only(top: 8),
                      child: ElevatedButton(
                          onPressed: () async {
                            await Navigator.push(
                              context,
                              MaterialPageRoute(
                                  builder: (context) => AboutPage()),
                            );
                          },
                          child: Text("About"))),
                  buildHeaderBodyContainer("Introduction",
                      "This application demonstrates common calls used in a Nami enabled application."),
                  buildHeaderBodyContainer("Instructions",
                      "If you suspend and resume this app three times in the simulator, an example paywall will be raised - or you can use the [Subscribe] button below to raise the same paywall."),
                  buildHeaderBodyContainer("Important info",
                      "Any Purchase will be remembered while the application is [Active, Suspended, Resume] but cleared when the application is launched.\nExamine the application source code for more details on calls used to respond and monitor purchases."),
                  Container(
                    margin: const EdgeInsets.only(top: 48),
                    child: ElevatedButton(
                      onPressed: () async {
                        NamiMLManager.coreAction("subscribe");
                        print('Subscribe clicked!');
                        var preparePaywallResult =
                            await NamiPaywallManager.preparePaywallForDisplay();
                        if (preparePaywallResult.success) {
                          NamiPaywallManager.raisePaywall();
                        } else {
                          print('preparePaywallForDisplay Error -> '
                              '${preparePaywallResult.error}');
                        }
                      },
                      child: Text('Subscribe'),
                    ),
                  )
                ])));
  }

  void _printAnalyticsEventData(NamiAnalyticsData analyticsData) {
    print('--------- Start ---------');
    print('analyticsEvents');
    print("TYPE " + analyticsData.type.toString());
    print("CAMPAIGN_ID " +
        analyticsData.eventData[NamiAnalyticsKeys.CAMPAIGN_ID]);
    print("CAMPAIGN_NAME " +
        analyticsData.eventData[NamiAnalyticsKeys.CAMPAIGN_NAME]);
    bool namiTriggered =
        analyticsData.eventData[NamiAnalyticsKeys.NAMI_TRIGGERED];
    print("NAMI_TRIGGERED " + namiTriggered.toString());
    print(
        "PAYWALL_ID " + analyticsData.eventData[NamiAnalyticsKeys.PAYWALL_ID]);
    print("PAYWALL_NAME " +
        analyticsData.eventData[NamiAnalyticsKeys.PAYWALL_NAME]);
    List<NamiSKU> products =
        analyticsData.eventData[NamiAnalyticsKeys.PAYWALL_PRODUCTS];
    print("PAYWALL_PRODUCTS " + products.toString());
    print("PAYWALL_TYPE " +
        analyticsData.eventData[NamiAnalyticsKeys.PAYWALL_TYPE]);
    dynamic purchaseActivityType =
        analyticsData.eventData[NamiAnalyticsKeys.PURCHASE_ACTIVITY_TYPE];
    print("PURCHASE_ACTIVITY_TYPE ${purchaseActivityType.toString()}");
    dynamic purchasedProduct =
        analyticsData.eventData[NamiAnalyticsKeys.PURCHASE_PRODUCT];
    print("PURCHASE_PRODUCT ${purchasedProduct.toString()}");
    dynamic purchaseTimestamp =
        analyticsData.eventData[NamiAnalyticsKeys.PURCHASE_TIMESTAMP];
    print("PURCHASE_TIMESTAMP ${purchaseTimestamp.toString()}");
    print('--------- End ---------');
  }
}

Container buildHeaderBodyContainer(String header, String body) {
  return Container(
      margin: const EdgeInsets.only(top: 16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Container(
              margin: const EdgeInsets.only(top: 8),
              child: Text(
                header,
                style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
              )),
          Text(
            body,
            style: TextStyle(
              fontSize: 16,
            ),
          ),
        ],
      ));
}

No need to learn the complexities of the StoreKit and Play Billing frameworks. Integrate our SDK in minutes into your Flutter app and start selling and growing your digital revenue today with our generous Free Tier.

  1. Create an account at https://app.namiml.com/join
  2. Follow our Flutter Quickstart Guide to implement in minutes.

Our Flutter package is available here on pub.dev.

Author: Namiml
Source Code: https://github.com/namiml/nami-flutter/ 
License: MIT license

#flutter #dart #subscriptions 

Nami-flutter: Subscription and IAP marketing platform for Flutter
Vinnie  Erdman

Vinnie Erdman

1647437943

How Set Up The Stripe Webhook to Update Subscriptions in Firestore

We set up the Stripe Webhook to update subscriptions in Firestore. Also, we styled our Vue project with Bootstrap CSS, and we created the Stripe Checkout and Customer Portal Sessions.

Part 1: ☞ https://morioh.com/p/4884f5bbaebb?

#enterflash  #stripe  #subscriptions #vue #firebase 

How Set Up The Stripe Webhook to Update Subscriptions in Firestore
Delbert  Ferry

Delbert Ferry

1623293670

GraphQL Subscriptions in Apollo Client

GraphQL subscriptions are based on a simple publish-subscribe system. In our server-side subscriptions package, when a client makes a subscription, we simply use a map from one subscription name to one or more channel names to subscribe to the right channels. The subscription query will be re-run every time something is published to one of these channels. We think a common pattern will be to publish mutation results to a channel, so a subscription can send a new result to clients whenever a mutation happens. This is why some people call subscriptions the result of someone else’s mutation .

#apollo client #subscriptions #graphql

GraphQL Subscriptions in Apollo Client
Delbert  Ferry

Delbert Ferry

1622278191

GraphQL Subscriptions on the Server

GraphQL Subscriptions
To notify the client when messages are added to a channel, we’ll use GraphQL subscriptions, which allow the client to make a query and be notified of new results in the case of specific, server-side events. In our implementation of the server, we’ll use an Express server with WebSockets for pushing updates to the client.

In this tutorial, we’ll first add a subscription that notifies the client about new messages. Next, we’ll add a field to our GraphQL schema and implement a resolver for the subscription. Finally, we’ll use an in-memory pub-sub object that will handle passing along notifications about added messages.

#graphql #subscriptions #graphql server

GraphQL Subscriptions on the Server
Delbert  Ferry

Delbert Ferry

1620641406

Understanding GraphQL with Web Socket Protocol and Subscriber

GraphQL is a great way to write customizable API’s and combine numerous services into one endpoint. Mainly, the idea has a single endpoint, but getting the different combinations of models is amazing, mostly for the organizations working with large-scale multi-platform applications with various developers and designers.

What is a Web-Socket?
According to MDN, “[The WebSocket API] is an advanced technology that makes it possible to open a two-way interactive communication session between the user’s browser and a server. With this API, you can send messages to a server and receive event-driven responses without having to poll the server for a reply.”

#graphql #subscriptions #websocket protocol

Understanding GraphQL with Web Socket Protocol and Subscriber
Delbert  Ferry

Delbert Ferry

1620203407

Building Resilient GraphQL Apps and Scaling Them to 1M Subscriptions

GraphQL provides a query language for APIs, giving UX developers autonomy over querying APIs and the database.

The advantages of using GraphQL for UX applications are well understood. GraphQL clients can retrieve only the data needed in the UI application. GraphQL provides a strong type system that avoids manual code parsing and data filtering that takes up precious processing cycles when rendering the UI to users. Additionally, GraphQL increases UX development’s velocity, and the feedback cycles needed for designing API responses are minimized.

#graphql #build resilient #scaling #subscriptions

Building Resilient GraphQL Apps and Scaling Them to 1M Subscriptions
Techtolia ✓

Techtolia ✓

1615558098

Accept Subscriptions/Recurring payments with Stripe in ASP.NET & C#

Subscriptions/Recurring payments with Stripe in ASP.NET

https://techtolia.com/StripeSubscriptions/

With the Stripe Billing APIs, you can create and manage invoices and recurring payments.

You can now model your business and product offerings in one place. Products define what you sell and Prices track how much and how often to charge. They can be used for recurring or one-time purchases and support various business structures from tiers to usage based billing.

If you are looking a solution to integrate Stripe’s subscriptions to your website (ASP.NET Web Forms or ASP.NET Core MVC) that is builded with .Net & C#, look at that demo → https://techtolia.com/StripeSubscriptions/

Fixed-Price Subscriptions: Charge customers a fixed amount each month. The Quantity is set to 1.
Per-Seat Subscriptions: Charge customers based on how many units of your product or service they purchase. You must set the Quantity variable to the number chosen by your customer.

How to model subscriptions

Learn how to model subscriptions using products and prices.

Products and prices
Products and Prices are the primary objects used to model subscriptions. Products define what you’re selling and prices define how to charge for the product. For example, if you offer photo hosting for 15 USD a month, photo hosting is your product and the 15 USD a month is your price. Or, maybe you offer physical goods like a weekly produce box for 10 USD per week. The produce box is your product and the 10 USD is your price.

Products can have multiple prices so you can vary pricing by billing interval, currency, or amount. You can also use multiple prices to phase out old prices that you no longer offer.

Users commonly create and manage products and prices in the Stripe Dashboard. If you offer many products or plans, however, you might prefer to create these objects with the API.

In a fixed-price model, customers are charged the same amount every billing period. For example, a photo hosting company that offers basic and premium options might have:

  • One product for the basic option
  • One product for the premium option
  • One price for the basic option (15 USD per month)
  • One price for the premium option (25 USD per month)

In a per-seat model, customers are charged based on the number of seats or units that they purchase. For example, a messaging service that offers basic and premium options might have:

  • One product for the basic option
  • One product for the premium option
  • One price for the basic option that charges the same amount per seat, regardless of the number of seats (5 USD per seat)
  • One price for the premium option that includes tiers so that the price per-seat goes down the more seats that are purchased (tier one: 15 USD per seat for 1–100 seats, tier two: 10 USD for 101+ seats)

Subscriptions with Stripe Elements

Build beautiful, smart checkout flows. Stripe Elements are rich, prebuilt UI components that help you create your own pixel-perfect checkout flows across desktop and mobile.

Subscriptions with Payment Request Button

Collect payment and address information from customers who use Apple Pay, Google Pay, Microsoft Pay, and the browser Payment Request API (Chrome, Opera, Edge, Safari).

Customers see a “Pay now” button or an Apple Pay button, depending on what their device and browser combination supports. If neither option is available, they don’t see the button. Supporting Apple Pay requires additional steps, but compatible devices automatically support browser-saved cards, Google Pay, and Microsoft Pay.

What is Strong Customer Authentication?

Strong Customer Authentication (SCA) is a new European regulatory requirement to reduce fraud and make online payments more secure. To accept payments and meet SCA requirements, you need to build additional authentication into your checkout flow. SCA requires authentication to use at least two of the following three elements.

Banks will need to start declining payments that require SCA and don’t meet these criteria. Although the regulation was introduced on 14 September 2019, we expect these requirements to be enforced by regulators over the course of 2020 and 2021.

3D Secure 2 - the new version of the authentication protocol rolling out in 2019 - will be the main method for authenticating online card payments and meeting the new SCA requirements. This new version introduces a better user experience that will help minimise some of the friction that authentication adds into the checkout flow.

The application supports 3D Secure 2

The Payment Intents API that uses Stripe’s SCA logic to apply the right exemption and trigger 3D Secure when necessary.

The application uses the Payment Intents API for card payments

Buy Now on CodeCanyon

Target Framework: .NET Framework 4.7.2 - Language: c#
Target Framework: .NET Core - ASP.NET Core 3.1 - Language: c#

https://techtolia.medium.com/accept-subscriptions-recurring-payments-with-stripe-in-asp-net-c-64f9caee1353

#stripe #subscriptions #recurring #payments #asp.net #csharp

Accept Subscriptions/Recurring payments with Stripe in ASP.NET & C#