Web3 backend and smart contract development for Python developers Musical NFTs part 16: Stripe integration

ilija

ilija

Posted on November 21, 2023

Web3 backend and smart contract development for Python developers Musical NFTs part 16: Stripe integration

At this point we are almost done with our version of Musical NFT platform. What we still need to do is to allow credit card buyers to buy our NFTs with credit card and to deploy our app somewhere live.

Let's start with Stripe integrations fore credit card buyers. Update credit_card_user.html doc with buy button and Buy NFT name. Now creadit_card_user.html file inside root templates folder should look something like this

 <table class="table  table-hover">
        <thead class="table-dark">
        <tr>
            <th scope="col">Name</th>
            <th scope="col">Email</th>
            <th scope="col">Total no. NFTs</th>
            <th scope="col">Means of payment</th>
            <th scope="col">Buy NFT</th>
        </tr>
        </thead>
        <tbody>
        <tr>
            <td> {{ customer.first_name }} {{ customer.last_name }} </td>
            <td> {{ customer.email }} </td>
            <td> {{ customer.total_no_of_nfts }} </td>
            <td><form action="" method="post">
                {%csrf_token%}
                {{orderForm.as_p}}
                <input type="submit" value="Save" class="btn btn-secondary">
            </form>
            </td>        
            <td>
            <form action="" method="post">
                <label for="NFT">Number of NFTs</label>
                {%csrf_token%}
                <input  type="number" id="NFT" ><br>
                </form>
                <input  id="creditCard" type="submit" value="Buy" class="btn btn-secondary">
                </td>   
            </tr>
        </tbody>
        </table>        
        {% for card in metadata%}
        {% if forloop.counter0|divisibleby:3 %} <div class="row">{%  endif %}
        <div class="card m-5 p-2" style="width: 18rem;">
            {% load static %}
            <img src="{% static 'nft/'%}{{card.cover_file_name}}"  class="card-img-top" alt="..." width="50" height="200"/>
            <div class="card-body">
            <h5 class="card-title">{{card.name}}</h5>
            <br>
            <p class="card-text">{{card.description}}</p>
            </div>
        </div>
        {%  if forloop.counter|divisibleby:3 or forloop.last %}</div> {%  endif %}
        <br>
        {% endfor %}
        <p> credit card </p>
Enter fullscreen mode Exit fullscreen mode

Let's pip install stripe python library.


    (env)$pip install stripe==5.5.0
Enter fullscreen mode Exit fullscreen mode

Next, register for a Stripe account (if you haven't already done so) and navigate to the dashboard. Click on "Developers":

Image description

Then click on "API keys":

Image description

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.

Add to your .env file STRIPE_SECRET_KEY and pass there your secret keys from Stipe. Then at the end of Django settings.py add this to two new lines of code

STRIPE_SECRET_KEY = env("STRIPE_SECRET_KEY")
STRIPE_PUBLISHABLE_KEY = "pk_test_51MgCeMHGy7fyfctt4TIWxu6ev25bm5180LalTib2YAx2YmBe3IAWNxPHDMSUzn0tx7K4Mrq8aoKIQyzHc8TRWRGG00p8ePmfug" # here your publishable key
Enter fullscreen mode Exit fullscreen mode

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

Image description

Next, we need to create a product to sell.

Click "Products" and then "Add product":

Image description

Add a product name, enter a price, and select "One time":

Image description

Click "Save product".

Now flow should go something like this:

After the user clicks the purchase button we need to do the following:

Get Publishable Key
1) Send an XHR request from the client to the server requesting the publishable key
2) Respond with the key
3) Use the key to create a new instance of Stripe.js

Create Checkout Session
1)Send another XHR request to the server requesting a new Checkout Session ID
2)Generate a new Checkout Session and send back the ID
3)Redirect to the checkout page for the user to finish their purchase
4) Redirect the User Appropriately:
Redirect to a success page after a successful payment
Redirect to a cancellation page after a cancelled payment

Confirm Payment with Stripe Webhooks
1)Set up the webhook endpoint
2)Test the endpoint using the Stripe CLI
3)Register the endpoint with Stripe

