Lanyard has a screen called My Card. It shows a QR code, the other person points their camera at it, and their phone offers to add me as a contact. The QR holds a vCard, which is a few lines of text.
Early on, the contacts it produced were wrong in one specific way. On an iPhone they were saved under the company, not the person. A card for Amara Okafor at Vector Labs came in as "Vector Labs".
The card
This is the shape of what I was generating:
BEGIN:VCARD
VERSION:3.0
FN:Amara Okafor
ORG:Vector Labs
EMAIL:amara@example.com
END:VCARD
FN is the formatted name, the string to display. It is right there, and it is correct. The card scanned and the contact was saved. The only symptom was the name.
What was missing
vCard has a second name property, N, which holds the name in parts: family, given, additional, prefix, suffix, separated by semicolons.
I had left it out. iOS Contacts ignores FN entirely when N is absent and falls back to ORG for the display name.
I had also misread the spec. I believed N was optional. It is optional in vCard 4.0 (RFC 6350). My card said VERSION:3.0, and in 3.0 (RFC 2426, section 3.1.2) the rule for N is one sentence: "The property MUST be present in the vCard object." So the card was not valid, and nothing along the way said so.
The fix
Always emit N:
BEGIN:VCARD
VERSION:3.0
N:Okafor;Amara;;;
FN:Amara Okafor
ORG:Vector Labs
EMAIL:amara@example.com
END:VCARD
That one line is easy to write and harder to produce, because the app only ever has the name as a single string. Someone types "Amara Okafor" into one field. To fill in N, the code has to decide which part is the family name:
function nameToN(name: string): string {
const tokens = name.trim().split(/\s+/);
if (tokens.length === 1) return `N:;${escapeVCardValue(tokens[0])};;;`;
const family = tokens[tokens.length - 1];
const given = tokens.slice(0, -1).join(' ');
return `N:${escapeVCardValue(family)};${escapeVCardValue(given)};;;`;
}The last word becomes the family name and everything before it the given name. A single-word name goes in the given slot with the family slot left empty.
What that rule gets wrong
"The last word is the surname" is an Anglophone assumption. It splits a Spanish name with two surnames in the wrong place, and it gets most East Asian name order backwards.
I shipped it anyway, for one reason: the failure is smaller than the one it replaces. A wrong split still displays the right full name, because the phone puts the parts back together. No N displays the company. But it is a guess the app makes about people's names without asking them, and it is the part of this project I am least comfortable with.
The better version asks. Collect given and family name as separate fields when someone sets up their own card, store the split, and keep the guess only for scanned business cards, where one printed string really is all there is.
If you're here because of the symptom
- A vCard that imports on iPhone with the company as the contact's name is very likely missing
N. - If the card says
VERSION:3.0,Nis required. Add it. Nisfamily;given;additional;prefix;suffix. Escape commas, semicolons and backslashes inside each part.- I have not tested what iOS does with a 4.0 card that leaves
Nout, where the spec allows it. I would includeNeither way.
The rest of the app is written up in the Lanyard case study.