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
Create the loyalty cards and stamp events Firestore schema
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.
Build the visual punch card UI with a GridView of stamp circles
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.
Display a QR code on the customer's app for staff scanning
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.
Build the staff stamping page with QR scanner and Cloud Function
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.
1// Cloud Function: addStamp2const functions = require('firebase-functions');3const admin = require('firebase-admin');4admin.initializeApp();56exports.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);1112 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.
Implement reward redemption and card reset
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
1// Cloud Function: Loyalty Card Stamp + Redeem System2const functions = require('firebase-functions');3const admin = require('firebase-admin');4admin.initializeApp();56// Add a stamp to a loyalty card7exports.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);1314 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');1718 const d = card.data();19 if (d.currentStamps >= d.maxStamps) {20 throw new functions.https.HttpsError('failed-precondition', 'Card already full');21 }2223 // Cooldown: prevent double-stamp within 5 minutes24 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 }3334 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});4546// Redeem the reward and reset card47exports.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);5253 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 }5960 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation