The feature request was simple and came up constantly. Someone downloads a ticket, taps the file, and ONEWallet is not in the list of apps that can open it. They have to remember the app has an import screen, open it, and go and find the file.
Fixing that took about ten times longer than the estimate, and the reason is a decent illustration of how Android file handling actually works.
Android does not have file types
There is no registry mapping extensions to applications. What there is instead is a matching system: an app declares intent filters describing what it can handle, and the system matches an incoming intent against them.
For a file, the intent carries a URI and, usually, a MIME type. So your filter says "I handle application/vnd.apple.pkpass" and everything works.
Except that the MIME type is whatever the app that produced the intent decided to put there, and they do not agree.
A browser sets the type from the HTTP Content-Type header. If the server is configured correctly, you get the pass MIME type. Plenty of servers are not, and send application/octet-stream or application/zip — a PKPASS is a zip archive, so this is not even wrong.
A file manager often guesses from the extension, using a table that may or may not include this one. Unknown extensions become application/octet-stream or, on some, */*.
A mail client uses the MIME type from the message part. Senders get this wrong regularly.
A messaging app may have stripped or replaced it entirely.
So a filter matching only the correct MIME type appears in the list roughly some of the time, and the times it does not are indistinguishable to the user from the app not supporting the feature at all.
Matching on the path instead
The other approach is to match the URI's path against a pattern — effectively, on the file extension.
This has its own set of problems, and they are the kind that only surface in the field.
Pattern matching for paths is limited and the syntax is not what you would expect from a regular expression, which means the obvious pattern for "ends in .pkpass" does not do what it looks like it does. Getting it right requires understanding a matching rule that most people, us included, first learn about by writing it wrong.
Case is not handled. A file named .PKPASS requires its own filter. So does any other capitalisation, and yes, they occur.
And a great many URIs have no useful path at all. A content URI from a modern file provider is frequently an opaque identifier with no filename in it. There is nothing to pattern match against.
The workable answer is all of the above at once: filters for the correct MIME type, for the generic types that file managers and browsers substitute, and for path patterns in the capitalisations that occur. It is inelegant, it is a lot of manifest, and it is what makes the app show up when the user taps the file.
Which means we must handle files that are not passes
Registering for application/octet-stream means being offered files that have nothing to do with us. A photograph, a document, an installer.
So the import path validates rather than assumes. A PKPASS is a zip containing a pass.json and a manifest, and we check that structure before doing anything else. If the file is not one, the app says so plainly — this file is not a pass — rather than failing in a way that suggests the pass is broken.
That distinction matters more than it sounds. "This is not a pass file" sends the user back to their email to find the right attachment. A generic import failure sends them to us, convinced their ticket is corrupt.
We also handle the case where a content URI gives us no filename. The pass's own contents supply the name, which is better than a filename anyway — passes are frequently delivered as download.pkpass or a string of digits.
The notification that was too pleased with itself
Second part of the release, and a much smaller thing that people cared about more.
ONEWallet can put a persistent notification in the shade as a shortcut to a card, so a frequently used pass is reachable from the lock screen without opening the app.
The original implementation was a normal-importance notification, re-posted when the card was updated. Which meant a shortcut, whose entire purpose is to sit quietly and be available, would occasionally make a sound and appear as a heads-up banner. A wallet interrupting you to announce that it is still a wallet.
It is now on a dedicated low-importance channel: present in the shade, silent, no banner, no re-announcement on update. It is also updated in place rather than re-posted, so it never re-triggers.
The general point, which we keep relearning: an ongoing notification and an alerting notification are different things that share an API, and the default is the alerting one. If a notification's job is to be available rather than to be noticed, its channel importance should say so.
Importing well
Import when you have a minute, not at the gate. The most common bad moment for this is standing in a queue discovering that the pass you thought you had is still an attachment in an email. Do it the evening before.
Check the pass after importing. Name, dates, seat or reference number, and that the barcode is present and renders. A pass can import successfully and be the wrong pass, or last year's version of the right one.
Keep the original message until the trip is over. Issuers reissue passes — a changed gate, a rescheduled time — and the reissued file arrives where the first one did. The imported copy does not update itself.
Watch for passes with rotating barcodes. Some transit and event passes use a code that changes over time and is validated against a service. A static copy of one of those will not work at the gate. This is not something a wallet app can fix, and it is something you want to discover before you travel.
What we have not implemented
The pass format includes a mechanism for issuers to push updates — a service URL and a token that let a wallet fetch a new version when something changes. We do not implement it. Doing so means network requests to issuer endpoints carrying a token, which is a meaningful change to an app that currently sends nothing anywhere, and it is not a change we want to make quietly.
The practical consequence: if your flight changes gate, ONEWallet will not know. The email will.