@proveanything/smartlinks 1.15.8 → 1.15.11
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 +53 -1
- package/dist/docs/appConfig.md +46 -5
- package/dist/docs/containers.md +2 -0
- package/dist/docs/item-context.md +183 -0
- package/dist/docs/utils.md +35 -2
- package/dist/docs/widgets.md +9 -0
- package/dist/openapi.yaml +65 -0
- package/dist/types/appConfiguration.d.ts +16 -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 +29 -3
- package/dist/utils/conditions.js +28 -1
- package/docs/API_SUMMARY.md +53 -1
- package/docs/appConfig.md +46 -5
- package/docs/containers.md +2 -0
- package/docs/item-context.md +183 -0
- package/docs/utils.md +35 -2
- package/docs/widgets.md +9 -0
- package/openapi.yaml +65 -0
- package/package.json +1 -1
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,32 @@ 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
|
+
* `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. `isRescan` is
|
|
131
|
+
* authentic but a duplicate/replayed tap (`status === 'rescan'`) — e.g.
|
|
132
|
+
* a page refresh or the back button — for suppressing "first scan"
|
|
133
|
+
* celebration UX without treating the tag as fake.
|
|
112
134
|
*/
|
|
113
135
|
export interface ItemStatusCondition extends BaseCondition {
|
|
114
136
|
type: 'itemStatus';
|
|
115
|
-
statusType: 'isClaimable' | 'notClaimable' | 'noProof' | 'hasProof' | 'isVirtual' | 'notVirtual';
|
|
137
|
+
statusType: 'isClaimable' | 'notClaimable' | 'noProof' | 'hasProof' | 'isVirtual' | 'notVirtual' | 'isAuthentic' | 'notAuthentic' | 'invalidProof' | 'isRescan';
|
|
116
138
|
}
|
|
117
139
|
/**
|
|
118
140
|
* Facet-based condition — gates on the facet values assigned to the current product.
|
|
@@ -263,6 +285,8 @@ export interface ConditionParams {
|
|
|
263
285
|
proof?: ProofInfo;
|
|
264
286
|
/** Collection information */
|
|
265
287
|
collection?: CollectionInfo;
|
|
288
|
+
/** Authenticity context for the item (proof) the URL points at, if any */
|
|
289
|
+
itemContext?: ItemContext;
|
|
266
290
|
/** Statistics/tracking information */
|
|
267
291
|
stats?: StatsInfo;
|
|
268
292
|
/** Function to fetch conditions by ID (optional) */
|
|
@@ -300,7 +324,9 @@ export interface ConditionDebugOptions {
|
|
|
300
324
|
* - **date** - Time-based conditions (before, after, between dates)
|
|
301
325
|
* - **geofence** - Location-based restrictions
|
|
302
326
|
* - **value** - Custom field comparisons
|
|
303
|
-
* - **itemStatus** - Proof/item status checks
|
|
327
|
+
* - **itemStatus** - Proof/item status checks: claimable, virtual, presence
|
|
328
|
+
* (`hasProof`/`noProof`), and authenticity (`isAuthentic`/`notAuthentic`/
|
|
329
|
+
* `invalidProof`/`isRescan`)
|
|
304
330
|
* - **condition** - Nested condition references
|
|
305
331
|
*
|
|
306
332
|
* 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`/`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;
|
|
820
823
|
switch (condition.statusType) {
|
|
821
824
|
case 'isClaimable':
|
|
822
825
|
return {
|
|
@@ -848,6 +851,30 @@ 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 'isRescan':
|
|
873
|
+
return {
|
|
874
|
+
passed: ((_g = params.itemContext) === null || _g === void 0 ? void 0 : _g.status) === 'rescan' || !!((_h = params.itemContext) === null || _h === void 0 ? void 0 : _h.isRescan),
|
|
875
|
+
detail: 'Checked that itemContext is authentic but a duplicate/replayed tap (status === rescan).',
|
|
876
|
+
context: { itemContextStatus: (_j = params.itemContext) === null || _j === void 0 ? void 0 : _j.status },
|
|
877
|
+
};
|
|
851
878
|
default:
|
|
852
879
|
return {
|
|
853
880
|
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.11 | Generated: 2026-07-20T13:46:09.781Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -1419,6 +1419,11 @@ interface AppliedOverridesSummary {
|
|
|
1419
1419
|
```typescript
|
|
1420
1420
|
interface SystemBlock {
|
|
1421
1421
|
basePlanId?: string
|
|
1422
|
+
* Stable capability tier microapps should branch on instead of
|
|
1423
|
+
* `basePlanId` — see docs/appConfig.md §4.1. Known tiers are `ProductMode`;
|
|
1424
|
+
* an unrecognised value is a future tier your code doesn't know about yet
|
|
1425
|
+
* — fail closed to the nearest tier you do understand rather than erroring.
|
|
1426
|
+
productMode?: ProductMode | (string & {})
|
|
1422
1427
|
addOnKeys?: string[]
|
|
1423
1428
|
apps?: string[]
|
|
1424
1429
|
* Explicit overrides only — an absent key is NOT "off". Resolve with
|
|
@@ -1451,6 +1456,8 @@ interface AppConfigSettings {
|
|
|
1451
1456
|
}
|
|
1452
1457
|
```
|
|
1453
1458
|
|
|
1459
|
+
**ProductMode** = `'simple_redirect' | 'rich_redirect' | 'inform' | 'engage'`
|
|
1460
|
+
|
|
1454
1461
|
### appManifest
|
|
1455
1462
|
|
|
1456
1463
|
**AppBundle** (interface)
|
|
@@ -5878,6 +5885,23 @@ interface InteractionEventContext {
|
|
|
5878
5885
|
|
|
5879
5886
|
**EffectConfig** = ``
|
|
5880
5887
|
|
|
5888
|
+
### itemContext
|
|
5889
|
+
|
|
5890
|
+
**ItemContext** (interface)
|
|
5891
|
+
```typescript
|
|
5892
|
+
interface ItemContext {
|
|
5893
|
+
isAuthentic: boolean
|
|
5894
|
+
status: ItemContextStatus
|
|
5895
|
+
source: 'nfc' | 'serial'
|
|
5896
|
+
errorMessage?: string
|
|
5897
|
+
isRescan?: boolean
|
|
5898
|
+
tag?: TagContext
|
|
5899
|
+
checkedAt: number
|
|
5900
|
+
}
|
|
5901
|
+
```
|
|
5902
|
+
|
|
5903
|
+
**ItemContextStatus** = ``
|
|
5904
|
+
|
|
5881
5905
|
### jobs
|
|
5882
5906
|
|
|
5883
5907
|
**Job** (interface)
|
|
@@ -6408,6 +6432,24 @@ interface NfcClaimTagRequest {
|
|
|
6408
6432
|
}
|
|
6409
6433
|
```
|
|
6410
6434
|
|
|
6435
|
+
**TagContext** (interface)
|
|
6436
|
+
```typescript
|
|
6437
|
+
interface TagContext {
|
|
6438
|
+
status: TagStatus
|
|
6439
|
+
tagId?: string
|
|
6440
|
+
claimSetId?: string
|
|
6441
|
+
codeId?: string
|
|
6442
|
+
count?: number
|
|
6443
|
+
previousCount?: number
|
|
6444
|
+
data?: Record<string, unknown>
|
|
6445
|
+
source: 'nfc-validate' | 'nfc-lookup' | 'tag-index' | 'none'
|
|
6446
|
+
validatedAt: number
|
|
6447
|
+
errorMessage?: string
|
|
6448
|
+
}
|
|
6449
|
+
```
|
|
6450
|
+
|
|
6451
|
+
**TagStatus** = `'valid' | 'rescan' | 'invalid' | 'error'`
|
|
6452
|
+
|
|
6411
6453
|
### order
|
|
6412
6454
|
|
|
6413
6455
|
**OrderItem** (interface)
|
|
@@ -7692,6 +7734,15 @@ interface SmartLinksWidgetProps {
|
|
|
7692
7734
|
* accepted for backward compatibility.
|
|
7693
7735
|
onNavigate?: (request: NavigationRequest | string) => void
|
|
7694
7736
|
publicPortalUrl?: string
|
|
7737
|
+
* Authenticity context for the specific item (proof) the URL points at,
|
|
7738
|
+
* resolved via an NFC tap or a serial proof URL. `undefined` for
|
|
7739
|
+
* collection- and product-only URLs, where there's no item to verify.
|
|
7740
|
+
* See docs/item-context.md.
|
|
7741
|
+
itemContext?: ItemContext
|
|
7742
|
+
* @deprecated Use `itemContext.tag` instead. Kept for one release for
|
|
7743
|
+
* backward compatibility with scanner-aware apps that read raw NFC/SUN
|
|
7744
|
+
* data directly. See docs/item-context.md.
|
|
7745
|
+
tag?: TagContext
|
|
7695
7746
|
size?: 'compact' | 'standard' | 'large'
|
|
7696
7747
|
lang?: string
|
|
7697
7748
|
translations?: Record<string, string>
|
|
@@ -7861,6 +7912,7 @@ interface ConditionParams {
|
|
|
7861
7912
|
product?: ProductInfo
|
|
7862
7913
|
proof?: ProofInfo
|
|
7863
7914
|
collection?: CollectionInfo
|
|
7915
|
+
itemContext?: ItemContext
|
|
7864
7916
|
stats?: StatsInfo
|
|
7865
7917
|
fetchCondition?: (collectionId: string, conditionId: string) => Promise<ConditionSet | null>
|
|
7866
7918
|
getLocation?: () => Promise<{ latitude: number; longitude: number }>
|
package/docs/appConfig.md
CHANGED
|
@@ -60,9 +60,15 @@ export interface AppEntry {
|
|
|
60
60
|
[k: string]: unknown; // per-module inline settings preserved
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/** Known `productMode` tiers, low → high — see §4.1. Not exhaustive; unrecognised values are future tiers. */
|
|
64
|
+
export type ProductMode = 'simple_redirect' | 'rich_redirect' | 'inform' | 'engage';
|
|
65
|
+
|
|
63
66
|
/** Resolved entitlements — safe for every client to read. */
|
|
64
67
|
export interface SystemBlock {
|
|
68
|
+
/** Internal billing identifier — not a capability signal, see §4. */
|
|
65
69
|
basePlanId?: string;
|
|
70
|
+
/** Stable capability tier — see §4.1. Prefer this over basePlanId. */
|
|
71
|
+
productMode?: ProductMode | (string & {});
|
|
66
72
|
addOnKeys?: string[];
|
|
67
73
|
apps?: string[]; // apps unlocked by add-ons
|
|
68
74
|
features?: Record<string, boolean>; // explicit overrides only — see §4.4 for absent-key resolution
|
|
@@ -184,12 +190,46 @@ Written only by `entitlements-reconcile`. Represents the union of:
|
|
|
184
190
|
- every add-on line item on their Stripe subscription
|
|
185
191
|
- `systemPrivate.overrides` (applied last, always wins)
|
|
186
192
|
|
|
187
|
-
|
|
193
|
+
`basePlanId` is an internal Stripe billing/plan identifier (string,
|
|
194
|
+
sourced from subscription metadata) — a billing concern, not a capability
|
|
195
|
+
signal. Don't branch app behavior on its literal value or its internal
|
|
196
|
+
plan names; that's what `productMode` is for.
|
|
197
|
+
|
|
198
|
+
### 4.1 `productMode`
|
|
199
|
+
|
|
200
|
+
Stable capability tier the app should branch on instead of `basePlanId`
|
|
201
|
+
(a billing identity that can change independently of capability). Written
|
|
202
|
+
by `entitlements-reconcile` on every sync. Typed as `ProductMode` in the
|
|
203
|
+
SDK (`src/types/appConfiguration.ts`) — a union of the known tiers below,
|
|
204
|
+
plus any other string, since future tiers will show up as new values
|
|
205
|
+
before the SDK type is updated to know their names.
|
|
206
|
+
|
|
207
|
+
Ladder, low → high:
|
|
208
|
+
|
|
209
|
+
| `productMode` | Unlocks (roughly) |
|
|
210
|
+
| ------------------ | --------------------------------------------------------------------------------- |
|
|
211
|
+
| `simple_redirect` | Redirect-only: scanning a tag/QR sends the user straight to a URL — no product/proof page, no catalog UI. |
|
|
212
|
+
| `rich_redirect` | Adds a rendered product/proof page (image, description, spec data) — still no deeper apps. |
|
|
213
|
+
| `inform` | Adds informational surfaces beyond the basic page — documentation, provenance, facts-style apps. |
|
|
214
|
+
| `engage` | Adds engagement/ownership capabilities — claim, CRM, loyalty, interaction tracking. Top of the ladder today. |
|
|
215
|
+
|
|
216
|
+
> These four one-liners are inferred from the tier names and ladder
|
|
217
|
+
> ordering, not copied from an authoritative source — worth a sanity check
|
|
218
|
+
> against the actual capability gating before treating them as precise.
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
const mode = cfg.system?.productMode;
|
|
222
|
+
if (mode === 'simple_redirect' || mode === 'rich_redirect') {
|
|
223
|
+
// redirect-only surface — hide catalog UI
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Unknown values (future tiers your code predates) should fail closed to
|
|
228
|
+
the nearest tier you understand, not throw or assume the top tier.
|
|
188
229
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
`hub_enrich`, `hub_engage`.
|
|
230
|
+
Not a feature flag and not a billing key — per-capability gating still
|
|
231
|
+
goes through `features` (§4.4); Stripe price selection still goes
|
|
232
|
+
through `basePlanId` + add-ons.
|
|
193
233
|
|
|
194
234
|
### 4.2 `addOnKeys[]`
|
|
195
235
|
|
|
@@ -253,6 +293,7 @@ about:
|
|
|
253
293
|
| Flag | Gates |
|
|
254
294
|
| -------------- | ---------------------------------------------------------------------- |
|
|
255
295
|
| `analytics` | Analytics surfaces (dashboards, reporting, event exploration). |
|
|
296
|
+
| `authenticity` | Live authenticity tracking — flags genuine vs. counterfeit items and enables auditing/tracing on them (e.g. impossible-travel detection). |
|
|
256
297
|
| `crm` | Customer interaction / CRM — contact records, broadcasts, segments. Sub-features hang off the `crm.*` dotted prefix, e.g. `crm.broadcasts`, `crm.segments`. |
|
|
257
298
|
| `hub` | The Hub module. |
|
|
258
299
|
| `portal` | QR code scan portals. |
|
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,183 @@
|
|
|
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
|
+
- `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 a fresh tap and a rescan
|
|
167
|
+
|
|
168
|
+
## Deprecated
|
|
169
|
+
|
|
170
|
+
The old top-level `tag` prop (`TagContext`) is still forwarded for one
|
|
171
|
+
release for backward compatibility with scanner-aware apps that read raw
|
|
172
|
+
SUN data. New code should read `itemContext.tag` and treat `itemContext`
|
|
173
|
+
as the source of truth.
|
|
174
|
+
|
|
175
|
+
## Note on terminology
|
|
176
|
+
|
|
177
|
+
Today the SDK still uses `proof` for a specific item record (a serialised
|
|
178
|
+
instance of a product). "Item record" is under consideration as a clearer
|
|
179
|
+
name — no API changes are planned imminently, but a future SDK version may
|
|
180
|
+
support both `proof`- and `item record`-shaped names for the same
|
|
181
|
+
functions/types side by side. `itemContext` is deliberately future-proof:
|
|
182
|
+
it describes context about whatever the URL points at, regardless of what
|
|
183
|
+
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'`, `'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 `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 |
|