Published
- 16 min read
Tessera Lab 5 - Buying tickets
Objective
Alright, buckle up for this one! In Tessera Lab 5, we’re diving into the world of buying tickets—like, really diving in. We’re going to build a slick seat map using a custom component so users can pick their favorite spots (no more fighting over who gets the aisle seat). Then, we’ll integrate Stripe’s test playground to handle the payment process. This means you’ll get hands-on experience with real-world payment flows, but don’t worry, it’s all play money—no actual credit cards needed! By the end of this lab, you’ll have built a fully functional ticket purchasing system that’s smooth, secure, and ready to roll. Let’s make some magic happen!
Interactive Seat Selection
Lets start with something really cool. A (very simple) seat map for our events! We want fans to be able to select the seats they want to purchase. For this, we’ll be using a custom seat picker component! Take a look below for an example:
Installation
I’ve gone ahead and created a reusable seat picker component for you. Its based off this library, however I’ve simplified the look and usage of it so its easier for us to use. Lets install it. Open up your package.json
file found at the root of your frontend
directory. Look for the dependencies
object and add this line to the end of the object
"tessera-seat-picker": "git://github.com/muhammadtalhas/tessera-seat-picker"
For example, my dependencies looks like this (yours may look slightly different, dont change anything - just add the new item at the end)
"dependencies": {
"@chakra-ui/icons": "^2.1.1",
"@chakra-ui/react": "^2.8.2",
"@elastic/datemath": "^5.0.3",
"@elastic/eui": "^95.3.0",
"@emotion/css": "^11.11.2",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"framer-motion": "^11.2.12",
"moment": "^2.30.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^5.2.1",
"react-router-dom": "^6.24.0",
"tessera-seat-picker": "git://github.com/muhammadtalhas/tessera-seat-picker"
}
Now, run the following command in a terminal within the frontend directory, to install the dependency
npm i --legacy-peer-dep
Note the legacy-peer-dep
flag. This is required as some of the underlying dependencies of the seat picker are not fully compatible with newer react versions (but dont worry, its fine for our use case).
Please Note: If you run into any weirdness or buggy behavior with the seat picker - please let me know and I will do my best to push out an update to fix it!
Usage
So how do we use it? Let me show you a barebones snippet of code to get you started. You’ll still have to do the heave lifting to get this to work with your architecture!
import React, { useState } from 'react';
import TesseraSeatPicker from 'tessera-seat-picker';
const rows = [
[
{ id: 1, number: 1, tooltip: "$30" },
{ id: 2, number: 2, tooltip: "$30" },
{ id: 3, number: 3, isReserved: true, tooltip: "$30" },
null,
{ id: 4, number: 4, tooltip: "$30" },
{ id: 5, number: 5, tooltip: "$30" },
{ id: 6, number: 6, tooltip: "$30" }
],
[
{ id: 7, number: 1, isReserved: true, tooltip: "$20" },
{ id: 8, number: 2, isReserved: true, tooltip: "$20" },
{ id: 9, number: 3, isReserved: true, tooltip: "$20" },
null,
{ id: 10, number: 4, tooltip: "$20" },
{ id: 11, number: 5, tooltip: "$20" },
{ id: 12, number: 6, tooltip: "$20" }
]
];
function EventDetailPage() {
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const addSeatCallback = async ({ row, number, id }, addCb) => {
setLoading(true);
try {
// Your custom logic to reserve the seat goes here...
// Assuming everything went well...
setSelected((prevItems) => [...prevItems, id]);
const updateTooltipValue = 'Added to cart';
// Important to call this function if the seat was successfully selected - it helps update the screen
addCb(row, number, id, updateTooltipValue);
} catch (error) {
// Handle any errors here
console.error('Error adding seat:', error);
} finally {
setLoading(false);
}
};
const removeSeatCallback = async ({ row, number, id }, removeCb) => {
setLoading(true);
try {
// Your custom logic to remove the seat goes here...
setSelected((list) => list.filter((item) => item !== id));
removeCb(row, number);
} catch (error) {
// Handle any errors here
console.error('Error removing seat:', error);
} finally {
setLoading(false);
}
};
return (
//.. A bunch of other stuff...
<TesseraSeatPicker
addSeatCallback={addSeatCallback}
removeSeatCallback={removeSeatCallback}
rows={rows}
maxReservableSeats={3}
alpha
visible
loading={loading}
/>
);
}
export default EventDetailPage;
Keep in mind, this is an extremely basic example. Its going to require a lot of work to get it fixed into our code. Let’s take a closer look at the component props. As this component is based off this open source library - all of the props documented on its documentation is supported. I will note that my version strictly controls the look and feel of the component and a few things are locked down - so it may not work exactly like described. Also - the tooltip feature, while cool, seems to be buggy with our version of react. I haven’t blocked its usage, but keep in mind it may not work properly if you chose to use the optional feature.
Below are select props that I’m highlighting because I think they’re the most important for our project. There are more and I encourage you to see if any of them are useful to your vision.
TesseraSeatPicker Props
Name | Type | Default | Required | Description/Usage |
---|---|---|---|---|
alpha | boolean | false | false | Should your rows be referred with letters (true) or number (false) |
visable | boolean | false | false | Show/render the row identifer on the screen |
loading | boolean | false | false | Makes the seat picker unclickable when true |
maxReservableSeats | number | 0 | false | Maximum number of seats someone can select 0 means infinite |
addSeatCallback | function | built in function that prints the selection | false | This function should be where your logic goes to actually reserve the seat. Remember to call addCb(row,number,id) to confirm the selection |
removeSeatCallback | function | built in function that prints the de-selection | false | This function should be where your logic goes to un-reserve the seat. Remember to call removeCb(row,number,id) to confirm the de-selection |
rows | array | null | true | 2 dimensional array of seats (see next section) |
Seat Props
This is a JSON representation of rows and seats. Any seat can also be null
. This is useful if you want to have aisles in your venue. Maybe you want to have different layouts for different events 🤔🤔🤔?
Name | Type | Default | Required | Description/Usage |
---|---|---|---|---|
id | number or string | undefined | true | A unique identifer for the seat (this is technically not required in the underlying library but I have made it required as there some weirdness when its not provided) |
number | number or string | undefined | true | This is the label that renders inside the seat. Again not required by the underlying library but I’ve made it required for stability reasons |
isReserved | boolean | false | false | Disables the option to select this seat |
orientation | string | north | false | Orientation of the seat render (north, south, east, west) |
tooltip | string | undefined | false | Optional text to display on hovering the seat (NOTE: this is buggy with our version of react. For my tessera, I used it to display the price of the seat. The tooltip sometimes dosent go away when you move your mouse. Your choice if you want to use it!) |
Task 1 - Show a seat picker on the event detail page
Get to work! Here’s a task list and some hints. Work together! Ask questions, design before you start coding, and take each step one at a time. It’s easier to solve 1 problem at a time rather than 30 at once!
- Start by rendering a very basic seat map on the event details page. Maybe one with three rows. Play around with setting some seats to reserved.
- Now, use the inventory endpoints we created last lab to pull in data for the current event
- Our data does not look like what our component needs - lets take the data we get from our backend, and store it in a state variable that represents the map.
- Replace the place holder data from step 1 with this new map representation.
- Write code to handle reserving the seat when it’s selected.
- Write code to handle un-reserving a seat when it’s de-selected
- Figure out a way to calculate a total price when seats are selected (notice that our seat picker component really dosent have any data value to store prices)
- Display a checkout button with the total cart price displayed
- For now - when the user clicks checkout, process the action (We’ll work on payments later)
Hints
- Try creating functions that help you convert and transform raw API data to map data
- A unique seat ID is required for the Seat Picker.
- A function that takes seat ID as an input and outputs the price would be very helpful
- Although theres nothing stopping us from using numerous state variables, try to keep them minimal and use data that you already have available to you
Extra Credit
Please do not try these until you have the basic functionality of this entire lab complete. Do not worry if you would like to skip these!
- Implement event configurations so that you can define where the aisles are so we dont have to hardcode them
- Implement Max Reservable Seats per event as a configuration
- Implement ticket fees in your total calculation
- Implement promo codes
- Think about edge cases (such as when an event is sold out!)
- Disallow orphaned single seats (its hard to sell a single seat in the middle of a row)
- Implement a “Best Available” option to automatically select the best seats currently open
Payment Processing
Alright, let’s talk money—specifically, how to handle payments in a web app without losing your mind. When it comes to processing payments online, you’ve got to deal with a bunch of complex stuff like securely handling credit card information, complying with various regulations, and making sure everything works smoothly across different countries and currencies. This is where a payment processor like Stripe comes in to save the day.
Stripe is like the cool kid in the payment processing world. It’s a platform that makes it super easy to accept payments online. Whether you’re selling event tickets (like we are) or running an e-commerce site, Stripe handles all the tricky stuff for you. With Stripe, you don’t have to worry about storing credit card numbers or dealing with banking regulations—Stripe takes care of all that under the hood. Plus, they provide awesome tools for developers, including a test environment (aka playground) where you can safely practice integrating payments without using real money. By the end of this lab, you’ll see just how easy it is to get up and running with payments using Stripe.
Stripe
Head over to (stripe.com)[stripe.com]. Sign up for a new account. DO NOT PROVIDE ANY OPTINAL INFORMATION LIKE BANKING INFO. We will be using Stripe’s playground environment which is a great way to learn about how to develop purchase flows, without charging credit cards! Skip any bloat stripe asks you about. Once your account is setup, you should head over to the Stripe Dashboard. Your dashboard should be set up in Test Mode. Note the publishable key and private key and secret key on the right hand side. We’ll be using these.
Backend Stripe Integration
It’s important to understand the general flow when making a payment with Stripe. Below is flow diagram showing how a ‘payment intent’ works. You can read more about it here.
Let’s break it down more in terms of tessera:
- The system calculates the total price the fan has to pay
- The frontend calls the backend to create a ‘payment intent’ after the fan inputs their payment details
- Our backend calls the stripe servers and creates the intent on stripes system. We then return an identifier for the intent
- The frontend receives the intent and uses the identifier to call stripes servers directly to run the payment
- The fans payment is successful and our frontend now needs to call our backend again with the payment intent identifier so our backend can confirm the payment has been received
- We award the fan the tickets they have purchased
Phew - thats a lot of work. I’m feeling cheery so why dont I provide you some code to get started. First, lets install the stripe python package:
pip install stripe
Looking at our breakdown above, we need two endpoints - one that creates a payment intent and one that confirms the intent was fulfilled. Here’s a naïve implementation of the code. You should take this as an outline for modifying/creating your own endpoints. Keep in mind everything we talked about security as well!
from flask import Flask, request, jsonify
import stripe
app = Flask(__name__)
# Get your key from your dashboard
stripe.api_key = 'your-stripe-secret-key'
@app.route('/create-payment-intent', methods=['POST'])
def create_payment_intent():
try:
data = request.json
amount = data['amount'] # Amount in cents
# More Docs: https://docs.stripe.com/api/payment_intents/create
payment_intent = stripe.PaymentIntent.create(
amount=amount,
currency='usd'
)
return jsonify({
'clientSecret': payment_intent['client_secret']
})
except Exception as e:
return jsonify(error=str(e)), 403
@app.route('/complete-purchase', methods=['POST'])
def complete_purchase():
try:
data = request.json
payment_intent_id = data['paymentIntentId']
seats = data['seats']
# More Docs: https://docs.stripe.com/api/payment_intents/retrieve
payment_intent = stripe.PaymentIntent.retrieve(payment_intent_id)
if payment_intent.status != 'succeeded':
return jsonify({"error": "Payment not successful"}), 400
### This is where you should process the sale
### Remember everything you need to assign seats to an account
### You'll probably need more inputs
### Create functions to help you with this! Break up your code
return jsonify({"message": "Purchase completed successfully"})
except Exception as e:
return jsonify(error=str(e)), 500
...
Payment processing in less than 100 lines of code????? How baller is that?! Although we’re building just for the playground environment - the same code would work for production! I expect to have shares in your next $100MM t-shirt storefront.
Frontend Stripe Integration
The backend integration was pretty simple. Let’s move on to the front end. Stipe provides customizable react components for developers. It’s almost like they read out minds. We’re going to be using their components to allow for a secure method for fans to input their payment information. I’ve created a sample React application and the code is provided below. You can see it in action as well!. I recommend you create a new test page within your code base where you can implement a similar setup to test it. I also recommend you handle payment information via a Modal. Its a super clean way to handle user inputs without having to navigate to other pages for checkout. However, this tessera is yours! Do as you please!
Check out my test page:
After hitting Pay, I navigated over to my Stripe dashboard and saw money sitting in my test account!
Here’s the code to build it. Please keep in mind this should be used as a starting point. Your final code could and should look different!
You also need to install the following libraries (Note: we must always use the --legacy-peer-dep
flag now - see discussion above)
npm install @stripe/react-stripe-js @stripe/stripe-js --legacy-peer-dep
PaymentForm.jsx
import React, { useState } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
import { Box, Button, Input, FormControl, FormLabel, Text } from '@chakra-ui/react';
// Get your key from your dashboard
const stripePromise = loadStripe('your-publishable-key');
const CheckoutForm = ({ totalAmount }) => {
const stripe = useStripe();
const elements = useElements();
const [paymentSuccess, setPaymentSuccess] = useState(false);
const [error, setError] = useState(null);
const handleSubmit = async (event) => {
event.preventDefault();
if (!stripe || !elements) {
return;
}
const cardElement = elements.getElement(CardElement);
const response = await fetch('http://localhost:5000/create-payment-intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ amount: totalAmount * 100 }), // Amount should be in the lowest denomination (For USD thats cents)
});
const { clientSecret } = await response.json();
const result = await stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: cardElement,
billing_details: {
name: 'Test User',
},
},
});
if (result.error) {
setError(result.error.message);
} else {
if (result.paymentIntent.status === 'succeeded') {
setPaymentSuccess(true);
}
}
};
return (
<Box as="form" onSubmit={handleSubmit} p={4}>
<FormControl mb={4}>
<FormLabel>Total Amount</FormLabel>
<Text fontSize="xl">${totalAmount.toFixed(2)}</Text>
</FormControl>
<FormControl>
<FormLabel>Card Details</FormLabel>
<CardElement />
</FormControl>
<Button mt={4} colorScheme="blue" type="submit" disabled={!stripe}>
Pay
</Button>
{paymentSuccess && <Text mt={4} color="green.500">Payment Successful!</Text>}
{error && <Text mt={4} color="red.500">{error}</Text>}
</Box>
);
};
const PaymentForm = ({ totalAmount }) => (
<Elements stripe={stripePromise}>
<CheckoutForm totalAmount={totalAmount} />
</Elements>
);
export default PaymentForm;
App.jsx
import React from 'react';
import { ChakraProvider, Box } from '@chakra-ui/react';
import PaymentForm from './PaymentForm';
function App() {
const totalAmount = 50.00; // Example amount in dollars
return (
<ChakraProvider>
<Box p={4}>
<PaymentForm totalAmount={totalAmount} />
</Box>
</ChakraProvider>
);
}
export default App;
Task 2 - Implement a payment flow
I’ve provided a bunch code and resources for you to understand payment processing using Stripe. Your task is to now implement a purchase flow in your tessera project. Please work together, this is probably the most complex part. Ask questions and when in doubt, find documentation!
Your task is simple:
- Implement a purchase flow where a user can enter their credit card information and process the payment through Stripe
As always, some hints:
- Take my test code and start with that.
- Keep in mind seperation of concerns. Create helper functions, helper classes, and break your code into smaller pieces.
- Click here to see test credit cards you can use
- Think before you code. Design your flows before you start coding
- Use git to checkpoint yourself. Reverting to working code is easier that way
- I suggest you use a Modal for accepting payments. That way, selecting seats and payments can all happen in one screen
- Dont forget to reroute your fan to another page once the seats are sold!
Your final Task
Before we dive into the final task, I just want to take a moment to say—wow, look at what you’ve accomplished! You’ve navigated through some seriously challenging concepts. These are no small feats, and you’ve tackled them with determination and skill. I know there were moments where it felt overwhelming, but here you are, standing at the finish line with a project that’s practically ready for the real world. You should be incredibly proud of what you’ve built and how far you’ve come. This is the kind of work that turns heads and opens doors in the industry. So, take a deep breath, give yourself a well-deserved pat on the back, and let’s tackle this last challenge together.
In our last task, we’re going to tackle emailing tickets to the fans. In the previous task, we learned how to integrate with 3rd party apps (like stripe). For emailing, I’d like you to use SendGrid. Send Grid makes it easy to send emails and they have a simple python SDK to integrate with. I’m not going to define requirements for this task. I want you to have fun and be creative. I will however provide you snippets of my code to help you along
import os
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
def send_ticket_email(to_email, seats):
sendgrid_api_key = 'your-sendgrid-api-key' # Replace with your SendGrid API key
subject = "Your Tickets from Tessera"
html_content = f"""
<h1>Thank you for your purchase!</h1>
<p>You have successfully purchased tickets for the following seats:</p>
<p>{seat_list}</p>
<p>Please bring this email to the event as your ticket confirmation.</p>
"""
seat_list = ', '.join(seats)
message = Mail(
from_email='[email protected]',
to_emails=to_email,
subject=subject,
html_content=html_content
)
try:
sg = SendGridAPIClient(sendgrid_api_key)
response = sg.send(message)
print(f"Email sent to {to_email}, status code: {response.status_code}")
except Exception as e:
print(f"An error occurred: {e}")
send_ticket_email('[email protected]', ['A1', 'A2', 'A3'])
Now that you know how to send emails… where else could you use this??
The End?
”Software is never finished, only abandoned.”