Web3 Loyalty Rewards for Local Shops — No ETH Required
Scan2Earn is a QR-based loyalty points platform for local businesses. Shopkeepers generate a QR code for their store; customers scan it to earn points and redeem rewards. The app is built as a pure-HTML frontend with Firebase for auth and data storage — no bundler, no Node server, no wallet required.
- Overview
- Problem & Solution
- Core User Flow
- Pages & Features
- Tech Stack
- Project Structure
- Firebase Setup
- Environment Variables
- Running Locally
- Demo Flow
- Security Notes
- Future Scope
- Troubleshooting
- License
| Role | What they do |
|---|---|
| Shopkeeper | Sign in → set store name & rewards → share QR code → track customer points |
| Customer | Sign in → scan QR → earn points → redeem rewards → view history |
Points live in Firebase Firestore. The gasless / blockchain layer is a planned upgrade; the current build is a fully functional Web2 demo with Web3 aesthetics and UX.
Traditional loyalty programs:
- Points are siloed per brand and expire arbitrarily
- Separate app per store
- Users lose rewards if a shop closes
Web3 loyalty usually fails because:
- Users need ETH and a wallet
- Gas fees confuse non-crypto users
Scan2Earn's approach:
- One account works across all participating stores
- No crypto knowledge needed — sign in with Google
- Points tracked on Firebase today, migratable to Base Sepolia later
- Gasless architecture planned via UGF (Universal Gasless Framework)
Landing page (index.html)
↓
Choose role → Login (login.html)
↓
Google OAuth via Firebase Auth
↓
┌─────────────────┬──────────────────┐
│ Shopkeeper │ Customer │
│ (shopkeeper │ (customer.html) │
│ .html) │ │
│ │ │
│ • Set store │ • View points │
│ • Define │ • Scan QR │
│ rewards │ (scan.html) │
│ • View │ • Redeem │
│ customers │ rewards │
│ • Show QR code │ • View history │
└─────────────────┴──────────────────┘
- Hero section with product pitch
- Role selector (Shopkeeper / Customer) stored in
localStorage - Dark mode toggle (persisted in
localStorage) - Animated points card demo
- Role toggle: Shopkeeper or Customer
- Google OAuth via Firebase Auth (compat SDK v9)
- New shopkeeper → modal prompts for store name
- Returning user → role data merged into existing Firestore doc
- Redirects to
shopkeeper.htmlorcustomer.htmlbased on role - Toast notifications for errors and success
- View store name, QR code, and reward rules
- Set points per purchase and reward thresholds
- View list of customers and their point balances
- Analytics summary
- Display total points balance
- Show available rewards and redemption thresholds
- Transaction history
- Link to scan page
- Simulated QR scan (paste URL or use demo button)
- Awards points from "Green Bean Coffee" demo store
- Updates
localStoragebalance and history - Redirects to wallet on success
| Layer | Technology |
|---|---|
| Frontend | Vanilla HTML, Tailwind CSS (CDN), Font Awesome |
| Fonts | Inter via Google Fonts |
| Auth | Firebase Authentication (Google OAuth) |
| Database | Firebase Firestore |
| Firebase SDK | Firebase compat v9 (CDN, no bundler) |
| Config module | firebase-config.js (ES module, Firebase v10 CDN) |
| State | localStorage for session/UI preferences |
| Package manager | npm (only firebase package listed; unused in prod) |
No bundler. All pages load Firebase directly from
gstatic.comCDN. Thepackage.jsonlistsfirebase ^10.14.1but the HTML files use the compat CDN builds — no build step is required.
scan2earn/
├── index.html # Landing / marketing page
├── login.html # Auth page (Google sign-in, role selection)
├── shopkeeper.html # Shopkeeper dashboard
├── customer.html # Customer wallet
├── scan.html # QR scan simulator
├── firebase-config.js # Firebase init (ES module, v10 CDN)
├── package.json # Lists firebase dependency
├── package-lock.json
├── .env # Firebase credentials (never commit)
├── .gitignore
└── README.md
- Go to https://console.firebase.google.com
- Create a project (e.g.
scan2earn-771b6) - Add a Web App and copy the config object
- Enable Authentication → Sign-in method → Google
- Enable Firestore Database → start in test mode
users/
{uid}/
uid: string
email: string
name: string
type: "shopkeeper" | "customer"
walletAddress: string # randomly generated mock address
createdAt: ISO string
roles:
shopkeeper:
storeName: string
pointsPerPurchase: number
rewards: [{ name, points }]
totalPoints: number
customers: []
customer:
totalPoints: number
history: [{ action, date, points }]
Copy .env and fill in your Firebase project values. These are used for reference — the actual credentials are hardcoded in firebase-config.js for the CDN module pattern. Replace with your own project values before deploying.
FIREBASE_API_KEY=your_api_key
FIREBASE_AUTH_DOMAIN=your_project.firebaseapp.com
FIREBASE_PROJECT_ID=your_project_id
FIREBASE_STORAGE_BUCKET=your_project.firebasestorage.app
FIREBASE_MESSAGING_SENDER_ID=your_sender_id
FIREBASE_APP_ID=your_app_id
⚠️ Never commit real credentials. Add.envandfirebase-config.js(if it contains keys) to.gitignore.
No build step needed. Just serve the files with any static server:
# Option 1 — VS Code Live Server extension
# Right-click index.html → Open with Live Server
# Option 2 — Python
python3 -m http.server 8080
# Option 3 — npx serve
npx serve .Then open http://localhost:8080 in your browser.
firebase-config.jsuses ES moduleimportsyntax, so the page must be served over HTTP (not opened asfile://).
- Open
index.html - Click "I'm a shopkeeper" → lands on
login.htmlwith Shopkeeper pre-selected - Click Continue with Google → enter store name in the modal
- Redirected to
shopkeeper.html— see store QR and reward rules - Open a new tab → go to
login.html, select Customer, sign in - Redirected to
customer.html— wallet shows 0 points - Click Scan QR →
scan.htmlopens - Click "Scan Demo Store" → earns 10 points from Green Bean Coffee
- Click Go to Wallet → points balance updated
- Do not commit
.envor expose API keys in public repos - Current Firestore rules are in test mode — lock them down before any public launch:
// Firestore rules — recommended minimum
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
}
}- The QR scan page currently has no server-side rate limiting — add abuse prevention before production use
- Mock wallet addresses are randomly generated strings, not real blockchain addresses
- Real QR scanning via camera (e.g.
html5-qrcodelibrary) - Base Sepolia smart contracts — migrate points to
LoyaltyToken.sol(ERC-20) - Gasless transactions via UGF (Universal Gasless Framework) so users never need ETH
- NFT loyalty badges for milestone rewards
- Multi-store wallet — one customer account, points across shops
- Analytics dashboard for shopkeepers
- WhatsApp / SMS notifications on point milestones
- GPS verification to prevent fake scans
| Problem | Fix |
|---|---|
| Blank page / module error | Serve via HTTP, not file:// |
| Google sign-in popup blocked | Allow popups for localhost in browser settings |
auth/unauthorized-domain error |
Add your domain to Firebase Auth → Authorized domains |
| Firestore permission denied | Check Firestore rules; ensure user is authenticated |
| Points not saving | Check browser console; Firestore write may have failed |
| Dark mode not persisting | localStorage must be enabled in browser |
MIT License — free to use, modify, and distribute.
Scan2Earn demonstrates how a Web3-ready loyalty product can be shipped with zero crypto complexity for end users — scan, earn, redeem.