Let's start => Get Publishable key:
Updated version of base.html:


    {% load static %}
    <!doctype html>
    <html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Muscial NFT</title>
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
        <script src="https://js.stripe.com/v3/"></script>  <!-- new -->
        <script type="text/javascript" src="{% static 'connect_wallet.js' %}"></script>
        <link rel="shortcut icon" type="image" href="{% static 'favicon.ico' %}" >
        <!-- <script type="module" src="{% static 'call_sc.js' %}"></script> -->
    </script>

    </head>
    <body onload="checkMetaMaskState()">
    <body>
        {% include "navbar.html"%}
        <div class="container ">       
        <br/>
        <br/>
        {% if messages %}
        {% for message in messages%}
        <div class="alert alert-warning alert-dismissible fade show" role="alert">
            {{ message }}
            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
        </div>

        {% endfor%}
        {% endif %} 

        {% block content %}
        {% endblock content %}
        </div>
        </body>
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
        <script src="https://cdn.ethers.io/scripts/ethers-v4.min.js"
        charset="utf-8"
        type="text/javascript"></script>
        <script type="text/javascript" src="{% static 'call_sc.js' %}"></script>
        <script type="text/javascript" src="{% static 'stripe_integration.js' %}"></script>
        </html>
Enter fullscreen mode Exit fullscreen mode

Inside our authentication app folder views.py, create new class StripeConfigView + we should add few new imports as well as one new method to handle XHR request:

 #new imports 
    from django.conf import settings # new
    from django.http.response import JsonResponse # new
    from django.views.decorators.csrf import csrf_exempt # new

    class StripeConfigView(TemplateView): # new

        @csrf_exempt
        def get(request):
            stripe_config = {'publicKey': settings.STRIPE_PUBLISHABLE_KEY}
            return JsonResponse(stripe_config, safe=False)
Enter fullscreen mode Exit fullscreen mode

Now update our authentication app level urls.py:

 from django.urls import path

    from .views import HomeView, LogoutUser, RegisterUser, StripeConfigView

    urlpatterns = [
        path("", HomeView.as_view(), name="home"),
        # path("login/", LoginUser.as_view(), name="login"),
        path("logout/", LogoutUser.as_view(), name="logout"),
        path("register/", RegisterUser.as_view(), name="register"),
        path("config/", StripeConfigView.as_view(), name="stripe_config"), # new
    ]

Enter fullscreen mode Exit fullscreen mode

Next, use the Fetch API to make an XHR (XMLHttpRequest) request to the new /config/ endpoint in static/stripe_integrations.js:

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

Include stripe.js in head tag of our base.html just bellow bootstrap

 `<script src="https://js.stripe.com/v3/"></script>` 
Enter fullscreen mode Exit fullscreen mode

With this we close down first item from our list:
Get Publishable Key

Send an XHR request from the client to the server requesting the publishable key
Respond with the key
Use the key to create a new instance of Stripe.js

What still needs to be done =>
Create Checkout Session

Send another XHR request to the server requesting a new Checkout Session ID
Generate a new Checkout Session and send back the ID
Redirect to the checkout page for the user to finish their purchase
Redirect the User Appropriately

Redirect to a success page after a successful payment
Redirect to a cancellation page after a cancelled payment
Confirm Payment with Stripe Webhooks

Set up the webhook endpoint
Test the endpoint using the Stripe CLI
Register the endpoint with Stripe

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

inside authentication/views.py crete new CreateCheckoutSession

 class CreateCheckoutSession(TemplateView): # new   

    @csrf_exempt
    def get(self, request):
        number_of_nfts = request.GET.get("number")
        domain_url = 'http://localhost:8000/'
        stripe.api_key = settings.STRIPE_SECRET_KEY
        try:
            # Create new Checkout Session for the order
            # Other optional params include:
            # [billing_address_collection] - to display billing address details on the page
            # [customer] - if you have an existing Stripe Customer ID
            # [payment_intent_data] - capture the payment later
            # [customer_email] - prefill the email input in the form
            # For full details see https://stripe.com/docs/api/checkout/sessions/create

            # ?session_id={CHECKOUT_SESSION_ID} means the redirect will have the session ID set as a query param
            checkout_session = stripe.checkout.Session.create(
                success_url=domain_url + 'success?session_id={CHECKOUT_SESSION_ID}',
                cancel_url=domain_url + 'cancelled/',
                payment_method_types=['card'],
                mode='payment',
                line_items=[
                    {
                        'price_data': {
                            'currency': 'usd',
                            'unit_amount': 2000,
                            'product_data': {
                                'name': 'MusicalNFT',
                            },
                            },
                            'quantity': number_of_nfts,
                    }
                ]
            )
            return JsonResponse({'sessionId': checkout_session['id']})
        except Exception as e:
            return JsonResponse({'error': str(e)})

