Skip to content

feat: Add loyalty cards feature#945

Open
Edigorin wants to merge 6 commits into
TomBursch:mainfrom
Edigorin:feat/loyalty-cards
Open

feat: Add loyalty cards feature#945
Edigorin wants to merge 6 commits into
TomBursch:mainfrom
Edigorin:feat/loyalty-cards

Conversation

@Edigorin

@Edigorin Edigorin commented Dec 29, 2025

Copy link
Copy Markdown

Summary

Add support for loyalty cards/membership cards that users can store and quickly access from the shopping list page via a wallet FAB.

Closes #462

Changes

Backend

  • Add LoyaltyCard model with name, barcode data/type, description, color
  • Add CRUD API endpoints for loyalty cards (/api/household/{id}/loyalty-card)
  • Add database migration for loyalty_cards table
  • Add household feature flag for loyalty cards
  • Add loyalty card import functionality
  • Add comprehensive API tests

Frontend

  • Add loyalty cards list page
  • Add loyalty card detail page with fullscreen barcode display
  • Add loyalty card add/edit page with barcode scanning
  • Add barcode scanning from camera (mobile) and webcam (web)
  • Add barcode scanning from gallery/image upload
  • Add wallet FAB on shopping list page to access loyalty cards
  • Support multiple barcode types: CODE128, QR, EAN13, EAN8, UPC-A, UPC-E, CODE39, ITF, Codabar, DataMatrix, Aztec, PDF417
  • Add localization strings for loyalty cards feature
  • Add loyalty cards option to import settings

Testing

  • Added backend/tests/api/test_api_loyalty_card.py with comprehensive API tests
  • Manually tested on Android, and Web platforms. No ability to test on IOS
chrome_99653v6vYu chrome_hfh8m63lnl chrome_knKkUHGaEP chrome_nYkMRzRzgm chrome_Zl9xBpXxYH

@TomBursch TomBursch added the enhancement New feature or request label Jan 23, 2026
@josephclaytonhansen

Copy link
Copy Markdown

Love this, hope to see it in!

@TomBursch TomBursch left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, thanks for the PR. Because this is such a huge change, I didn't have time until now to review it. The code looks really good, and I'm happy to merge it.

I just have some minor questions:

Why does the loyalty model store a photo but it's never set to anything and not used? I also can't upload a loyalty card without a barcode.

And why do you implement the barcode scanning on the web yourself? Why not use the web functionality of the mobile scanner? https://pub.dev/packages/mobile_scanner#web

Copilot AI review requested due to automatic review settings February 10, 2026 14:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a full “loyalty cards” feature across backend + Flutter client, including storage, CRUD APIs, import support, and UI flows for listing, viewing, creating/editing, and scanning barcodes from camera/gallery.

Changes:

  • Backend: introduce LoyaltyCard model, migration, CRUD endpoints, import integration, and API tests.
  • Frontend: add loyalty card pages/widgets, routing, FAB entrypoint from shopping list, and barcode rendering/scanning dependencies.
  • Localization + platform permissions updates to support barcode scanning UX on Android/iOS.

Reviewed changes

Copilot reviewed 40 out of 41 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
kitchenowl/pubspec.yaml Adds barcode rendering/scanning dependencies.
kitchenowl/lib/widgets/shopping_list_fab.dart Replaces shopping-list FAB behavior and adds wallet FAB entrypoint.
kitchenowl/lib/router.dart Adds routes for loyalty card list + detail pages.
kitchenowl/lib/pages/loyalty_card_list_page.dart Implements loyalty cards list UI and navigation into details.
kitchenowl/lib/pages/loyalty_card_page.dart Implements loyalty card detail view with fullscreen barcode display and actions.
kitchenowl/lib/pages/loyalty_card_add_update_page.dart Implements add/edit flow including scanning from camera/gallery.
kitchenowl/lib/pages/barcode_scanner_page.dart New camera barcode scanner page using mobile_scanner.
kitchenowl/lib/models/loyalty_card.dart Frontend model for loyalty cards + JSON serialization.
kitchenowl/lib/models/household.dart Adds household feature flag field for loyalty cards.
kitchenowl/lib/l10n/app_en.arb Adds localization strings for loyalty card/scanning UI.
backend/app/models/loyalty_card.py New backend DB model and household relationship.
backend/app/controller/loyalty_card/loyalty_card_controller.py New loyalty card CRUD endpoints.
backend/migrations/versions/a1b2c3d4e5f6_loyalty_cards.py Adds loyalty_card table and loyalty_cards_feature column.
backend/app/service/importServices/import_loyalty_card.py Adds loyalty card import helper.
backend/tests/api/test_api_loyalty_card.py Adds API test coverage for loyalty cards endpoints.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread kitchenowl/lib/l10n/app_en.arb Outdated
Comment on lines +842 to +899
"you": "you",
"loyaltyCards": "Cards",
"@loyaltyCards": {},
"loyaltyCardsEmpty": "No loyalty cards yet. Add one to get started!",
"@loyaltyCardsEmpty": {},
"loyaltyCardAdd": "Add card",
"@loyaltyCardAdd": {},
"loyaltyCardEdit": "Edit card",
"@loyaltyCardEdit": {},
"loyaltyCardNameHint": "e.g., Target, Costco",
"@loyaltyCardNameHint": {},
"loyaltyCardDeleteConfirmation": "Are you sure you want to delete {name}?",
"@loyaltyCardDeleteConfirmation": {
"placeholders": {
"name": {
"example": "Target",
"type": "String"
}
}
},
"barcodeData": "Barcode data",
"@barcodeData": {},
"barcodeDataHint": "Enter barcode number",
"@barcodeDataHint": {},
"barcodeType": "Barcode type",
"@barcodeType": {},
"copyBarcode": "Copy barcode",
"@copyBarcode": {},
"scanBarcode": "Scan barcode",
"@scanBarcode": {},
"scanFromCamera": "Scan with camera",
"@scanFromCamera": {},
"scanFromGallery": "Import from image",
"@scanFromGallery": {},
"pointCameraAtBarcode": "Point camera at barcode",
"@pointCameraAtBarcode": {},
"barcodeDetected": "Barcode detected",
"@barcodeDetected": {},
"autoDetected": "auto-detected",
"@autoDetected": {},
"enterManually": "Enter manually",
"@enterManually": {},
"flashOn": "Flash on",
"@flashOn": {},
"flashOff": "Flash off",
"@flashOff": {},
"switchCamera": "Switch camera",
"@switchCamera": {},
"noBarcodesFound": "No barcodes found in image",
"@noBarcodesFound": {},
"webcam": "Webcam",
"@webcam": {},
"camera": "Camera",
"@camera": {},
"gallery": "Gallery",
"@gallery": {},
"keepScreenOn": "Screen stays on for scanning",
"@keepScreenOn": {}

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

