Workflow: Identifier Scan Flow
Workflow: Identifier Scan Flow
This flow describes what happens immediately after a QR or NFC identifier is scanned. The goal is to decide what the user sees next and which backend calls are triggered.
Resolver Contract
resolveIdentifier(input) → {
success: boolean,
routing: { action, destination },
object: ObjectContext | null, // null = no object materialized for this identifier yet
scanEvent, error
}
“Unknown” means no object has been materialized for this identifier yet (lazy creation) — distinct from an unrecognised/invalid identifier, which surfaces as
error.
Branches
1. Unknown Tag
| Variant | UI Dialog | Primary Buttons |
|---|---|---|
| Unknown, General Class | “We don’t recognise this item.” | • Create new object (pre-attaches identifier) • Tag existing object (opens search) |
| Unknown, Specific Class | Shows class name + placeholder image | • Create new object (pre-fills class) • Tag existing object (opens search) • Cancel |
“Tag existing object” is available for both general and specific classes — the class pointer is a loose assertion (see Identifier–Object Lifecycle), not a binding. The general/specific distinction only changes what “Create new object” pre-fills.
2. Known Object
| User Rights | Behaviour |
|---|---|
canEdit = true |
Navigate straight to Object Detail with full edit actions |
false |
Load Object Detail in Read-Only mode. Banner shows owning org. If object has sale/rent flags, show Purchase / Rent / Borrow CTA |
3. Background Actions
- Every scan triggers
createScanEvent()(audit & analytics).
4. Error & Edge Cases
- Invalid code → toast “Identifier not valid”.
- Duplicate tag → list of candidate objects to pick from.
- Offline – cache last success & queue events until online.
GraphQL Operations
mutation resolveIdentifier(input: ResolveIdentifierInput!)– resolve a scanned tag (returnssuccess,routing,object).mutation assignTagToObject(objectId: ID!, tagData: TagDataInput!)– if the user chooses Tag existing object. Returns the object, the assigned short code, or aconflictif the tag is already on another object.mutation createObject(input: CreateObjectInput!)– if the user creates a new object (passtagInstanceKey/tagDatato pre-attach the scanned tag).
The earlier
linkIdentifierToObject/createObjectWithIdentifiernames were never implemented and assumed the identifier already existed as a node. The real flow passes scannedtagDataand upserts the identifier (lazy creation — see Identifier–Object Lifecycle).
Object-First Tagging (assign-tag-v2)
The scan-first flow above requires the user to begin with a physical scan. As of assign-tag-v2, a parallel object-first path is available: the user opens an object directly from the app’s spatial navigator and tags it from there — no prior scan needed.
Why this is now reachable: v1 deferred object-first tagging because the only way to reach an object in the app was to scan its existing tag — making an untagged object unreachable (a chicken-and-egg). That premise no longer holds. The iOS app is local-first: the spatial navigator builds the complete object graph from myObjectsLight and lets the user open any object — including objects that were imported, created by name, or never scanned — straight from the spatial tree. Object-first tagging corrects v1’s “presupposes the object is already tagged” framing: the path is reachable for any object, tagged or not.
Object-First Flow
- User opens an object from the navigator.
- Has tags: a compact tag affordance row in the header (short code +
+Nsuffix if multiple) — tap to open the Taggar management sheet. - No tags: a prominent “Tagga objekt” button in the header — launches the scanner directly.
- Has tags: a compact tag affordance row in the header (short code +
- “Lägg till tagg” (in the sheet) or “Tagga objekt” (header button) presents the tag scanner.
- On scan,
resolveIdentifierruns first (warn-don’t-steal — see inherited principle):- Unknown tag →
assignTagToObject(objectId, tagData)is called for this object. Short code appears in the header instantly (optimistic local update). - Tag already on another object → user is warned (“Taggen sitter redan på ‘X’”); nothing is written. No transfer is offered.
- Tag already on this object → informational “Redan kopplad”, no-op.
- Unknown tag →
- The server
assignTagToObjectconflictreturn remains the race backstop: if a conflict slips past the scan-time check (race condition), the queued job lands in the needs-attention queue.
The assignTagToObject mutation and its queue job (.assignTag) are reused unchanged from v1. The object-first path adds only a thin UI entry point and reuses the identical enqueue logic. Cross-link: Identifier–Object Lifecycle principle applies here unchanged — the tag’s class pointer never blocks attachment.
Unassign (assign-tag-v2)
A user-assigned (non-manufacturer) tag can be removed from an object via the Taggar sheet (swipe-to-delete). The removal is also available via the new mutation:
removeTagFromObject(objectId: ID!, shortCode: String!): RemoveTagResult!
type RemoveTagResult {
object: ObjectInstance # the object after removal
removed: Boolean! # true if an edge was deleted; false if not present (still success)
}
Key properties:
- Keyed on
shortCode, scoped to the object. The short code is the user-facing value and is unique-enough within a single object’s tag set to unambiguously identify which edge to drop. - Idempotent: removing a tag that is not on the object returns
removed: false— not an error. A retried queue job never fails with a spurious error. - Manufacturer tags are not removable (decision D1). A manufacturer tag is the genuineness anchor (key-derivation verification); the UI badges them and does not offer swipe-to-delete. The server also rejects removal attempts. See the genuineness verification plan for background.
- Removal is optimistic: the short code disappears from the header and sheet immediately; the
.removeTagqueue job syncs in the background; errors land in needs-attention.
New read field — identifiers
The object-detail query (getObjectDetailsComplete) now returns:
identifiers: [TagIdentifier!] # nullable; populated only by getObjectDetailsComplete, null on lightweight queries
type TagIdentifier { shortCode: String! isManufacturerTag: Boolean! }
This field powers the Taggar sheet: it provides each short code and the manufacturer flag the UI needs to badge tags and decide which ones are swipe-removable. It is populated only by the detail query (null on lightweight myObjectsLight queries, which carry only shortCodes). The long instanceKey is deliberately not exposed to clients.
Open Items / TODO
- Rate-limit scan events for spamming protection.
- Deep-link sharing of results (
/o/:idvs modal). - UI wireframes (link to Figma) for all dialog variants.