@adland/react 0.3.0 → 0.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 +18 -0
- package/package.json +7 -5
- package/src/components/Ad.tsx +203 -17
- package/src/hooks/useFetch.ts +133 -0
- package/src/index.ts +3 -0
- package/src/utils/constants.ts +14 -0
- package/src/utils/fetchCache.ts +51 -0
- package/src/utils/sdk.ts +60 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @adland/react
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- c2df20a: Add `useFetch` hook with global in-memory cache and improve Ad component:
|
|
8
|
+
|
|
9
|
+
- Add `useFetch` hook with React Query-like global cache that persists across component lifecycles
|
|
10
|
+
- Add `fetchCache` utility for global cache management
|
|
11
|
+
- Improve Ad component to use relative URLs for local development
|
|
12
|
+
- Add loading and error states to Ad component
|
|
13
|
+
- Prevent loader flicker when switching tabs by using persistent cache
|
|
14
|
+
|
|
15
|
+
## 0.4.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- 5cee97e: add different ad types
|
|
20
|
+
|
|
3
21
|
## 0.3.0
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adland/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -22,19 +22,21 @@
|
|
|
22
22
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@
|
|
26
|
-
|
|
27
|
-
"devDependencies": {
|
|
25
|
+
"@adland/data": "^0.4.0",
|
|
26
|
+
"@farcaster/miniapp-sdk": "0.2.1",
|
|
28
27
|
"@types/node": "^20",
|
|
29
28
|
"@types/react": "^19.1.16",
|
|
30
29
|
"@types/react-dom": "^18.3.1",
|
|
30
|
+
"lucide-react": "0.561.0",
|
|
31
|
+
"tsup": "^8.0.1"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
31
34
|
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
|
32
35
|
"@typescript-eslint/parser": "^7.13.1",
|
|
33
36
|
"@vercel/style-guide": "^6.0.0",
|
|
34
37
|
"eslint": "^8",
|
|
35
38
|
"eslint-config-turbo": "2.0.6",
|
|
36
39
|
"eslint-plugin-only-warn": "^1.1.0",
|
|
37
|
-
"tsup": "^8.0.1",
|
|
38
40
|
"typescript": "^5.4.5"
|
|
39
41
|
},
|
|
40
42
|
"scripts": {
|
package/src/components/Ad.tsx
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import { useEffect, useRef, useCallback } from "react";
|
|
2
2
|
import { sendTrackRequest } from "../utils/sdk";
|
|
3
|
+
import sdk from "@farcaster/miniapp-sdk";
|
|
4
|
+
import { adCardIcon } from "../utils/constants";
|
|
5
|
+
import { Loader, SquareDashed } from "lucide-react";
|
|
6
|
+
import { type AdData } from "@adland/data";
|
|
7
|
+
import { useFetch } from "../hooks/useFetch";
|
|
3
8
|
|
|
4
9
|
/**
|
|
5
10
|
* Get the base URL for the network
|
|
6
|
-
*
|
|
11
|
+
* Uses relative URL in browser (for local dev), otherwise defaults to production
|
|
7
12
|
*/
|
|
8
|
-
const
|
|
13
|
+
const getBaseUrl = (): string => {
|
|
14
|
+
if (typeof window !== "undefined") {
|
|
15
|
+
// In browser - use relative URL for local development
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
// SSR fallback - use production URL
|
|
19
|
+
return "https://testnet.adland.space";
|
|
20
|
+
};
|
|
9
21
|
|
|
10
22
|
export interface AdProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
11
23
|
/**
|
|
@@ -15,15 +27,15 @@ export interface AdProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
15
27
|
/**
|
|
16
28
|
* Unique identifier for the ad
|
|
17
29
|
*/
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* The content to display in the ad
|
|
21
|
-
*/
|
|
22
|
-
children: React.ReactNode;
|
|
30
|
+
slotId: string;
|
|
23
31
|
/**
|
|
24
32
|
* Network to use for tracking requests (currently only "testnet" is supported)
|
|
25
33
|
*/
|
|
26
34
|
network?: "testnet";
|
|
35
|
+
/**
|
|
36
|
+
* Optional base URL override. If not provided, uses relative URL in browser or production URL in SSR
|
|
37
|
+
*/
|
|
38
|
+
baseUrl?: string;
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
/**
|
|
@@ -39,33 +51,56 @@ export interface AdProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
39
51
|
*/
|
|
40
52
|
export function Ad({
|
|
41
53
|
owner,
|
|
42
|
-
|
|
43
|
-
children,
|
|
54
|
+
slotId,
|
|
44
55
|
network = "testnet",
|
|
56
|
+
baseUrl,
|
|
45
57
|
...props
|
|
46
58
|
}: AdProps) {
|
|
47
59
|
const ref = useRef<HTMLDivElement>(null);
|
|
60
|
+
const {
|
|
61
|
+
data: adData,
|
|
62
|
+
isLoading,
|
|
63
|
+
error,
|
|
64
|
+
} = useFetch<AdData>(
|
|
65
|
+
`ad-data-${owner}-${slotId}`,
|
|
66
|
+
async () => {
|
|
67
|
+
const res = await fetch(
|
|
68
|
+
`${networkBaseUrl}/api/data/owner/${owner.toLowerCase()}/slot/${slotId}`,
|
|
69
|
+
{
|
|
70
|
+
method: "GET",
|
|
71
|
+
headers: {
|
|
72
|
+
"Content-Type": "application/json",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
return res.json();
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
enabled: !!owner && !!slotId,
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
const networkBaseUrl = baseUrl ?? getBaseUrl();
|
|
48
83
|
|
|
49
84
|
const send = useCallback(
|
|
50
85
|
(type: "view" | "click") => {
|
|
51
|
-
const trackEndpoint = `${
|
|
86
|
+
const trackEndpoint = `${networkBaseUrl}/api/analytics/track`;
|
|
52
87
|
|
|
53
88
|
sendTrackRequest(trackEndpoint, {
|
|
54
89
|
type,
|
|
55
|
-
adId,
|
|
90
|
+
adId: slotId,
|
|
56
91
|
adOwner: owner,
|
|
57
92
|
}).catch((error: unknown) => {
|
|
58
93
|
console.error(`[@adland/react] Failed to track ${type}:`, error);
|
|
59
94
|
});
|
|
60
95
|
},
|
|
61
|
-
[
|
|
96
|
+
[slotId, owner, networkBaseUrl],
|
|
62
97
|
);
|
|
63
98
|
|
|
64
99
|
useEffect(() => {
|
|
65
100
|
const el = ref.current;
|
|
66
101
|
if (!el) return;
|
|
67
102
|
|
|
68
|
-
const key = `ad_view_${
|
|
103
|
+
const key = `ad_view_${slotId}`;
|
|
69
104
|
|
|
70
105
|
const obs = new IntersectionObserver(
|
|
71
106
|
(entries) => {
|
|
@@ -89,16 +124,167 @@ export function Ad({
|
|
|
89
124
|
|
|
90
125
|
obs.observe(el);
|
|
91
126
|
return () => obs.disconnect();
|
|
92
|
-
}, [
|
|
127
|
+
}, [slotId, send]);
|
|
93
128
|
|
|
94
129
|
// CLICK tracking
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
130
|
+
// Only track clicks that aren't on links or buttons (let those handle their own navigation)
|
|
131
|
+
const onClick = useCallback(
|
|
132
|
+
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
133
|
+
// Don't track if clicking on a link, button, or other interactive element
|
|
134
|
+
const target = e.target as HTMLElement;
|
|
135
|
+
const isInteractiveElement =
|
|
136
|
+
target.tagName === "A" ||
|
|
137
|
+
target.tagName === "BUTTON" ||
|
|
138
|
+
target.closest("a") !== null ||
|
|
139
|
+
target.closest("button") !== null;
|
|
140
|
+
|
|
141
|
+
if (!isInteractiveElement) {
|
|
142
|
+
send("click");
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
[send],
|
|
146
|
+
);
|
|
98
147
|
|
|
99
148
|
return (
|
|
100
149
|
<div ref={ref} onClick={onClick} {...props}>
|
|
150
|
+
{(() => {
|
|
151
|
+
if (isLoading) {
|
|
152
|
+
return <LoadingAdContent />;
|
|
153
|
+
}
|
|
154
|
+
if (error) {
|
|
155
|
+
return <ErrorAdContent error={error} />;
|
|
156
|
+
}
|
|
157
|
+
if (adData) {
|
|
158
|
+
return (
|
|
159
|
+
<>
|
|
160
|
+
{adData.type === "link" && <LinkAdContent data={adData.data} />}
|
|
161
|
+
|
|
162
|
+
{adData.type === "cast" && <CastAdContent data={adData.data} />}
|
|
163
|
+
|
|
164
|
+
{adData.type === "miniapp" && (
|
|
165
|
+
<MiniAppAdContent data={adData.data} />
|
|
166
|
+
)}
|
|
167
|
+
</>
|
|
168
|
+
);
|
|
169
|
+
} else {
|
|
170
|
+
return (
|
|
171
|
+
<EmtpyAdContent
|
|
172
|
+
data={{ url: networkBaseUrl + `${owner}/${slotId}` }}
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
})()}
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const BasicAdBody = ({
|
|
182
|
+
children,
|
|
183
|
+
...rest
|
|
184
|
+
}: { children: React.ReactNode } & React.HTMLAttributes<HTMLDivElement>) => {
|
|
185
|
+
return (
|
|
186
|
+
<div
|
|
187
|
+
className="border p-2 md:p-4 flex hover:cursor-pointer hover:bg-primary/10 flex-row rounded-md justify-between items-center gap-2 relative border-b-2 border-b-primary"
|
|
188
|
+
{...rest}
|
|
189
|
+
>
|
|
101
190
|
{children}
|
|
191
|
+
<div className="inline-flex items-center border p-1 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent bg-primary text-primary-foreground hover:bg-primary/80">
|
|
192
|
+
AD
|
|
193
|
+
</div>
|
|
102
194
|
</div>
|
|
103
195
|
);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
function ErrorAdContent({ error }: { error: unknown }) {
|
|
199
|
+
return (
|
|
200
|
+
<BasicAdBody>
|
|
201
|
+
<div className="flex flex-row items-center gap-2">
|
|
202
|
+
<SquareDashed className="w-5 h-5 md:w-7 md:h-7" />
|
|
203
|
+
<p className="font-bold text-primary">ERROR</p>
|
|
204
|
+
</div>
|
|
205
|
+
</BasicAdBody>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function LoadingAdContent() {
|
|
210
|
+
return (
|
|
211
|
+
<BasicAdBody>
|
|
212
|
+
<div className="flex flex-row items-center gap-2">
|
|
213
|
+
<Loader className="w-5 h-5 md:w-7 md:h-7 animate-spin" />
|
|
214
|
+
<p className="font-bold text-primary">LOADING...</p>
|
|
215
|
+
</div>
|
|
216
|
+
</BasicAdBody>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function EmtpyAdContent({ data }: { data: { url: string } }) {
|
|
221
|
+
return (
|
|
222
|
+
<BasicAdBody
|
|
223
|
+
onClick={() => {
|
|
224
|
+
sdk.actions.openMiniApp({
|
|
225
|
+
url: data.url,
|
|
226
|
+
});
|
|
227
|
+
}}
|
|
228
|
+
>
|
|
229
|
+
<div className="flex flex-row items-center gap-2">
|
|
230
|
+
<SquareDashed className="w-5 h-5 md:w-7 md:h-7" />
|
|
231
|
+
<p className="font-bold text-primary">NO AD</p>
|
|
232
|
+
</div>
|
|
233
|
+
</BasicAdBody>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Link Ad Component
|
|
238
|
+
function LinkAdContent({ data }: { data: { url: string } }) {
|
|
239
|
+
const Icon = adCardIcon["link"];
|
|
240
|
+
return (
|
|
241
|
+
<BasicAdBody
|
|
242
|
+
onClick={() => {
|
|
243
|
+
sdk.actions.openUrl(data.url);
|
|
244
|
+
}}
|
|
245
|
+
>
|
|
246
|
+
<div className="flex flex-row items-center gap-2">
|
|
247
|
+
<Icon className="w-5 h-5 md:w-7 md:h-7" />
|
|
248
|
+
<p className="font-bold text-primary">LINK</p>
|
|
249
|
+
</div>
|
|
250
|
+
</BasicAdBody>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Cast Ad Component
|
|
255
|
+
function CastAdContent({ data }: { data: { hash: string } }) {
|
|
256
|
+
const Icon = adCardIcon["cast"];
|
|
257
|
+
return (
|
|
258
|
+
<BasicAdBody
|
|
259
|
+
onClick={() => {
|
|
260
|
+
sdk.actions.viewCast({
|
|
261
|
+
hash: data.hash,
|
|
262
|
+
});
|
|
263
|
+
}}
|
|
264
|
+
>
|
|
265
|
+
<div className="flex flex-row items-center gap-2">
|
|
266
|
+
<Icon className="w-5 h-5 md:w-7 md:h-7" />
|
|
267
|
+
<p className="font-bold text-primary">CAST</p>
|
|
268
|
+
</div>
|
|
269
|
+
</BasicAdBody>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// MiniApp Ad Component
|
|
274
|
+
function MiniAppAdContent({ data }: { data: { url: string } }) {
|
|
275
|
+
const Icon = adCardIcon["miniapp"];
|
|
276
|
+
return (
|
|
277
|
+
<BasicAdBody
|
|
278
|
+
onClick={() => {
|
|
279
|
+
sdk.actions.openMiniApp({
|
|
280
|
+
url: data.url,
|
|
281
|
+
});
|
|
282
|
+
}}
|
|
283
|
+
>
|
|
284
|
+
<div className="flex flex-row items-center gap-2">
|
|
285
|
+
<Icon className="w-5 h-5 md:w-7 md:h-7" />
|
|
286
|
+
<p className="font-bold text-primary">MINIAPP</p>
|
|
287
|
+
</div>
|
|
288
|
+
</BasicAdBody>
|
|
289
|
+
);
|
|
104
290
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import FetchCache from "../utils/fetchCache";
|
|
3
|
+
|
|
4
|
+
type Status = "idle" | "loading" | "success" | "error";
|
|
5
|
+
|
|
6
|
+
const globalCache = new FetchCache();
|
|
7
|
+
|
|
8
|
+
export const fetchCache = {
|
|
9
|
+
clear: () => globalCache.clear(),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function useFetch<T>(
|
|
13
|
+
key: string,
|
|
14
|
+
fetcher: () => Promise<T>,
|
|
15
|
+
opts?: {
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
ttl?: number; // ms
|
|
18
|
+
},
|
|
19
|
+
) {
|
|
20
|
+
const { enabled = true, ttl = 0 } = opts ?? {};
|
|
21
|
+
|
|
22
|
+
// Get cached data from global cache
|
|
23
|
+
const getCachedData = (): T | null => {
|
|
24
|
+
return globalCache.get<T>(key, ttl || undefined);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const hasCachedData = () => {
|
|
28
|
+
return globalCache.has(key, ttl || undefined);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Check if there's an active fetch for this key (dedupe concurrent requests)
|
|
32
|
+
const getActiveFetch = (): Promise<T> | undefined => {
|
|
33
|
+
return globalCache.getActiveFetch<T>(key);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const cachedData = getCachedData();
|
|
37
|
+
const [data, setData] = useState<T | null>(cachedData);
|
|
38
|
+
const [error, setError] = useState<unknown>(null);
|
|
39
|
+
// If we have cached data, start with success status to avoid showing loader
|
|
40
|
+
const [status, setStatus] = useState<Status>(cachedData ? "success" : "idle");
|
|
41
|
+
|
|
42
|
+
const refetch = async () => {
|
|
43
|
+
// ALWAYS check cache first - never show loading if we have valid cached data
|
|
44
|
+
const cached = getCachedData();
|
|
45
|
+
if (cached) {
|
|
46
|
+
// Already have valid cached data, ensure state is correct
|
|
47
|
+
setData(cached);
|
|
48
|
+
setStatus("success");
|
|
49
|
+
return cached;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check if there's already an active fetch for this key (dedupe)
|
|
53
|
+
const activeFetch = getActiveFetch();
|
|
54
|
+
if (activeFetch) {
|
|
55
|
+
try {
|
|
56
|
+
const res = await activeFetch;
|
|
57
|
+
setData(res);
|
|
58
|
+
setStatus("success");
|
|
59
|
+
return res;
|
|
60
|
+
} catch (e) {
|
|
61
|
+
setError(e);
|
|
62
|
+
setStatus("error");
|
|
63
|
+
throw e;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Only set loading if we actually need to fetch
|
|
68
|
+
setStatus("loading");
|
|
69
|
+
setError(null);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
// Create fetch promise and store it for deduplication
|
|
73
|
+
const fetchPromise = fetcher();
|
|
74
|
+
globalCache.setActiveFetch(key, fetchPromise);
|
|
75
|
+
|
|
76
|
+
const res = await fetchPromise;
|
|
77
|
+
globalCache.set(key, res);
|
|
78
|
+
setData(res);
|
|
79
|
+
setStatus("success");
|
|
80
|
+
return res;
|
|
81
|
+
} catch (e) {
|
|
82
|
+
setError(e);
|
|
83
|
+
setStatus("error");
|
|
84
|
+
throw e;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (!enabled) return;
|
|
90
|
+
|
|
91
|
+
const cached = getCachedData();
|
|
92
|
+
|
|
93
|
+
if (cached) {
|
|
94
|
+
setData(cached);
|
|
95
|
+
setStatus("success");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const activeFetch = getActiveFetch();
|
|
100
|
+
if (activeFetch) {
|
|
101
|
+
setStatus("loading");
|
|
102
|
+
activeFetch
|
|
103
|
+
.then((res) => {
|
|
104
|
+
setData(res);
|
|
105
|
+
setStatus("success");
|
|
106
|
+
})
|
|
107
|
+
.catch((e) => {
|
|
108
|
+
setError(e);
|
|
109
|
+
setStatus("error");
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (status !== "loading" && status !== "success") {
|
|
115
|
+
refetch();
|
|
116
|
+
}
|
|
117
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
118
|
+
}, [key, enabled]);
|
|
119
|
+
|
|
120
|
+
const hasValidCache = hasCachedData();
|
|
121
|
+
const isLoading = status === "loading" && !hasValidCache;
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
data,
|
|
125
|
+
error,
|
|
126
|
+
status,
|
|
127
|
+
isIdle: status === "idle",
|
|
128
|
+
isLoading,
|
|
129
|
+
isSuccess: status === "success",
|
|
130
|
+
isError: status === "error",
|
|
131
|
+
refetch,
|
|
132
|
+
};
|
|
133
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AdType } from "@adland/data";
|
|
2
|
+
import { ForwardRefExoticComponent, RefAttributes } from "react";
|
|
3
|
+
import { Link, MessageCircle, LayoutGrid, LucideProps } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
export const adCardIcon: Record<
|
|
6
|
+
AdType,
|
|
7
|
+
ForwardRefExoticComponent<
|
|
8
|
+
Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>
|
|
9
|
+
>
|
|
10
|
+
> = {
|
|
11
|
+
link: Link,
|
|
12
|
+
cast: MessageCircle,
|
|
13
|
+
miniapp: LayoutGrid,
|
|
14
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
class FetchCache {
|
|
2
|
+
private cache = new Map<string, { data: unknown; ts: number }>();
|
|
3
|
+
private activeFetches = new Map<string, Promise<unknown>>();
|
|
4
|
+
|
|
5
|
+
get<T>(key: string, ttl?: number): T | null {
|
|
6
|
+
const cached = this.cache.get(key);
|
|
7
|
+
if (!cached) return null;
|
|
8
|
+
|
|
9
|
+
if (ttl && Date.now() - cached.ts > ttl) {
|
|
10
|
+
// Cache expired, remove it
|
|
11
|
+
this.cache.delete(key);
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return cached.data as T;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
has(key: string, ttl?: number): boolean {
|
|
19
|
+
const cached = this.cache.get(key);
|
|
20
|
+
if (!cached) return false;
|
|
21
|
+
|
|
22
|
+
if (ttl && Date.now() - cached.ts > ttl) {
|
|
23
|
+
this.cache.delete(key);
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
set<T>(key: string, data: T): void {
|
|
31
|
+
this.cache.set(key, { data, ts: Date.now() });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getActiveFetch<T>(key: string): Promise<T> | undefined {
|
|
35
|
+
return this.activeFetches.get(key) as Promise<T> | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setActiveFetch<T>(key: string, promise: Promise<T>): void {
|
|
39
|
+
this.activeFetches.set(key, promise);
|
|
40
|
+
promise.finally(() => {
|
|
41
|
+
this.activeFetches.delete(key);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
clear(): void {
|
|
46
|
+
this.cache.clear();
|
|
47
|
+
this.activeFetches.clear();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default FetchCache;
|
package/src/utils/sdk.ts
CHANGED
|
@@ -53,16 +53,74 @@ export async function sendTrackRequest(
|
|
|
53
53
|
throw new Error("[@adland/react] SDK quickAuth is not available");
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
const requestOptions = {
|
|
57
57
|
method: "POST",
|
|
58
58
|
headers: {
|
|
59
59
|
"Content-Type": "application/json",
|
|
60
60
|
"x-auth-type": "farcaster",
|
|
61
61
|
},
|
|
62
62
|
body: JSON.stringify(data),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Log request details for debugging
|
|
66
|
+
console.log("[@adland/react] Sending track request:", {
|
|
67
|
+
url,
|
|
68
|
+
method: requestOptions.method,
|
|
69
|
+
headers: requestOptions.headers,
|
|
70
|
+
body: data,
|
|
63
71
|
});
|
|
72
|
+
|
|
73
|
+
const response = await sdk.quickAuth.fetch(url, requestOptions);
|
|
74
|
+
|
|
75
|
+
// Check if response is ok
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
const statusText = response.statusText;
|
|
78
|
+
const status = response.status;
|
|
79
|
+
|
|
80
|
+
// Try to get error body if available
|
|
81
|
+
let errorBody = null;
|
|
82
|
+
try {
|
|
83
|
+
const contentType = response.headers.get("content-type");
|
|
84
|
+
if (contentType?.includes("application/json")) {
|
|
85
|
+
errorBody = await response.clone().json();
|
|
86
|
+
} else {
|
|
87
|
+
errorBody = await response.clone().text();
|
|
88
|
+
}
|
|
89
|
+
} catch (e) {
|
|
90
|
+
// Ignore errors when reading response body
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.error("[@adland/react] Track request failed:", {
|
|
94
|
+
url,
|
|
95
|
+
status,
|
|
96
|
+
statusText,
|
|
97
|
+
errorBody,
|
|
98
|
+
requestBody: data,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
throw new Error(
|
|
102
|
+
`[@adland/react] Track request failed: ${status} ${statusText}${errorBody ? ` - ${JSON.stringify(errorBody)}` : ""}`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return response;
|
|
64
107
|
} catch (error) {
|
|
65
|
-
|
|
108
|
+
// Enhanced error logging
|
|
109
|
+
if (error instanceof Error) {
|
|
110
|
+
console.error("[@adland/react] Error sending track request:", {
|
|
111
|
+
message: error.message,
|
|
112
|
+
name: error.name,
|
|
113
|
+
stack: error.stack,
|
|
114
|
+
url,
|
|
115
|
+
requestData: data,
|
|
116
|
+
});
|
|
117
|
+
} else {
|
|
118
|
+
console.error("[@adland/react] Unknown error sending track request:", {
|
|
119
|
+
error,
|
|
120
|
+
url,
|
|
121
|
+
requestData: data,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
66
124
|
throw error;
|
|
67
125
|
}
|
|
68
126
|
}
|