Enter fullscreen mode Exit fullscreen mode

Here we pick up fron front-end number of NFTs user wants. Then 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 to import stripe into views.py

    import stripe
Enter fullscreen mode Exit fullscreen mode

Add to authentication urls.py

 from django.urls import path

    from .views import HomeView, LogoutUser, RegisterUser, StripeConfigView, CreateCheckoutSession

    urlpatterns = [
        path("", HomeView.as_view(), name="home"),
        path("logout/", LogoutUser.as_view(), name="logout"),
        path("register/", RegisterUser.as_view(), name="register"),
        path("config/", StripeConfigView.as_view(), name="stripe_config"),
        path('create-checkout-session/', CreateCheckoutSession.as_view(), name = "create_checkout_session"), # new
    ]
Enter fullscreen mode Exit fullscreen mode

XHR Request

Add the event handler and subsequent XHR request to static/stripe_integrations.js:

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

    // Event handler
    document.querySelector("#creditCard").addEventListener("click", () => {
    // Get Checkout Session ID
    // pass input values to backend
    let num = Number(document.getElementById("NFT").value);

    fetch(`/create-checkout-session?` + new URLSearchParams({number: `${num}`}))
    .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);
        });
    });
    });
Enter fullscreen mode Exit fullscreen mode

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

Navigate to user profile page on button click you should be redirected to an instance of Stripe Checkout (a Stripe-hosted page to securely collect payment information) with the NFT product information:

Image description

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, but the redirect will fail since we have not set up the /success/ URL yet.

To confirm a charge was actually made, go back to the Stripe dashboard under "Payments":

To review, we used the secret key to create a unique Checkout Session ID on the server. This ID was then used to create a Checkout instance, which the end user gets redirected to after clicking the payment button. After the charge occurred, they are then redirected back to the success page.

Ok, at this point we resolve first two items from our list: 1) Get publishable key and 2) Create Checkout session. What still need to be doe is to 3) Redirect User based on success or failure of transaction. And after that, finaly, 4) confirm payment over Stripe webhook, 5) minti new NFT to platform to custodial wallet and 6) database bookeeping staff (to keep all things in the sync).

To redirect user after transacrtion success or failure we can make two additional templates: canceled.html and success.html in project root templates folder

success.html

 <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Django + Stripe Checkout</title>
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
    </head>
    <body>
        {% block content%}
        <section class="section">
        <div class="container">
            <p>Your payment succeeded!</p>
            <a href="{% url 'home' %}"> Return to user profile page </a>
        </div>
        </section>
        {% endblock content%}
    </body>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
    </html>

Enter fullscreen mode Exit fullscreen mode

And then canceled.html

  <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>Django + Stripe Checkout</title>
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">

    </head>
    <body>
        {% block content %}
        <section class="section">
        <div class="container">
            <p>Your payment was cancelled.</p>
            <a href="{% url 'home' %}"> Return to user page </a>
        </div>
        </section>
        {% endblock content %}
    </body>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
    </html>

Enter fullscreen mode Exit fullscreen mode

Then inside authenticaiton views.py add this two new classes:

  class SuccessView(TemplateView):
        template_name = 'success.html'


    class CancelledView(TemplateView):
        template_name = 'cancelled.html'
Enter fullscreen mode Exit fullscreen mode

This two templates will be used to redirect user to correct page from inside our CreateCheckoutSession class.

If tx was succesfule user should see something like this

Image description

but before that we need to update our urls.py with two additional paths

  # payments/urls.py
    from django.urls import path

    from . import views

    urlpatterns = [
        path('', views.HomePageView.as_view(), name='home'),
        path('config/', views.stripe_config),
        path('create-checkout-session/', views.create_checkout_session),
        path('success/', views.SuccessView.as_view()), # new
        path('cancelled/', views.CancelledView.as_view()), # new
    ]

Enter fullscreen mode Exit fullscreen mode

Ok, refresh the web page at http://localhost:8000/. Click on the payment button and use the credit card number 4242 4242 4242 4242 again along with the rest of the dummy info. Submit the payment. You should be redirected back to http://localhost:8000/success/.

