Push Notifications
How push notifications reach a phone, how permissions, app states and deep links change what users see, what usually breaks, and how testers trigger and test pushes on iOS, Android and the web.
It’s 8 pm, you’re hungry, and your phone buzzes: “Marco picked up your order. Arriving in 18 min.” You tap it, and the app opens straight on the order with a live map. That moment feels effortless, but behind it are your app, your company’s server, Apple or Google, the phone’s operating system, a permission the user gave months ago, and a deep link. Any of them can break.
And when push breaks, users notice fast. The notification arrives twice. It arrives at 3 am. It says “Your order from {{restaurant}}”. It keeps coming after you turned it off, so people uninstall the app. This lesson shows you how push notificationsPush notificationA message an app sends to the device that appears even when the app is closed.Open in glossary → work, where they fail, and how to test them without waiting for a real order.
How it works
A push notification is a message your company’s server sends to an app on a phone, even when the app isn’t open. The server doesn’t contact the phone directly. It goes through a delivery service run by the platform owner:
- APNs (Apple Push Notification service) for iPhone and iPad.
- FCM (Firebase Cloud Messaging) from Google for Android (and it can forward to APNs for iOS too).
1Your app
Asks the user for permission, then asks the operating system for a device token.
token: a1f3…9c2APNs (Apple) / FCM (Google)
Issues the token. It is the "postal address" of this app on this device.
3Your company’s server
Stores the token next to the user account. When the order changes, it sends a message to APNs or FCM.
{ to: a1f3…9c, title: "On the way" }4APNs / FCM
Delivers the message to the device (now, or later if the phone is offline).
5The device
The operating system shows the banner, sound and badge. The app decides what happens on tap.
- The app asks for permission. Without it, the phone won’t show anything (more on this below).
- The app registers and gets a device token. A long random string from APNs or FCM that identifies this app on this device. Think of it as a postal address.
- The app sends the token to your server. The server stores it next to the user account, e.g. “Anna has an iPhone with token a1f3…9c”.
- Something happens. The restaurant confirms, the driver picks up the food, a sale starts.
- The server sends a message to APNs or FCM with the token, the text and extra data (like which order it’s about).
- APNs or FCM delivers it to the phone. If the phone is offline, they keep it for a while and deliver later.
- The operating system shows it. Banner, sound, badge, lock screen. When the user taps it, the app opens and should take them to the right screen.
Push, local and in-app notifications
Not every notification is a push. Testers should know which kind they’re looking at, because they break in different ways:
| Kind | Who creates it | Example |
|---|---|---|
| Push (remote) | Your server, via APNs/FCM | “Your order is on the way” |
| Local | The app itself, scheduled on the phone, no internet needed | “Time for your 7 am workout”, a timer that finished |
| In-app message | The app’s own UI, only while the app is open | A banner or toastToastA short, temporary message that appears briefly and disappears on its own, like 'Message sent'.Open in glossary → inside the app: “Marco is 2 minutes away” |
A common bug is mixing them up: the server sends a push and the app schedules a local reminder for the same thing, so the user gets both.
Permission: asking at the right moment
For every permission type, the system dialogs and what happens after each answer, see App Permissions.
Users decide whether an app may notify them, and they can change their mind any time in system Settings. This is one of the app permissionsApp permissionsThe user's approval for an app to use things like the camera, location, contacts or notifications.Open in glossary → that every tester should know well.
iOS
- The app shows Apple’s system dialog: “QuickBite” Would Like to Send You Notifications, with Allow and Don’t Allow.
- iOS shows this dialog only once per install. After a “Don’t Allow”, the app can never show it again. The only way back is Settings › Notifications › QuickBite, so a good app explains that and offers a button to open Settings.
- Timing matters. Asking on the very first screen, before the user understands the app, gets a lot of “Don’t Allow”. Good apps ask in context: right after the user places their first order (“Want to know when your food is on the way?”). See Onboarding for this “permission priming” pattern.
- Provisional (quiet) notifications (iOS 12 and later): an app can skip the dialog and deliver notifications quietly to Notification Center only (no banner, no sound). Each one asks the user “Keep” or “Turn off”. Test that these really arrive quietly.
Android
- Android 13 and newer need the runtime permission
POST_NOTIFICATIONS: the app shows a system dialog with Allow and Don’t allow. If the user denies it twice, Android stops showing the dialog, and only system Settings can turn notifications on. - Android 12 and older have no dialog: notifications are on by default, and users turn them off in Settings. So test both an old and a new Android version.
- Upgrades are tricky: existing apps that already sent notifications usually keep them after the phone updates to Android 13 (the permission is pre-granted), while apps that still target Android 12L or older get the dialog automatically when they create their first notification channel. Test what existing users see after the update.
Anatomy of a notification
iOS banner
On the way
nowMarco picked up your Pasta Roma order. Arriving in 18 min.
Long-press to see the image and actions
Android (expanded)
On the way
Marco picked up your Pasta Roma order.
- 1App icon and name. Shows who sent it. Android uses a small monochrome icon in the status bar too.
- 2Title. Short and specific. Gets cut off first on small screens and in other languages.
- 3Body. The details. Check placeholders are filled in and long text is truncated nicely.
- 4Image (rich media). Optional picture. Must not block the notification if it fails to download.
- 5Actions. Buttons like "Track" or "Call driver". Each must do exactly what it says, even when the app is closed.
- 6Badge. The red count on the app icon. It should go down when the user reads the updates.
- 7Timestamp and grouping. Several messages from one order or chat should stack together, newest first.
- 8Channel (Android). Every notification belongs to a channel such as "Order updates" with its own importance and sound.
A few platform details that affect testing:
- Android channels. Since Android 8, every notification belongs to a channel (like “Order updates” or “Offers”) and users can turn each channel on or off, or change its sound. The app sets each channel’s importance once: high pops up as a heads-up banner with sound, default makes a sound, low is silent. After that, the user owns the channel, and the app can’t change it in an update. A promo channel created with high importance by mistake stays noisy for every existing user.
- iOS interruption levels. Notifications can be passive (silent), active (normal), time-sensitive (may break through a Focus mode) or critical (always plays a sound; needs special approval from Apple, e.g. for health alerts). A marketing push marked time-sensitive is a bug.
- Grouping. Several messages about one order or one chat should stack together. Test five updates in a row: are they grouped and in the right order?
- Badge. The red number on the app icon. On iOS the server sends the number in the message, and the app is responsible for clearing it when the user has seen the updates.
What happens in each app state
The same push behaves differently depending on the state of the appApp lifecycleThe states an app moves through: not running, foreground, background, suspended and terminated.Open in glossary →. This is where many push bugs live, so always test all three:
| App state | What the user should see | What to check |
|---|---|---|
| Foreground (open, on screen) | Usually no system banner. The app decides: show an in-app banner and refresh the screen | The order status updates live; nothing is silently ignored |
| Background (opened earlier, now hidden) | A normal system banner, sound and badge | Tap opens the right screen; data is fresh when the app returns |
| Killed (swiped away, or never opened since restart) | A normal system banner | Tap starts the app from scratch (a cold startCold startLaunching an app whose process isn't running, the slowest kind of launch, compared with a warm or hot start.Open in glossary →) and still opens the right screen |
Tapping a notification: deep links
Tapping “Your order is on the way” should open that order, not the Home screen. The push carries a deep linkDeep linkA link that opens a specific screen inside an app instead of just its home screen.Open in glossary → or an ID (like order_id: 4821) that the app uses to navigate.
The classic bug: it works when the app is in the background, but from a killed state the app starts, shows the splash screen, and lands on Home. That’s because the code path for a cold start is different. Other things to try: tapping while logged out, tapping an old notification for an order that’s already delivered, and tapping when the app is open on another screen. The lesson Background and foreground explains these app states in depth.
The tricky details
- Do Not Disturb and Focus. The phone delivers the notification silently to Notification Center. The app can’t override this (except time-sensitive or critical notifications, when the user allows them). Test that nothing is lost, it’s just quiet.
- Quiet hours and time zonesTime zoneThe local time of a region. Bugs appear when apps mix up the user's time zone with the server's.Open in glossary →. “No offers between 21:00 and 08:00” must use the user’s local time, not the server’s. A server in Frankfurt sending a “lunch deal” at 12:00 Frankfurt time wakes up users in New York at 6 am. Also think about Daylight saving time (DST)Daylight saving time (DST)Moving clocks forward in spring and back in autumn, creating a skipped hour and a repeated hour.Open in glossary → changes.
- Unsubscribe and settings sync. When the user turns off “Offers” in the app, the server must know. If the switch only changes on the phone, offers keep coming. Also test after reinstalling and on a second device: is the setting kept?
- Duplicates. The same message twice is usually a token problem: the server stores the same device twice, or keeps an old token after a reinstall, or both a push and a local notification fire. Pushes should also be safe to retry without double delivery (idempotencyIdempotencyAn action that gives the same result no matter how many times it is repeated.Open in glossary →).
- localizationLocalizationAdapting an app to a language and region, including translations, dates, currency and layout.Open in glossary →. The push text is often built on the server, so it uses the language the server thinks the user has. Test with the phone in another language: right language, placeholders filled in, no cut-off titles in German or Finnish, correct date and number formats.
- Logout. After logging out (or when someone else logs in on the same phone), the old user’s notifications must stop. Otherwise the next person sees Anna’s orders on the lock screen.
How testers trigger pushes
You can’t wait for a real driver to pick up a real order. Ask your developers which of these your team uses:
- A staging admin panel or “push console”. Many teams build an internal page on the stagingStagingA pre-release environment that mirrors production as closely as possible.Open in glossary → server: pick a user, pick a message type, press Send. This is the most realistic option because it uses the real server code and templates.
- Firebase console. In Firebase: Messaging › New campaign › Notifications, write a title and text, then Send test message and paste the device’s FCM registration token (developers can show it in a debug menu or log it to logcatLogcatAndroid's system log, read with adb logcat, where apps write errors and crash details.Open in glossary →).
- iOS Simulator. Save a payload as a
.apnsfile and runxcrun simctl push booted com.quickbite.app order.apns, or drag the file onto the Simulator window (for drag and drop, the file must also contain a"Simulator Target Bundle"key with the app’s bundle ID). Great for checking text, deep links and app states. Since Xcode 14 (macOS 13, Apple silicon or T2 Macs), the Simulator can also receive real remote pushes. - Apple’s Push Notifications Console. A web tool in the Apple Developer account where you paste a device token and send a test push to a real iPhone.
- A real deviceReal deviceA physical phone or tablet used for testing, as opposed to an emulator or simulator.Open in glossary →, end to end. Before release, always test real pushes on real iPhones and Android phones, through the real server. Simulators can’t tell you about delivery delays, battery savers or a manufacturer’s aggressive background limits.
Here’s what a simple iOS payload looks like. Even if you never write one, it helps to recognize the fields in a bug report:
{
"aps": {
"alert": { "title": "On the way", "body": "Marco picked up your order." },
"badge": 3,
"sound": "default",
"thread-id": "order-4821"
},
"order_id": "4821"
}
alert is the text, badge the number on the icon, thread-id groups notifications together, and order_id is custom data the app uses to open the right screen.
Example requirements
Criteria 3, 4, 6 and 7 are the ones teams most often forget to test, because they need a specific app state, a second device or a different time zone.
What usually goes wrong
| # | Bug | What you see | Typical severity |
|---|---|---|---|
| 1 | Wrong screen on tap | Tapping a push from a killed app opens Home, not the order | Medium–High |
| 2 | Duplicates | The same notification twice (or once per reinstall) | Medium |
| 3 | Opt-out ignored | Offers keep coming after turning them off in the app | High (trust, legal) |
| 4 | Foreground push lost | App is open: no banner, the screen shows an old status | Medium |
| 5 | Text problems | Raw {{restaurant}}, wrong language, title cut off | Medium |
| 6 | Badge never clears | Red “7” on the icon even after reading everything | Low–Medium |
| 7 | Wrong time | Promo arrives at 3 am because the server used its own time zone | Medium–High |
| 8 | Permission asked too early | Dialog on first launch, user taps Don’t Allow, no way back | High (business) |
| 9 | Notifications after logout | Next user sees the previous user’s orders on the lock screen | High (privacy) |
| 10 | Private data on the lock screen | Full bank balance or a health result in the preview | High (privacy) |
Find the bug
QuickBite’s order notifications have six bugs. You control the phone (app state, permission, Do Not Disturb, language, time zone) and a fake server console that sends pushes, just like a staging admin panel. Try this:
- Send an update while the app is open on the order screen.
- Go to the Home screen, send updates, tap them.
- Force quit the app, send an update, tap it. Then send another one.
- Turn things off in the app’s notification settings, then send that type.
- Switch the language to Deutsch and send each event.
- Watch the badge on the app icon, and the console log.
🐞 Bug hunt: QuickBite delivery notifications
This demo has 6 hidden bugs. Explore it like a tester, then check your findings.
QuickBite
Your order
Near you
Device
App state: Foreground (open)
Server console (staging)
Order #4821: Placed
Server time 20:30 UTC · user’s time 20:30
Anna’s settings: orders on · promos on
Device tokens (1): a1f3…9c
- Send a push to see what happens…
From the spec
- Tapping an order update opens that order.
- In the open app, updates show an in-app banner and refresh the screen.
- Users choose which types they get. Offers are never sent 21:00–08:00 their time.
- The badge shows unread updates.
- 1
Hint: Force quit the app, send "Picked up", then tap the notification.
- 2
Hint: Force quit, open the app again, go to the Home screen and send any update. Check "Device tokens" in the console.
- 3
Hint: In the app, open settings and turn off "Offers & promotions". Go to the Home screen and send the promo.
- 4
Hint: Keep the app open on the order screen and send "Driver arriving".
- 5
Hint: Switch the language to Deutsch, go to the Home screen and send "Order confirmed".
- 6
Hint: Receive a few pushes, open the app and the order, then go back to the Home screen.
Test scenarios
Functional
- Each event (confirmed, picked up, arriving, delivered) sends exactly one push with the right text.
- Tapping each push opens the right screen in foreground, background and killed states.
- With the app open, an in-app banner appears and the screen refreshes live.
- In-app settings: turning a type off stops it, turning it on brings it back. Check the server really changed (a second device, or ask a developer).
- The badge count matches unread updates and clears when they’re seen.
- Notification actions (“Track order”, “Call driver”) do what they say, also from a killed app.
Negative
- Permission denied: no notifications, no crash, settings screen explains how to turn them on.
- Permission denied, then turned on later in system Settings: notifications start working without reinstalling.
- Logged out: no more pushes for that account. Log in as another user: you get only their pushes.
- The order was canceled or deleted: tapping an old push shows a friendly message, not a crash or blank screen.
- Phone offline when the push is sent: when it comes back online, the notification arrives (once) or is skipped if it’s out of date.
Edge cases
- Reinstall the app: still exactly one notification per event (no duplicate tokens).
- Two devices with the same account: both get the push; reading it on one doesn’t break the other.
- Many pushes quickly (five status changes in a few seconds): grouped, in the right order, no duplicates.
- Time zones and quiet hours: change the phone’s time zone; offers respect the user’s local time. Try the night of a daylight saving time change.
- Do Not Disturb / Focus on: notifications arrive silently and are still there afterwards.
- Low Power Mode and Android battery savers: pushes still arrive (maybe delayed).
- App update and OS update: permission and in-app settings are kept.
- Long text and other languages: German title, a restaurant name with emoji or accents (
Café Zürich), right-to-left languages.
UI & usability
- Text is short, specific and useful: “Marco is 2 minutes away”, not “Order update”.
- The right icon, app name and image; the image failing to load doesn’t block the notification.
- Notifications are grouped per order or chat.
- Sounds and vibration fit the importance (no loud sound for marketing).
- Permission is asked in context, with an explanation first.
Security & privacy
- No sensitive data in the preview text: no full card numbers, passwords, one-time codes for other services, health results. Check the lock screen with Show Previews: When Unlocked too.
- Deep links from a push can’t open screens the user isn’t allowed to see (e.g. another user’s order).
- After logout or account deletion, the device token is removed on the server.
Accessibility
- With a screen readerScreen readerSoftware that reads the screen aloud for blind and low-vision users, like VoiceOver or TalkBack.Open in glossary → (VoiceOver, TalkBack) the notification is read in a sensible order: app name, title, text.
- Notifications aren’t the only way to get important information: the same status is visible in the app.
- Don’t rely on sound or vibration alone (deaf users, phones on silent).
- Actions have clear names, and the in-app banner is announced by screen readers.
Example test cases
TC-PUSH-001Tap push from killed app opens the orderpositivehigh
Preconditions
- Notifications allowed
- Active order #4821
- App force quit
Steps
- Send "Picked up" for order #4821 from the staging console
- Wait for the notification
- Tap it
Expected result
The app starts and opens Order #4821 with status "On the way".
TC-PUSH-002Foreground push shows in-app banner and updates the screenpositivehigh
Preconditions
- App open on Order #4821 with status "On the way"
Steps
- Send "Driver arriving" from the staging console
- Watch the screen without touching it
Expected result
An in-app banner "Driver arriving" appears and the status changes to "Arriving" without a manual refresh.
TC-PUSH-003Turning off offers stops promo pushesnegativehigh
Preconditions
- Offers turned on
- App in the background
Steps
- Open the app › Notification settings
- Turn off "Offers & promotions"
- Go to the Home screen
- Send a promo from the staging console
Expected result
No notification arrives. The console shows the user has opted out of offers.
TC-PUSH-004No duplicate notifications after reinstalledgemedium
Preconditions
- User logged in on one device
Steps
- Delete and reinstall the app
- Log in and allow notifications
- Send "Order confirmed"
Expected result
Exactly one notification arrives. The server has one active token for this device.
TC-PUSH-005Push text in German has no placeholdersedgemedium
Preconditions
- Phone language set to Deutsch
Steps
- Send each order event from the staging console
- Read every notification
Test data
Restaurant: Pasta Roma, driver: Marco
Expected result
All texts are German, names are filled in (e.g. "Pasta Roma bereitet dein Essen zu."), nothing is cut off.
TC-PUSH-006Offers respect quiet hours in the user time zoneedgemedium
Preconditions
- Server time 20:30 UTC
- Phone time zone Berlin (21:30 local)
Steps
- Send a promo from the staging console
- Change the time zone to New York (15:30 local) and send again
Expected result
Berlin: the promo is held until 08:00 local time. New York: the promo is delivered.
TC-PUSH-007Permission denied, then enabled in Settingsnegativemedium
Preconditions
- Fresh install
Steps
- Place the first order and tap "Don't Allow" on the permission dialog
- Open the app's notification settings
- Follow the link to system Settings and allow notifications
- Send "Picked up"
Expected result
The in-app screen explains notifications are off and links to Settings. After allowing, the push arrives.
TC-PUSH-008Badge clears after reading updatespositivelow
Preconditions
- App in the background
- Badge is 0
Steps
- Send three order updates
- Check the app icon
- Open the app and the order
- Go back to the Home screen
Expected result
The icon shows 3, then the badge is gone after the order was opened.
Tip: click a test case to expand it.
Mobile vs web
- One chance to ask. The permission dialog appears once per install. Test the “Don’t Allow” path carefully. To see the dialog again, delete and reinstall the app.
- Settings › Notifications › App lets users turn off banners, sounds, badges, lock screen and previews separately. Try each.
- Focus modes and Scheduled Summary can delay or bundle notifications. Time-sensitive ones may break through.
- Simulator:
xcrun simctl pushor drag an.apnsfile onto the Simulator. A TestFlight or App Store build uses the production APNs environment, while a debug build from Xcode uses the sandbox, so a token from one doesn’t work with the other. A common “push doesn’t arrive” cause in testing. - Foreground: iOS doesn’t show the banner unless the app asks for it. Check the app does something.
- Android 13+ asks with
POST_NOTIFICATIONS; older versions don’t ask at all. Test on both. - Channels: check Settings › Apps › QuickBite › Notifications. Each channel should have a clear name, sensible importance and should be switchable on its own.
- Manufacturers differ. Battery savers on some Samsung, Xiaomi or Huawei phones delay or block pushes for apps in the background. Include such phones in your device matrix.
- Force stop means no pushes until the user opens the app again. That’s expected platform behavior.
- Foreground: FCM hands the message to the app instead of showing it. The app must show its own in-app banner or notification.
- Debugging: developers can log the FCM token and incoming messages to logcatLogcatAndroid's system log, read with adb logcat, where apps write errors and crash details.Open in glossary →, read with adbadbAndroid Debug Bridge: a command-line tool for installing apps, reading logs and controlling Android devices.Open in glossary → or Android Studio.
- Web push works in Chrome, Edge, Firefox and Safari through a background script called a service worker. On iPhone and iPad (iOS 16.4+), only for web apps added to the Home Screen.
- The browser asks (“shop.example.com wants to show notifications”). Many browsers only allow this after a user click, and Chrome hides prompts from sites that ask too aggressively. Test the prompt is triggered by a clear button, not on page load.
- Blocked is blocked. If the user clicks Block, the site can’t ask again; the user must change it in the browser’s site settings. The site should explain how.
- Operating system settings can also block browser notifications (macOS, Windows Focus Assist).
- Test in DevTools: in Chrome, Application › Service workers has a Push button that sends a test message to the service worker.
- Unsubscribe and clearing site data must stop the notifications.
Checklist
Push notifications checklist
0 of 14 checked
Check your understanding
Quick quiz
0/5 answered · 0 correct
1.Who actually delivers a push notification to an iPhone?
2.Tapping a push opens the order when the app is in the background, but opens Home when the app was killed. What should you test next?
3.A user turned off "Offers" in the app but still gets promo pushes. Where is the bug most likely?
4.On iOS, the user tapped "Don't Allow" on the notification dialog. What should the app do later?
5.A promo arrives at 3 am for users in New York. Which requirement probably uses the wrong time?
Key takeaways
- Push goes app → token → your server → APNs/FCM → device. Bugs can hide at every hop.
- Permission is asked once on iOS (and denied for good after two “no”s on Android 13+), so ask in context and always offer a way back via Settings.
- Test every push in foreground, background and killed states, and check that tapping opens the right screen.
- Classic bugs: duplicates, opt-outs that don’t sync, raw placeholders or wrong language, badges that never clear, wrong time zone for quiet hours.
- Trigger pushes yourself with a staging console, Firebase test messages or
simctl push, and confirm on real devices before release.
Finished this page?Nice work, this page is complete!
Mark it as done to track your progress. It's saved in this browser, no account needed.
Its review cards are now in Bites ⚡