@proveanything/smartlinks 1.15.8 → 1.15.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/docs/API_SUMMARY.md +46 -1
- package/dist/docs/containers.md +2 -0
- package/dist/docs/item-context.md +184 -0
- package/dist/docs/utils.md +35 -2
- package/dist/docs/widgets.md +9 -0
- package/dist/openapi.yaml +62 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/itemContext.d.ts +30 -0
- package/dist/types/itemContext.js +2 -0
- package/dist/types/nfc.d.ts +38 -0
- package/dist/types/widgets.d.ts +15 -0
- package/dist/utils/conditions.d.ts +33 -3
- package/dist/utils/conditions.js +34 -1
- package/docs/API_SUMMARY.md +46 -1
- package/docs/containers.md +2 -0
- package/docs/item-context.md +184 -0
- package/docs/utils.md +35 -2
- package/docs/widgets.md +9 -0
- package/openapi.yaml +62 -0
- package/package.json +1 -1
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 1.15.
|
|
3
|
+
Version: 1.15.10 | Generated: 2026-07-19T13:35:07.171Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -5878,6 +5878,23 @@ interface InteractionEventContext {
|
|
|
5878
5878
|
|
|
5879
5879
|
**EffectConfig** = ``
|
|
5880
5880
|
|
|
5881
|
+
### itemContext
|
|
5882
|
+
|
|
5883
|
+
**ItemContext** (interface)
|
|
5884
|
+
```typescript
|
|
5885
|
+
interface ItemContext {
|
|
5886
|
+
isAuthentic: boolean
|
|
5887
|
+
status: ItemContextStatus
|
|
5888
|
+
source: 'nfc' | 'serial'
|
|
5889
|
+
errorMessage?: string
|
|
5890
|
+
isRescan?: boolean
|
|
5891
|
+
tag?: TagContext
|
|
5892
|
+
checkedAt: number
|
|
5893
|
+
}
|
|
5894
|
+
```
|
|
5895
|
+
|
|
5896
|
+
**ItemContextStatus** = ``
|
|
5897
|
+
|
|
5881
5898
|
### jobs
|
|
5882
5899
|
|
|
5883
5900
|
**Job** (interface)
|
|
@@ -6408,6 +6425,24 @@ interface NfcClaimTagRequest {
|
|
|
6408
6425
|
}
|
|
6409
6426
|
```
|
|
6410
6427
|
|
|
6428
|
+
**TagContext** (interface)
|
|
6429
|
+
```typescript
|
|
6430
|
+
interface TagContext {
|
|
6431
|
+
status: TagStatus
|
|
6432
|
+
tagId?: string
|
|
6433
|
+
claimSetId?: string
|
|
6434
|
+
codeId?: string
|
|
6435
|
+
count?: number
|
|
6436
|
+
previousCount?: number
|
|
6437
|
+
data?: Record<string, unknown>
|
|
6438
|
+
source: 'nfc-validate' | 'nfc-lookup' | 'tag-index' | 'none'
|
|
6439
|
+
validatedAt: number
|
|
6440
|
+
errorMessage?: string
|
|
6441
|
+
}
|
|
6442
|
+
```
|
|
6443
|
+
|
|
6444
|
+
**TagStatus** = `'valid' | 'rescan' | 'invalid' | 'error'`
|
|
6445
|
+
|
|
6411
6446
|
### order
|
|
6412
6447
|
|
|
6413
6448
|
**OrderItem** (interface)
|
|
@@ -7692,6 +7727,15 @@ interface SmartLinksWidgetProps {
|
|
|
7692
7727
|
* accepted for backward compatibility.
|
|
7693
7728
|
onNavigate?: (request: NavigationRequest | string) => void
|
|
7694
7729
|
publicPortalUrl?: string
|
|
7730
|
+
* Authenticity context for the specific item (proof) the URL points at,
|
|
7731
|
+
* resolved via an NFC tap or a serial proof URL. `undefined` for
|
|
7732
|
+
* collection- and product-only URLs, where there's no item to verify.
|
|
7733
|
+
* See docs/item-context.md.
|
|
7734
|
+
itemContext?: ItemContext
|
|
7735
|
+
* @deprecated Use `itemContext.tag` instead. Kept for one release for
|
|
7736
|
+
* backward compatibility with scanner-aware apps that read raw NFC/SUN
|
|
7737
|
+
* data directly. See docs/item-context.md.
|
|
7738
|
+
tag?: TagContext
|
|
7695
7739
|
size?: 'compact' | 'standard' | 'large'
|
|
7696
7740
|
lang?: string
|
|
7697
7741
|
translations?: Record<string, string>
|
|
@@ -7861,6 +7905,7 @@ interface ConditionParams {
|
|
|
7861
7905
|
product?: ProductInfo
|
|
7862
7906
|
proof?: ProofInfo
|
|
7863
7907
|
collection?: CollectionInfo
|
|
7908
|
+
itemContext?: ItemContext
|
|
7864
7909
|
stats?: StatsInfo
|
|
7865
7910
|
fetchCondition?: (collectionId: string, conditionId: string) => Promise<ConditionSet | null>
|
|
7866
7911
|
getLocation?: () => Promise<{ latitude: number; longitude: number }>
|
package/dist/docs/containers.md
CHANGED
|
@@ -231,6 +231,8 @@ All standard widget props apply:
|
|
|
231
231
|
| `productId` | string | ❌ | Product context |
|
|
232
232
|
| `proofId` | string | ❌ | Proof context |
|
|
233
233
|
| `user` | object | ❌ | Current user info |
|
|
234
|
+
| `itemContext` | `ItemContext` | ❌ | Authenticity context for the specific item (proof) the URL points at; `undefined` for collection/product-only URLs — see [item-context.md](item-context.md) |
|
|
235
|
+
| `tag` | `TagContext` | ❌ | **Deprecated** — use `itemContext.tag` instead. Kept for one release. |
|
|
234
236
|
| `onNavigate` | function | ❌ | Navigation callback (accepts `NavigationRequest` or legacy string) |
|
|
235
237
|
| `size` | string | ❌ | `"compact"`, `"standard"`, or `"large"` |
|
|
236
238
|
| `lang` | string | ❌ | Language code (e.g., `"en"`) |
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Item Context (container prop)
|
|
2
|
+
|
|
3
|
+
> **Copy this file into `node_modules/@proveanything/smartlinks/docs/item-context.md`** in the published SDK package.
|
|
4
|
+
|
|
5
|
+
When the URL points at a specific item — either a **serial proof URL** or an
|
|
6
|
+
**NFC tap** — the portal derives an `ItemContext` describing what it found
|
|
7
|
+
and hands it to the container as the **`itemContext`** prop.
|
|
8
|
+
|
|
9
|
+
For collection- and product-only URLs there is no item to describe, so
|
|
10
|
+
`itemContext` is `undefined`. Sub-apps that don't care about authenticity
|
|
11
|
+
can simply ignore the prop.
|
|
12
|
+
|
|
13
|
+
The prop is named `itemContext` (not `item`) to make clear it's *context
|
|
14
|
+
about the item*, not the item record itself. The underlying record is
|
|
15
|
+
passed separately via `proof`, `product` and `collection` props.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
export default function MyContainer({ itemContext, collection, product, proof }) {
|
|
19
|
+
if (!itemContext) {
|
|
20
|
+
// Collection or product view — nothing to verify.
|
|
21
|
+
return <BrowseView collection={collection} product={product} />;
|
|
22
|
+
}
|
|
23
|
+
if (itemContext.isAuthentic) {
|
|
24
|
+
return <RealContent proof={proof} />;
|
|
25
|
+
}
|
|
26
|
+
if (itemContext.status === 'not-found' || itemContext.status === 'invalid') {
|
|
27
|
+
return <NotGenuine message={itemContext.errorMessage} />;
|
|
28
|
+
}
|
|
29
|
+
// status === 'error' — transport failure
|
|
30
|
+
return <TransientError message={itemContext.errorMessage} />;
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Shape
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
interface ItemContext {
|
|
40
|
+
/** True only for `valid` and `rescan`. Read this for the simple case. */
|
|
41
|
+
isAuthentic: boolean;
|
|
42
|
+
|
|
43
|
+
status:
|
|
44
|
+
| 'valid' // genuine
|
|
45
|
+
| 'rescan' // genuine, but this exact NFC tap has been seen before
|
|
46
|
+
| 'invalid' // SDK rejected the NFC payload (bad crypto)
|
|
47
|
+
| 'not-found' // identifier well-formed but 404
|
|
48
|
+
| 'error'; // transport / network failure — cannot tell
|
|
49
|
+
|
|
50
|
+
source: 'nfc' | 'serial';
|
|
51
|
+
|
|
52
|
+
/** Set for `invalid` / `not-found` / `error`. Safe to display. */
|
|
53
|
+
errorMessage?: string;
|
|
54
|
+
|
|
55
|
+
/** True when SDK reports this exact NFC tap was seen before. */
|
|
56
|
+
isRescan?: boolean;
|
|
57
|
+
|
|
58
|
+
/** Full NFC payload (SUN counter, tagId, claimSetId, codeId, mirror…). Present only when source === 'nfc'. */
|
|
59
|
+
tag?: TagContext;
|
|
60
|
+
|
|
61
|
+
/** Epoch ms when the check was derived. */
|
|
62
|
+
checkedAt: number;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Also exported from `@proveanything/smartlinks` as `ItemContext` /
|
|
67
|
+
`ItemContextStatus` (`src/types/itemContext.ts`). The nested `TagContext`
|
|
68
|
+
shape is unchanged (`src/types/nfc.ts`).
|
|
69
|
+
|
|
70
|
+
## When `itemContext` is present
|
|
71
|
+
|
|
72
|
+
| URL points at | itemContext |
|
|
73
|
+
| ---------------------------------------------- | -------------------- |
|
|
74
|
+
| Collection root | `undefined` |
|
|
75
|
+
| Product page (no proof) | `undefined` |
|
|
76
|
+
| Serial proof URL (any outcome) | present, source `serial` |
|
|
77
|
+
| NFC tap (any outcome) | present, source `nfc` |
|
|
78
|
+
|
|
79
|
+
## Status matrix (when present)
|
|
80
|
+
|
|
81
|
+
| Situation | source | status | isAuthentic |
|
|
82
|
+
| ----------------------------------------------------- | -------- | ------------- | :---------: |
|
|
83
|
+
| Fresh NFC tap, SUN counter validated | `nfc` | `valid` | ✅ |
|
|
84
|
+
| Same NFC tap replayed / refresh | `nfc` | `rescan` | ✅ |
|
|
85
|
+
| NFC payload rejected (bad crypto / bad params) | `nfc` | `invalid` | ❌ |
|
|
86
|
+
| Serial proof URL, proof fetched OK | `serial` | `valid` | ✅ |
|
|
87
|
+
| Serial proof URL, proof returned 404 | `serial` | `not-found` | ❌ |
|
|
88
|
+
| Network / 5xx while resolving proof or NFC | (either) | `error` | ❌ |
|
|
89
|
+
|
|
90
|
+
`isAuthentic === (status === 'valid' || status === 'rescan')`.
|
|
91
|
+
|
|
92
|
+
## How it's delivered
|
|
93
|
+
|
|
94
|
+
**Direct-mounted containers** (default public mode):
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
type Props = {
|
|
98
|
+
collection: Collection;
|
|
99
|
+
product?: Product;
|
|
100
|
+
proof?: Proof;
|
|
101
|
+
itemContext?: ItemContext; // ← only when URL targets a specific item
|
|
102
|
+
// …plus the normal navigation / theme / auth props
|
|
103
|
+
};
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
NFC-aware apps that need the raw SUN / tag payload read `itemContext.tag`.
|
|
107
|
+
|
|
108
|
+
**Iframe containers**: the portal serialises `ItemContext` to JSON, base64-
|
|
109
|
+
encodes it (UTF-8 safe) and appends it as an `itemContext` query param on
|
|
110
|
+
the iframe URL — omitted entirely when there is no item context:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
function readItemContext(): ItemContext | undefined {
|
|
114
|
+
const raw = new URLSearchParams(location.search).get('itemContext');
|
|
115
|
+
if (!raw) return undefined;
|
|
116
|
+
try { return JSON.parse(decodeURIComponent(escape(atob(raw)))); }
|
|
117
|
+
catch { return undefined; }
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Guarantees
|
|
122
|
+
|
|
123
|
+
- `itemContext` is present on every container mount whose URL targeted an
|
|
124
|
+
item (proof or NFC tap). Absent otherwise.
|
|
125
|
+
- `SL.nfc.validate` is called at most once per `(claimSetId, codeId)` per
|
|
126
|
+
browser session — SUN counters are never double-consumed on refresh.
|
|
127
|
+
- SSR uses `SL.nfc.lookupTag` (non-consuming) only.
|
|
128
|
+
- `isAuthentic === true` implies the SDK either validated an NFC payload
|
|
129
|
+
or returned a full proof document. It is never inferred from URL shape.
|
|
130
|
+
- The portal will **not** render its full-page error surface for a 404 on
|
|
131
|
+
a proof — that surfaces as `status: 'not-found'` on `itemContext`, and
|
|
132
|
+
the container is still mounted so it can present the message its own way.
|
|
133
|
+
|
|
134
|
+
## What containers should do
|
|
135
|
+
|
|
136
|
+
| Status | Recommended UI |
|
|
137
|
+
| ------------- | ------------------------------------------------------------------ |
|
|
138
|
+
| `valid` | Full experience. Show ownership, actions, etc. |
|
|
139
|
+
| `rescan` | Same as `valid`, optionally with a subtle "seen before" hint. |
|
|
140
|
+
| `invalid` | Clear "this doesn't look genuine" message. Do NOT reveal data. |
|
|
141
|
+
| `not-found` | "We don't recognise this item." Offer a link home. |
|
|
142
|
+
| `error` | Neutral "couldn't verify right now." Offer retry. |
|
|
143
|
+
| _absent_ | Standard browse UI. Don't mention authenticity at all. |
|
|
144
|
+
|
|
145
|
+
## Gating UX with conditions
|
|
146
|
+
|
|
147
|
+
Sub-apps should standardize on passing `itemContext` into `validateCondition()`
|
|
148
|
+
and gating with `itemStatus` — the same condition type already used for
|
|
149
|
+
claim/virtual checks, extended with authenticity `statusType`s that read
|
|
150
|
+
`itemContext` instead of `proof`:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { validateCondition } from '@proveanything/smartlinks';
|
|
154
|
+
|
|
155
|
+
const showFakeWarning = await validateCondition({
|
|
156
|
+
condition: {
|
|
157
|
+
conditions: [{ type: 'itemStatus', statusType: 'invalidProof' }],
|
|
158
|
+
},
|
|
159
|
+
itemContext, // the prop delivered to the container
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- `isAuthentic` — passes when `itemContext.isAuthentic` is true (`valid`/`rescan`)
|
|
164
|
+
- `notAuthentic` — passes when an `itemContext` was resolved but isn't authentic (`invalid`/`not-found`/`error`)
|
|
165
|
+
- `invalidProof` — passes specifically when resolution was attempted and came back `invalid` or `not-found` — the "someone scanned a fake tag / typed a bad serial" case, as distinct from `noProof` ("nothing was on the URL to check at all")
|
|
166
|
+
- `isFirstScan` — the single "this is good, show the full experience" check: authentic AND this is the first time it's been seen (`status === 'valid'`)
|
|
167
|
+
- `isRescan` — authentic but a duplicate/replayed tap (`status === 'rescan'`) — use this to suppress "first scan" celebration UX without treating the tag as fake; `isAuthentic` alone is true for both `isFirstScan` and `isRescan` cases
|
|
168
|
+
|
|
169
|
+
## Deprecated
|
|
170
|
+
|
|
171
|
+
The old top-level `tag` prop (`TagContext`) is still forwarded for one
|
|
172
|
+
release for backward compatibility with scanner-aware apps that read raw
|
|
173
|
+
SUN data. New code should read `itemContext.tag` and treat `itemContext`
|
|
174
|
+
as the source of truth.
|
|
175
|
+
|
|
176
|
+
## Note on terminology
|
|
177
|
+
|
|
178
|
+
Today the SDK still uses `proof` for a specific item record (a serialised
|
|
179
|
+
instance of a product). "Item record" is under consideration as a clearer
|
|
180
|
+
name — no API changes are planned imminently, but a future SDK version may
|
|
181
|
+
support both `proof`- and `item record`-shaped names for the same
|
|
182
|
+
functions/types side by side. `itemContext` is deliberately future-proof:
|
|
183
|
+
it describes context about whatever the URL points at, regardless of what
|
|
184
|
+
we ultimately call the record itself.
|
package/dist/docs/utils.md
CHANGED
|
@@ -485,9 +485,12 @@ await utils.validateCondition({
|
|
|
485
485
|
```
|
|
486
486
|
|
|
487
487
|
#### Item Status Conditions
|
|
488
|
-
Check proof/item status
|
|
488
|
+
Check proof/item status — claim/ownership state, and authenticity of the
|
|
489
|
+
item (proof) the URL points at (via NFC tap or serial proof URL — see
|
|
490
|
+
[item-context.md](item-context.md)):
|
|
489
491
|
|
|
490
492
|
```typescript
|
|
493
|
+
// Claim/ownership — unchanged, reads `proof`
|
|
491
494
|
await utils.validateCondition({
|
|
492
495
|
condition: {
|
|
493
496
|
type: 'and',
|
|
@@ -498,9 +501,38 @@ await utils.validateCondition({
|
|
|
498
501
|
},
|
|
499
502
|
proof: { claimable: true }
|
|
500
503
|
})
|
|
504
|
+
|
|
505
|
+
// Authenticity — reads `itemContext`
|
|
506
|
+
await utils.validateCondition({
|
|
507
|
+
condition: {
|
|
508
|
+
type: 'and',
|
|
509
|
+
conditions: [{
|
|
510
|
+
type: 'itemStatus',
|
|
511
|
+
statusType: 'invalidProof'
|
|
512
|
+
}]
|
|
513
|
+
},
|
|
514
|
+
itemContext: { isAuthentic: false, status: 'invalid', source: 'nfc', checkedAt: Date.now() }
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
// Fresh scan vs. duplicate/replayed scan — both authentic, different UX
|
|
518
|
+
await utils.validateCondition({
|
|
519
|
+
condition: {
|
|
520
|
+
type: 'and',
|
|
521
|
+
conditions: [{
|
|
522
|
+
type: 'itemStatus',
|
|
523
|
+
statusType: 'isRescan'
|
|
524
|
+
}]
|
|
525
|
+
},
|
|
526
|
+
itemContext: { isAuthentic: true, status: 'rescan', source: 'nfc', checkedAt: Date.now() }
|
|
527
|
+
})
|
|
501
528
|
```
|
|
502
529
|
|
|
503
|
-
Status types:
|
|
530
|
+
Status types:
|
|
531
|
+
- Claim/ownership (reads `proof`): `'isClaimable'`, `'notClaimable'`, `'isVirtual'`, `'notVirtual'`
|
|
532
|
+
- Presence (reads `proof`): `'hasProof'`, `'noProof'` — `noProof` only means *nothing was attempted*, not that an attempt failed
|
|
533
|
+
- Authenticity (reads `itemContext`): `'isAuthentic'`, `'notAuthentic'`, `'invalidProof'`, `'isFirstScan'`, `'isRescan'`
|
|
534
|
+
- `invalidProof` is specifically "an identifier was passed and resolution came back `invalid`/`not-found`", distinct from `noProof`'s "nothing on the URL to check at all"
|
|
535
|
+
- `isAuthentic` is true for both a fresh tap and a rescan. Use `isFirstScan` (authentic AND `status === 'valid'`) as the single "this is good, show the full experience" check, and `isRescan` (authentic but `status === 'rescan'`) to suppress "first scan" celebration UX without treating the tag as fake
|
|
504
536
|
|
|
505
537
|
#### Version Conditions
|
|
506
538
|
For A/B testing or versioned content:
|
|
@@ -720,6 +752,7 @@ See [examples/utils-demo.ts](../examples/utils-demo.ts) for comprehensive exampl
|
|
|
720
752
|
- **[API Summary](API_SUMMARY.md)** - Complete SDK reference with all namespaces and functions
|
|
721
753
|
- **[QR Codes](API_SUMMARY.md#qr)** - QR code lookup functions that work with generated paths
|
|
722
754
|
- **[NFC](API_SUMMARY.md#nfc)** - NFC tag claiming and validation
|
|
755
|
+
- **[Item Context](item-context.md)** - `itemContext` prop delivered to containers describing the item (proof) the URL points at and whether it's authentic
|
|
723
756
|
- **[Collections](API_SUMMARY.md#collection)** - Collection management functions
|
|
724
757
|
- **[Products](API_SUMMARY.md#product)** - Product CRUD operations
|
|
725
758
|
- **[Batches](API_SUMMARY.md#batch)** - Batch management
|
package/dist/docs/widgets.md
CHANGED
|
@@ -159,6 +159,13 @@ interface SmartLinksWidgetProps {
|
|
|
159
159
|
// Base URL to the full public portal for deep linking
|
|
160
160
|
publicPortalUrl?: string;
|
|
161
161
|
|
|
162
|
+
// Authenticity context for the specific item (proof) the URL points at.
|
|
163
|
+
// undefined for collection- and product-only URLs.
|
|
164
|
+
itemContext?: ItemContext;
|
|
165
|
+
|
|
166
|
+
// @deprecated Use itemContext.tag instead — kept for one release
|
|
167
|
+
tag?: TagContext;
|
|
168
|
+
|
|
162
169
|
// Size hint for responsive rendering
|
|
163
170
|
size?: 'compact' | 'standard' | 'large';
|
|
164
171
|
|
|
@@ -180,6 +187,8 @@ interface SmartLinksWidgetProps {
|
|
|
180
187
|
| `SL` | `typeof SL` | Pre-initialized SmartLinks SDK |
|
|
181
188
|
| `onNavigate` | `function?` | Callback to navigate within parent app (accepts `NavigationRequest` or legacy string) |
|
|
182
189
|
| `publicPortalUrl` | `string?` | Base URL to full portal for deep links |
|
|
190
|
+
| `itemContext` | `ItemContext?` | Authenticity context for the specific item (proof) the URL points at; `undefined` for collection/product-only URLs — see [item-context.md](item-context.md) |
|
|
191
|
+
| `tag` | `TagContext?` | **Deprecated** — use `itemContext.tag` instead. Kept for one release. |
|
|
183
192
|
| `size` | `string?` | Size hint: 'compact', 'standard', or 'large' |
|
|
184
193
|
| `lang` | `string?` | Language code (e.g., 'en', 'de', 'fr') |
|
|
185
194
|
| `translations` | `object?` | Translation overrides |
|
package/dist/openapi.yaml
CHANGED
|
@@ -22203,6 +22203,31 @@ components:
|
|
|
22203
22203
|
- metadata
|
|
22204
22204
|
- broadcastId
|
|
22205
22205
|
- journeyId
|
|
22206
|
+
ItemContext:
|
|
22207
|
+
type: object
|
|
22208
|
+
properties:
|
|
22209
|
+
isAuthentic:
|
|
22210
|
+
type: boolean
|
|
22211
|
+
status:
|
|
22212
|
+
$ref: "#/components/schemas/ItemContextStatus"
|
|
22213
|
+
source:
|
|
22214
|
+
type: string
|
|
22215
|
+
enum:
|
|
22216
|
+
- nfc
|
|
22217
|
+
- serial
|
|
22218
|
+
errorMessage:
|
|
22219
|
+
type: string
|
|
22220
|
+
isRescan:
|
|
22221
|
+
type: boolean
|
|
22222
|
+
tag:
|
|
22223
|
+
$ref: "#/components/schemas/TagContext"
|
|
22224
|
+
checkedAt:
|
|
22225
|
+
type: number
|
|
22226
|
+
required:
|
|
22227
|
+
- isAuthentic
|
|
22228
|
+
- status
|
|
22229
|
+
- source
|
|
22230
|
+
- checkedAt
|
|
22206
22231
|
Job:
|
|
22207
22232
|
type: object
|
|
22208
22233
|
properties:
|
|
@@ -23034,6 +23059,39 @@ components:
|
|
|
23034
23059
|
- claimSetId
|
|
23035
23060
|
- codeId
|
|
23036
23061
|
- data
|
|
23062
|
+
TagContext:
|
|
23063
|
+
type: object
|
|
23064
|
+
properties:
|
|
23065
|
+
status:
|
|
23066
|
+
$ref: "#/components/schemas/TagStatus"
|
|
23067
|
+
tagId:
|
|
23068
|
+
type: string
|
|
23069
|
+
claimSetId:
|
|
23070
|
+
type: string
|
|
23071
|
+
codeId:
|
|
23072
|
+
type: string
|
|
23073
|
+
count:
|
|
23074
|
+
type: number
|
|
23075
|
+
previousCount:
|
|
23076
|
+
type: number
|
|
23077
|
+
data:
|
|
23078
|
+
type: object
|
|
23079
|
+
additionalProperties: true
|
|
23080
|
+
source:
|
|
23081
|
+
type: string
|
|
23082
|
+
enum:
|
|
23083
|
+
- nfc-validate
|
|
23084
|
+
- nfc-lookup
|
|
23085
|
+
- tag-index
|
|
23086
|
+
- none
|
|
23087
|
+
validatedAt:
|
|
23088
|
+
type: number
|
|
23089
|
+
errorMessage:
|
|
23090
|
+
type: string
|
|
23091
|
+
required:
|
|
23092
|
+
- status
|
|
23093
|
+
- source
|
|
23094
|
+
- validatedAt
|
|
23037
23095
|
OrderItem:
|
|
23038
23096
|
type: object
|
|
23039
23097
|
properties:
|
|
@@ -25211,6 +25269,10 @@ components:
|
|
|
25211
25269
|
additionalProperties: true
|
|
25212
25270
|
publicPortalUrl:
|
|
25213
25271
|
type: string
|
|
25272
|
+
itemContext:
|
|
25273
|
+
$ref: "#/components/schemas/ItemContext"
|
|
25274
|
+
tag:
|
|
25275
|
+
$ref: "#/components/schemas/TagContext"
|
|
25214
25276
|
size:
|
|
25215
25277
|
type: string
|
|
25216
25278
|
enum:
|
package/dist/types/index.d.ts
CHANGED
package/dist/types/index.js
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { TagContext } from './nfc';
|
|
2
|
+
/**
|
|
3
|
+
* Authenticity status for a specific item — a proof, resolved via an NFC
|
|
4
|
+
* tap or a serial proof URL. Collection- and product-only URLs have no
|
|
5
|
+
* item to describe, so there is no "nothing checkable" status here: absence
|
|
6
|
+
* of an `ItemContext` altogether is how that case is represented (see the
|
|
7
|
+
* `itemContext` prop on `SmartLinksWidgetProps`).
|
|
8
|
+
*/
|
|
9
|
+
export type ItemContextStatus = 'valid' | 'rescan' | 'invalid' | 'not-found' | 'error';
|
|
10
|
+
/**
|
|
11
|
+
* Describes a specific item the current URL points at and whether it's
|
|
12
|
+
* authentic — delivered to containers as the `itemContext` prop.
|
|
13
|
+
* Supersedes the NFC-only `TagContext`: still available, nested at
|
|
14
|
+
* `itemContext.tag`, and for one release also as a deprecated top-level
|
|
15
|
+
* `tag` prop. See docs/item-context.md.
|
|
16
|
+
*/
|
|
17
|
+
export interface ItemContext {
|
|
18
|
+
/** True only for `valid` and `rescan`. Read this for the simple case. */
|
|
19
|
+
isAuthentic: boolean;
|
|
20
|
+
status: ItemContextStatus;
|
|
21
|
+
source: 'nfc' | 'serial';
|
|
22
|
+
/** Set for `invalid` / `not-found` / `error`. Safe to display. */
|
|
23
|
+
errorMessage?: string;
|
|
24
|
+
/** True when the SDK reports this exact NFC tap was seen before. */
|
|
25
|
+
isRescan?: boolean;
|
|
26
|
+
/** Full NFC payload (SUN counter, tagId, claimSetId, codeId, mirror…). Present only when `source === 'nfc'`. */
|
|
27
|
+
tag?: TagContext;
|
|
28
|
+
/** Epoch ms when the check was derived. */
|
|
29
|
+
checkedAt: number;
|
|
30
|
+
}
|
package/dist/types/nfc.d.ts
CHANGED
|
@@ -39,3 +39,41 @@ export interface NfcClaimTagRequest {
|
|
|
39
39
|
codeId: string;
|
|
40
40
|
data: Record<string, any>;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Outcome of validating the NFC/QR tag that brought the visitor to a
|
|
44
|
+
* SmartLinks app.
|
|
45
|
+
*
|
|
46
|
+
* - `valid` — fresh, genuine tap. SUN counter advanced (or a non-consuming
|
|
47
|
+
* lookup succeeded).
|
|
48
|
+
* - `rescan` — genuine tag, but this exact tap was already seen (refresh,
|
|
49
|
+
* back button, or a non-advancing SUN counter).
|
|
50
|
+
* - `invalid` — validation rejected the payload outright (bad crypto,
|
|
51
|
+
* unknown tag, tampered URL) — i.e. a fake/counterfeit tag or serial.
|
|
52
|
+
* - `error` — network/transport failure; authenticity could not be
|
|
53
|
+
* confirmed either way.
|
|
54
|
+
*/
|
|
55
|
+
export type TagStatus = 'valid' | 'rescan' | 'invalid' | 'error';
|
|
56
|
+
/**
|
|
57
|
+
* Result of resolving the tag identifiers on the entry URL, handed to
|
|
58
|
+
* sub-apps so they can render status-appropriate UX instead of a generic
|
|
59
|
+
* error. See docs/tag-context.md for the full contract.
|
|
60
|
+
*/
|
|
61
|
+
export interface TagContext {
|
|
62
|
+
/** Always present when a `TagContext` is delivered at all. */
|
|
63
|
+
status: TagStatus;
|
|
64
|
+
/** Identifiers, when known. */
|
|
65
|
+
tagId?: string;
|
|
66
|
+
claimSetId?: string;
|
|
67
|
+
codeId?: string;
|
|
68
|
+
/** SUN counter values, present when validation returned them. */
|
|
69
|
+
count?: number;
|
|
70
|
+
previousCount?: number;
|
|
71
|
+
/** Arbitrary tag-level payload returned by validation/lookup. */
|
|
72
|
+
data?: Record<string, unknown>;
|
|
73
|
+
/** How the identifiers were resolved. */
|
|
74
|
+
source: 'nfc-validate' | 'nfc-lookup' | 'tag-index' | 'none';
|
|
75
|
+
/** Epoch ms at resolution time. */
|
|
76
|
+
validatedAt: number;
|
|
77
|
+
/** Human-readable message for `invalid` / `error`. */
|
|
78
|
+
errorMessage?: string;
|
|
79
|
+
}
|
package/dist/types/widgets.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { TagContext } from './nfc';
|
|
2
|
+
import type { ItemContext } from './itemContext';
|
|
1
3
|
/**
|
|
2
4
|
* Structured navigation request emitted via the `onNavigate` prop when a
|
|
3
5
|
* widget or container needs to navigate the parent platform shell to another
|
|
@@ -62,6 +64,19 @@ export interface SmartLinksWidgetProps {
|
|
|
62
64
|
onNavigate?: (request: NavigationRequest | string) => void;
|
|
63
65
|
/** Base URL of the full public portal, used for constructing deep links */
|
|
64
66
|
publicPortalUrl?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Authenticity context for the specific item (proof) the URL points at,
|
|
69
|
+
* resolved via an NFC tap or a serial proof URL. `undefined` for
|
|
70
|
+
* collection- and product-only URLs, where there's no item to verify.
|
|
71
|
+
* See docs/item-context.md.
|
|
72
|
+
*/
|
|
73
|
+
itemContext?: ItemContext;
|
|
74
|
+
/**
|
|
75
|
+
* @deprecated Use `itemContext.tag` instead. Kept for one release for
|
|
76
|
+
* backward compatibility with scanner-aware apps that read raw NFC/SUN
|
|
77
|
+
* data directly. See docs/item-context.md.
|
|
78
|
+
*/
|
|
79
|
+
tag?: TagContext;
|
|
65
80
|
/** Responsive size hint */
|
|
66
81
|
size?: 'compact' | 'standard' | 'large';
|
|
67
82
|
/** BCP-47 language code (e.g. `'en'`, `'fr'`) */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ItemContext } from '../types/itemContext';
|
|
1
2
|
/**
|
|
2
3
|
* Geographic region definitions for country-based conditions
|
|
3
4
|
*/
|
|
@@ -108,11 +109,36 @@ export interface ValueCondition extends BaseCondition {
|
|
|
108
109
|
value: string | number | boolean;
|
|
109
110
|
}
|
|
110
111
|
/**
|
|
111
|
-
* Item status condition
|
|
112
|
+
* Item status condition — one condition type for everything conditions
|
|
113
|
+
* need to know about "the item," so sub-apps don't have to juggle a
|
|
114
|
+
* separate authenticity condition alongside this one.
|
|
115
|
+
*
|
|
116
|
+
* Two families of `statusType`, kept in one enum on purpose:
|
|
117
|
+
* - **Claim/ownership** (unchanged): `isClaimable`, `notClaimable`,
|
|
118
|
+
* `isVirtual`, `notVirtual`, `hasProof`, `noProof`. These read
|
|
119
|
+
* `params.proof` — whether a resolved proof record was supplied at all,
|
|
120
|
+
* and its claim/virtual flags. `noProof` only means "nothing was even
|
|
121
|
+
* attempted"; it does NOT distinguish that from "an identifier was
|
|
122
|
+
* passed but didn't resolve" — see `invalidProof` below for that case.
|
|
123
|
+
* - **Authenticity** (reads `params.itemContext` — see `ItemContext`,
|
|
124
|
+
* docs/item-context.md): `isAuthentic`, `notAuthentic`, `invalidProof`,
|
|
125
|
+
* `isFirstScan`, `isRescan`.
|
|
126
|
+
* - `invalidProof` is specifically "an identifier was passed and resolution
|
|
127
|
+
* was attempted, but it came back invalid or not-found" — the "someone
|
|
128
|
+
* scanned a fake tag / typed a bad serial" case, as opposed to `noProof`
|
|
129
|
+
* ("nothing was on the URL to check at all").
|
|
130
|
+
* - `isAuthentic` is true for both a fresh tap and a rescan — use
|
|
131
|
+
* `isFirstScan` / `isRescan` when the fresh-vs-duplicate distinction
|
|
132
|
+
* matters. `isFirstScan` is the common "this is good, show the full
|
|
133
|
+
* experience" check (authentic AND not seen before, `status === 'valid'`).
|
|
134
|
+
* `isRescan` is authentic but a duplicate/replayed tap
|
|
135
|
+
* (`status === 'rescan'`) — e.g. a page refresh or the back button —
|
|
136
|
+
* for suppressing "first scan" celebration UX without treating the tag
|
|
137
|
+
* as fake.
|
|
112
138
|
*/
|
|
113
139
|
export interface ItemStatusCondition extends BaseCondition {
|
|
114
140
|
type: 'itemStatus';
|
|
115
|
-
statusType: 'isClaimable' | 'notClaimable' | 'noProof' | 'hasProof' | 'isVirtual' | 'notVirtual';
|
|
141
|
+
statusType: 'isClaimable' | 'notClaimable' | 'noProof' | 'hasProof' | 'isVirtual' | 'notVirtual' | 'isAuthentic' | 'notAuthentic' | 'invalidProof' | 'isFirstScan' | 'isRescan';
|
|
116
142
|
}
|
|
117
143
|
/**
|
|
118
144
|
* Facet-based condition — gates on the facet values assigned to the current product.
|
|
@@ -263,6 +289,8 @@ export interface ConditionParams {
|
|
|
263
289
|
proof?: ProofInfo;
|
|
264
290
|
/** Collection information */
|
|
265
291
|
collection?: CollectionInfo;
|
|
292
|
+
/** Authenticity context for the item (proof) the URL points at, if any */
|
|
293
|
+
itemContext?: ItemContext;
|
|
266
294
|
/** Statistics/tracking information */
|
|
267
295
|
stats?: StatsInfo;
|
|
268
296
|
/** Function to fetch conditions by ID (optional) */
|
|
@@ -300,7 +328,9 @@ export interface ConditionDebugOptions {
|
|
|
300
328
|
* - **date** - Time-based conditions (before, after, between dates)
|
|
301
329
|
* - **geofence** - Location-based restrictions
|
|
302
330
|
* - **value** - Custom field comparisons
|
|
303
|
-
* - **itemStatus** - Proof/item status checks
|
|
331
|
+
* - **itemStatus** - Proof/item status checks: claimable, virtual, presence
|
|
332
|
+
* (`hasProof`/`noProof`), and authenticity (`isAuthentic`/`notAuthentic`/
|
|
333
|
+
* `invalidProof`/`isFirstScan`/`isRescan`)
|
|
304
334
|
* - **condition** - Nested condition references
|
|
305
335
|
*
|
|
306
336
|
* Conditions can be combined with AND or OR logic.
|
package/dist/utils/conditions.js
CHANGED
|
@@ -173,7 +173,9 @@ async function evaluateConditionEntry(condition, params) {
|
|
|
173
173
|
* - **date** - Time-based conditions (before, after, between dates)
|
|
174
174
|
* - **geofence** - Location-based restrictions
|
|
175
175
|
* - **value** - Custom field comparisons
|
|
176
|
-
* - **itemStatus** - Proof/item status checks
|
|
176
|
+
* - **itemStatus** - Proof/item status checks: claimable, virtual, presence
|
|
177
|
+
* (`hasProof`/`noProof`), and authenticity (`isAuthentic`/`notAuthentic`/
|
|
178
|
+
* `invalidProof`/`isFirstScan`/`isRescan`)
|
|
177
179
|
* - **condition** - Nested condition references
|
|
178
180
|
*
|
|
179
181
|
* Conditions can be combined with AND or OR logic.
|
|
@@ -817,6 +819,7 @@ async function validateFacet(condition, params) {
|
|
|
817
819
|
* Validate item status condition
|
|
818
820
|
*/
|
|
819
821
|
async function validateItemStatus(condition, params) {
|
|
822
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
820
823
|
switch (condition.statusType) {
|
|
821
824
|
case 'isClaimable':
|
|
822
825
|
return {
|
|
@@ -848,6 +851,36 @@ async function validateItemStatus(condition, params) {
|
|
|
848
851
|
passed: !!(params.proof && !params.proof.virtual),
|
|
849
852
|
detail: 'Checked proof.virtual for falsiness.',
|
|
850
853
|
};
|
|
854
|
+
case 'isAuthentic':
|
|
855
|
+
return {
|
|
856
|
+
passed: !!((_a = params.itemContext) === null || _a === void 0 ? void 0 : _a.isAuthentic),
|
|
857
|
+
detail: 'Checked itemContext.isAuthentic for truthiness.',
|
|
858
|
+
context: { itemContextStatus: (_b = params.itemContext) === null || _b === void 0 ? void 0 : _b.status },
|
|
859
|
+
};
|
|
860
|
+
case 'notAuthentic':
|
|
861
|
+
return {
|
|
862
|
+
passed: !!(params.itemContext && !params.itemContext.isAuthentic),
|
|
863
|
+
detail: 'Checked that an itemContext was resolved and isAuthentic is false.',
|
|
864
|
+
context: { itemContextStatus: (_c = params.itemContext) === null || _c === void 0 ? void 0 : _c.status },
|
|
865
|
+
};
|
|
866
|
+
case 'invalidProof':
|
|
867
|
+
return {
|
|
868
|
+
passed: ((_d = params.itemContext) === null || _d === void 0 ? void 0 : _d.status) === 'invalid' || ((_e = params.itemContext) === null || _e === void 0 ? void 0 : _e.status) === 'not-found',
|
|
869
|
+
detail: 'Checked that resolution was attempted and came back invalid or not-found (as opposed to noProof, where nothing was attempted at all).',
|
|
870
|
+
context: { itemContextStatus: (_f = params.itemContext) === null || _f === void 0 ? void 0 : _f.status },
|
|
871
|
+
};
|
|
872
|
+
case 'isFirstScan':
|
|
873
|
+
return {
|
|
874
|
+
passed: ((_g = params.itemContext) === null || _g === void 0 ? void 0 : _g.status) === 'valid',
|
|
875
|
+
detail: 'Checked that itemContext is authentic and this is the first time it has been seen (status === valid, not rescan).',
|
|
876
|
+
context: { itemContextStatus: (_h = params.itemContext) === null || _h === void 0 ? void 0 : _h.status },
|
|
877
|
+
};
|
|
878
|
+
case 'isRescan':
|
|
879
|
+
return {
|
|
880
|
+
passed: ((_j = params.itemContext) === null || _j === void 0 ? void 0 : _j.status) === 'rescan' || !!((_k = params.itemContext) === null || _k === void 0 ? void 0 : _k.isRescan),
|
|
881
|
+
detail: 'Checked that itemContext is authentic but a duplicate/replayed tap (status === rescan).',
|
|
882
|
+
context: { itemContextStatus: (_l = params.itemContext) === null || _l === void 0 ? void 0 : _l.status },
|
|
883
|
+
};
|
|
851
884
|
default:
|
|
852
885
|
return {
|
|
853
886
|
passed: false,
|
package/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 1.15.
|
|
3
|
+
Version: 1.15.10 | Generated: 2026-07-19T13:35:07.171Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -5878,6 +5878,23 @@ interface InteractionEventContext {
|
|
|
5878
5878
|
|
|
5879
5879
|
**EffectConfig** = ``
|
|
5880
5880
|
|
|
5881
|
+
### itemContext
|
|
5882
|
+
|
|
5883
|
+
**ItemContext** (interface)
|
|
5884
|
+
```typescript
|
|
5885
|
+
interface ItemContext {
|
|
5886
|
+
isAuthentic: boolean
|
|
5887
|
+
status: ItemContextStatus
|
|
5888
|
+
source: 'nfc' | 'serial'
|
|
5889
|
+
errorMessage?: string
|
|
5890
|
+
isRescan?: boolean
|
|
5891
|
+
tag?: TagContext
|
|
5892
|
+
checkedAt: number
|
|
5893
|
+
}
|
|
5894
|
+
```
|
|
5895
|
+
|
|
5896
|
+
**ItemContextStatus** = ``
|
|
5897
|
+
|
|
5881
5898
|
### jobs
|
|
5882
5899
|
|
|
5883
5900
|
**Job** (interface)
|
|
@@ -6408,6 +6425,24 @@ interface NfcClaimTagRequest {
|
|
|
6408
6425
|
}
|
|
6409
6426
|
```
|
|
6410
6427
|
|
|
6428
|
+
**TagContext** (interface)
|
|
6429
|
+
```typescript
|
|
6430
|
+
interface TagContext {
|
|
6431
|
+
status: TagStatus
|
|
6432
|
+
tagId?: string
|
|
6433
|
+
claimSetId?: string
|
|
6434
|
+
codeId?: string
|
|
6435
|
+
count?: number
|
|
6436
|
+
previousCount?: number
|
|
6437
|
+
data?: Record<string, unknown>
|
|
6438
|
+
source: 'nfc-validate' | 'nfc-lookup' | 'tag-index' | 'none'
|
|
6439
|
+
validatedAt: number
|
|
6440
|
+
errorMessage?: string
|
|
6441
|
+
}
|
|
6442
|
+
```
|
|
6443
|
+
|
|
6444
|
+
**TagStatus** = `'valid' | 'rescan' | 'invalid' | 'error'`
|
|
6445
|
+
|
|
6411
6446
|
### order
|
|
6412
6447
|
|
|
6413
6448
|
**OrderItem** (interface)
|
|
@@ -7692,6 +7727,15 @@ interface SmartLinksWidgetProps {
|
|
|
7692
7727
|
* accepted for backward compatibility.
|
|
7693
7728
|
onNavigate?: (request: NavigationRequest | string) => void
|
|
7694
7729
|
publicPortalUrl?: string
|
|
7730
|
+
* Authenticity context for the specific item (proof) the URL points at,
|
|
7731
|
+
* resolved via an NFC tap or a serial proof URL. `undefined` for
|
|
7732
|
+
* collection- and product-only URLs, where there's no item to verify.
|
|
7733
|
+
* See docs/item-context.md.
|
|
7734
|
+
itemContext?: ItemContext
|
|
7735
|
+
* @deprecated Use `itemContext.tag` instead. Kept for one release for
|
|
7736
|
+
* backward compatibility with scanner-aware apps that read raw NFC/SUN
|
|
7737
|
+
* data directly. See docs/item-context.md.
|
|
7738
|
+
tag?: TagContext
|
|
7695
7739
|
size?: 'compact' | 'standard' | 'large'
|
|
7696
7740
|
lang?: string
|
|
7697
7741
|
translations?: Record<string, string>
|
|
@@ -7861,6 +7905,7 @@ interface ConditionParams {
|
|
|
7861
7905
|
product?: ProductInfo
|
|
7862
7906
|
proof?: ProofInfo
|
|
7863
7907
|
collection?: CollectionInfo
|
|
7908
|
+
itemContext?: ItemContext
|
|
7864
7909
|
stats?: StatsInfo
|
|
7865
7910
|
fetchCondition?: (collectionId: string, conditionId: string) => Promise<ConditionSet | null>
|
|
7866
7911
|
getLocation?: () => Promise<{ latitude: number; longitude: number }>
|
package/docs/containers.md
CHANGED
|
@@ -231,6 +231,8 @@ All standard widget props apply:
|
|
|
231
231
|
| `productId` | string | ❌ | Product context |
|
|
232
232
|
| `proofId` | string | ❌ | Proof context |
|
|
233
233
|
| `user` | object | ❌ | Current user info |
|
|
234
|
+
| `itemContext` | `ItemContext` | ❌ | Authenticity context for the specific item (proof) the URL points at; `undefined` for collection/product-only URLs — see [item-context.md](item-context.md) |
|
|
235
|
+
| `tag` | `TagContext` | ❌ | **Deprecated** — use `itemContext.tag` instead. Kept for one release. |
|
|
234
236
|
| `onNavigate` | function | ❌ | Navigation callback (accepts `NavigationRequest` or legacy string) |
|
|
235
237
|
| `size` | string | ❌ | `"compact"`, `"standard"`, or `"large"` |
|
|
236
238
|
| `lang` | string | ❌ | Language code (e.g., `"en"`) |
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Item Context (container prop)
|
|
2
|
+
|
|
3
|
+
> **Copy this file into `node_modules/@proveanything/smartlinks/docs/item-context.md`** in the published SDK package.
|
|
4
|
+
|
|
5
|
+
When the URL points at a specific item — either a **serial proof URL** or an
|
|
6
|
+
**NFC tap** — the portal derives an `ItemContext` describing what it found
|
|
7
|
+
and hands it to the container as the **`itemContext`** prop.
|
|
8
|
+
|
|
9
|
+
For collection- and product-only URLs there is no item to describe, so
|
|
10
|
+
`itemContext` is `undefined`. Sub-apps that don't care about authenticity
|
|
11
|
+
can simply ignore the prop.
|
|
12
|
+
|
|
13
|
+
The prop is named `itemContext` (not `item`) to make clear it's *context
|
|
14
|
+
about the item*, not the item record itself. The underlying record is
|
|
15
|
+
passed separately via `proof`, `product` and `collection` props.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
export default function MyContainer({ itemContext, collection, product, proof }) {
|
|
19
|
+
if (!itemContext) {
|
|
20
|
+
// Collection or product view — nothing to verify.
|
|
21
|
+
return <BrowseView collection={collection} product={product} />;
|
|
22
|
+
}
|
|
23
|
+
if (itemContext.isAuthentic) {
|
|
24
|
+
return <RealContent proof={proof} />;
|
|
25
|
+
}
|
|
26
|
+
if (itemContext.status === 'not-found' || itemContext.status === 'invalid') {
|
|
27
|
+
return <NotGenuine message={itemContext.errorMessage} />;
|
|
28
|
+
}
|
|
29
|
+
// status === 'error' — transport failure
|
|
30
|
+
return <TransientError message={itemContext.errorMessage} />;
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Shape
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
interface ItemContext {
|
|
40
|
+
/** True only for `valid` and `rescan`. Read this for the simple case. */
|
|
41
|
+
isAuthentic: boolean;
|
|
42
|
+
|
|
43
|
+
status:
|
|
44
|
+
| 'valid' // genuine
|
|
45
|
+
| 'rescan' // genuine, but this exact NFC tap has been seen before
|
|
46
|
+
| 'invalid' // SDK rejected the NFC payload (bad crypto)
|
|
47
|
+
| 'not-found' // identifier well-formed but 404
|
|
48
|
+
| 'error'; // transport / network failure — cannot tell
|
|
49
|
+
|
|
50
|
+
source: 'nfc' | 'serial';
|
|
51
|
+
|
|
52
|
+
/** Set for `invalid` / `not-found` / `error`. Safe to display. */
|
|
53
|
+
errorMessage?: string;
|
|
54
|
+
|
|
55
|
+
/** True when SDK reports this exact NFC tap was seen before. */
|
|
56
|
+
isRescan?: boolean;
|
|
57
|
+
|
|
58
|
+
/** Full NFC payload (SUN counter, tagId, claimSetId, codeId, mirror…). Present only when source === 'nfc'. */
|
|
59
|
+
tag?: TagContext;
|
|
60
|
+
|
|
61
|
+
/** Epoch ms when the check was derived. */
|
|
62
|
+
checkedAt: number;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Also exported from `@proveanything/smartlinks` as `ItemContext` /
|
|
67
|
+
`ItemContextStatus` (`src/types/itemContext.ts`). The nested `TagContext`
|
|
68
|
+
shape is unchanged (`src/types/nfc.ts`).
|
|
69
|
+
|
|
70
|
+
## When `itemContext` is present
|
|
71
|
+
|
|
72
|
+
| URL points at | itemContext |
|
|
73
|
+
| ---------------------------------------------- | -------------------- |
|
|
74
|
+
| Collection root | `undefined` |
|
|
75
|
+
| Product page (no proof) | `undefined` |
|
|
76
|
+
| Serial proof URL (any outcome) | present, source `serial` |
|
|
77
|
+
| NFC tap (any outcome) | present, source `nfc` |
|
|
78
|
+
|
|
79
|
+
## Status matrix (when present)
|
|
80
|
+
|
|
81
|
+
| Situation | source | status | isAuthentic |
|
|
82
|
+
| ----------------------------------------------------- | -------- | ------------- | :---------: |
|
|
83
|
+
| Fresh NFC tap, SUN counter validated | `nfc` | `valid` | ✅ |
|
|
84
|
+
| Same NFC tap replayed / refresh | `nfc` | `rescan` | ✅ |
|
|
85
|
+
| NFC payload rejected (bad crypto / bad params) | `nfc` | `invalid` | ❌ |
|
|
86
|
+
| Serial proof URL, proof fetched OK | `serial` | `valid` | ✅ |
|
|
87
|
+
| Serial proof URL, proof returned 404 | `serial` | `not-found` | ❌ |
|
|
88
|
+
| Network / 5xx while resolving proof or NFC | (either) | `error` | ❌ |
|
|
89
|
+
|
|
90
|
+
`isAuthentic === (status === 'valid' || status === 'rescan')`.
|
|
91
|
+
|
|
92
|
+
## How it's delivered
|
|
93
|
+
|
|
94
|
+
**Direct-mounted containers** (default public mode):
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
type Props = {
|
|
98
|
+
collection: Collection;
|
|
99
|
+
product?: Product;
|
|
100
|
+
proof?: Proof;
|
|
101
|
+
itemContext?: ItemContext; // ← only when URL targets a specific item
|
|
102
|
+
// …plus the normal navigation / theme / auth props
|
|
103
|
+
};
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
NFC-aware apps that need the raw SUN / tag payload read `itemContext.tag`.
|
|
107
|
+
|
|
108
|
+
**Iframe containers**: the portal serialises `ItemContext` to JSON, base64-
|
|
109
|
+
encodes it (UTF-8 safe) and appends it as an `itemContext` query param on
|
|
110
|
+
the iframe URL — omitted entirely when there is no item context:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
function readItemContext(): ItemContext | undefined {
|
|
114
|
+
const raw = new URLSearchParams(location.search).get('itemContext');
|
|
115
|
+
if (!raw) return undefined;
|
|
116
|
+
try { return JSON.parse(decodeURIComponent(escape(atob(raw)))); }
|
|
117
|
+
catch { return undefined; }
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Guarantees
|
|
122
|
+
|
|
123
|
+
- `itemContext` is present on every container mount whose URL targeted an
|
|
124
|
+
item (proof or NFC tap). Absent otherwise.
|
|
125
|
+
- `SL.nfc.validate` is called at most once per `(claimSetId, codeId)` per
|
|
126
|
+
browser session — SUN counters are never double-consumed on refresh.
|
|
127
|
+
- SSR uses `SL.nfc.lookupTag` (non-consuming) only.
|
|
128
|
+
- `isAuthentic === true` implies the SDK either validated an NFC payload
|
|
129
|
+
or returned a full proof document. It is never inferred from URL shape.
|
|
130
|
+
- The portal will **not** render its full-page error surface for a 404 on
|
|
131
|
+
a proof — that surfaces as `status: 'not-found'` on `itemContext`, and
|
|
132
|
+
the container is still mounted so it can present the message its own way.
|
|
133
|
+
|
|
134
|
+
## What containers should do
|
|
135
|
+
|
|
136
|
+
| Status | Recommended UI |
|
|
137
|
+
| ------------- | ------------------------------------------------------------------ |
|
|
138
|
+
| `valid` | Full experience. Show ownership, actions, etc. |
|
|
139
|
+
| `rescan` | Same as `valid`, optionally with a subtle "seen before" hint. |
|
|
140
|
+
| `invalid` | Clear "this doesn't look genuine" message. Do NOT reveal data. |
|
|
141
|
+
| `not-found` | "We don't recognise this item." Offer a link home. |
|
|
142
|
+
| `error` | Neutral "couldn't verify right now." Offer retry. |
|
|
143
|
+
| _absent_ | Standard browse UI. Don't mention authenticity at all. |
|
|
144
|
+
|
|
145
|
+
## Gating UX with conditions
|
|
146
|
+
|
|
147
|
+
Sub-apps should standardize on passing `itemContext` into `validateCondition()`
|
|
148
|
+
and gating with `itemStatus` — the same condition type already used for
|
|
149
|
+
claim/virtual checks, extended with authenticity `statusType`s that read
|
|
150
|
+
`itemContext` instead of `proof`:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { validateCondition } from '@proveanything/smartlinks';
|
|
154
|
+
|
|
155
|
+
const showFakeWarning = await validateCondition({
|
|
156
|
+
condition: {
|
|
157
|
+
conditions: [{ type: 'itemStatus', statusType: 'invalidProof' }],
|
|
158
|
+
},
|
|
159
|
+
itemContext, // the prop delivered to the container
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- `isAuthentic` — passes when `itemContext.isAuthentic` is true (`valid`/`rescan`)
|
|
164
|
+
- `notAuthentic` — passes when an `itemContext` was resolved but isn't authentic (`invalid`/`not-found`/`error`)
|
|
165
|
+
- `invalidProof` — passes specifically when resolution was attempted and came back `invalid` or `not-found` — the "someone scanned a fake tag / typed a bad serial" case, as distinct from `noProof` ("nothing was on the URL to check at all")
|
|
166
|
+
- `isFirstScan` — the single "this is good, show the full experience" check: authentic AND this is the first time it's been seen (`status === 'valid'`)
|
|
167
|
+
- `isRescan` — authentic but a duplicate/replayed tap (`status === 'rescan'`) — use this to suppress "first scan" celebration UX without treating the tag as fake; `isAuthentic` alone is true for both `isFirstScan` and `isRescan` cases
|
|
168
|
+
|
|
169
|
+
## Deprecated
|
|
170
|
+
|
|
171
|
+
The old top-level `tag` prop (`TagContext`) is still forwarded for one
|
|
172
|
+
release for backward compatibility with scanner-aware apps that read raw
|
|
173
|
+
SUN data. New code should read `itemContext.tag` and treat `itemContext`
|
|
174
|
+
as the source of truth.
|
|
175
|
+
|
|
176
|
+
## Note on terminology
|
|
177
|
+
|
|
178
|
+
Today the SDK still uses `proof` for a specific item record (a serialised
|
|
179
|
+
instance of a product). "Item record" is under consideration as a clearer
|
|
180
|
+
name — no API changes are planned imminently, but a future SDK version may
|
|
181
|
+
support both `proof`- and `item record`-shaped names for the same
|
|
182
|
+
functions/types side by side. `itemContext` is deliberately future-proof:
|
|
183
|
+
it describes context about whatever the URL points at, regardless of what
|
|
184
|
+
we ultimately call the record itself.
|
package/docs/utils.md
CHANGED
|
@@ -485,9 +485,12 @@ await utils.validateCondition({
|
|
|
485
485
|
```
|
|
486
486
|
|
|
487
487
|
#### Item Status Conditions
|
|
488
|
-
Check proof/item status
|
|
488
|
+
Check proof/item status — claim/ownership state, and authenticity of the
|
|
489
|
+
item (proof) the URL points at (via NFC tap or serial proof URL — see
|
|
490
|
+
[item-context.md](item-context.md)):
|
|
489
491
|
|
|
490
492
|
```typescript
|
|
493
|
+
// Claim/ownership — unchanged, reads `proof`
|
|
491
494
|
await utils.validateCondition({
|
|
492
495
|
condition: {
|
|
493
496
|
type: 'and',
|
|
@@ -498,9 +501,38 @@ await utils.validateCondition({
|
|
|
498
501
|
},
|
|
499
502
|
proof: { claimable: true }
|
|
500
503
|
})
|
|
504
|
+
|
|
505
|
+
// Authenticity — reads `itemContext`
|
|
506
|
+
await utils.validateCondition({
|
|
507
|
+
condition: {
|
|
508
|
+
type: 'and',
|
|
509
|
+
conditions: [{
|
|
510
|
+
type: 'itemStatus',
|
|
511
|
+
statusType: 'invalidProof'
|
|
512
|
+
}]
|
|
513
|
+
},
|
|
514
|
+
itemContext: { isAuthentic: false, status: 'invalid', source: 'nfc', checkedAt: Date.now() }
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
// Fresh scan vs. duplicate/replayed scan — both authentic, different UX
|
|
518
|
+
await utils.validateCondition({
|
|
519
|
+
condition: {
|
|
520
|
+
type: 'and',
|
|
521
|
+
conditions: [{
|
|
522
|
+
type: 'itemStatus',
|
|
523
|
+
statusType: 'isRescan'
|
|
524
|
+
}]
|
|
525
|
+
},
|
|
526
|
+
itemContext: { isAuthentic: true, status: 'rescan', source: 'nfc', checkedAt: Date.now() }
|
|
527
|
+
})
|
|
501
528
|
```
|
|
502
529
|
|
|
503
|
-
Status types:
|
|
530
|
+
Status types:
|
|
531
|
+
- Claim/ownership (reads `proof`): `'isClaimable'`, `'notClaimable'`, `'isVirtual'`, `'notVirtual'`
|
|
532
|
+
- Presence (reads `proof`): `'hasProof'`, `'noProof'` — `noProof` only means *nothing was attempted*, not that an attempt failed
|
|
533
|
+
- Authenticity (reads `itemContext`): `'isAuthentic'`, `'notAuthentic'`, `'invalidProof'`, `'isFirstScan'`, `'isRescan'`
|
|
534
|
+
- `invalidProof` is specifically "an identifier was passed and resolution came back `invalid`/`not-found`", distinct from `noProof`'s "nothing on the URL to check at all"
|
|
535
|
+
- `isAuthentic` is true for both a fresh tap and a rescan. Use `isFirstScan` (authentic AND `status === 'valid'`) as the single "this is good, show the full experience" check, and `isRescan` (authentic but `status === 'rescan'`) to suppress "first scan" celebration UX without treating the tag as fake
|
|
504
536
|
|
|
505
537
|
#### Version Conditions
|
|
506
538
|
For A/B testing or versioned content:
|
|
@@ -720,6 +752,7 @@ See [examples/utils-demo.ts](../examples/utils-demo.ts) for comprehensive exampl
|
|
|
720
752
|
- **[API Summary](API_SUMMARY.md)** - Complete SDK reference with all namespaces and functions
|
|
721
753
|
- **[QR Codes](API_SUMMARY.md#qr)** - QR code lookup functions that work with generated paths
|
|
722
754
|
- **[NFC](API_SUMMARY.md#nfc)** - NFC tag claiming and validation
|
|
755
|
+
- **[Item Context](item-context.md)** - `itemContext` prop delivered to containers describing the item (proof) the URL points at and whether it's authentic
|
|
723
756
|
- **[Collections](API_SUMMARY.md#collection)** - Collection management functions
|
|
724
757
|
- **[Products](API_SUMMARY.md#product)** - Product CRUD operations
|
|
725
758
|
- **[Batches](API_SUMMARY.md#batch)** - Batch management
|
package/docs/widgets.md
CHANGED
|
@@ -159,6 +159,13 @@ interface SmartLinksWidgetProps {
|
|
|
159
159
|
// Base URL to the full public portal for deep linking
|
|
160
160
|
publicPortalUrl?: string;
|
|
161
161
|
|
|
162
|
+
// Authenticity context for the specific item (proof) the URL points at.
|
|
163
|
+
// undefined for collection- and product-only URLs.
|
|
164
|
+
itemContext?: ItemContext;
|
|
165
|
+
|
|
166
|
+
// @deprecated Use itemContext.tag instead — kept for one release
|
|
167
|
+
tag?: TagContext;
|
|
168
|
+
|
|
162
169
|
// Size hint for responsive rendering
|
|
163
170
|
size?: 'compact' | 'standard' | 'large';
|
|
164
171
|
|
|
@@ -180,6 +187,8 @@ interface SmartLinksWidgetProps {
|
|
|
180
187
|
| `SL` | `typeof SL` | Pre-initialized SmartLinks SDK |
|
|
181
188
|
| `onNavigate` | `function?` | Callback to navigate within parent app (accepts `NavigationRequest` or legacy string) |
|
|
182
189
|
| `publicPortalUrl` | `string?` | Base URL to full portal for deep links |
|
|
190
|
+
| `itemContext` | `ItemContext?` | Authenticity context for the specific item (proof) the URL points at; `undefined` for collection/product-only URLs — see [item-context.md](item-context.md) |
|
|
191
|
+
| `tag` | `TagContext?` | **Deprecated** — use `itemContext.tag` instead. Kept for one release. |
|
|
183
192
|
| `size` | `string?` | Size hint: 'compact', 'standard', or 'large' |
|
|
184
193
|
| `lang` | `string?` | Language code (e.g., 'en', 'de', 'fr') |
|
|
185
194
|
| `translations` | `object?` | Translation overrides |
|
package/openapi.yaml
CHANGED
|
@@ -22203,6 +22203,31 @@ components:
|
|
|
22203
22203
|
- metadata
|
|
22204
22204
|
- broadcastId
|
|
22205
22205
|
- journeyId
|
|
22206
|
+
ItemContext:
|
|
22207
|
+
type: object
|
|
22208
|
+
properties:
|
|
22209
|
+
isAuthentic:
|
|
22210
|
+
type: boolean
|
|
22211
|
+
status:
|
|
22212
|
+
$ref: "#/components/schemas/ItemContextStatus"
|
|
22213
|
+
source:
|
|
22214
|
+
type: string
|
|
22215
|
+
enum:
|
|
22216
|
+
- nfc
|
|
22217
|
+
- serial
|
|
22218
|
+
errorMessage:
|
|
22219
|
+
type: string
|
|
22220
|
+
isRescan:
|
|
22221
|
+
type: boolean
|
|
22222
|
+
tag:
|
|
22223
|
+
$ref: "#/components/schemas/TagContext"
|
|
22224
|
+
checkedAt:
|
|
22225
|
+
type: number
|
|
22226
|
+
required:
|
|
22227
|
+
- isAuthentic
|
|
22228
|
+
- status
|
|
22229
|
+
- source
|
|
22230
|
+
- checkedAt
|
|
22206
22231
|
Job:
|
|
22207
22232
|
type: object
|
|
22208
22233
|
properties:
|
|
@@ -23034,6 +23059,39 @@ components:
|
|
|
23034
23059
|
- claimSetId
|
|
23035
23060
|
- codeId
|
|
23036
23061
|
- data
|
|
23062
|
+
TagContext:
|
|
23063
|
+
type: object
|
|
23064
|
+
properties:
|
|
23065
|
+
status:
|
|
23066
|
+
$ref: "#/components/schemas/TagStatus"
|
|
23067
|
+
tagId:
|
|
23068
|
+
type: string
|
|
23069
|
+
claimSetId:
|
|
23070
|
+
type: string
|
|
23071
|
+
codeId:
|
|
23072
|
+
type: string
|
|
23073
|
+
count:
|
|
23074
|
+
type: number
|
|
23075
|
+
previousCount:
|
|
23076
|
+
type: number
|
|
23077
|
+
data:
|
|
23078
|
+
type: object
|
|
23079
|
+
additionalProperties: true
|
|
23080
|
+
source:
|
|
23081
|
+
type: string
|
|
23082
|
+
enum:
|
|
23083
|
+
- nfc-validate
|
|
23084
|
+
- nfc-lookup
|
|
23085
|
+
- tag-index
|
|
23086
|
+
- none
|
|
23087
|
+
validatedAt:
|
|
23088
|
+
type: number
|
|
23089
|
+
errorMessage:
|
|
23090
|
+
type: string
|
|
23091
|
+
required:
|
|
23092
|
+
- status
|
|
23093
|
+
- source
|
|
23094
|
+
- validatedAt
|
|
23037
23095
|
OrderItem:
|
|
23038
23096
|
type: object
|
|
23039
23097
|
properties:
|
|
@@ -25211,6 +25269,10 @@ components:
|
|
|
25211
25269
|
additionalProperties: true
|
|
25212
25270
|
publicPortalUrl:
|
|
25213
25271
|
type: string
|
|
25272
|
+
itemContext:
|
|
25273
|
+
$ref: "#/components/schemas/ItemContext"
|
|
25274
|
+
tag:
|
|
25275
|
+
$ref: "#/components/schemas/TagContext"
|
|
25214
25276
|
size:
|
|
25215
25277
|
type: string
|
|
25216
25278
|
enum:
|