The E7 ticketing platform solved the web checkout. But mobile web has a ceiling. Pinch-to-zoom on an SVG seat map inside a mobile browser is tolerable — not good. And once someone buys a ticket, the email-to-PDF-to-screenshot-to-show-at-the-door flow is a papercut that compounds at every event.
E7 needed a native app. I built it in 10 weeks with React Native and Expo.
Why native, not PWA
This was a real decision, not a default. I wrote the full native-vs-PWA framework separately — here's how the factors played out for E7:
Seat map gestures killed the PWA path. The seat picker needs pinch-to-zoom, double-tap-to-center, and momentum-based panning — all at 60fps on a 3,000-node SVG. Mobile Safari and Chrome handle this passably for photos, not for interactive SVGs where every node is a tap target. We prototyped in the browser and hit 24fps on a Pixel 4a. Native with Reanimated 3 hit 60fps on the same device.
Offline ticket storage needs real device APIs. A PWA can use the Cache API, but it can't reliably hold encrypted ticket data across app restarts on iOS Safari. React Native with AsyncStorage + SecureStore gives us encrypted offline storage that persists.
Push notifications for event reminders. Technically possible with PWA, but adoption is ~15% (users don't enable). Native push via Expo Notifications sees 72% opt-in. For an event app, reminders are the second-most-used feature after tickets.
Constraints
10 weeks. The web platform was already live; the app needed to ship before the next concert season.
Single codebase, two platforms. No budget for separate iOS and Android teams. React Native + Expo was the only path that delivered both from one codebase in the timeline.
Must share the backend. The app hits the same API as the web platform. No separate backend, no data duplication, no sync issues. One Prisma schema, one Postgres database, one Stripe account.
What we shipped
The seat picker — 60fps on a budget phone
This was the engineering centerpiece. A 3,000-seat SVG that you can pinch, zoom, pan, and tap at native speed.
The architecture:
-
Reanimated 3 for gestures. Pinch-to-zoom and pan run on the native UI thread, not the JS thread. This means gesture processing doesn't block React rendering. On a Pixel 4a, the difference is 24fps (JS-thread gestures) vs 60fps (native thread).
-
Virtualized seat rendering. Only seats visible in the current viewport are rendered as interactive
Pressablecomponents. Out-of-viewport seats are a single static<Path>element. When you pan, seats enter and leave the interactive zone. This keeps the node count under 200 regardless of venue size. -
Optimistic seat locking. When you tap a seat, the UI shows it as "yours" immediately (optimistic update) while the API lock request flies in the background. If the lock fails (someone else got it first), the seat snaps back to available with a haptic buzz. Perceived latency: zero. Actual round-trip: ~150ms.
-
Color-coded states. Available seats are white. Your seats are green. Other people's locked seats are grey. Sold seats are dark. No legend needed — the states are obvious from the color alone.
The offline architecture post goes deeper on the sync protocol. The short version: we built a 4-state queue (pending → syncing → confirmed → failed) that handles network drops mid-checkout without losing the user's selection.
The QR wallet
After purchasing, tickets appear in an in-app wallet. Each ticket shows:
- Event name, date, venue
- Seat/section information
- A QR code that refreshes every 30 seconds (to prevent screenshot sharing)
- Venue directions (deep link to Maps)
The wallet works fully offline. Ticket data + QR signing keys are encrypted and stored locally at purchase time. Even if the user is underground at the venue with zero signal, they can show their ticket and the staff scanner verifies it.
The rotating QR was a late addition. The original QR was static — same image every time. Two weeks after launch, we saw screenshots of QR codes being shared on social media. Adding a time-based rotation (QR encodes ticket ID + current 30-second window, signed with HMAC) killed screenshot sharing without affecting legitimate users.
Push notifications
Three notification types, tuned by engagement data:
- Event reminder: 24 hours before the event. 68% tap-through rate.
- Gate open: When venue check-in starts. Sent to ticket holders only.
- New events: Weekly digest for users who opted in. 22% open rate — decent for a non-spam app.
Expo Notifications handles the cross-platform delivery. Firebase Cloud Messaging on Android, APNs on iOS, one API for both. Notification preferences are per-category — users can mute new events without losing reminders.
Deep links
The purchase confirmation email includes a deep link: e7shop://ticket/abc123. If the app is installed, it opens directly to the ticket. If not, it falls back to the web ticket page with an app install banner.
This single feature increased app installs by 40% in the first month. People buy on web → get an email → tap the link → install the app → see their ticket. Frictionless onboarding.
The numbers
Six months of production data:
| Metric | Value |
|---|---|
| Total installs | 8,200+ |
| App Store rating | 4.8★ (iOS) / 4.7★ (Android) |
| Crash-free sessions | 99.7% |
| Mobile checkout completion | 82% (vs 78% web) |
| Avg. seat selection time | 12s |
| Offline tickets served | ~1,400 |
| Push opt-in rate | 72% |
The 82% checkout completion on mobile app vs 78% on mobile web is modest, but the user satisfaction gap is wider than the numbers suggest. App users rate the seat picker 4.9/5 in post-purchase surveys; mobile web users rate it 3.4/5. The experience is fundamentally different even if the conversion delta is small.
The 1,400 offline tickets served means 1,400 people who would have been stuck at the door with "no signal" if we'd built a PWA. At an underground venue in Bratislava, that's roughly half the audience.
App Store review timeline
A reality check for anyone planning a mobile launch:
| Milestone | Day |
|---|---|
| First build submitted to TestFlight | Week 6 |
| Internal testing complete | Week 8 |
| App Store submission | Week 9, Monday |
| Apple review — first rejection | Week 9, Thursday |
| Fixed: added "Restore Purchases" button | Week 9, Friday |
| Resubmission | Week 9, Friday |
| Approved | Week 10, Tuesday |
| Google Play submission | Week 9, Monday |
| Approved | Week 9, Wednesday |
Apple rejected us for not having a "Restore Purchases" button, even though we don't sell in-app purchases — the tickets are bought via Stripe on the web, and the app just displays them. Apple's reviewer interpreted the ticket display as a purchase flow. We added a no-op "Restore Purchases" button that shows "All tickets are synced from your account" and got approved on resubmission.
Lesson: Budget a full week for App Store review, even for a v1. Google Play approved in 2 days. Apple took 8 days including the rejection cycle.
What surprised us
Android back-button handling broke the seat picker. React Navigation's default back behavior popped the entire screen when users pressed the hardware back button during seat selection. They expected "deselect last seat," not "leave the page." Custom BackHandler logic fixed it, but we only caught it because a beta tester reported losing their selection.
Battery drain from QR refresh. The 30-second QR rotation originally kept the screen awake and re-rendered the QR component on a timer. On some Android devices, this drained 8% battery per hour in the ticket view. We switched to requestAnimationFrame-based updates that only run when the ticket screen is focused, cutting drain to under 1%.
Users didn't read the onboarding. We built a 3-screen onboarding explaining the app's features. Analytics showed 78% of users swiped through all three screens in under 2 seconds — they weren't reading. We replaced it with contextual tooltips that appear the first time you use each feature. Engagement with help content went from 4% to 31%.
What I'd do differently
I'd ship Expo Updates from day one. We used EAS Build for binary releases but didn't enable over-the-air updates until week 4 post-launch. That meant two critical bug fixes required full App Store resubmission (3-day cycle each). Expo Updates would have pushed those fixes in minutes.
I'd use Expo Router instead of React Navigation. At build time, Expo Router was pre-v1 and we chose the stable option. Now it's mature, file-based, and handles deep links automatically. Would have saved ~2 days of manual navigation setup and deep link configuration.
I'd A/B test the seat map zoom level. We default to showing the full venue. Most users immediately zoom into their preferred section. Starting zoomed into the most popular section (based on historical sales data) would save a gesture and probably improve selection time.
The business result
E7 now has a native app that 8,200+ people have installed, with a 4.8★ rating and 72% push notification opt-in. That's a direct marketing channel to their audience — no ad spend, no reseller, no intermediary.
Mobile checkout completion is higher on the app than on the web. Offline tickets work at underground venues. The rotating QR killed screenshot sharing. And the deep link flow from email to app drives installs without a single "download our app" pop-up.
The app cost E7 about 30% of what a native agency quoted for iOS-only. They got both platforms, a shared backend, and a team of one who shipped in 10 weeks.
If you're building a mobile-first product and wondering whether React Native can deliver native-feel performance — it can, for the right use cases. Seat pickers, maps, gesture-heavy UIs — all doable at 60fps with the right architecture. Let's talk about yours.