Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Create a Loyalty Card System in FlutterFlow

Build a digital loyalty punch card with a loyalty_cards collection tracking currentStamps out of maxStamps (typically 10), a stamp_events log, and a reward trigger. The punch card UI uses a GridView of 10 circles that fill with checkmarks as stamps accumulate. Staff stamp customers by scanning a QR code displayed on the customer's app, which calls a Cloud Function to validate and increment the stamp count. When maxStamps is reached, the reward unlocks and the card resets.

What you'll learn

  • How to model loyalty cards with stamp counts and reward thresholds in Firestore
  • How to build a visual punch card grid with filled and empty stamp circles
  • How to implement staff-verified stamping via QR code scanning
  • How to trigger reward redemption and card reset when the stamp goal is reached
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner8 min read20-25 minFlutterFlow Free+March 2026RapidDev Engineering Team
TL;DR

Build a digital loyalty punch card with a loyalty_cards collection tracking currentStamps out of maxStamps (typically 10), a stamp_events log, and a reward trigger. The punch card UI uses a GridView of 10 circles that fill with checkmarks as stamps accumulate. Staff stamp customers by scanning a QR code displayed on the customer's app, which calls a Cloud Function to validate and increment the stamp count. When maxStamps is reached, the reward unlocks and the card resets.

Building a Digital Loyalty Punch Card System in FlutterFlow

Digital loyalty cards replace physical punch cards with a more reliable and engaging experience. This tutorial builds the full system: a visual punch card UI, staff-side QR verification for stamping, automatic reward triggering when the card is full, and card reset for repeat loyalty. It is ideal for retail, food service, or any business that rewards repeat customers.

Prerequisites

  • A FlutterFlow project with Firebase Authentication enabled
  • Firestore database set up in your Firebase project
  • Basic understanding of FlutterFlow GridView, Conditional Styling, and Custom Widgets
  • QR code generation and scanning packages (qr_flutter and mobile_scanner)

Step-by-step guide

1

Create the loyalty cards and stamp events Firestore schema

Create a loyalty_cards collection with fields: userId (String), storeId (String), currentStamps (Integer, default 0), maxStamps (Integer, default 10), rewardDescription (String, e.g., 'Free coffee'), isRedeemable (Boolean, default false), and createdAt (Timestamp). Create a stamp_events collection with fields: cardId (String), userId (String), stampedAt (Timestamp), and stampedBy (String, the staff userId). The stamp_events log provides an audit trail of every stamp for fraud detection.

Expected result: Firestore has loyalty_cards and stamp_events collections with the required fields. Each user can have one loyalty card per store.

2

Build the visual punch card UI with a GridView of stamp circles

Create a LoyaltyCard page. Add a Backend Query for the user's loyalty card document (query loyalty_cards where userId == currentUser AND storeId == selectedStore). Display a Container styled as a card with the store name and reward description at the top. Add a GridView with crossAxisCount: 5 and childCount: 10 (from maxStamps). Each cell is a Container with a Circle shape. Use Conditional Styling: if the cell index is less than currentStamps, fill the circle with a green background and a checkmark Icon. Otherwise, show an outlined empty circle. Below the grid, add a Text showing 'X of 10 stamps' with the current count.

Expected result: The punch card displays 10 circles in a 5x2 grid. Stamped positions show green checkmarks, and remaining positions show empty outlines.

3

Display a QR code on the customer's app for staff scanning

Create a Custom Widget using the qr_flutter package. Add qr_flutter: ^4.1.0 to Pubspec Dependencies. The widget takes a data String parameter containing the customer's userId and cardId encoded as a JSON string. Render QrImageView(data: widget.data, size: 200). On the LoyaltyCard page, add this Custom Widget below the stamp grid with data set to a JSON string like '{"userId":"uid","cardId":"cardId"}'. Add a Text label: 'Show this to staff to collect your stamp'. The QR code is the verification mechanism that prevents customers from stamping their own cards.

Expected result: The customer's loyalty card page shows a QR code containing their userId and cardId that staff can scan to add a stamp.

4

Build the staff stamping page with QR scanner and Cloud Function

