Developing a Mobile App for Digital Ticket
Digital Ticket is not just PDF with QR-code as file. Modern Digital Ticket is live object: updates when event postponed, shows seat schema, lets enter via NFC or Wallet, validates on scanner without internet on controller's side.
Ticket Storage and Delivery Formats
App must support multiple storage channels:
In-app storage—ticket stored in app as database object (Core Data / Room), QR generated on-the-fly from ticketToken. Requires internet for first load.
Apple Wallet / Google Wallet—added via PKAddPassesViewController or Google Pay SDK. Available without internet, updates via push. Separate integration.
PDF—generated server-side (PDFKit/wkhtmltopdf), downloaded by user. Backup option.
For most apps do all three—user chooses convenient variant.
QR Generation and Validation
QR must contain not just order number but signed token—otherwise forgeable via screenshot copy.
Scheme: server generates ticketToken = HMAC-SHA256(ticketId + userId + expiresAt, secret). Controller scans QR → controller app sends token to POST /tickets/validate → server verifies HMAC and usage status.
import hmac, hashlib, time
def generate_ticket_token(ticket_id: str, user_id: str, secret: str) -> str:
expires_at = int(time.time()) + 86400 * 30 # valid 30 days
message = f"{ticket_id}:{user_id}:{expires_at}"
signature = hmac.new(
secret.encode(), message.encode(), hashlib.sha256
).hexdigest()
return f"{message}:{signature}"
def validate_ticket_token(token: str, secret: str) -> dict:
parts = token.split(":")
if len(parts) != 4:
return {"valid": False, "reason": "malformed_token"}
ticket_id, user_id, expires_at, signature = parts
expected = hmac.new(
secret.encode(),
f"{ticket_id}:{user_id}:{expires_at}".encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
return {"valid": False, "reason": "invalid_signature"}
if int(expires_at) < int(time.time()):
return {"valid": False, "reason": "expired"}
return {"valid": True, "ticketId": ticket_id, "userId": user_id}
For high-resale-risk events use rotating QR—token changes every 30–60 seconds (TOTP logic). Screenshot immediately becomes stale.
Offline Validation
Controller at entrance may be offline. Two approaches:
Offline List—controller app pre-loads valid ticketId list (e.g., hour before event). Scans QR, checks locally. Risk: can't mark ticket used until sync.
Digital Signature Without Server—controller verifies HMAC with public key embedded in app. PAN not stored, only public key. Can't revoke single ticket offline—only blacklist pre-loaded.
Seat Schema and Selection
If event has numbered seats—need interactive seat map. Via SVG or custom Canvas. React Native has react-native-svg, Flutter—CustomPainter.
// Flutter: custom painter for seat rows
class SeatMapPainter extends CustomPainter {
final List<Seat> seats;
final Set<String> selectedSeats;
@override
void paint(Canvas canvas, Size size) {
for (final seat in seats) {
final paint = Paint()
..color = selectedSeats.contains(seat.id)
? Colors.blue
: seat.isAvailable ? Colors.green : Colors.grey;
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(seat.x, seat.y, 28, 24),
const Radius.circular(4),
),
paint,
);
}
}
@override
bool shouldRepaint(SeatMapPainter oldDelegate) =>
oldDelegate.selectedSeats != selectedSeats;
}
Schema loaded as JSON with coordinates. On zoom use InteractiveViewer (Flutter) or UIPinchGestureRecognizer + CATransform3D (iOS).
Refund and Transfer
Ticket refund logic on server. App displays status: ACTIVE, USED, REFUNDED, TRANSFERRED. On event postponement server sends push → app updates ticket data.
Important edge case: ticket should remain visible after USED—user wants to see visit history.
Timeline Estimates
Basic version (in-app QR, purchase, history): 4–6 weeks. Adding seat schema with selection—another 2–3 weeks. Wallet integration (Apple / Google)—another 3–5 days. Pricing is calculated individually.







