@windrun-huaiin/third-ui 14.0.1 → 14.0.2

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.
@@ -10,6 +10,9 @@ var fingerprintProvider = require('./fingerprint-provider.js');
10
10
 
11
11
  exports.FINGERPRINT_CONSTANTS = fingerprintShared.FINGERPRINT_CONSTANTS;
12
12
  exports.FINGERPRINT_COOKIE_NAME = fingerprintShared.FINGERPRINT_COOKIE_NAME;
13
+ exports.FINGERPRINT_FIRST_TOUCH_COOKIE_NAME = fingerprintShared.FINGERPRINT_FIRST_TOUCH_COOKIE_NAME;
14
+ exports.FINGERPRINT_FIRST_TOUCH_HEADER = fingerprintShared.FINGERPRINT_FIRST_TOUCH_HEADER;
15
+ exports.FINGERPRINT_FIRST_TOUCH_STORAGE_KEY = fingerprintShared.FINGERPRINT_FIRST_TOUCH_STORAGE_KEY;
13
16
  exports.FINGERPRINT_HEADER_NAME = fingerprintShared.FINGERPRINT_HEADER_NAME;
14
17
  exports.FINGERPRINT_SOURCE_REFER = fingerprintShared.FINGERPRINT_SOURCE_REFER;
15
18
  exports.FINGERPRINT_STORAGE_KEY = fingerprintShared.FINGERPRINT_STORAGE_KEY;
@@ -19,6 +22,7 @@ exports.createFingerprintFetch = fingerprintClient.createFingerprintFetch;
19
22
  exports.createFingerprintHeaders = fingerprintClient.createFingerprintHeaders;
20
23
  exports.generateFingerprintId = fingerprintClient.generateFingerprintId;
21
24
  exports.getFingerprintId = fingerprintClient.getFingerprintId;
25
+ exports.getOrCreateFirstTouchData = fingerprintClient.getOrCreateFirstTouchData;
22
26
  exports.getOrGenerateFingerprintId = fingerprintClient.getOrGenerateFingerprintId;
23
27
  exports.setFingerprintId = fingerprintClient.setFingerprintId;
24
28
  exports.useFingerprintHeaders = fingerprintClient.useFingerprintHeaders;
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- export { FINGERPRINT_CONSTANTS, FINGERPRINT_COOKIE_NAME, FINGERPRINT_HEADER_NAME, FINGERPRINT_SOURCE_REFER, FINGERPRINT_STORAGE_KEY, isValidFingerprintId } from './fingerprint-shared.mjs';
3
- export { clearFingerprintId, createFingerprintFetch, createFingerprintHeaders, generateFingerprintId, getFingerprintId, getOrGenerateFingerprintId, setFingerprintId, useFingerprintHeaders } from './fingerprint-client.mjs';
2
+ export { FINGERPRINT_CONSTANTS, FINGERPRINT_COOKIE_NAME, FINGERPRINT_FIRST_TOUCH_COOKIE_NAME, FINGERPRINT_FIRST_TOUCH_HEADER, FINGERPRINT_FIRST_TOUCH_STORAGE_KEY, FINGERPRINT_HEADER_NAME, FINGERPRINT_SOURCE_REFER, FINGERPRINT_STORAGE_KEY, isValidFingerprintId } from './fingerprint-shared.mjs';
3
+ export { clearFingerprintId, createFingerprintFetch, createFingerprintHeaders, generateFingerprintId, getFingerprintId, getOrCreateFirstTouchData, getOrGenerateFingerprintId, setFingerprintId, useFingerprintHeaders } from './fingerprint-client.mjs';
4
4
  export { useFingerprint } from './use-fingerprint.mjs';
5
5
  export { FingerprintProvider, FingerprintStatus, useFingerprintContext, useFingerprintContextSafe, withFingerprint } from './fingerprint-provider.mjs';
@@ -7,6 +7,9 @@ var fingerprintServer = require('./fingerprint-server.js');
7
7
 
8
8
  exports.FINGERPRINT_CONSTANTS = fingerprintShared.FINGERPRINT_CONSTANTS;
9
9
  exports.FINGERPRINT_COOKIE_NAME = fingerprintShared.FINGERPRINT_COOKIE_NAME;
