Clone a Google Play Redeem Page
Build a Flask web app that clones Google Play's gift card redeem page.
Introduction
30 Second Summary
You have probably redeemed a gift card code online before. You type a code into a form, click a button, and see a confirmation page. Behind that simple interaction is a web server handling your submission and rendering a response.
In this project, you will build a clone of the Google Play redeem code page from scratch. You will design the frontend form, wire up a Flask backend to process submissions, and expose it to the public internet using ngrok.
What You'll Build
A self-hosted replica of the Google Play "Redeem your code" web page, complete with a Material Design styled form, Flask-powered backend, and a public HTTPS URL anyone can visit.
By the end of this project, you'll have:
- A Flask web application with two routes that serve a code entry form and a success confirmation page.
- An HTML/CSS frontend that mimics Google Play's redeem page design with clean Material Design styling.
- A public HTTPS URL via ngrok that serves your app to anyone on the internet.
- Secret Mission: Add realistic client-side code format validation with error messages and a loading spinner animation on submit.
Are there any prerequisites?
Basic familiarity with HTML/CSS and Python is helpful. You do not need prior experience with Flask or ngrok.
Before We Start
You are about to build a clone of the Google Play redeem code page. This clone will look and behave like the real thing, accepting a gift card code and displaying a success confirmation.
To get there, you need three tools working together: a Python web framework to handle form submissions, a way to write and serve HTML pages, and a tunnel to make your local server reachable from the internet. This step gets all three tools installed and ready.
In this step, get ready to:
- Create a project folder with a Python virtual environment and Flask installed.
- Download and install ngrok for public tunneling.
- Set up a free ngrok account and link it to your local agent.
Create the project directory and virtual environment
- Press the Super key to open the Activities overview.
- Type Visual Studio Code and press Enter to open it.
- Open the integrated terminal by pressing Ctrl+`.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir google-play-clone
cd google-play-clone
- Confirm the folder was created by running:
pwd
You should see /home/your-username/Desktop/google-play-clone in the output.
A virtual environment is an isolated Python workspace. It keeps this project's packages separate from your system Python so nothing conflicts.
- Create and activate a virtual environment by running these commands:
python3 -m venv venv
source venv/bin/activate
You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.
Don't see (venv) in your prompt?
Make sure you ran source venv/bin/activate (not just python3 -m venv venv). The first command creates the environment. The second activates it.
help me activate my Python virtual environment on Fedora
- Install Flask inside the virtual environment by running:
pip install Flask==3.1.3
What is Flask?
Flask is a lightweight Python web framework. It lets you define URL routes, handle form submissions, and serve HTML pages with just a few lines of code.
You are pinning it to version 3.1.3 so your project behaves exactly the same as this tutorial.
- Verify Flask is installed correctly by running:
python -c "import flask; print(flask.__version__)"
You should see 3.1.3 printed in the terminal.
Seeing a ModuleNotFoundError?
Check that (venv) still appears in your prompt. If it disappeared, run source venv/bin/activate again before retrying the install command.
help me fix a ModuleNotFoundError for Flask
Download and install ngrok
Ngrok creates a secure tunnel from the public internet to your local machine. Once it is running, anyone with the generated URL can reach your Flask app without you deploying it to a hosting provider.
- Download the ngrok binary and extract it to your PATH by running these commands:
wget https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-amd64.tgz
sudo tar xvzf ./ngrok-v3-stable-linux-amd64.tgz -C /usr/local/bin
What do these commands do?
The wget command downloads the ngrok compressed archive from the official source. The tar command extracts the binary directly into /usr/local/bin so you can run ngrok from anywhere in your terminal.
The sudo prefix is needed because /usr/local/bin is a system directory that requires elevated permissions.
- Your system will ask for your password after the sudo command. Type it and press Enter (the characters will not appear on screen).
- Verify ngrok is installed by running:
ngrok help
You should see ngrok's help text listing available commands and options.
Seeing command not found?
The tar extraction may have failed silently. Run ls /usr/local/bin/ngrok to check if the file exists. If not, re-run the download and extract commands.
Also check that you are still in the same terminal where you ran the commands. If you opened a new terminal tab, the shell may not have refreshed its PATH cache.
help me fix ngrok command not found on Fedora
Create your ngrok account and authenticate
Ngrok requires a free account to create tunnels. The account gives you an authtoken that links your local ngrok agent to your identity.
- Open https://dashboard.ngrok.com/signup in your browser.
- Sign up for a free account using your email or a social login.
- After signing in, you will land on the ngrok dashboard. Look for your Authtoken on the getting started page or under Your Authtoken in the left sidebar.
- Copy the authtoken value.
- Back in your VS Code terminal, configure ngrok with your authtoken by running:
ngrok config add-authtoken your-authtoken-here
- Replace your-authtoken-here with the authtoken you just copied from the dashboard.
You should see a message confirming the authtoken was saved to your configuration file.
Why do I need an authtoken?
The authtoken tells ngrok's servers that you are a registered user. Without it, the ngrok http command will refuse to create a tunnel. You only need to run this configuration once per machine.
Seeing an authentication error?
Make sure you copied the full authtoken string with no extra spaces. You can find it again on your ngrok dashboard under Your Authtoken.
help me fix ngrok authtoken configuration
Your development environment is locked and loaded. Next up, you will build the HTML and CSS that makes your page look like the real Google Play redeem page.
Build the Redeem Page
Your project environment is ready with Flask installed and ngrok authenticated. Now it is time to build the visual frontend that visitors will actually see.
Google Play's redeem code page uses a clean, centered card layout with minimal distractions. In this step, you will recreate that look using HTML for structure and CSS for styling.
In this step, get ready to:
- Create the Flask template and static file directories.
- Build the redeem page HTML with a form for code entry.
- Style the page with Material Design CSS to match Google's look.
Create the project file structure
Flask expects a specific folder layout. HTML templates go in a templates/ directory, and static assets like CSS go in a static/ directory.
- Create both directories inside your project folder by running these commands:
mkdir templates static
- Confirm the directories exist by running:
ls
You should see templates and static listed in the output alongside your virtual environment folder.
Why these folder names?
Flask automatically looks for HTML files inside a folder called templates/ and serves CSS, JavaScript, and images from a folder called static/.
Using these exact names means Flask finds your files without any extra configuration.
Now you will create the HTML template for the redeem page. This is the page visitors see when they first arrive.
- In VS Code, click File in the top menu bar.
- Click New File.
- Type templates/redeem.html as the file name and press Enter.
- Paste the following code into redeem.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Redeem your code</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="card">
<div class="logo">
<img src="https://www.gstatic.com/images/branding/googlelogo/2x/googlelogo_color_74x24dp.png" alt="Google" class="google-logo">
</div>
<h1>Redeem your code</h1>
<p class="subtitle">Enter your gift card or promotional code below</p>
<form action="/redeem" method="POST">
<div class="input-group">
<input type="text" name="code" id="code" placeholder="Enter code" required maxlength="30" autocomplete="off">
</div>
<div class="actions">
<button type="submit" class="btn-redeem">Redeem</button>
</div>
</form>
</div>
<div class="footer">
<p>Educational purposes only. Not affiliated with Google.</p>
</div>
</div>
</body>
</html>
What does this template do?
- The {{ url_for('static', filename='style.css') }} tag tells Flask where to find your CSS file.
- The Google Fonts link loads the Roboto font family, which Google uses across its web properties.
- The <form> sends a POST request to /redeem when the user clicks the button.
- The required attribute on the input prevents empty submissions.
- Save redeem.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Confirm the file exists by running this in your terminal:
ls templates/
You should see redeem.html listed in the output.
Don't see redeem.html in the output?
- Make sure you typed templates/redeem.html (with the folder prefix) when creating the file in VS Code.
- If you accidentally created it in the wrong location, drag it into the templates/ folder using the VS Code Explorer sidebar.
Still stuck? Help me find where my redeem.html file was saved.
✔️ Awesome, I've got everything!
Great. Make sure redeem.html is saved before moving on.
ⓧ I'd like to double check the full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Redeem your code</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="card">
<div class="logo">
<img src="https://www.gstatic.com/images/branding/googlelogo/2x/googlelogo_color_74x24dp.png" alt="Google" class="google-logo">
</div>
<h1>Redeem your code</h1>
<p class="subtitle">Enter your gift card or promotional code below</p>
<form action="/redeem" method="POST">
<div class="input-group">
<input type="text" name="code" id="code" placeholder="Enter code" required maxlength="30" autocomplete="off">
</div>
<div class="actions">
<button type="submit" class="btn-redeem">Redeem</button>
</div>
</form>
</div>
<div class="footer">
<p>Educational purposes only. Not affiliated with Google.</p>
</div>
</div>
</body>
</html>
Style the page with Material Design CSS
Google uses a design system called Material Design across its products. The key elements are a white card on a light gray background, clean typography, and a bold blue action button.
- In VS Code, click File then New File.
- Type static/style.css as the file name and press Enter.
- Paste the following CSS into style.css to set up the page layout and card structure:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Roboto', sans-serif;
background-color: #f8f9fa;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
width: 100%;
max-width: 450px;
padding: 20px;
}
.card {
background: #ffffff;
border-radius: 8px;
padding: 48px 40px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
text-align: center;
}
.logo {
margin-bottom: 24px;
}
.google-logo {
height: 24px;
}
h1 {
font-size: 24px;
font-weight: 400;
color: #202124;
margin-bottom: 8px;
}
.subtitle {
font-size: 14px;
color: #5f6368;
margin-bottom: 32px;
}
What does this code do?
- The * reset removes default browser margins and padding so you start from a clean slate.
- The body uses flexbox to center the card vertically and horizontally on the page.
- The .card class creates the white elevated card with a subtle shadow, matching Google's signature card style.
- The font-weight: 400 on h1 keeps the heading light and clean, just like Google's actual pages.
You won't see a visual change yet. The next chunk adds the form and button styles.
- Add the following CSS below your existing code in style.css to style the form input and buttons:
.input-group {
margin-bottom: 32px;
}
.input-group input {
width: 100%;
padding: 12px 16px;
font-size: 16px;
border: 1px solid #dadce0;
border-radius: 4px;
outline: none;
transition: border-color 0.2s;
}
.input-group input:focus {
border-color: #1a73e8;
border-width: 2px;
padding: 11px 15px;
}
.actions {
display: flex;
justify-content: flex-end;
}
.btn-redeem {
background-color: #1a73e8;
color: #ffffff;
border: none;
border-radius: 4px;
padding: 10px 24px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-redeem:hover {
background-color: #1557b0;
}
What do these styles do?
- The input:focus rule turns the border blue when the user clicks into the field, giving clear visual feedback.
- The padding shifts by 1px on focus to account for the thicker border, preventing the input from "jumping" in size.
- The .btn-redeem uses Google's signature blue (#1a73e8) and darkens on hover for interactivity.
You won't see a visual change yet. The next chunk adds the remaining styles.
- Add the final CSS below your existing code in style.css for the success page elements and footer:
.btn-done {
display: inline-block;
background-color: #1a73e8;
color: #ffffff;
border: none;
border-radius: 4px;
padding: 10px 24px;
font-size: 14px;
font-weight: 500;
text-decoration: none;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-done:hover {
background-color: #1557b0;
}
.success-icon {
margin-bottom: 16px;
}
.account-info {
background-color: #f8f9fa;
border-radius: 4px;
padding: 16px;
margin: 24px 0;
text-align: left;
}
.account-info p {
font-size: 14px;
color: #5f6368;
}
.account-info .code-display {
margin-bottom: 12px;
color: #202124;
}
.account-info .balance-label {
margin-bottom: 4px;
}
.account-info .email {
color: #202124;
font-weight: 500;
}
.footer {
text-align: center;
margin-top: 24px;
}
.footer p {
font-size: 12px;
color: #5f6368;
}
Why add styles for the success page now?
These styles target the confirmation page you will build in the next step. Adding them now keeps all CSS in one file, so both pages share the same design language without duplicating code.
- Save style.css by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Confirm the file exists by running:
ls static/
You should see style.css in the output. You will not see a visual result until you start the Flask server in the next step.
Don't see style.css in the output?
- Check that you typed static/style.css (with the folder prefix) when creating the file.
- Open the VS Code Explorer sidebar and verify the file is inside the static/ folder, not at the project root.
Need a hand? Help me find my style.css file and move it into the static folder.
✔️ Awesome, I've got everything!
Great. Make sure style.css is saved before moving on.
ⓧ I'd like to double check the full code
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Roboto', sans-serif;
background-color: #f8f9fa;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
width: 100%;
max-width: 450px;
padding: 20px;
}
.card {
background: #ffffff;
border-radius: 8px;
padding: 48px 40px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
text-align: center;
}
.logo {
margin-bottom: 24px;
}
.google-logo {
height: 24px;
}
h1 {
font-size: 24px;
font-weight: 400;
color: #202124;
margin-bottom: 8px;
}
.subtitle {
font-size: 14px;
color: #5f6368;
margin-bottom: 32px;
}
.input-group {
margin-bottom: 32px;
}
.input-group input {
width: 100%;
padding: 12px 16px;
font-size: 16px;
border: 1px solid #dadce0;
border-radius: 4px;
outline: none;
transition: border-color 0.2s;
}
.input-group input:focus {
border-color: #1a73e8;
border-width: 2px;
padding: 11px 15px;
}
.actions {
display: flex;
justify-content: flex-end;
}
.btn-redeem {
background-color: #1a73e8;
color: #ffffff;
border: none;
border-radius: 4px;
padding: 10px 24px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-redeem:hover {
background-color: #1557b0;
}
.btn-done {
display: inline-block;
background-color: #1a73e8;
color: #ffffff;
border: none;
border-radius: 4px;
padding: 10px 24px;
font-size: 14px;
font-weight: 500;
text-decoration: none;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-done:hover {
background-color: #1557b0;
}
.success-icon {
margin-bottom: 16px;
}
.account-info {
background-color: #f8f9fa;
border-radius: 4px;
padding: 16px;
margin: 24px 0;
text-align: left;
}
.account-info p {
font-size: 14px;
color: #5f6368;
}
.account-info .code-display {
margin-bottom: 12px;
color: #202124;
}
.account-info .balance-label {
margin-bottom: 4px;
}
.account-info .email {
color: #202124;
font-weight: 500;
}
.footer {
text-align: center;
margin-top: 24px;
}
.footer p {
font-size: 12px;
color: #5f6368;
}
Your redeem page frontend is ready. Next up, you will write the Flask server that actually serves this page and processes the code submissions.
Create the Flask Server
Your redeem page looks great, but it has no brain behind it. Right now, nobody can actually serve that HTML to a browser or process the code a visitor types in.
In this step, you will create a Flask backend that serves your redeem page and handles form submissions. When a visitor clicks the Redeem button, Flask will receive the code and return a styled success confirmation page.
In this step, get ready to:
- Create the Flask application with routes for serving the form and processing submissions.
- Build the success confirmation template that displays the redeemed code.
- Run the development server and test the full submit flow locally.
Create the Flask application file
Flask uses routes to decide what happens when a visitor requests a specific URL. You need two routes: one to show the redeem page, and one to handle the form submission.
- In VS Code, create a new file called app.py in your project root (the same level as your templates/ and static/ folders).
- Add the imports, app instance, and the GET route by pasting this code into app.py:
from flask import Flask, render_template, request
# Create the Flask application instance
app = Flask(__name__)
# Serve the redeem page when someone visits the root URL
@app.route("/")
def redeem():
return render_template("redeem.html")
What does this code do?
This sets up the foundation of your Flask app.
- from flask import Flask, render_template, request pulls in the three Flask tools you need: the app class, a function to render HTML templates, and an object to read form data.
- app = Flask(__name__) creates your application instance. Flask uses __name__ to locate your templates and static files.
- The @app.route("/") decorator tells Flask to call the redeem() function whenever someone visits the homepage.
You won't see a change yet. The next chunk adds the route that processes form submissions.
- Below the redeem() function in app.py, add the POST route and the main entry point:
# Handle the form submission when someone clicks Redeem
@app.route("/redeem", methods=["POST"])
def process_redeem():
code = request.form.get("code", "")
return render_template("success.html", code=code)
# Start the development server when running this file directly
if __name__ == "__main__":
app.run(debug=True, port=5000)
What does this code do?
This chunk handles what happens after a visitor submits the form.
- @app.route("/redeem", methods=["POST"]) only responds to POST requests. When the form submits to /redeem, Flask calls process_redeem().
- request.form.get("code", "") reads the value the visitor typed into the code input field. The "" provides an empty string as a fallback if nothing was submitted.
- The if __name__ == "__main__" block starts Flask's development server on port 5000 when you run the file directly with Python.
- Save app.py.
You won't see a change yet. You need the success template before you can test the full flow.
Seeing a red underline on the imports?
If VS Code highlights from flask import Flask with a warning, make sure your Python interpreter is set to the virtual environment where you installed Flask.
Help me fix the Flask import error in VS Code
✔️ Awesome, I've got everything!
Great. Double check you've saved app.py before moving on.
ⓧ I'd like to double check the full code
from flask import Flask, render_template, request
app = Flask(__name__)
@app.route("/")
def redeem():
return render_template("redeem.html")
@app.route("/redeem", methods=["POST"])
def process_redeem():
code = request.form.get("code", "")
return render_template("success.html", code=code)
if __name__ == "__main__":
app.run(debug=True, port=5000)
Create the success confirmation template
When a visitor submits a code, they should see a convincing confirmation page. This template displays a green checkmark, the code they entered, and a mock account email to complete the experience.
- Create a new file called success.html inside your templates/ folder.
- Paste the following code into templates/success.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Code Redeemed</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="card">
<div class="logo">
<img src="https://www.gstatic.com/images/branding/googlelogo/2x/googlelogo_color_74x24dp.png" alt="Google" class="google-logo">
</div>
<!-- Green checkmark icon for visual confirmation -->
<div class="success-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#34a853" width="64" height="64">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
</div>
<h1>Code redeemed successfully</h1>
<p class="subtitle">Your code has been applied to your account.</p>
<!-- Display the submitted code and mock account details -->
<div class="account-info">
<p class="code-display">Code: <strong>{{ code }}</strong></p>
<p class="balance-label">Balance added to:</p>
<p class="email">user@gmail.com</p>
</div>
<div class="actions">
<a href="/" class="btn-done">Done</a>
</div>
</div>
<div class="footer">
<p>Educational purposes only. Not affiliated with Google.</p>
</div>
</div>
</body>
</html>
What does this template do?
This page renders after Flask processes the form submission.
- The {{ code }} placeholder is a Jinja2 template variable. Flask replaces it with the actual code the visitor typed.
- The green checkmark SVG and "Code redeemed successfully" heading give instant visual confirmation.
- The btn-done link points back to / so visitors can redeem another code.
- Save templates/success.html.
Both templates are ready. In the next substep, you will start the server and test the full flow.
Seeing a typo warning in the HTML?
Make sure the {{ code }} placeholder uses double curly braces on each side. A single brace will not render the variable.
Why is my template showing literal curly braces instead of the code value?
✔️ Awesome, I've got everything!
Great. Double check you've saved templates/success.html before moving on.
ⓧ I'd like to double check the full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Code Redeemed</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="card">
<div class="logo">
<img src="https://www.gstatic.com/images/branding/googlelogo/2x/googlelogo_color_74x24dp.png" alt="Google" class="google-logo">
</div>
<div class="success-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#34a853" width="64" height="64">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
</div>
<h1>Code redeemed successfully</h1>
<p class="subtitle">Your code has been applied to your account.</p>
<div class="account-info">
<p class="code-display">Code: <strong>{{ code }}</strong></p>
<p class="balance-label">Balance added to:</p>
<p class="email">user@gmail.com</p>
</div>
<div class="actions">
<a href="/" class="btn-done">Done</a>
</div>
</div>
<div class="footer">
<p>Educational purposes only. Not affiliated with Google.</p>
</div>
</div>
</body>
</html>
Run and test locally
Your Flask app and both templates are in place. Time to start the server and see your redeem page come alive.
- Start the Flask development server by running this command in your terminal (make sure your virtual environment is activated):
python app.py
You should see output indicating Flask is running on http://127.0.0.1:5000 with debug mode on.
Command not found or module error?
Make sure your virtual environment is activated. You should see the environment name in your terminal prompt. If not, activate it again with source venv/bin/activate and retry.
Help me fix the ModuleNotFoundError for Flask
- Open your browser and navigate to http://127.0.0.1:5000.
- You should see your Google Play redeem page with the code input field and Redeem button.
- Type any text into the code field (for example, ABC123DEF456) and click Redeem.
- Verify that the success page appears with a green checkmark, your submitted code displayed back, and the mock email user@gmail.com.
- Click Done to confirm it takes you back to the redeem page.
What should I see?
After clicking Redeem, the browser navigates to a new page showing a green circle checkmark, the heading "Code redeemed successfully", and the exact code you typed displayed in a gray info box. The email user@gmail.com appears below it.
Seeing a 404 or blank page?
Check that templates/success.html is inside a folder named exactly templates (not template without the 's'). Flask looks for templates in that specific folder name by default.
Also confirm that app.py is in the same directory as the templates/ folder, not inside it.
Help me debug my Flask 404 error
Your Flask server is handling the full redeem flow locally. Next up, you will expose this server to the public internet so anyone can visit your page.
Go Public with ngrok
Your Flask app works perfectly on your own machine. But right now, nobody else on the internet can see it.
In this step, you'll use ngrok to create a public HTTPS tunnel that exposes your local server to the internet. Anyone with the link will be able to visit your redeem page from anywhere in the world.
In this step, get ready to:
- Start an ngrok tunnel pointing to your Flask server.
- Test the public URL from another device or browser.
Start ngrok tunnel
Your Flask server is already listening on port 5000 locally. ngrok creates a secure tunnel from a public URL to that local port, forwarding all incoming traffic straight to your app.
- Make sure your Flask development server is still running in its terminal. If you stopped it, restart it by running:
python app.py
- Open a second terminal. Press Ctrl+Shift+` in VS Code to open a new terminal panel alongside your running server.
- Start the ngrok tunnel by running this command:
ngrok http 5000
ngrok takes over the terminal and displays a status dashboard. Look for the Forwarding line. It shows a public URL like https://your-assigned-name.ngrok-free.dev.
- Copy the full https:// URL shown next to Forwarding. You'll use this to access your app from anywhere.
What is this URL?
ngrok assigns you a permanent dev domain on the free tier (format: your-assigned-name.ngrok-free.dev). This domain stays the same across sessions, so you don't get a random URL each time.
All traffic to that URL flows through ngrok's servers and arrives at your local Flask app on port 5000. Your computer is acting as the web server.
Seeing an error when starting ngrok?
- If you see an authentication error, make sure you ran the authtoken command from Step 1. Your terminal needs to be authenticated before ngrok can open a tunnel.
- If port 5000 is reported as not available, confirm your Flask server is still running in the first terminal.
ngrok won't start a tunnel
Test from another device
The real test of a public URL is accessing it from outside your own machine. This confirms that the tunnel is working and your app is truly available on the internet.
- Open the ngrok URL in a different browser, an incognito window, or on your phone.
What is the ngrok interstitial page?
On the free tier, first-time visitors see a brief ngrok warning page before reaching your app. This is a security measure to prevent phishing attacks.
Click the Visit Site button to proceed through to your redeem page. This only appears once per browser session.
- Click through the ngrok interstitial page if it appears.
- Enter any test code (e.g. ABCD1234EFGH5678) in the code input field.
- Click Redeem.
You should see the green checkmark success page with your code displayed. The full flow works over the public internet.
Share with a friend
Send the ngrok URL to someone else and ask them to try submitting a code. You'll see their request hit your Flask server in real time in the first terminal.
Page not loading from another device?
- Make sure you copied the full URL including https:// at the beginning.
- Confirm both terminals are still running. If the Flask server or ngrok stopped, the URL won't respond.
- Check that your computer hasn't gone to sleep. The tunnel only works while your machine is active.
Help me get my ngrok URL working
Secret mission
Add Code Validation and Loading Animation
Your redeem page accepts any text right now, even a single letter. Real Google Play codes follow a specific format. Can you add client-side validation that rejects bad codes before they hit the server, plus a loading spinner that makes the submit feel polished?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally with ngrok's free tier, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Flask development server (running on port 5000).
- ngrok tunnel (running with a public HTTPS URL).
- Project files (app.py, templates/, static/).
- Python virtual environment (inside your project folder).
- ngrok binary (installed to /usr/local/bin).
Keep everything running
No action needed. Choose this if you are still actively building or want to keep experimenting with the page.
- Your Flask server and ngrok tunnel will keep running until you close the terminal windows or press Ctrl+C in each.
- Your project files stay on your Desktop and are ready to edit whenever you return.
- Your ngrok free account remains active. You can reuse it for future projects.
Pause - I'll come back to this later
Shut down the running processes to free up your terminal. Your project files stay on disk so you can restart everything later.
- Switch to the terminal running your Flask server and press Ctrl+C to stop it.
- Switch to the terminal running ngrok and press Ctrl+C to stop the tunnel.
- Your public ngrok URL will stop working immediately. When you come back, re-run the Flask server and ngrok to get a new tunnel.
Delete - I don't want to use this again
Remove all project resources and start fresh. This deletes everything you built in this project.
- Stop both running processes by pressing Ctrl+C in the Flask terminal and the ngrok terminal.
- Remove your project folder (the folder on your Desktop containing app.py) by running:
cd ~/Desktop
rm -rf google-play-redeem
What if my folder has a different name?
Replace google-play-redeem with whatever you named your project folder in Step 1. You can run ls ~/Desktop to find it.
- Remove the ngrok binary by running:
sudo rm /usr/local/bin/ngrok
- Confirm both are gone by running:
ls ~/Desktop
ngrok help
You should no longer see your project folder listed, and the ngrok help command should return "command not found."
That's a Wrap!
That's a Wrap!
Nice work! You just built a full-stack web application that clones Google Play's redeem code page, wired up form handling with Flask, and made it accessible to anyone on the internet.
You've learned how to:
- Built an HTML/CSS clone of the Google Play redeem code page matching Material Design styling.
- Created a Flask backend with routes for serving the form and processing submissions, rendering dynamic success confirmations using Flask templates.
- Exposed your local server to the public internet using ngrok's free tier.
- Secret Mission: Added client-side code validation with error states and a loading animation.
Ready to quiz yourself?