To confirm a charge was actually made, go back to the Stripe dashboard under "Payments":

Image description

To review, we used the secret key to create a unique Checkout Session ID on the server. This ID was then used to create a Checkout instance, which the end user gets redirected to after clicking the payment button. After the charge occurred, they are then redirected back to the success page.

At this point we have just few more things to add to finish credit card functionaliy. Precasly three more things: 1) confirm payment over Stripe webhook; 2) minti new NFT to custodial wallet ones confirmaton arrive over wevhook and 3) user database bookeeping.

Ok, webhooks! Our app works well at this point, but we still can't programmatically confirm payments and run smart contract related code if a payment was successful. 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 buys a NFT). By using webhooks, we can be absolutely sure the payment went through successfully (and based on that mint new NFT to custodial wallet and asisgne ownership to the user in our Postgres database). In order to use webhooks, we need to:
Create Stripe request handler inside our views.py
Test the endpoint using the Stripe CLI
Register the endpoint with Stripe

Create a new view functon called stripe_webhook which prints a message every time a payment goes through successfully (more about code in comments and bellowe code):

  from django.http.response import JsonResponse, HttpResponse

    # payments/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':
            # Mint new NFT
            result = contract_interface.mint_nft("ipfs://uri.test")
            suc, result = contract_interface.event()
            if suc:
                try:
                    # if tx was sucesfule update custodial wallet related DB as well as user
                    c1 = Customer.objects.get(eth_address=result.args.owner)
                    if result.args.numberOfNFT not in c1.nft_ids:
                        # update custodial wallet information db
                        c1.nft_ids.append(result.args.numberOfNFT)
                        c1.total_no_of_nfts += 1
                        c1.save()
                        # update customer db
                        user = event["data"]["object"]["customer_details"]["name"]
                        name, last = user.split()
                        c2 = Customer.objects.get(first_name=name, last_name=last)
                        c2.nft_ids.append(result.args.numberOfNFT)
                        c2.total_no_of_nfts += 1         
                        c2.save()   
                        print("Payment success!)                                
                except Exception as e:
                    print (e)
        return HttpResponse(status=200)

Enter fullscreen mode Exit fullscreen mode

And then update urls.py

  from django.urls import path

    from . import views

    urlpatterns = [
        path('', views.HomePageView.as_view(), name='home'),
        path('config/', views.stripe_config),
        path('create-checkout-session/', views.create_checkout_session),
        path('success/', views.SuccessView.as_view()),
        path('cancelled/', views.CancelledView.as_view()),
        path('webhook/', views.stripe_webhook), # new
    ]
Enter fullscreen mode Exit fullscreen mode

Logic here is that over our webhook we are listen for request from Stripe. Then ones we get that request we are checking if sender is authroized, if type is completed and there is no any error alonge the way. Only if this is the case we assigne custodial wallet user and assigne id from newly minted NFT as well as increase total number of NFTs we have in custodial wallet. Then we query our database for user who acctualy pay this NFT with his card and then update his nft_ids field and total_no_of_nfts. Because he payed with credit card we minted new NFT to custodial wallet not to his one (he don't have one). But also in db we made record about the fact that he is owner of this NFT minted to custodial wallet.

Now let's test our 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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

This will also generate a webhook signing secret:

  > Ready! Your webhook signing secret is whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (^C to quit)
Enter fullscreen mode Exit fullscreen mode

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

    STRIPE_SECRET_KEY = env("STRIPE_SECRET_KEY")
    STRIPE_PUBLISHABLE_KEY = "pk_test_51MybU0KtroSirNQXi4fuyC99SsypbcWbqLZtfYtGWTUmwTyoNkPaPvu7vy2twd5JjyzHTaL9EirWX7GsFJV3xFsj00xVvZo3C8" 
    STRIPE_ENDPOINT_SECRET=env("STRIPE_ENDPOINT")
Enter fullscreen mode Exit fullscreen mode

Make sure that you have in your root .env STRIPE_ENDPOINT_SECRET variable

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 Payment was successful. message.

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

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

With this basically we have also credit card buyer covered. And what's left in this moment is deployment of our platform somewhere live. And as you can guess this will be topic of our final blog post...

Code can be found in this repo

p.s. Huge thanks to tesdrive.io for all knowledge they share!

💖 💪 🙅 🚩
ilija
ilija

Posted on November 21, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related