app_en.arb currently defines several localization keys twice (e.g., loyaltyCards, loyaltyCardsEmpty, loyaltyCardAdd, barcodeData, camera, gallery, etc.). ARB/JSON with duplicate keys is invalid and whichever key appears last will override earlier translations (here it also changes values like loyaltyCards from "Loyalty Cards" to "Cards"). Remove the duplicated block appended near the end of the file and keep a single canonical definition per key (with its @... metadata).

Suggested change
"you": "you",
"loyaltyCards": "Cards",
"@loyaltyCards": {},
"loyaltyCardsEmpty": "No loyalty cards yet. Add one to get started!",
"@loyaltyCardsEmpty": {},
"loyaltyCardAdd": "Add card",
"@loyaltyCardAdd": {},
"loyaltyCardEdit": "Edit card",
"@loyaltyCardEdit": {},
"loyaltyCardNameHint": "e.g., Target, Costco",
"@loyaltyCardNameHint": {},
"loyaltyCardDeleteConfirmation": "Are you sure you want to delete {name}?",
"@loyaltyCardDeleteConfirmation": {
"placeholders": {
"name": {
"example": "Target",
"type": "String"
}
}
},
"barcodeData": "Barcode data",
"@barcodeData": {},
"barcodeDataHint": "Enter barcode number",
"@barcodeDataHint": {},
"barcodeType": "Barcode type",
"@barcodeType": {},
"copyBarcode": "Copy barcode",
"@copyBarcode": {},
"scanBarcode": "Scan barcode",
"@scanBarcode": {},
"scanFromCamera": "Scan with camera",
"@scanFromCamera": {},
"scanFromGallery": "Import from image",
"@scanFromGallery": {},
"pointCameraAtBarcode": "Point camera at barcode",
"@pointCameraAtBarcode": {},
"barcodeDetected": "Barcode detected",
"@barcodeDetected": {},
"autoDetected": "auto-detected",
"@autoDetected": {},
"enterManually": "Enter manually",
"@enterManually": {},
"flashOn": "Flash on",
"@flashOn": {},
"flashOff": "Flash off",
"@flashOff": {},
"switchCamera": "Switch camera",
"@switchCamera": {},
"noBarcodesFound": "No barcodes found in image",
"@noBarcodesFound": {},
"webcam": "Webcam",
"@webcam": {},
"camera": "Camera",
"@camera": {},
"gallery": "Gallery",
"@gallery": {},
"keepScreenOn": "Screen stays on for scanning",
"@keepScreenOn": {}
"you": "you"

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +24
// Check if loyalty cards feature is enabled
final household = context.read<HouseholdCubit>().state.household;
final showLoyaltyCardsFab = household.featureLoyaltyCards ?? true;

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

showLoyaltyCardsFab defaults to true when featureLoyaltyCards is null. Since Household.fromJson leaves featureLoyaltyCards null when the backend doesn’t send the flag, this can surface the loyalty-cards UI against older backends that don’t support the endpoints. Consider defaulting to false when the flag is missing (or explicitly gate on server capability) to preserve backward compatibility.

