fontdue-js 3.4.1 → 3.5.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/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 3.5.0
2
+
3
+ - Adding to the cart now sends the buyer's analytics context (cookie consent, anonymous ID, Meta's `_fbp`/`_fbc`) with the request, the same way opening the cart already did. Fontdue records a "Product Added" event for each add and a "Checkout Started" event the first time the buyer submits their contact details in checkout.
4
+ - `createFontdueFetch` from `fontdue-js/server` now sends the `fontdue-client-version` header, so server-rendered requests report the package version to Fontdue the way browser requests already did.
5
+
1
6
  ## 3.4.1
2
7
 
3
8
  - Fixed the standalone type tester dropping an axis slider when its `variable-settings` starting value is negative. `variable-settings="slnt -11"` was rejected as unparseable, so no slant slider appeared and nothing in the markup explained why. Any axis with a negative range was affected. Extra spaces between an axis and its value are tolerated now too.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @generated SignedSource<<fa9a3fe0cf1077d643c7e9bbcf053058>>
2
+ * @generated SignedSource<<74cae87a2fa5e1e7b1ee3b6482faabae>>
3
3
  * @lightSyntaxTransform
4
4
  * @nogrep
5
5
  */
@@ -10,6 +10,7 @@ export type CreateOrderItemsInput = {
10
10
  licenseeIsBillingIdentity?: boolean | null;
11
11
  orderVariableSelections?: ReadonlyArray<OrderVariableSelectionInput> | null;
12
12
  skuIds: ReadonlyArray<string | null>;
13
+ tracking?: OrderTrackingInput | null;
13
14
  };
14
15
  export type LicenseSelectionInput = {
15
16
  id?: string | null;
@@ -23,6 +24,13 @@ export type OrderVariableSelectionInput = {
23
24
  orderVariableId: string;
24
25
  orderVariableOptionId?: string | null;
25
26
  };
27
+ export type OrderTrackingInput = {
28
+ analyticsConsent?: boolean | null;
29
+ anonymousId?: string | null;
30
+ fbc?: string | null;
31
+ fbp?: string | null;
32
+ url?: string | null;
33
+ };
26
34
  export type PrecartAddToCartMutation$variables = {
27
35
  input: CreateOrderItemsInput;
28
36
  };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @generated SignedSource<<fa9a3fe0cf1077d643c7e9bbcf053058>>
2
+ * @generated SignedSource<<74cae87a2fa5e1e7b1ee3b6482faabae>>
3
3
  * @lightSyntaxTransform
4
4
  * @nogrep
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @generated SignedSource<<bdbd57be989b0c0a5ac24293c9e1a97b>>
2
+ * @generated SignedSource<<fef9b63617978230edc13ef9b3412c2e>>
3
3
  * @lightSyntaxTransform
4
4
  * @nogrep
5
5
  */
@@ -10,6 +10,7 @@ export type CreateOrderItemsInput = {
10
10
  licenseeIsBillingIdentity?: boolean | null;
11
11
  orderVariableSelections?: ReadonlyArray<OrderVariableSelectionInput> | null;
12
12
  skuIds: ReadonlyArray<string | null>;
13
+ tracking?: OrderTrackingInput | null;
13
14
  };
14
15
  export type LicenseSelectionInput = {
15
16
  id?: string | null;
@@ -23,6 +24,13 @@ export type OrderVariableSelectionInput = {
23
24
  orderVariableId: string;
24
25
  orderVariableOptionId?: string | null;
25
26
  };
27
+ export type OrderTrackingInput = {
28
+ analyticsConsent?: boolean | null;
29
+ anonymousId?: string | null;
30
+ fbc?: string | null;
31
+ fbp?: string | null;
32
+ url?: string | null;
33
+ };
26
34
  export type StoreModalProductSummaryAddToCartMutation$variables = {
27
35
  input: CreateOrderItemsInput;
28
36
  };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @generated SignedSource<<bdbd57be989b0c0a5ac24293c9e1a97b>>
2
+ * @generated SignedSource<<fef9b63617978230edc13ef9b3412c2e>>
3
3
  * @lightSyntaxTransform
4
4
  * @nogrep
5
5
  */
@@ -245,6 +245,22 @@ describe('createFontdueFetch', () => {
245
245
  expect(init.next).toBeUndefined();
246
246
  });
247
247
  });
