Common Orientation Change Bugs in Ticketing Apps: Causes and Fixes

Orientation change bugs happen when a ticketing app loses, duplicates, or corrupts state while moving between portrait and rotated modes. The issue is rarely “rotation” itself. It is usually state man

February 21, 2026 · 5 min read · Common Issues

What causes orientation change bugs in ticketing apps

Orientation change bugs happen when a ticketing app loses, duplicates, or corrupts state while moving between portrait and rotated modes. The issue is rarely “rotation” itself. It is usually state management, layout recreation, or async work not surviving the activity or view lifecycle.

Common technical root causes

Ticketing apps are especially vulnerable because a purchase flow combines inventory, pricing, user identity, payment, and fulfillment in one session.

Real-world impact

Orientation bugs in ticketing apps are not cosmetic. They directly affect conversion and trust.

Bug typeUser impactBusiness impact
Selected seats disappearUser must restart checkoutAbandoned purchase
Promo code resetsUser pays more than expectedSupport tickets and refunds
Payment button duplicatesUser may be charged twiceChargebacks and reputation damage
QR ticket fails to renderUser cannot enter venueRefunds and venue support load
Countdown timer resetsUser loses reserved inventoryConfusion and checkout errors
Seat map becomes unusableAccessibility and usability failuresLost sales from mobile users

Users usually describe these bugs in short, harsh reviews: “lost my seats,” “charged me twice,” “QR code disappeared,” or “app crashed while paying.” For ticketing apps, that translates into lower store ratings, more customer support volume, and revenue leakage at the most sensitive part of the funnel.

How orientation bugs show up in ticketing apps

1. Selected seats are lost after rotation

A user selects aisle seats, rotates the device, and the seat map reloads. Previously selected seats may appear available again, or the checkout button may remain disabled.

Why it happens: seat selection state is stored in a view instead of a lifecycle-aware model.

Fix: keep selected seats in a persistent checkout state object, not in the seat map view.


class CheckoutViewModel : ViewModel() {
    val selectedSeats: MutableStateFlow<Set<SeatId>> = MutableStateFlow(emptySet())

    fun toggleSeat(seat: SeatId) {
        selectedSeats.update { current ->
            if (seat in current) current - seat else current + seat
        }
    }
}

Also persist the state across process death:


viewModel.stateIn(scope, SharingStarted.WhileSubscribed(5000), CheckoutState())

or use SavedStateHandle for primitive checkout fields.

2. Ticket quantity or price resets

The user changes quantity from 2 to 4, rotates, and the app returns to 1 ticket. In dynamic pricing flows, this can also change the displayed total.

Why it happens: quantity is stored in an activity field or form component that resets on recreation.

Fix: store quantity, selected tier, fees, taxes, and total in a single checkout state. Recalculate totals from the same source of truth after rotation.


data class CheckoutState(
    val ticketTypeId: String,
    val quantity: Int,
    val fees: Money,
    val taxes: Money
)

Avoid storing only the final total. Store the inputs so the total can be recalculated safely.

3. Promo codes disappear or apply twice

A user enters a discount code, rotates, and the code is gone. Or worse, the app submits the same promo code twice and applies an invalid discount.

Why it happens: form fields are not restored, and promo validation calls are not idempotent.

Fix: save promo code state in ViewModel or SavedStateHandle, and make promo validation idempotent on the backend.


@Suppress("DEPRECATION")
savedStateHandle["promoCode"] = code

Prefer a checkout session model where promoCode, discountStatus, and discountError update together.

4. Checkout payment is duplicated

The user taps “Pay,” rotates during processing, and the app shows two payment spinners or submits two payment requests.

Why it happens: the payment action is tied to a button click and can be invoked again after the UI recreates.

Fix: use an idempotency key per payment attempt and disable repeat submission until the terminal state returns.


data class PaymentAttempt(
    val idempotencyKey: String,
    val status: PaymentStatus
)

On the backend, reject or safely ignore repeated requests with the same key. On the client, do not restart payment when the activity recreates.

5. QR ticket or barcode disappears after purchase

The user completes purchase, rotates, and the QR code is blank, cropped, or no longer scannable.

Why it happens: the QR is generated for the previous layout size, cached incorrectly, or rendered outside the new bounds.

Fix: regenerate the QR after layout changes and verify contrast, quiet zone, and minimum size.


LaunchedEffect(qrPayload, containerWidth) {
    qrBitmap = QrGenerator.generate(
        payload = qrPayload,
        size = min(containerWidth, 480.dp.toPx().toInt())
    )
}

Store the ticket token securely, but generate the visual code from the current view size.

6. Seat map becomes unreadable or inaccessible

After rotation, labels overlap, selected seats are hidden, zoom resets, or screen readers lose focus.

Why it happens: custom seat maps rely on fixed coordinates and do not recalculate hit targets or accessibility nodes.

Fix: recalculate seat positions from the available canvas size and expose accessibility metadata for each selectable seat.


fun buildSeatMap(width: Int, height: Int): SeatLayout {
    return SeatPlacer.place(
        seats = seats,
        bounds = Rect(0, 0, width, height),
        minTouchTarget = 48.dp.toPx().toInt()
    )
}

Also restore zoom level and selected seat focus after rotation.

7. Countdown timer shows impossible values

The ticket hold timer says 09:59, the user rotates, and it resets to 10:00. Or it keeps running after the checkout screen is destroyed.

Why it happens: the timer is restarted from UI state instead of a server-backed expiration time.

Fix: store the server timestamp and expiration time. Render remaining time from that immutable value.


val remaining = max(0, expiresAtMillis - currentTimeMillis())

Do not trust a countdown value saved only in the UI.

How to detect orientation change bugs

Use both automated and exploratory testing. Manual rotation testing catches obvious layout issues, but automated tests are better for repeated checkout flows.

What to test

Useful tools and techniques

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free