Copilot uses AI. Check for mistakes.
Comment on lines +69 to +71
void _openLoyaltyCards(BuildContext context, dynamic household) {
context.push('/household/${household.id}/loyalty-cards');
}

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_openLoyaltyCards takes dynamic household, even though callers pass a Household. Using dynamic here removes type-safety and can hide runtime errors (e.g., missing id). Prefer Household household as the parameter type.

Copilot uses AI. Check for mistakes.
Comment on lines +122 to +128
Text(
'Store all your loyalty cards in one place and access them quickly at checkout.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This empty-state description string is hardcoded in English and bypasses localization. Please move it into the ARB files and use AppLocalizations so it’s translated consistently with the rest of the app.

Copilot uses AI. Check for mistakes.
Comment on lines +347 to +352
Text(
'Invalid barcode',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These error messages are hardcoded ('Invalid barcode') instead of being localized via AppLocalizations. Please add localization keys and use them here so the barcode error UI is translated.

Copilot uses AI. Check for mistakes.
Comment on lines +367 to +372
Text(
'Cannot display barcode',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error message ('Cannot display barcode') is hardcoded instead of being localized via AppLocalizations. Please add a localization key and use it here.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +28
from ...models import LoyaltyCard
from app.config import db


def importLoyaltyCard(household, card_data):
card = LoyaltyCard.query.filter_by(
household_id=household.id, name=card_data["name"]
).first()

if not card:
card = LoyaltyCard()
card.household_id = household.id
card.name = card_data["name"]

if "barcode_data" in card_data:
card.barcode_data = card_data["barcode_data"]

if "barcode_type" in card_data:
card.barcode_type = card_data["barcode_type"]

if "description" in card_data:
card.description = card_data["description"]

if "color" in card_data:
card.color = card_data["color"]

db.session.add(card)
db.session.commit()

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import service commits directly via db.session.commit() for each card and bypasses the model’s save() pattern used by other import services (e.g., importItem/importExpense). Prefer using card.save() (and avoid committing per-record inside the helper) for consistency and to reduce transaction overhead when importing many cards.

Copilot uses AI. Check for mistakes.
Comment thread backend/app/controller/__init__.py Outdated
from .health_controller import health
from .analytics import *
from .report import *
from .loyalty_card import *

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import pollutes the enclosing namespace, as the imported module app.controller.loyalty_card does not define 'all'.

Suggested change
from .loyalty_card import *
from . import loyalty_card

Copilot uses AI. Check for mistakes.
Edigorin and others added 6 commits March 7, 2026 11:37
Add support for loyalty cards/membership cards that users can store
and access from the shopping list page via a wallet FAB.

Backend:
- Add LoyaltyCard model with name, barcode data/type, description, color
- Add CRUD API endpoints for loyalty cards
- Add database migration for loyalty_cards table
- Add household feature flag for loyalty cards
- Add loyalty card import functionality
- Add comprehensive API tests

Frontend:
- Add loyalty cards list page with search functionality
- Add loyalty card detail page with fullscreen barcode display
- Add loyalty card add/edit page with barcode scanning
- Add barcode scanning from camera (mobile) and webcam (web)
- Add barcode scanning from gallery/image upload
- Add wallet FAB on shopping list page to access loyalty cards
- Support multiple barcode types: CODE128, QR, EAN13, etc.
- Add localization strings for loyalty cards feature
- Add loyalty cards import settings option

Closes TomBursch#462
- Remove redundant dart:typed_data import (Uint8List already provided by flutter/foundation.dart)
- Fix featureLoyaltyCards deserialization to preserve null values (fixes failing test)
…onal, remove custom web scanner

- Remove photo/photo_file column from loyalty card model, migration, schemas, and controller
- Remove photo-related relationships from File model
- Make barcode_type and barcode_data nullable in model, migration, and schemas
- Allow creating loyalty cards without a barcode (frontend validation removed)
- Conditionally render barcode UI only when barcode data is present
- Delete custom web barcode scanner (web_barcode_scanner.dart, barcode_scanner.js)
- Remove html5-qrcode CDN script from index.html
- Hide gallery scan button on web (analyzeImage not supported)
- Use mobile_scanner's built-in web support for camera scanning
- Add test for creating loyalty card without barcode
Remove 57-line duplicate block in app_en.arb, add 14 new loyalty card
value keys with metadata, and localize 3 hardcoded strings in
loyalty_card_page.dart and loyalty_card_list_page.dart.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Change featureLoyaltyCards default from true to false for backward
compatibility when backend omits the field, and replace dynamic with
Household type annotation.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Replace db.session.add/commit with card.save() to match existing import
services, switch to absolute import for LoyaltyCard model, and replace
wildcard import with explicit named imports in controller init.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@TomBursch TomBursch force-pushed the feat/loyalty-cards branch from ffc8cf9 to c0cd578 Compare March 7, 2026 10:37
@thieneret

Copy link
Copy Markdown

I would really like this feature, so I would like to ask if there is a chance that this PR will be merged in the near future?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: Add Support for Loyalty Cards

5 participants