248
+ describe('fontdue-client-version header', () => {
249
+ it('sends the package version, like the Relay layer does', async () => {
250
+ const fetchMock = mockFetch(() => ({
251
+ status: 200,
252
+ json: async () => ({
253
+ data: {}
254
+ })
255
+ }));
256
+ const fetchGraphql = createFontdueFetch({
257
+ url: 'https://acme.fontdue.com'
258
+ });
259
+ await fetchGraphql('Q', 'query Q { __typename }');
260
+ const init = fetchMock.mock.calls[0][1];
261
+ expect(init.headers['fontdue-client-version']).toBe('0.0.0-test');
262
+ });
263
+ });
248
264
  describe('fontdue-preview header', () => {
249
265
  it('sends "false" by default so a public/session-only request never reveals hidden fonts', async () => {
250
266
  const fetchMock = mockFetch(() => ({
@@ -111,6 +111,44 @@ describe('createNetworkFetch (server)', () => {
111
111
  function headersOf(fetchMock) {
112
112
  return fetchMock.mock.calls[0][1].headers;
113
113
  }
114
+ describe('createNetworkFetch (fontdue-client-version header)', () => {
115
+ // The server records this per tenant (Fontage.ClientVersions) to find sites
116
+ // that must upgrade before a client-dependent feature works for them.
117
+ it('sends the package version on every request', async () => {
118
+ vi.stubEnv('FONTDUE_URL', 'https://acme.fontdue.com');
119
+ const fetchMock = vi.fn(async () => ({
120
+ json: async () => ({
121
+ data: {}
122
+ })
123
+ }));
124
+ vi.stubGlobal('fetch', fetchMock);
125
+ const {
126
+ createNetworkFetch,
127
+ version
128
+ } = await import("../relay/environment.js");
129
+ await createNetworkFetch()(request, {});
130
+ expect(version).toBe('0.0.0-test');
131
+ expect(headersOf(fetchMock)['fontdue-client-version']).toBe('0.0.0-test');
132
+ });
133
+ it('cannot be overridden by per-call headers', async () => {
134
+ vi.stubEnv('FONTDUE_URL', 'https://acme.fontdue.com');
135
+ const fetchMock = vi.fn(async () => ({
136
+ json: async () => ({
137
+ data: {}
138
+ })
139
+ }));
140
+ vi.stubGlobal('fetch', fetchMock);
141
+ const {
142
+ createNetworkFetch
143
+ } = await import("../relay/environment.js");
144
+ await createNetworkFetch({
145
+ headers: {
146
+ 'fontdue-client-version': '9.9.9'
147
+ }
148
+ })(request, {});
149
+ expect(headersOf(fetchMock)['fontdue-client-version']).toBe('0.0.0-test');
150
+ });
151
+ });
114
152
  describe('createNetworkFetch (fontdue-preview header)', () => {
115
153
  it('sends fontdue-preview: false on a public server fetch (no token)', async () => {
116
154
  vi.stubEnv('FONTDUE_URL', 'https://acme.fontdue.com');
@@ -1,10 +1,27 @@
1
1
  import { Environment } from 'relay-runtime';
2
+ export type OrderTrackingInput = {
3
+ analyticsConsent: boolean;
4
+ anonymousId?: string;
5
+ fbp?: string;
6
+ fbc?: string;
7
+ url: string;
8
+ };
2
9
  /**
3
- * Stores the buyer's analytics context (cookie consent, Meta browser IDs,
4
- * checkout page URL) on the current order. The server emits the purchase
5
- * event from a Stripe webhook outside the browser — so this is how that
6
- * event respects the consent banner and carries attribution.
10
+ * The buyer's analytics context as the order mutations take it: cookie
11
+ * consent, the anonymous ID, Meta's browser IDs (`_fbp`/`_fbc`) and the page.
12
+ * Sent with add-to-cart (`createOrderItems`) and cart/checkout open
13
+ * (`updateOrderTracking`) alike, so the server captures the same identifiers
14
+ * on every event it emits for the order — "Product Added", "Checkout Started"
15
+ * (which the server emits when the buyer submits their contact details, not
16
+ * from these calls) and the purchase, which fires from a Stripe webhook with
17
+ * no browser at all.
7
18
  *
8
- * Fire-and-forget: tracking must never break checkout.
19
+ * Returns undefined outside a browser or if anything throws: tracking must
20
+ * never break the cart.
21
+ */
22
+ export declare function orderTrackingInput(): OrderTrackingInput | undefined;
23
+ /**
24
+ * Stores the buyer's analytics context on the current order as the cart or
25
+ * checkout opens. Fire-and-forget: tracking must never break checkout.
9
26
  */
10
27
  export declare function sendOrderTracking(environment: Environment): void;
@@ -5,27 +5,46 @@ function readCookie(name) {
5
5
  const match = document.cookie.match(new RegExp('(?:^|;\\s*)' + name + '=([^;]*)'));
6
6
  return match ? decodeURIComponent(match[1]) : undefined;
7
7
  }
8
-
9
8
  /**
10
- * Stores the buyer's analytics context (cookie consent, Meta browser IDs,
11
- * checkout page URL) on the current order. The server emits the purchase
12
- * event from a Stripe webhook outside the browser — so this is how that
13
- * event respects the consent banner and carries attribution.
9
+ * The buyer's analytics context as the order mutations take it: cookie
10
+ * consent, the anonymous ID, Meta's browser IDs (`_fbp`/`_fbc`) and the page.
11
+ * Sent with add-to-cart (`createOrderItems`) and cart/checkout open
12
+ * (`updateOrderTracking`) alike, so the server captures the same identifiers
13
+ * on every event it emits for the order — "Product Added", "Checkout Started"
14
+ * (which the server emits when the buyer submits their contact details, not
15
+ * from these calls) and the purchase, which fires from a Stripe webhook with
16
+ * no browser at all.
14
17
  *
15
- * Fire-and-forget: tracking must never break checkout.
18
+ * Returns undefined outside a browser or if anything throws: tracking must
19
+ * never break the cart.
20
+ */
21
+ export function orderTrackingInput() {
22
+ try {
23
+ if (typeof window === 'undefined') return undefined;
24
+ return {
25
+ analyticsConsent: hasConsent('analytics'),
26
+ anonymousId: getClientAnonymousId(),
27
+ fbp: readCookie('_fbp'),
28
+ fbc: readCookie('_fbc'),
29
+ url: window.location.href
30
+ };
31
+ } catch {
32
+ return undefined;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Stores the buyer's analytics context on the current order as the cart or
38
+ * checkout opens. Fire-and-forget: tracking must never break checkout.
16
39
  */
17
40
  export function sendOrderTracking(environment) {
41
+ const input = orderTrackingInput();
42
+ if (!input) return;
18
43
  try {
19
44
  commitMutation(environment, {
20
45
  mutation: (_orderTrackingUpdateOrderTrackingMutation.hash && _orderTrackingUpdateOrderTrackingMutation.hash !== "d59a127a7f140424f507ae549731bac7" && console.error("The definition of 'orderTrackingUpdateOrderTrackingMutation' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _orderTrackingUpdateOrderTrackingMutation),
21
46
  variables: {
22
- input: {
23
- analyticsConsent: hasConsent('analytics'),
24
- anonymousId: getClientAnonymousId(),
25
- fbp: readCookie('_fbp'),
26
- fbc: readCookie('_fbc'),
27
- url: window.location.href
28
- }
47
+ input
29
48
  },
30
49
  onCompleted: () => undefined,
31
50
  onError: () => undefined
@@ -12,6 +12,7 @@ import License from './License.js';
12
12
  import { Price } from '../Price/index.js';
13
13
  import { pluralize } from '../../utils.js';
14
14
  import ComponentsContext from '../ComponentsContext.js';
15
+ import { orderTrackingInput } from '../Cart/orderTracking.js';
15
16
  function skuName(sku) {
16
17
  if (sku.product && 'name' in sku.product) return sku.product.name;
17
18
  return null;
@@ -153,7 +154,8 @@ function Precart(_ref3) {
153
154
  const variables = {
154
155
  input: {
155
156
  skuIds: selectedItemsArray(),
156
- licenseSelections: licenseSelectionsArray()
157
+ licenseSelections: licenseSelectionsArray(),
158
+ tracking: orderTrackingInput()
157
159
  }
158
160
  };
159
161
  const mutation = (_PrecartAddToCartMutation.hash && _PrecartAddToCartMutation.hash !== "c5ead46ecc07099bee68d4f4f1ecb5bf" && console.error("The definition of 'PrecartAddToCartMutation' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _PrecartAddToCartMutation);
@@ -11,6 +11,7 @@ import { pluralize } from '../../utils.js';
11
11
  import { useLicenseAndOrderVariables } from '../../hooks/useLicenseAndOrderVariables.js';
12
12
  import { useRefetchOnLicenseChanges } from '../../hooks/useRefetchOnLicenseChanges.js';
13
13
  import ConfigContext from '../ConfigContext.js';
14
+ import { orderTrackingInput } from '../Cart/orderTracking.js';
14
15
  const addToCartMutation = (_StoreModalProductSummaryAddToCartMutation.hash && _StoreModalProductSummaryAddToCartMutation.hash !== "91ea762e3842af2919da3a4c710a48cc" && console.error("The definition of 'StoreModalProductSummaryAddToCartMutation' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _StoreModalProductSummaryAddToCartMutation);
15
16
  const clearCartMutation = (_StoreModalProductSummaryClearCartMutation.hash && _StoreModalProductSummaryClearCartMutation.hash !== "b5c37f74432030a297c1bcf2be63b6f7" && console.error("The definition of 'StoreModalProductSummaryClearCartMutation' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _StoreModalProductSummaryClearCartMutation);
16
17
  const countStyles = viewer => {
@@ -105,7 +106,8 @@ const StoreModalProductSummary = _ref => {
105
106
  });
106
107
  }, []),
107
108
  orderVariableSelections,
108
- licenseeIsBillingIdentity
109
+ licenseeIsBillingIdentity,
110
+ tracking: orderTrackingInput()
109
111
  }
110
112
  };
111
113
  commitAddToCart({
@@ -1,5 +1,6 @@
1
1
  import { Environment, RequestParameters, QueryResponseCache, Variables, GraphQLResponse } from 'relay-runtime';
2
- export declare const version: string;
2
+ import { version } from '../version.js';
3
+ export { version };
3
4
  export declare function fontdueBaseUrl(): string | undefined;
4
5
  export declare function createNetworkFetch(options?: CreateRelayEnvironmentOptions): (request: RequestParameters, variables: Variables) => Promise<GraphQLResponse>;
5
6
  export declare const networkFetch: (request: RequestParameters, variables: Variables) => Promise<GraphQLResponse>;
@@ -16,4 +17,3 @@ interface CreateRelayEnvironmentOptions {
16
17
  }
17
18
  export declare function createEnvironment(options: CreateRelayEnvironmentOptions): Environment;
18
19
  export declare function useCurrentEnvironment(options: CreateRelayEnvironmentOptions): Environment;
19
- export {};
@@ -3,12 +3,11 @@ import { handlePossibleCorsError } from '../corsError.js';
3
3
  import { resolveFontdueServerConfig } from './serverConfig.js';
4
4
  import { PREVIEW_HEADER, hasPreviewMarkerCookie } from '../preview/constants.js';
5
5
  import { NODE_ACCESS_HEADER } from '../nodeAccess.js';
6
+ import { CLIENT_VERSION_HEADER, version } from '../version.js';
6
7
 
7
- // `__FONTDUE_JS_VERSION__` is replaced by an inline babel plugin
8
- // (defineVersionPlugin in .babelrc.cjs) with the literal package.json#version.
9
- // Exported so UI (the admin toolbar) can surface it without re-reading the
10
- // build-time global in a 'use client' module.
11
- export const version = "3.4.1";
8
+ // Re-exported so UI (the admin toolbar) can surface the package version
9
+ // without re-reading the build-time global in a 'use client' module.
10
+ export { version };
12
11
  const IS_SERVER = typeof window === typeof undefined;
13
12
 
14
13
  // Opt server fetches into Next's data cache only in production; dev stays
@@ -78,7 +77,7 @@ export function createNetworkFetch(options) {
78
77
  Accept: 'application/json',
79
78
  'Content-Type': 'application/json',
80
79
  'fontdue-stripe-integration': (options === null || options === void 0 ? void 0 : options.stripeIntegration) ?? STRIPE_INTEGRATION ?? 'dynamic',
81
- 'fontdue-client-version': version
80
+ [CLIENT_VERSION_HEADER]: version
82
81
  };
83
82
 
84
83
  // Whether this request is an admin *preview*. On the server that's a
@@ -47,6 +47,7 @@
47
47
 
48
48
  import { resolveFontdueServerConfig, setFontdueServerConfig, resolveNodeAccessRecovery } from '../relay/serverConfig.js';
49
49
  import { PREVIEW_HEADER } from '../preview/constants.js';
50
+ import { CLIENT_VERSION_HEADER, version } from '../version.js';
50
51
  function readEnv(name) {
51
52
  if (typeof process !== 'undefined' && process.env) {
52
53
  const v = process.env[name];
@@ -146,7 +147,11 @@ export function createFontdueFetch() {
146
147
  const headers = {
147
148
  'content-type': 'application/json',
148
149
  ...(config === null || config === void 0 ? void 0 : config.headers),
149
- ...options.headers
150
+ ...options.headers,
151
+ // Which fontdue-js this site runs – the server records it per tenant so
152
+ // sites that need to upgrade for a feature can be found and warned.
153
+ // Set last so a caller's headers can't misreport it.
154
+ [CLIENT_VERSION_HEADER]: version
150
155
  };
151
156
 
152
157
  // Declare preview intent explicitly: a forwarded admin token means this is a
@@ -0,0 +1,2 @@
1
+ export declare const CLIENT_VERSION_HEADER = "fontdue-client-version";
2
+ export declare const version: string;
@@ -0,0 +1,11 @@
1
+ // `__FONTDUE_JS_VERSION__` is replaced by an inline babel plugin
2
+ // (defineVersionPlugin in .babelrc.cjs) with the literal package.json#version;
3
+ // the CDN bundle gets the same literal from vite.config.ts `define`.
4
+ //
5
+ // Every GraphQL request sends it as the `fontdue-client-version` header
6
+ // (relay/environment.ts, server/index.ts, and the script-tag bootstrap in
7
+ // assets/fontdue/index.tsx). The server records the version per tenant
8
+ // (Fontage.ClientVersions) to spot sites that need to upgrade before a
9
+ // feature that depends on a newer client can work for them.
10
+ export const CLIENT_VERSION_HEADER = 'fontdue-client-version';
11
+ export const version = "3.5.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fontdue-js",
3
- "version": "3.4.1",
3
+ "version": "3.5.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "npm run relay && run-p build-js build-css build-ts",