10
+ exports.FINGERPRINT_FIRST_TOUCH_COOKIE_NAME = fingerprintShared.FINGERPRINT_FIRST_TOUCH_COOKIE_NAME;
11
+ exports.FINGERPRINT_FIRST_TOUCH_HEADER = fingerprintShared.FINGERPRINT_FIRST_TOUCH_HEADER;
12
+ exports.FINGERPRINT_FIRST_TOUCH_STORAGE_KEY = fingerprintShared.FINGERPRINT_FIRST_TOUCH_STORAGE_KEY;
10
13
  exports.FINGERPRINT_HEADER_NAME = fingerprintShared.FINGERPRINT_HEADER_NAME;
11
14
  exports.FINGERPRINT_SOURCE_REFER = fingerprintShared.FINGERPRINT_SOURCE_REFER;
12
15
  exports.FINGERPRINT_STORAGE_KEY = fingerprintShared.FINGERPRINT_STORAGE_KEY;
@@ -1,2 +1,2 @@
1
- export { FINGERPRINT_CONSTANTS, FINGERPRINT_COOKIE_NAME, FINGERPRINT_HEADER_NAME, FINGERPRINT_SOURCE_REFER, FINGERPRINT_STORAGE_KEY, isValidFingerprintId } from './fingerprint-shared.mjs';
1
+ export { FINGERPRINT_CONSTANTS, FINGERPRINT_COOKIE_NAME, FINGERPRINT_FIRST_TOUCH_COOKIE_NAME, FINGERPRINT_FIRST_TOUCH_HEADER, FINGERPRINT_FIRST_TOUCH_STORAGE_KEY, FINGERPRINT_HEADER_NAME, FINGERPRINT_SOURCE_REFER, FINGERPRINT_STORAGE_KEY, isValidFingerprintId } from './fingerprint-shared.mjs';
2
2
  export { extractFingerprintFromNextRequest, extractFingerprintFromNextStores, extractFingerprintId, generateServerFingerprintId } from './fingerprint-server.mjs';