Create a StaffStamp page accessible only to users with a staff role. Add a Custom Widget using mobile_scanner that scans QR codes. When a QR code is detected, parse the JSON to extract userId and cardId. Call a Cloud Function called addStamp that receives cardId and staffUserId. The Cloud Function validates that the card exists, currentStamps < maxStamps, and no stamp was added in the last 5 minutes (cooldown to prevent accidental double-stamps). If valid, it increments currentStamps, creates a stamp_event document, and if currentStamps now equals maxStamps, sets isRedeemable to true.

addStamp.js
1// Cloud Function: addStamp
2const functions = require('firebase-functions');
3const admin = require('firebase-admin');
4admin.initializeApp();
5
6exports.addStamp = functions.https.onCall(async (data, context) => {
7 const { cardId } = data;
8 const staffId = context.auth.uid;
9 const db = admin.firestore();
10 const cardRef = db.collection('loyalty_cards').doc(cardId);
11
12 return db.runTransaction(async (tx) => {
13 const card = await tx.get(cardRef);
14 if (!card.exists) throw new functions.https.HttpsError('not-found', 'Card not found');
15 const cardData = card.data();
16 if (cardData.currentStamps >= cardData.maxStamps) {
17 throw new functions.https.HttpsError('failed-precondition', 'Card is full');
18 }
19 const newStamps = cardData.currentStamps + 1;
20 const isRedeemable = newStamps >= cardData.maxStamps;
21 tx.update(cardRef, { currentStamps: newStamps, isRedeemable });
22 tx.create(db.collection('stamp_events').doc(), {
23 cardId, userId: cardData.userId, stampedAt: admin.firestore.FieldValue.serverTimestamp(), stampedBy: staffId,
24 });
25 return { newStamps, isRedeemable };
26 });
27});

Expected result: Staff scan the customer's QR code, and the Cloud Function securely adds a stamp, preventing fraud, double-stamps, and over-stamping.

5

Implement reward redemption and card reset

On the LoyaltyCard page, add Conditional Visibility on a 'Redeem Reward' Button that shows only when isRedeemable is true. Style the full punch card with a celebratory gold border when redeemable. On the Redeem button tap, show a confirmation dialog explaining the reward. On confirm, call a Cloud Function called redeemReward that sets isRedeemable to false, resets currentStamps to 0, and creates a new blank loyalty card or resets the existing one. Log the redemption in a reward_redemptions collection for tracking. Show a success SnackBar confirming the reward was applied.

Expected result: When all stamps are collected, the Redeem Reward button appears. Tapping it resets the card and logs the redemption. The customer starts a fresh punch card.

Complete working example

addStamp.js
1// Cloud Function: Loyalty Card Stamp + Redeem System
2const functions = require('firebase-functions');
3const admin = require('firebase-admin');
4admin.initializeApp();
5
6// Add a stamp to a loyalty card
7exports.addStamp = functions.https.onCall(async (data, context) => {
8 if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');
9 const { cardId } = data;
10 const staffId = context.auth.uid;
11 const db = admin.firestore();
12 const cardRef = db.collection('loyalty_cards').doc(cardId);
13
14 return db.runTransaction(async (tx) => {
15 const card = await tx.get(cardRef);
16 if (!card.exists) throw new functions.https.HttpsError('not-found', 'Card not found');
17
18 const d = card.data();
19 if (d.currentStamps >= d.maxStamps) {
20 throw new functions.https.HttpsError('failed-precondition', 'Card already full');
21 }
22
23 // Cooldown: prevent double-stamp within 5 minutes
24 const recentStamps = await db.collection('stamp_events')
25 .where('cardId', '==', cardId)
26 .orderBy('stampedAt', 'desc').limit(1).get();
27 if (!recentStamps.empty) {
28 const lastStamp = recentStamps.docs[0].data().stampedAt.toMillis();
29 if (Date.now() - lastStamp < 5 * 60 * 1000) {
30 throw new functions.https.HttpsError('failed-precondition', 'Please wait 5 minutes');
31 }
32 }
33
34 const newStamps = d.currentStamps + 1;
35 const isRedeemable = newStamps >= d.maxStamps;
36 tx.update(cardRef, { currentStamps: newStamps, isRedeemable });
37 tx.create(db.collection('stamp_events').doc(), {
38 cardId, userId: d.userId,
39 stampedAt: admin.firestore.FieldValue.serverTimestamp(),
40 stampedBy: staffId,
41 });
42 return { newStamps, isRedeemable };
43 });
44});
45
46// Redeem the reward and reset card
47exports.redeemReward = functions.https.onCall(async (data, context) => {
48 if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');
49 const { cardId } = data;
50 const db = admin.firestore();
51 const cardRef = db.collection('loyalty_cards').doc(cardId);
52
53 return db.runTransaction(async (tx) => {
54 const card = await tx.get(cardRef);
55 if (!card.exists) throw new functions.https.HttpsError('not-found', 'Card not found');
56 if (!card.data().isRedeemable) {
57 throw new functions.https.HttpsError('failed-precondition', 'Not redeemable');
58 }
59
60 tx.update(cardRef, { currentStamps: 0, isRedeemable: false });
61 tx.create(db.collection('reward_redemptions').doc(), {
62 cardId, userId: card.data().userId,
63 reward: card.data().rewardDescription,
64 redeemedAt: admin.firestore.FieldValue.serverTimestamp(),
65 });
66 return { success: true };
67 });
68});

