NFC & QR

NFC: Card Writing System

This page covers the NFC card writing system: the Web NFC API, URL format, writing flow, and the support hooks that make it all work.


What is Web NFC?

The Web NFC API lets websites read and write to NFC (Near Field Communication) tags using compatible Android devices. It's a browser API, so no app installation is needed!

Browser Support

Web NFC is only supported on Chrome for Android. It does NOT work on:

  • iPhones / Safari (Apple blocks Web NFC)
  • Desktop browsers
  • Firefox, Edge on mobile

This means NFC card writing must be done from an Android phone or tablet. The system handles unsupported devices gracefully with a fallback.


NFC URL Format

Each NFC card is written with a URL that points to the user's profile:

// From nfcUtils.ts
export const generateNfcProfileUrl = (token: string): string => {
  const stage = process.env.NEXT_PUBLIC_REACT_APP_STAGE;

  if (stage === "production") {
    return `https://app.ubcbiztech.com/profile/${token}?scan=true`;
  } else if (stage === "local") {
    return `http://localhost:3000/profile/${token}?scan=true`;
  } else {
    return `https://dev.app.ubcbiztech.com/profile/${token}?scan=true`;
  }
};

The token is the user's profile UUID. The ?scan=true parameter tells the profile page it was opened via NFC scan (which triggers the connection flow).


The NFC Writing Flow

There are two components that work together:

1. NFCPopup (the preview)

NFCPopup.tsx is shown after a successful check-in. It displays:

  • The user's name and profile picture (or a generated avatar from DiceBear)
  • A "Write to Card" button
  • A fallback for unsupported devices
┌─────────────────────────────┐
[Profile Picture]
│                              │
Alex does not have a        │
│  membership card.            
│                              │
[Write to Card]
│                              │
[close]
└─────────────────────────────┘

If the device doesn't support NFC, the popup shows the NFC URL as text with a "Copy" button. This lets the admin copy the URL and use a separate NFC writing app.

2. NFCWriter (the actual writer)

NFCWriter.tsx handles the physical NFC writing process. It uses the Web NFC API:

// Create NFC reader/writer
const ndef = new NDEFReader();

// Start scanning for nearby NFC tags
await ndef.scan();

// When a tag is detected...
ndef.addEventListener("reading", async (event) => {
  // Write the profile URL to the tag
  await ndef.write({
    records: [{ recordType: "url", data: nfcUrl }]
  });

  // Update the user's card count in the database
  await fetchBackend({
    endpoint: `/members/${email}`,
    method: "PATCH",
    data: { cardCount: numCards + 1 }
  });
});

Writer States

The writer goes through these visual states:

StateVisualDescription
loadingPulsing blue ringsChecking NFC support
readyBlue rings + iconReady, waiting for tag
writingBlue rings + iconNFC tag detected, writing...
completedBlue rings + warningUser already has a card (but can still write)
successGreen rings + ✓Successfully written!
errorRed rings + ✗Something went wrong
not_supportedError messageDevice doesn't support NFC
non_memberError messageUser isn't a member

The visual design uses animated concentric rings that change color based on the state: blue for neutral, green for success, red for error.


Timeout Handling

The writer has a 10-second timeout. If no NFC tag is detected within 10 seconds, it shows an error with a "Try Again" button:

const setOpTimeout = () => {
  timeoutIdRef.current = window.setTimeout(() => {
    setStatus("error");
    setErrorMessage("Operation timed out. Please try again.");
  }, 10000);
};

Auto-Close

After a successful write, the component automatically closes after 3 seconds:

window.setTimeout(() => {
  if (mountedRef.current) closeAll();
}, 3000);

NFC Support Hooks

useNFCSupport

A simple hook that checks if the current browser supports Web NFC:

const { isNFCSupported, isLoading } = useNFCSupport();

It checks for the NDEFReader object on the window:

const hasNFC = "NDEFReader" in window;

useUserNeedsCard

Checks whether a user needs a membership card by looking up their member record:

const { checkUserNeedsCard, isLoading, error } = useUserNeedsCard();

const result = await checkUserNeedsCard("jane@student.ubc.ca");
// result = { needsCard: true, profileID: "abc-123-..." }

The logic:

  1. Fetch the member record via GET /members/{email}
  2. If member not found (404) → doesn't need card (not a member)
  3. If cardCount > 0 → doesn't need card (already has one)
  4. Otherwise → needs a card, return their profileID
Previous
QR Code Check-In