@@ -37,6 +37,8 @@ function useFingerprint(config) {
37
37
  */
38
38
  const initializeFingerprintId = React.useCallback(() => tslib_es6.__awaiter(this, void 0, void 0, function* () {
39
39
  try {
40
+ // Capture first-touch as early as possible before any in-site navigation can overwrite context.
41
+ fingerprintClient.getOrCreateFirstTouchData();
40
42
  const currentFingerprintId = yield fingerprintClient.getOrGenerateFingerprintId();
41
43
  console.log('Initialized fingerprintId:', currentFingerprintId);
42
44
  setFingerprintIdState(currentFingerprintId);
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { __awaiter } from '../../node_modules/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.46.2_tslib@2.8.1_typescript@5.9.3/node_modules/tslib/tslib.es6.mjs';
3
3
  import { useState, useCallback, useEffect } from 'react';
4
- import { getOrGenerateFingerprintId, createFingerprintHeaders } from './fingerprint-client.mjs';
4
+ import { getOrCreateFirstTouchData, getOrGenerateFingerprintId, createFingerprintHeaders } from './fingerprint-client.mjs';
5
5
  import { FINGERPRINT_SOURCE_REFER } from './fingerprint-shared.mjs';
6
6
 
7
7
  /**
@@ -35,6 +35,8 @@ function useFingerprint(config) {
35
35
  */
36
36
  const initializeFingerprintId = useCallback(() => __awaiter(this, void 0, void 0, function* () {
37
37
  try {
38
+ // Capture first-touch as early as possible before any in-site navigation can overwrite context.
39
+ getOrCreateFirstTouchData();
38
40
  const currentFingerprintId = yield getOrGenerateFingerprintId();
39
41
  console.log('Initialized fingerprintId:', currentFingerprintId);
40
42
  setFingerprintIdState(currentFingerprintId);
@@ -0,0 +1,7 @@
1
+ import { type ImageProps } from "next/image";
2
+ interface DelayedImgProps extends ImageProps {
3
+ wrapperClassName?: string;
4
+ placeholderClassName?: string;
5
+ }
6
+ export declare function DelayedImg({ alt, wrapperClassName, placeholderClassName, className, onLoad, ...imageProps }: DelayedImgProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,39 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var tslib_es6 = require('../node_modules/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.46.2_tslib@2.8.1_typescript@5.9.3/node_modules/tslib/tslib.es6.js');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var Image = require('next/image');
7
+ var React = require('react');
8
+ var lib = require('@windrun-huaiin/base-ui/lib');
9
+ var utils = require('@windrun-huaiin/lib/utils');
10
+
11
+ var _a, _b;
12
+ const ENV_DELAY_ENABLED = process.env.NEXT_PUBLIC_DELAYED_IMG_ENABLED === "true" ||
13
+ process.env.NEXT_PUBLIC_DELAY_REVEAL_ENABLED === "true";
14
+ const rawDelaySeconds = (_b = (_a = process.env.NEXT_PUBLIC_DELAYED_IMG_SECONDS) !== null && _a !== void 0 ? _a : process.env.NEXT_PUBLIC_DELAY_REVEAL_SECONDS) !== null && _b !== void 0 ? _b : "0";
15
+ const parsedDelaySeconds = Number(rawDelaySeconds);
16
+ const ENV_DELAY_MS = Number.isFinite(parsedDelaySeconds) && parsedDelaySeconds > 0
17
+ ? parsedDelaySeconds * 1000
18
+ : 0;
19
+ function DelayedImg(_a) {
20
+ var { alt, wrapperClassName, placeholderClassName, className, onLoad } = _a, imageProps = tslib_es6.__rest(_a, ["alt", "wrapperClassName", "placeholderClassName", "className", "onLoad"]);
21
+ const shouldDelay = ENV_DELAY_ENABLED && ENV_DELAY_MS > 0;
22
+ const [isMounted, setIsMounted] = React.useState(!shouldDelay);
23
+ const [isLoaded, setIsLoaded] = React.useState(false);
24
+ React.useEffect(() => {
25
+ if (!shouldDelay || isMounted) {
26
+ return;
27
+ }
28
+ const timer = window.setTimeout(() => {
29
+ setIsMounted(true);
30
+ }, ENV_DELAY_MS);
31
+ return () => window.clearTimeout(timer);
32
+ }, [isMounted, shouldDelay]);
33
+ return (jsxRuntime.jsxs("div", { className: utils.cn("relative", wrapperClassName), children: [(!isMounted || !isLoaded) && (jsxRuntime.jsxs("div", { "aria-hidden": "true", className: utils.cn("absolute inset-0 rounded-[inherit] border animate-pulse shadow-sm bg-white/70 dark:bg-white/5", lib.themeBgColor, placeholderClassName), children: [jsxRuntime.jsx("div", { className: utils.cn("absolute inset-x-0 top-0 h-28 rounded-[inherit] bg-linear-to-b from-white/80 to-transparent dark:from-white/14 dark:to-transparent", lib.themeViaColor) }), jsxRuntime.jsx("div", { className: "absolute inset-0 rounded-[inherit] bg-white/20 dark:bg-white/0" })] })), isMounted && (jsxRuntime.jsx(Image, Object.assign({}, imageProps, { alt: alt, onLoad: (event) => {
34
+ setIsLoaded(true);
35
+ onLoad === null || onLoad === void 0 ? void 0 : onLoad(event);
36
+ }, className: utils.cn("transition duration-300", isLoaded ? "opacity-100" : "opacity-0", className) })))] }));
37
+ }
38
+
39
+ exports.DelayedImg = DelayedImg;
@@ -0,0 +1,37 @@
1
+ "use client";
2
+ import { __rest } from '../node_modules/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.46.2_tslib@2.8.1_typescript@5.9.3/node_modules/tslib/tslib.es6.mjs';
3
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
+ import Image from 'next/image';
5
+ import { useState, useEffect } from 'react';
6
+ import { themeViaColor, themeBgColor } from '@windrun-huaiin/base-ui/lib';
7
+ import { cn } from '@windrun-huaiin/lib/utils';
8
+
9
+ var _a, _b;
10
+ const ENV_DELAY_ENABLED = process.env.NEXT_PUBLIC_DELAYED_IMG_ENABLED === "true" ||
11
+ process.env.NEXT_PUBLIC_DELAY_REVEAL_ENABLED === "true";
12
+ const rawDelaySeconds = (_b = (_a = process.env.NEXT_PUBLIC_DELAYED_IMG_SECONDS) !== null && _a !== void 0 ? _a : process.env.NEXT_PUBLIC_DELAY_REVEAL_SECONDS) !== null && _b !== void 0 ? _b : "0";
13
+ const parsedDelaySeconds = Number(rawDelaySeconds);
14
+ const ENV_DELAY_MS = Number.isFinite(parsedDelaySeconds) && parsedDelaySeconds > 0
15
+ ? parsedDelaySeconds * 1000
16
+ : 0;
17
+ function DelayedImg(_a) {
18
+ var { alt, wrapperClassName, placeholderClassName, className, onLoad } = _a, imageProps = __rest(_a, ["alt", "wrapperClassName", "placeholderClassName", "className", "onLoad"]);
19
+ const shouldDelay = ENV_DELAY_ENABLED && ENV_DELAY_MS > 0;
20
+ const [isMounted, setIsMounted] = useState(!shouldDelay);
21
+ const [isLoaded, setIsLoaded] = useState(false);
22
+ useEffect(() => {
23
+ if (!shouldDelay || isMounted) {
24
+ return;
25
+ }
26
+ const timer = window.setTimeout(() => {
27
+ setIsMounted(true);
28
+ }, ENV_DELAY_MS);
29
+ return () => window.clearTimeout(timer);
30
+ }, [isMounted, shouldDelay]);
31
+ return (jsxs("div", { className: cn("relative", wrapperClassName), children: [(!isMounted || !isLoaded) && (jsxs("div", { "aria-hidden": "true", className: cn("absolute inset-0 rounded-[inherit] border animate-pulse shadow-sm bg-white/70 dark:bg-white/5", themeBgColor, placeholderClassName), children: [jsx("div", { className: cn("absolute inset-x-0 top-0 h-28 rounded-[inherit] bg-linear-to-b from-white/80 to-transparent dark:from-white/14 dark:to-transparent", themeViaColor) }), jsx("div", { className: "absolute inset-0 rounded-[inherit] bg-white/20 dark:bg-white/0" })] })), isMounted && (jsx(Image, Object.assign({}, imageProps, { alt: alt, onLoad: (event) => {
32
+ setIsLoaded(true);
33
+ onLoad === null || onLoad === void 0 ? void 0 : onLoad(event);
34
+ }, className: cn("transition duration-300", isLoaded ? "opacity-100" : "opacity-0", className) })))] }));
35
+ }
36
+
37
+ export { DelayedImg };
@@ -8,6 +8,7 @@ export * from './rich-text-expert';
8
8
  export * from './faq-interactive';
9
9
  export * from './price-plan-interactive';
10
10
  export * from './gallery/gallery-interactive';
11
+ export * from './delayed-img';
11
12
  export { MoneyPriceInteractive } from './money-price/money-price-interactive';
12
13
  export { MoneyPriceButton } from './money-price/money-price-button';
13
14
  export { CreditOverviewClient } from './credit/credit-overview-client';
@@ -11,6 +11,7 @@ var richTextExpert = require('./rich-text-expert.js');
11
11
  var faqInteractive = require('./faq-interactive.js');
12
12
  var pricePlanInteractive = require('./price-plan-interactive.js');
13
13
  var galleryInteractive = require('./gallery/gallery-interactive.js');
14
+ var delayedImg = require('./delayed-img.js');
14
15
  var moneyPriceInteractive = require('./money-price/money-price-interactive.js');
15
16
  var moneyPriceButton = require('./money-price/money-price-button.js');
16
17
  var creditOverviewClient = require('./credit/credit-overview-client.js');
@@ -29,6 +30,7 @@ exports.richText = richTextExpert.richText;
29
30
  exports.FAQInteractive = faqInteractive.FAQInteractive;
30
31
  exports.PricePlanInteractive = pricePlanInteractive.PricePlanInteractive;
31
32
  exports.GalleryInteractive = galleryInteractive.GalleryInteractive;
33
+ exports.DelayedImg = delayedImg.DelayedImg;
32
34
  exports.MoneyPriceInteractive = moneyPriceInteractive.MoneyPriceInteractive;
33
35
  exports.MoneyPriceButton = moneyPriceButton.MoneyPriceButton;
34
36
  exports.CreditOverviewClient = creditOverviewClient.CreditOverviewClient;
@@ -9,6 +9,7 @@ export { createRichTextRenderer, richText } from './rich-text-expert.mjs';
9
9
  export { FAQInteractive } from './faq-interactive.mjs';
10
10
  export { PricePlanInteractive } from './price-plan-interactive.mjs';
11
11
  export { GalleryInteractive } from './gallery/gallery-interactive.mjs';
12
+ export { DelayedImg } from './delayed-img.mjs';
12
13
  export { MoneyPriceInteractive } from './money-price/money-price-interactive.mjs';
13
14
  export { MoneyPriceButton } from './money-price/money-price-button.mjs';
14
15
  export { CreditOverviewClient } from './credit/credit-overview-client.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windrun-huaiin/third-ui",
3
- "version": "14.0.1",
3
+ "version": "14.0.2",
4
4
  "description": "Third-party integrated UI components for windrun-huaiin projects",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -5,7 +5,40 @@
5
5
  */
6
6
 
7
7
  import FingerprintJS from '@fingerprintjs/fingerprintjs';
8
- import { FINGERPRINT_COOKIE_NAME, FINGERPRINT_HEADER_NAME, FINGERPRINT_SOURCE_REFER, FINGERPRINT_STORAGE_KEY, isValidFingerprintId } from './fingerprint-shared';
8
+ import {
9
+ FINGERPRINT_COOKIE_NAME,
10
+ FINGERPRINT_FIRST_TOUCH_COOKIE_NAME,
11
+ FINGERPRINT_FIRST_TOUCH_HEADER,
12
+ FINGERPRINT_FIRST_TOUCH_STORAGE_KEY,
13
+ FINGERPRINT_HEADER_NAME,
14
+ FINGERPRINT_SOURCE_REFER,
15
+ FINGERPRINT_STORAGE_KEY,
16
+ isValidFingerprintId
17
+ } from './fingerprint-shared';
18
+
19
+ const FIRST_TOUCH_MAX_LENGTH = 2048;
20
+ const FIRST_TOUCH_COOKIE_DAYS = 30;
21
+
22
+ type FirstTouchData = {
23
+ landingUrl?: string;
24
+ landingPath?: string;
25
+ landingHost?: string;
26
+ externalReferrer?: string;
27
+ capturedAt?: string;
28
+ ref?: string;
29
+ utmSource?: string;
30
+ utmMedium?: string;
31
+ utmCampaign?: string;
32
+ utmTerm?: string;
33
+ utmContent?: string;
34
+ utmId?: string;
35
+ gclid?: string;
36
+ fbclid?: string;
37
+ msclkid?: string;
38
+ ttclid?: string;
39
+ twclid?: string;
40
+ liFatId?: string;
41
+ };
9
42
 
10
43
  /**
11
44
  * 检查浏览器存储(localStorage 和 cookie)中的指纹 ID
@@ -17,7 +50,7 @@ function checkStoredFingerprintId(): string | null {
17
50
  }
18
51
 
19
52
  // 优先检查 localStorage
20
- const localStorageId = localStorage.getItem(FINGERPRINT_STORAGE_KEY);
53
+ const localStorageId = getLocalStorageValue(FINGERPRINT_STORAGE_KEY);
21
54
  if (localStorageId && isValidFingerprintId(localStorageId)) {
22
55
  return localStorageId;
23
56
  }
@@ -26,13 +59,149 @@ function checkStoredFingerprintId(): string | null {
26
59
  const cookieId = getCookieValue(FINGERPRINT_COOKIE_NAME);
27
60
  if (cookieId && isValidFingerprintId(cookieId)) {
28
61
  // 同步到 localStorage
29
- localStorage.setItem(FINGERPRINT_STORAGE_KEY, cookieId);
62
+ setLocalStorageValue(FINGERPRINT_STORAGE_KEY, cookieId);
30
63
  return cookieId;
31
64
  }
32
65
 
33
66
  return null;
34
67
  }
35
68
 
69
+ function normalizeString(value: string | null | undefined, maxLength = FIRST_TOUCH_MAX_LENGTH): string | undefined {
70
+ if (!value) {
71
+ return undefined;
72
+ }
73
+
74
+ const trimmed = value.trim();
75
+ if (!trimmed) {
76
+ return undefined;
77
+ }
78
+
79
+ return trimmed.length > maxLength ? trimmed.slice(0, maxLength) : trimmed;
80
+ }
81
+
82
+ function readFirstTouchFromStorage(): FirstTouchData | null {
83
+ if (typeof window === 'undefined') {
84
+ return null;
85
+ }
86
+
87
+ const localStorageValue = getLocalStorageValue(FINGERPRINT_FIRST_TOUCH_STORAGE_KEY);
88
+ if (localStorageValue) {
89
+ const parsed = parseFirstTouchValue(localStorageValue);
90
+ if (parsed) {
91
+ syncFirstTouchStorage(parsed);
92
+ return parsed;
93
+ }
94
+ }
95
+
96
+ const cookieValue = getCookieValue(FINGERPRINT_FIRST_TOUCH_COOKIE_NAME);
97
+ if (cookieValue) {
98
+ const parsed = parseFirstTouchValue(cookieValue);
99
+ if (parsed) {
100
+ syncFirstTouchStorage(parsed);
101
+ return parsed;
102
+ }
103
+ }
104
+
105
+ return null;
106
+ }
107
+
108
+ function parseFirstTouchValue(value: string): FirstTouchData | null {
109
+ try {
110
+ const decoded = decodeURIComponent(value);
111
+ const parsed = JSON.parse(decoded) as FirstTouchData;
112
+ return sanitizeFirstTouchData(parsed);
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ function sanitizeFirstTouchData(data: FirstTouchData | null | undefined): FirstTouchData | null {
119
+ if (!data) {
120
+ return null;
121
+ }
122
+
123
+ const sanitized: FirstTouchData = {
124
+ landingUrl: normalizeString(data.landingUrl),
125
+ landingPath: normalizeString(data.landingPath, 512),
126
+ landingHost: normalizeString(data.landingHost, 255),
127
+ externalReferrer: normalizeString(data.externalReferrer),
128
+ capturedAt: normalizeString(data.capturedAt, 64),
129
+ ref: normalizeString(data.ref, 512),
130
+ utmSource: normalizeString(data.utmSource, 512),
131
+ utmMedium: normalizeString(data.utmMedium, 512),
132
+ utmCampaign: normalizeString(data.utmCampaign, 512),
133
+ utmTerm: normalizeString(data.utmTerm, 512),
134
+ utmContent: normalizeString(data.utmContent, 512),
135
+ utmId: normalizeString(data.utmId, 512),
136
+ gclid: normalizeString(data.gclid, 512),
137
+ fbclid: normalizeString(data.fbclid, 512),
138
+ msclkid: normalizeString(data.msclkid, 512),
139
+ ttclid: normalizeString(data.ttclid, 512),
140
+ twclid: normalizeString(data.twclid, 512),
141
+ liFatId: normalizeString(data.liFatId, 512),
142
+ };
143
+
144
+ return Object.values(sanitized).some(Boolean) ? sanitized : null;
145
+ }
146
+
147
+ function serializeFirstTouchData(data: FirstTouchData): string {
148
+ return encodeURIComponent(JSON.stringify(data));
149
+ }
150
+
151
+ function syncFirstTouchStorage(data: FirstTouchData): void {
152
+ if (typeof window === 'undefined') {
153
+ return;
154
+ }
155
+
156
+ const serialized = serializeFirstTouchData(data);
157
+ setLocalStorageValue(FINGERPRINT_FIRST_TOUCH_STORAGE_KEY, serialized);
158
+ setCookie(FINGERPRINT_FIRST_TOUCH_COOKIE_NAME, serialized, FIRST_TOUCH_COOKIE_DAYS);
159
+ }
160
+
161
+ function buildFirstTouchData(): FirstTouchData | null {
162
+ if (typeof window === 'undefined') {
163
+ return null;
164
+ }
165
+
166
+ const url = new URL(window.location.href);
167
+ const data = sanitizeFirstTouchData({
168
+ landingUrl: url.toString(),
169
+ landingPath: url.pathname,
170
+ landingHost: url.host,
171
+ externalReferrer: document.referrer || undefined,
172
+ capturedAt: new Date().toISOString(),
173
+ ref: url.searchParams.get('ref') ?? undefined,
174
+ utmSource: url.searchParams.get('utm_source') ?? undefined,
175
+ utmMedium: url.searchParams.get('utm_medium') ?? undefined,
176
+ utmCampaign: url.searchParams.get('utm_campaign') ?? undefined,
177
+ utmTerm: url.searchParams.get('utm_term') ?? undefined,
178
+ utmContent: url.searchParams.get('utm_content') ?? undefined,
179
+ utmId: url.searchParams.get('utm_id') ?? undefined,
180
+ gclid: url.searchParams.get('gclid') ?? undefined,
181
+ fbclid: url.searchParams.get('fbclid') ?? undefined,
182
+ msclkid: url.searchParams.get('msclkid') ?? undefined,
183
+ ttclid: url.searchParams.get('ttclid') ?? undefined,
184
+ twclid: url.searchParams.get('twclid') ?? undefined,
185
+ liFatId: url.searchParams.get('li_fat_id') ?? undefined,
186
+ });
187
+
188
+ return data;
189
+ }
190
+
191
+ export function getOrCreateFirstTouchData(): FirstTouchData | null {
192
+ const existing = readFirstTouchFromStorage();
193
+ if (existing) {
194
+ return existing;
195
+ }
196
+
197
+ const created = buildFirstTouchData();
198
+ if (created) {
199
+ syncFirstTouchStorage(created);
200
+ }
201
+
202
+ return created;
203
+ }
204
+
36
205
  /**
37
206
  * 生成基于真实浏览器特征的fingerprint ID
38
207
  * 使用 FingerprintJS 收集浏览器特征并生成唯一标识
@@ -56,7 +225,7 @@ export async function generateFingerprintId(): Promise<string> {
56
225
  const fingerprintId = `fp_${result.visitorId}`;
57
226
 
58
227
  // 存储到 localStorage 和 cookie
59
- localStorage.setItem(FINGERPRINT_STORAGE_KEY, fingerprintId);
228
+ setLocalStorageValue(FINGERPRINT_STORAGE_KEY, fingerprintId);
60
229
  setCookie(FINGERPRINT_COOKIE_NAME, fingerprintId, 365);
61
230
 
62
231
  console.log('Generated new fingerprint ID:', fingerprintId);
@@ -65,7 +234,7 @@ export async function generateFingerprintId(): Promise<string> {
65
234
  console.warn('Failed to generate fingerprint with FingerprintJS:', error);
66
235
  // 降级方案:生成基于时间戳和随机数的 ID
67
236
  const fallbackId = `fp_fallback_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
68
- localStorage.setItem(FINGERPRINT_STORAGE_KEY, fallbackId);
237
+ setLocalStorageValue(FINGERPRINT_STORAGE_KEY, fallbackId);
69
238
  setCookie(FINGERPRINT_COOKIE_NAME, fallbackId, 365);
70
239
 
71
240
  console.log('Generated fallback fingerprint ID:', fallbackId);
@@ -92,7 +261,7 @@ export function setFingerprintId(fingerprintId: string): void {
92
261
  throw new Error('Invalid fingerprint ID');
93
262
  }
94
263
 
95
- localStorage.setItem(FINGERPRINT_STORAGE_KEY, fingerprintId);
264
+ setLocalStorageValue(FINGERPRINT_STORAGE_KEY, fingerprintId);
96
265
  setCookie(FINGERPRINT_COOKIE_NAME, fingerprintId, 365);
97
266
  }
98
267
 
@@ -104,7 +273,7 @@ export function clearFingerprintId(): void {
104
273
  throw new Error('clearFingerprintId can only be used in browser environment');
105
274
  }
106
275
 
107
- localStorage.removeItem(FINGERPRINT_STORAGE_KEY);
276
+ removeLocalStorageValue(FINGERPRINT_STORAGE_KEY);
108
277
  deleteCookie(FINGERPRINT_COOKIE_NAME);
109
278
  }
110
279
 
@@ -127,9 +296,16 @@ export async function getOrGenerateFingerprintId(): Promise<string> {
127
296
  */
128
297
  export async function createFingerprintHeaders(): Promise<Record<string, string>> {
129
298
  const fingerprintId = await getOrGenerateFingerprintId();
130
- return {
299
+ const headers: Record<string, string> = {
131
300
  [FINGERPRINT_HEADER_NAME]: fingerprintId,
132
301
  };
302
+
303
+ const firstTouch = getOrCreateFirstTouchData();
304
+ if (firstTouch) {
305
+ headers[FINGERPRINT_FIRST_TOUCH_HEADER] = serializeFirstTouchData(firstTouch);
306
+ }
307
+
308
+ return headers;
133
309
  }
134
310
 
135
311
  /**
@@ -164,12 +340,16 @@ function getCookieValue(name: string): string | null {
164
340
  return null;
165
341
  }
166
342
 
167
- const value = `; ${document.cookie}`;
168
- const parts = value.split(`; ${name}=`);
169
- if (parts.length === 2) {
170
- return parts.pop()?.split(';').shift() || null;
343
+ try {
344
+ const value = `; ${document.cookie}`;
345
+ const parts = value.split(`; ${name}=`);
346
+ if (parts.length === 2) {
347
+ return parts.pop()?.split(';').shift() || null;
348
+ }
349
+ return null;
350
+ } catch {
351
+ return null;
171
352
  }
172
- return null;
173
353
  }
174
354
 
175
355
  function setCookie(name: string, value: string, days: number): void {
@@ -177,9 +357,13 @@ function setCookie(name: string, value: string, days: number): void {
177
357
  return;
178
358
  }
179
359
 
180
- const expires = new Date();
181
- expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
182
- document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`;
360
+ try {
361
+ const expires = new Date();
362
+ expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
363
+ document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`;
364
+ } catch {
365
+ // Ignore storage failures so attribution never blocks page flow.
366
+ }
183
367
  }
184
368
 
185
369
  function deleteCookie(name: string): void {
@@ -187,5 +371,45 @@ function deleteCookie(name: string): void {
187
371
  return;
188
372
  }
189
373
 
190
- document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`;
191
- }
374
+ try {
375
+ document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`;
376
+ } catch {
377
+ // Ignore storage failures so attribution never blocks page flow.
378
+ }
379
+ }
380
+
381
+ function getLocalStorageValue(key: string): string | null {
382
+ if (typeof window === 'undefined') {
383
+ return null;
384
+ }
385
+
386
+ try {
387
+ return window.localStorage.getItem(key);
388
+ } catch {
389
+ return null;
390
+ }
391
+ }
392
+
393
+ function setLocalStorageValue(key: string, value: string): void {
394
+ if (typeof window === 'undefined') {
395
+ return;
396
+ }
397
+
398
+ try {
399
+ window.localStorage.setItem(key, value);
400
+ } catch {
401
+ // Ignore storage failures so attribution never blocks page flow.
402
+ }
403
+ }
404
+
405
+ function removeLocalStorageValue(key: string): void {
406
+ if (typeof window === 'undefined') {
407
+ return;
408
+ }
409
+
410
+ try {
411
+ window.localStorage.removeItem(key);
412
+ } catch {
413
+ // Ignore storage failures so attribution never blocks page flow.
414
+ }
415
+ }
@@ -8,6 +8,9 @@ export const FINGERPRINT_STORAGE_KEY = '__x_fingerprint_id';
8
8
  export const FINGERPRINT_HEADER_NAME = 'x-fingerprint-id-v8';
9
9
  export const FINGERPRINT_COOKIE_NAME = '__x_fingerprint_id';
10
10
  export const FINGERPRINT_SOURCE_REFER = 'x-source-ref';
11
+ export const FINGERPRINT_FIRST_TOUCH_STORAGE_KEY = '__x_first_touch';
12
+ export const FINGERPRINT_FIRST_TOUCH_COOKIE_NAME = '__x_first_touch';
13
+ export const FINGERPRINT_FIRST_TOUCH_HEADER = 'x-first-touch';
11
14
 
12
15
  /**
13
16
  * 验证fingerprint ID格式
@@ -27,4 +30,7 @@ export const FINGERPRINT_CONSTANTS = {
27
30
  STORAGE_KEY: FINGERPRINT_STORAGE_KEY,
28
31
  HEADER_NAME: FINGERPRINT_HEADER_NAME,
29
32
  COOKIE_NAME: FINGERPRINT_COOKIE_NAME,
30
- } as const;
33
+ FIRST_TOUCH_STORAGE_KEY: FINGERPRINT_FIRST_TOUCH_STORAGE_KEY,
34
+ FIRST_TOUCH_COOKIE_NAME: FINGERPRINT_FIRST_TOUCH_COOKIE_NAME,
35
+ FIRST_TOUCH_HEADER: FINGERPRINT_FIRST_TOUCH_HEADER,
36
+ } as const;
@@ -3,6 +3,7 @@
3
3
  import { useCallback, useEffect, useState } from 'react';
4
4
  import {
5
5
  createFingerprintHeaders,
6
+ getOrCreateFirstTouchData,
6
7
  getOrGenerateFingerprintId
7
8
  } from './fingerprint-client';
8
9
  import type {
@@ -47,6 +48,8 @@ export function useFingerprint(config: FingerprintConfig): UseFingerprintResult
47
48
  */
48
49
  const initializeFingerprintId = useCallback(async () => {
49
50
  try {
51
+ // Capture first-touch as early as possible before any in-site navigation can overwrite context.
52
+ getOrCreateFirstTouchData();
50
53
  const currentFingerprintId = await getOrGenerateFingerprintId();
51
54
  console.log('Initialized fingerprintId:', currentFingerprintId);
52
55
  setFingerprintIdState(currentFingerprintId);
@@ -185,4 +188,4 @@ export function useFingerprint(config: FingerprintConfig): UseFingerprintResult
185
188
  initializeAnonymousUser,
186
189
  refreshUserData,
187
190
  };
188
- }
191
+ }