@standhigher/besttrack-page-extension 0.3.0 → 0.4.0
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/README.md +69 -32
- package/dist/chunk-KD7KFFFK.js +127 -0
- package/dist/index.d.ts +359 -16
- package/dist/index.js +1943 -515
- package/dist/validation.d.ts +9 -0
- package/dist/validation.js +121 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,34 +1,71 @@
|
|
|
1
1
|
# @standhigher/besttrack-page-extension
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
`
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
`
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
3
|
+
This package provides the built-in Ready-to-go, Branded, and Sales tracking
|
|
4
|
+
templates for `@standhigher/puck-page-builder`. Each template has stable
|
|
5
|
+
namespaced v1 block IDs, default block order, theme tokens, and exported
|
|
6
|
+
`TemplatePolicy` metadata. All default blocks are singleton. Protected
|
|
7
|
+
query/structure blocks now use the core `BlockDefinition.policy` contract
|
|
8
|
+
(`required`, `singleton`, and `allowDelete: false`), which the editor enforces.
|
|
9
|
+
|
|
10
|
+
## Query provider
|
|
11
|
+
|
|
12
|
+
Branded and Sales still receive a host-owned `TrackingPageQuery` through `query`.
|
|
13
|
+
Ready-to-go storefront pages that keep the original Shopify Track Page backend
|
|
14
|
+
should inject `transport` instead of inventing a new request body:
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
<ReadyToGoRuntimeProvider
|
|
18
|
+
transport={{
|
|
19
|
+
post: (url, body) => apiPost(withShopifyAppProxyPrefix(url, APP_PROXY_PREFIX), body)
|
|
20
|
+
}}
|
|
21
|
+
>
|
|
22
|
+
<WebRenderer document={document} registry={registry} />
|
|
23
|
+
</ReadyToGoRuntimeProvider>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
That path reuses the original lookup: `POST /track/query?_t=`, snake_case
|
|
27
|
+
`{ order_number, email, tracking_number, lang }`, one retry, independent
|
|
28
|
+
`POST /products/recommend` with `{ page, page_size }`, and the original
|
|
29
|
+
progress / shipping / empty-state mapping. The host only supplies fetch,
|
|
30
|
+
App Proxy prefix, and credentials.
|
|
31
|
+
|
|
32
|
+
Branded and Sales keep the discriminated `query` callback:
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
const query: TrackingPageQuery = async (request) => {
|
|
36
|
+
if (request.mode === "tracking") {
|
|
37
|
+
return serverAuthorizedTrackingLookup(request.trackingNumber);
|
|
38
|
+
}
|
|
39
|
+
return serverAuthorizedOrderLookup(request.orderNumber, request.email);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
<SalesRuntimeProvider query={query} watermark={{ visible: true }}>
|
|
43
|
+
<WebRenderer document={document} registry={registry} />
|
|
44
|
+
</SalesRuntimeProvider>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`TrackingPageQueryRequest` is either `{ mode: "tracking", trackingNumber }`
|
|
48
|
+
or `{ mode: "order", orderNumber, email }`. Ready-to-go matches the original
|
|
49
|
+
Track Page and only requires non-empty trimmed fields. Branded and Sales still
|
|
50
|
+
perform local PRD format checks. The host must authorize again on the server.
|
|
51
|
+
A host must not submit an order number through the tracking-number mode.
|
|
52
|
+
|
|
53
|
+
Branded and Sales render a two-tab query card on the Hero’s right edge at
|
|
54
|
+
desktop widths and as a single full-width card on narrow screens. Each tab
|
|
55
|
+
retains its own input. Loading, error, empty, and replacement results remain
|
|
56
|
+
inside the scrollable query card; successful requests scroll the result into
|
|
57
|
+
view. The package never stores tokens and never writes a query result into a
|
|
58
|
+
`PageDocument`.
|
|
59
|
+
|
|
60
|
+
## Watermarks and data safety
|
|
61
|
+
|
|
62
|
+
The optional `watermark` provider prop is a host override. Ready-to-go uses the
|
|
63
|
+
original powered-by hide rule when it is omitted. Branded and Sales only render
|
|
64
|
+
`watermark.visible` and its optional label; they do not infer entitlement.
|
|
65
|
+
Query results and watermark state are transient.
|
|
66
|
+
|
|
67
|
+
Document props and bindings must remain JSON-only and must not contain a token,
|
|
68
|
+
secret, customer order data, email address, or live query result. Merchant
|
|
69
|
+
resources use stable Shopify IDs; product prices and availability are Runtime
|
|
70
|
+
data. Production URLs must be public HTTPS URLs. Missing display data and
|
|
71
|
+
unsafe resource URLs render controlled fallbacks.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// src/tracking-page-runtime.ts
|
|
2
|
+
function isValidTrackingNumber(value) {
|
|
3
|
+
return /^[A-Za-z0-9_-]{6,64}$/.test(value);
|
|
4
|
+
}
|
|
5
|
+
function isValidOrderNumber(value) {
|
|
6
|
+
return /^\S{1,64}$/.test(value);
|
|
7
|
+
}
|
|
8
|
+
function isValidOrderEmail(value) {
|
|
9
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) && value.length <= 254;
|
|
10
|
+
}
|
|
11
|
+
function isEmptyTrackingPageResult(result) {
|
|
12
|
+
return result.outcome === "empty";
|
|
13
|
+
}
|
|
14
|
+
function isTrackingPageResourceReference(value, kind) {
|
|
15
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
16
|
+
const candidate = value;
|
|
17
|
+
return typeof candidate.id === "string" && candidate.id.startsWith("gid://shopify/") && (candidate.kind === "product" || candidate.kind === "collection" || candidate.kind === "media") && (kind === void 0 || candidate.kind === kind);
|
|
18
|
+
}
|
|
19
|
+
function formatTrackingPageMoney(value, locale = "en-US") {
|
|
20
|
+
if (!Number.isInteger(value.amount) || !/^[A-Z]{3}$/.test(value.currencyCode)) return void 0;
|
|
21
|
+
try {
|
|
22
|
+
const amount = new Intl.NumberFormat(locale, { style: "currency", currency: value.currencyCode }).format(value.amount / 100);
|
|
23
|
+
const compareAt = value.compareAtAmount !== void 0 && Number.isInteger(value.compareAtAmount) && value.compareAtAmount > value.amount ? new Intl.NumberFormat(locale, { style: "currency", currency: value.currencyCode }).format(value.compareAtAmount / 100) : void 0;
|
|
24
|
+
return { amount, compareAt, startsAt: value.startsAt === true };
|
|
25
|
+
} catch {
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/tracking-page-url.ts
|
|
31
|
+
function hasControlCharacter(value) {
|
|
32
|
+
return [...value].some((character) => character.charCodeAt(0) < 32);
|
|
33
|
+
}
|
|
34
|
+
function isLocalDevelopmentHost(hostname) {
|
|
35
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
36
|
+
}
|
|
37
|
+
function allowsLocalhostByDefault() {
|
|
38
|
+
return typeof window !== "undefined" && isLocalDevelopmentHost(window.location.hostname);
|
|
39
|
+
}
|
|
40
|
+
function safeTrackingPageUrl(value, options = {}) {
|
|
41
|
+
if (typeof value !== "string" || !value || value.trim() !== value || hasControlCharacter(value)) return void 0;
|
|
42
|
+
try {
|
|
43
|
+
const url = new URL(value);
|
|
44
|
+
if (url.username || url.password) return void 0;
|
|
45
|
+
if (url.protocol === "https:" && !isLocalDevelopmentHost(url.hostname)) return url.toString();
|
|
46
|
+
if (url.protocol === "http:" && isLocalDevelopmentHost(url.hostname) && (options.allowLocalhost ?? allowsLocalhostByDefault())) return url.toString();
|
|
47
|
+
return void 0;
|
|
48
|
+
} catch {
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function isSafeTrackingPageUrl(value, options) {
|
|
53
|
+
return safeTrackingPageUrl(value, options) !== void 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/shopify-resource-contract.ts
|
|
57
|
+
var resourcePattern = /^gid:\/\/shopify\/(Product|Collection)\/[1-9]\d*$/;
|
|
58
|
+
function isShopifyResourceReference(value, kind) {
|
|
59
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
60
|
+
const candidate = value;
|
|
61
|
+
const match = typeof candidate.id === "string" ? resourcePattern.exec(candidate.id) : null;
|
|
62
|
+
const expectedKind = match?.[1] === "Product" ? "product" : match?.[1] === "Collection" ? "collection" : void 0;
|
|
63
|
+
return candidate.kind === expectedKind && expectedKind !== void 0 && (kind === void 0 || expectedKind === kind) && typeof candidate.title === "string" && Boolean(candidate.title.trim()) && candidate.title.length <= 160 && (candidate.handle === void 0 || typeof candidate.handle === "string" && /^[a-z0-9][a-z0-9-]{0,254}$/i.test(candidate.handle));
|
|
64
|
+
}
|
|
65
|
+
function resourceKey(reference) {
|
|
66
|
+
return `${reference.kind}:${reference.id}`;
|
|
67
|
+
}
|
|
68
|
+
function isResolvedResource(value) {
|
|
69
|
+
if (!isShopifyResourceReference(value)) return false;
|
|
70
|
+
const candidate = value;
|
|
71
|
+
return candidate.status === "resolved" && (candidate.availability === "available" || candidate.availability === "sold-out" || candidate.availability === "unavailable" || candidate.availability === "unknown") && (candidate.href === void 0 || typeof candidate.href === "string") && (candidate.imageUrl === void 0 || typeof candidate.imageUrl === "string");
|
|
72
|
+
}
|
|
73
|
+
async function resolveShopifyResources(references, resolver) {
|
|
74
|
+
const resources = {};
|
|
75
|
+
const errors = {};
|
|
76
|
+
const valid = /* @__PURE__ */ new Map();
|
|
77
|
+
references.forEach((reference, index) => {
|
|
78
|
+
if (!isShopifyResourceReference(reference)) {
|
|
79
|
+
errors[`invalid:${index}`] = { code: "invalid-reference" };
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
valid.set(resourceKey(reference), reference);
|
|
83
|
+
});
|
|
84
|
+
if (!valid.size) return { resources, errors };
|
|
85
|
+
let resolved;
|
|
86
|
+
try {
|
|
87
|
+
resolved = await resolver.resolve({ references: [...valid.values()] });
|
|
88
|
+
} catch {
|
|
89
|
+
valid.forEach((reference) => {
|
|
90
|
+
errors[resourceKey(reference)] = { code: "resolution-failed" };
|
|
91
|
+
});
|
|
92
|
+
return { resources, errors };
|
|
93
|
+
}
|
|
94
|
+
resolved.forEach((candidate) => {
|
|
95
|
+
if (!isResolvedResource(candidate)) return;
|
|
96
|
+
const key = resourceKey(candidate);
|
|
97
|
+
if (!valid.has(key)) return;
|
|
98
|
+
const href = safeTrackingPageUrl(candidate.href);
|
|
99
|
+
const imageUrl = safeTrackingPageUrl(candidate.imageUrl);
|
|
100
|
+
resources[key] = { id: candidate.id, kind: candidate.kind, title: candidate.title.trim(), ...candidate.handle ? { handle: candidate.handle } : {}, status: "resolved", availability: candidate.availability, ...href ? { href } : {}, ...imageUrl ? { imageUrl } : {} };
|
|
101
|
+
});
|
|
102
|
+
valid.forEach((reference, key) => {
|
|
103
|
+
if (!resources[key]) errors[key] = { code: "missing" };
|
|
104
|
+
});
|
|
105
|
+
return { resources, errors };
|
|
106
|
+
}
|
|
107
|
+
function getResolvedShopifyResource(reference, resolution) {
|
|
108
|
+
return isShopifyResourceReference(reference) ? resolution?.resources[resourceKey(reference)] : void 0;
|
|
109
|
+
}
|
|
110
|
+
function getShopifyResourceResolutionError(reference, resolution) {
|
|
111
|
+
return isShopifyResourceReference(reference) ? resolution?.errors[resourceKey(reference)] : void 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export {
|
|
115
|
+
isValidTrackingNumber,
|
|
116
|
+
isValidOrderNumber,
|
|
117
|
+
isValidOrderEmail,
|
|
118
|
+
isEmptyTrackingPageResult,
|
|
119
|
+
isTrackingPageResourceReference,
|
|
120
|
+
formatTrackingPageMoney,
|
|
121
|
+
safeTrackingPageUrl,
|
|
122
|
+
isSafeTrackingPageUrl,
|
|
123
|
+
isShopifyResourceReference,
|
|
124
|
+
resolveShopifyResources,
|
|
125
|
+
getResolvedShopifyResource,
|
|
126
|
+
getShopifyResourceResolutionError
|
|
127
|
+
};
|