Common mistakes when creating a Loyalty Card System in FlutterFlow

Why it's a problem: Letting customers stamp their own card without staff verification

How to avoid: Require staff to initiate the stamp either by scanning the customer's QR code or entering a staff PIN. The Cloud Function should validate the staff role.

Why it's a problem: Not adding a cooldown between stamps

How to avoid: Add a 5-minute cooldown in the Cloud Function that checks the timestamp of the most recent stamp_event for the card before allowing a new stamp.

Why it's a problem: Resetting the card on the client side instead of through a Cloud Function

How to avoid: Handle redemption and reset exclusively in a Cloud Function that atomically marks the card as redeemed and resets stamps.

Best practices

  • Use a GridView with conditional styling for the punch card visual rather than hardcoded images
  • Store the stamp count on the card document for quick reads, and keep stamp_events as an audit trail
  • Display the reward description prominently on the card so customers know what they are working toward
  • Add a celebratory animation (confetti or gold glow) when the card becomes redeemable
  • Allow multiple loyalty cards per user for different stores or campaigns
  • Log all stamp events with the staff userId for accountability and fraud investigation
  • Set Firestore Security Rules so only staff-role users can call the addStamp function

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I need to build a digital loyalty punch card system in FlutterFlow. Show me the Firestore schema for loyalty cards and stamp events, the visual GridView punch card UI with conditional stamp circles, and the Cloud Function for staff-verified stamping with cooldown and reward triggering.

FlutterFlow Prompt

Create a loyalty card page with a GridView of 10 circles that fill with checkmarks based on a currentStamps count from Firestore. Add a QR code display for staff scanning and a Redeem Reward button that appears when all stamps are collected.

Frequently asked questions

Can I customize the number of stamps required per card?

Yes. The maxStamps field is configurable per card. Different stores or campaigns can require 5, 10, or 15 stamps for a reward. The GridView dynamically adjusts its childCount to match.

How do I prevent staff from stamping cards without a real purchase?

Integrate the stamp action with your POS or order system. Only allow stamping when linked to a completed transaction, or require a manager approval for manual stamps.

Can customers have multiple loyalty cards for different stores?

Yes. Query loyalty_cards where userId matches the current user to show all their cards. Each card has a storeId field linking it to a specific business.

How do I handle expired loyalty cards?

Add an expiresAt Timestamp field to loyalty_cards. Run a scheduled Cloud Function that sets expired cards to inactive. In the UI, show expired cards greyed out with a message.

Can I offer different rewards for different stamp milestones?

Yes. Create a rewards array on the card with milestone thresholds (e.g., 5 stamps = 10% off, 10 stamps = free item). Check against the array on each stamp to unlock the appropriate reward.

Can RapidDev help build a branded loyalty program?

Yes. RapidDev can implement a full loyalty platform with custom branding, multi-store support, tiered rewards, analytics dashboards for redemption rates, and integration with POS systems.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.