@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.
- package/dist/clerk/fingerprint/fingerprint-client.d.ts +22 -0
- package/dist/clerk/fingerprint/fingerprint-client.js +190 -16
- package/dist/clerk/fingerprint/fingerprint-client.mjs +191 -18
- package/dist/clerk/fingerprint/fingerprint-shared.d.ts +6 -0
- package/dist/clerk/fingerprint/fingerprint-shared.js +9 -0
- package/dist/clerk/fingerprint/fingerprint-shared.mjs +7 -1
- package/dist/clerk/fingerprint/index.js +4 -0
- package/dist/clerk/fingerprint/index.mjs +2 -2
- package/dist/clerk/fingerprint/server.js +3 -0
- package/dist/clerk/fingerprint/server.mjs +1 -1
- package/dist/clerk/fingerprint/use-fingerprint.js +2 -0
- package/dist/clerk/fingerprint/use-fingerprint.mjs +3 -1
- package/dist/main/delayed-img.d.ts +7 -0
- package/dist/main/delayed-img.js +39 -0
- package/dist/main/delayed-img.mjs +37 -0
- package/dist/main/index.d.ts +1 -0
- package/dist/main/index.js +2 -0
- package/dist/main/index.mjs +1 -0
- package/package.json +1 -1
- package/src/clerk/fingerprint/fingerprint-client.ts +242 -18
- package/src/clerk/fingerprint/fingerprint-shared.ts +7 -1
- package/src/clerk/fingerprint/use-fingerprint.ts +4 -1
- package/src/main/delayed-img.tsx +88 -0
- package/src/main/index.ts +1 -0
|
@@ -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 };
|
package/dist/main/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/main/index.js
CHANGED
|
@@ -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;
|
package/dist/main/index.mjs
CHANGED
|
@@ -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
|
@@ -5,7 +5,40 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import FingerprintJS from '@fingerprintjs/fingerprintjs';
|
|
8
|
-
import {
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|