Skip to content

Commit cd654cf

Browse files
committed
feat:universal links analytics handler for sdk
1 parent 42d492a commit cd654cf

3 files changed

Lines changed: 84 additions & 25 deletions

File tree

packages/react-native-detour/src/expo-router/nativeIntent.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import { OPENED_VIA_UNIVERSAL_LINK } from "../analytics/const/definedEvents";
2-
import { analyticsEmitter } from "../analytics/utils/analyticsEmitter";
3-
import { checkClickLimit } from "../links/api/checkClickLimit";
41
import { resolveShortLink } from "../links/api/resolveShortLink";
2+
import { sendUniversalLinkClick } from "../links/api/sendUniversalLinkClick";
53
import type { Config } from "../links/types";
64
import { getRouteFromDeepLink } from "../links/utils/urlHelpers";
75

@@ -289,28 +287,23 @@ export const createDetourNativeIntentHandler = (
289287
return path;
290288
}
291289

292-
analyticsEmitter.emit({
293-
eventName: OPENED_VIA_UNIVERSAL_LINK,
294-
data: { url: path },
295-
});
296-
297290
if (!options.config) {
298291
return fallbackPath;
299292
}
300293

301-
const clickLimitStatus = await checkClickLimit({
294+
const clickResult = await sendUniversalLinkClick({
302295
apiKey: options.config.apiKey,
303296
appID: options.config.appID,
297+
url: url.toString(),
304298
});
305299

306-
if (!clickLimitStatus.allowed) {
300+
if (!clickResult.allowed) {
307301
console.error("🔗[Detour:CLICK_LIMIT_ERROR] Native-intent routing blocked:", {
308302
path,
309-
status: clickLimitStatus.status,
310-
error: clickLimitStatus.error,
311-
code: clickLimitStatus.code,
312-
clicksInPeriod: clickLimitStatus.clicksInPeriod,
313-
effectiveLimit: clickLimitStatus.effectiveLimit,
303+
error: clickResult.error,
304+
code: clickResult.code,
305+
clicksInPeriod: clickResult.clicksInPeriod,
306+
effectiveLimit: clickResult.effectiveLimit,
314307
});
315308
return fallbackPath;
316309
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { SDK_HEADER_VALUE } from "../../version";
2+
import type { RequiredConfig } from "../types";
3+
4+
const API_URL = "https://godetour.dev/api/link/universal-link-click";
5+
6+
type UniversalLinkClickResponseBody = {
7+
allowed?: boolean;
8+
error?: string;
9+
code?: string;
10+
clicksInPeriod?: number;
11+
effectiveLimit?: number;
12+
remainingClicks?: number;
13+
clickId?: string | null;
14+
};
15+
16+
export type UniversalLinkClickResult =
17+
| { allowed: true; clickId: string | null }
18+
| {
19+
allowed: false;
20+
error: string;
21+
code?: string;
22+
clicksInPeriod?: number;
23+
effectiveLimit?: number;
24+
};
25+
26+
export const sendUniversalLinkClick = async ({
27+
apiKey: API_KEY,
28+
appID,
29+
url,
30+
}: Pick<RequiredConfig, "apiKey" | "appID"> & {
31+
url: string;
32+
}): Promise<UniversalLinkClickResult> => {
33+
try {
34+
const response = await fetch(API_URL, {
35+
method: "POST",
36+
headers: {
37+
"Content-Type": "application/json",
38+
Authorization: `Bearer ${API_KEY}`,
39+
"X-App-ID": appID,
40+
"X-SDK": SDK_HEADER_VALUE,
41+
},
42+
body: JSON.stringify({ url, timestamp: Date.now() }),
43+
});
44+
45+
let body: UniversalLinkClickResponseBody | null = null;
46+
try {
47+
body = (await response.json()) as UniversalLinkClickResponseBody;
48+
} catch {
49+
body = null;
50+
}
51+
52+
const isExplicitDeny = body?.allowed === false || response.status === 402;
53+
if (isExplicitDeny) {
54+
return {
55+
allowed: false,
56+
error: body?.error ?? "Click limit exceeded",
57+
code: body?.code,
58+
clicksInPeriod: body?.clicksInPeriod,
59+
effectiveLimit: body?.effectiveLimit,
60+
};
61+
}
62+
63+
if (!response.ok) {
64+
// Fail-open for temporary backend/network issues so apps keep working.
65+
return { allowed: true, clickId: null };
66+
}
67+
68+
return { allowed: true, clickId: body?.clickId ?? null };
69+
} catch {
70+
// Fail-open on transport errors; limit enforcement only happens on explicit deny.
71+
return { allowed: true, clickId: null };
72+
}
73+
};

packages/react-native-detour/src/links/hooks/useDetour.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import { useCallback, useEffect, useState } from "react";
22

33
import { Linking } from "react-native";
44

5-
import { OPENED_VIA_UNIVERSAL_LINK } from "../../analytics/const/definedEvents";
6-
import { analyticsEmitter } from "../../analytics/utils/analyticsEmitter";
5+
import { sendUniversalLinkClick } from "../api/sendUniversalLinkClick";
76
import { checkClickLimit } from "../api/checkClickLimit";
87
import { getDeferredLink } from "../api/getDeferredLink";
98
import { resolveShortLink } from "../api/resolveShortLink";
@@ -162,10 +161,7 @@ export const useDetour = ({
162161
const resolved = await resolveLink(url);
163162
if (resolved) {
164163
if (resolved.type !== "scheme") {
165-
analyticsEmitter.emit({
166-
eventName: OPENED_VIA_UNIVERSAL_LINK,
167-
data: { url },
168-
});
164+
sendUniversalLinkClick({ apiKey, appID, url });
169165
}
170166
setLink(resolved);
171167
}
@@ -193,10 +189,7 @@ export const useDetour = ({
193189
const resolved = await resolveLink(initialUrl);
194190
if (resolved) {
195191
if (resolved.type !== "scheme") {
196-
analyticsEmitter.emit({
197-
eventName: OPENED_VIA_UNIVERSAL_LINK,
198-
data: { url: initialUrl },
199-
});
192+
sendUniversalLinkClick({ apiKey, appID, url: initialUrl });
200193
}
201194
setLink(resolved);
202195
}

0 commit comments

Comments
 (0)