@adtogether/web-sdk 0.5.0 → 0.5.6
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 +4 -0
- package/README.md +5 -4
- package/dist/{chunk-5UAFUJUK.mjs → chunk-NXWVGW5H.mjs} +34 -47
- package/dist/index.d.mts +7 -8
- package/dist/index.d.ts +7 -8
- package/dist/index.js +34 -47
- package/dist/index.mjs +1 -1
- package/dist/react/index.js +111 -57
- package/dist/react/index.mjs +78 -11
- package/package.json +1 -1
- package/src/core/AdTogether.ts +42 -53
- package/src/core/types.ts +5 -0
- package/src/react/AdTogetherBanner.tsx +60 -8
- package/src/react/AdTogetherInterstitial.tsx +40 -2
- package/errors.txt +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
## 0.5.6
|
|
2
|
+
* **Security**: Build ad-serve URLs with URLSearchParams, discard a response whose ad type differs from the requested type, and stop reporting client-derived country values.
|
|
3
|
+
* **Viewability**: Built-in banner and interstitial components report an impression only after one continuous visible second.
|
|
4
|
+
|
|
1
5
|
## 0.5.0
|
|
2
6
|
* **Sync**: Version parity (0.5.0) across all AdTogether SDKs.
|
|
3
7
|
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
This SDK allows web developers (React and Vanilla JS) to easily integrate AdTogether ads into their applications. By displaying ads from other community members, you earn **Ad Credits** that allow your own app's ads to be shown across the AdTogether network.
|
|
23
23
|
|
|
24
24
|
<p align="center">
|
|
25
|
-
<img src="https://www.ad-together.org/dashboard.
|
|
25
|
+
<img src="https://www.ad-together.org/dashboard.webp" width="800" alt="Developer Dashboard">
|
|
26
26
|
</p>
|
|
27
27
|
|
|
28
28
|
### 🖼️ Visualizing the Experience
|
|
@@ -37,6 +37,7 @@ This SDK allows web developers (React and Vanilla JS) to easily integrate AdToge
|
|
|
37
37
|
- 🖼️ **Banner Ads** — Native React `AdTogetherBanner` component with automatic impression tracking and dark mode support.
|
|
38
38
|
- 📺 **Interstitial Ads** — Full-screen `AdTogetherInterstitial` component with a configurable close-button countdown.
|
|
39
39
|
- 🌑 **Dark Mode Support** — All ad components automatically adapt to system preference, or can be set to `'dark'` / `'light'` explicitly.
|
|
40
|
+
- 👁️ **Viewable Impressions** — Web ads count after at least 50% is visible for one continuous second while the page is in the foreground.
|
|
40
41
|
- ⚖️ **Fair Exchange** — Automated impression and click tracking ensures fair distribution of ad credits.
|
|
41
42
|
- 🔌 **Easy Integration** — A single `initialize()` call and one component is all you need to start earning credits and increase conversions.
|
|
42
43
|
- 🧩 **Vanilla JS Support** — Core logic decoupled for use without React or any framework.
|
|
@@ -59,12 +60,12 @@ AdTogether provides an official Model Context Protocol (MCP) server that gives y
|
|
|
59
60
|
"mcp-remote@latest",
|
|
60
61
|
"https://www.ad-together.org/api/mcp",
|
|
61
62
|
"--header",
|
|
62
|
-
"Authorization: Bearer
|
|
63
|
+
"Authorization: Bearer YOUR_FRESH_FIREBASE_ID_TOKEN"
|
|
63
64
|
]
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
```
|
|
67
|
-
This ensures you have the exact API signatures and security invariants required to integrate AdTogether successfully without hallucinations.
|
|
68
|
+
Use a fresh Firebase ID token from the signed-in account owner; App IDs cannot authenticate MCP requests. This ensures you have the exact API signatures and security invariants required to integrate AdTogether successfully without hallucinations.
|
|
68
69
|
|
|
69
70
|
---
|
|
70
71
|
|
|
@@ -93,7 +94,7 @@ AdTogether.initialize({
|
|
|
93
94
|
|
|
94
95
|
### Banner Ads
|
|
95
96
|
|
|
96
|
-
Use the `AdTogetherBanner` component anywhere in your React tree. It fetches an ad, renders it, and
|
|
97
|
+
Use the `AdTogetherBanner` component anywhere in your React tree. It fetches an ad, renders it, and tracks an impression after at least 50% of the component remains visible for one continuous second while the page is in the foreground.
|
|
97
98
|
|
|
98
99
|
```jsx
|
|
99
100
|
import { AdTogetherBanner } from '@adtogether/web-sdk/react';
|
|
@@ -46,23 +46,13 @@ var AdTogether = class _AdTogether {
|
|
|
46
46
|
}
|
|
47
47
|
try {
|
|
48
48
|
const sdk = _AdTogether.shared;
|
|
49
|
-
|
|
50
|
-
if (adType) {
|
|
51
|
-
url += `&adType=${adType}`;
|
|
52
|
-
}
|
|
53
|
-
if (sdk.lastAdId) {
|
|
54
|
-
url += `&exclude=${sdk.lastAdId}`;
|
|
55
|
-
}
|
|
56
|
-
if (sdk.bundleId) {
|
|
57
|
-
url += `&bundleId=${sdk.bundleId}`;
|
|
58
|
-
}
|
|
59
|
-
url += `&allowSelfAds=${sdk.allowSelfAds}`;
|
|
60
|
-
if (typeof window !== "undefined") {
|
|
61
|
-
url += `&sourceUrl=${encodeURIComponent(window.location.href)}`;
|
|
62
|
-
}
|
|
49
|
+
const url = _AdTogether.buildServeUrl(sdk, adUnitId, adType);
|
|
63
50
|
const response = await fetch(url);
|
|
64
51
|
if (response.ok) {
|
|
65
52
|
const ad = await response.json();
|
|
53
|
+
if (adType && ad.adType !== adType) {
|
|
54
|
+
throw new Error("Received an ad with a different type than requested: " + (ad.adType ?? "missing"));
|
|
55
|
+
}
|
|
66
56
|
sdk.lastAdId = ad.id;
|
|
67
57
|
return ad;
|
|
68
58
|
} else if (response.status === 401 || response.status === 403) {
|
|
@@ -74,41 +64,37 @@ var AdTogether = class _AdTogether {
|
|
|
74
64
|
throw err;
|
|
75
65
|
}
|
|
76
66
|
}
|
|
77
|
-
static
|
|
78
|
-
|
|
67
|
+
static buildServeUrl(sdk, adUnitId, adType) {
|
|
68
|
+
const fallbackBaseUrl = typeof window !== "undefined" ? window.location.origin : "https://www.ad-together.org";
|
|
69
|
+
const url = new URL(sdk.baseUrl || fallbackBaseUrl, fallbackBaseUrl);
|
|
70
|
+
url.pathname = url.pathname.replace(/\/+$/, "") + "/api/ads/serve";
|
|
71
|
+
url.search = "";
|
|
72
|
+
url.hash = "";
|
|
73
|
+
url.searchParams.set("adUnitId", adUnitId);
|
|
74
|
+
url.searchParams.set("appId", sdk.appId ?? "");
|
|
75
|
+
url.searchParams.set("allowSelfAds", String(sdk.allowSelfAds));
|
|
76
|
+
url.searchParams.set("platform", "web");
|
|
77
|
+
if (adType) {
|
|
78
|
+
url.searchParams.set("adType", adType);
|
|
79
|
+
}
|
|
80
|
+
if (sdk.lastAdId) {
|
|
81
|
+
url.searchParams.set("exclude", sdk.lastAdId);
|
|
82
|
+
}
|
|
83
|
+
if (sdk.bundleId) {
|
|
84
|
+
url.searchParams.set("bundleId", sdk.bundleId);
|
|
85
|
+
}
|
|
86
|
+
if (typeof window !== "undefined") {
|
|
87
|
+
url.searchParams.set("sourceUrl", window.location.href);
|
|
88
|
+
}
|
|
89
|
+
return url.toString();
|
|
90
|
+
}
|
|
91
|
+
static trackImpression(adId, token, viewability) {
|
|
92
|
+
this.trackEvent("/api/ads/impression", adId, token, viewability);
|
|
79
93
|
}
|
|
80
94
|
static trackClick(adId, token) {
|
|
81
95
|
this.trackEvent("/api/ads/click", adId, token);
|
|
82
96
|
}
|
|
83
|
-
|
|
84
|
-
* Detect the user's country code from the browser locale.
|
|
85
|
-
* Uses Intl API (most reliable) with navigator.language as fallback.
|
|
86
|
-
* Returns an ISO 3166-1 alpha-2 code like "US", "DE", "JP", or null.
|
|
87
|
-
*/
|
|
88
|
-
static detectCountry() {
|
|
89
|
-
try {
|
|
90
|
-
if (typeof Intl !== "undefined" && Intl.DateTimeFormat) {
|
|
91
|
-
const resolved = Intl.DateTimeFormat().resolvedOptions();
|
|
92
|
-
if (resolved.locale) {
|
|
93
|
-
const parts = resolved.locale.split("-");
|
|
94
|
-
if (parts.length >= 2) {
|
|
95
|
-
const region = parts[parts.length - 1].toUpperCase();
|
|
96
|
-
if (region.length === 2) return region;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
if (typeof navigator !== "undefined" && navigator.language) {
|
|
101
|
-
const parts = navigator.language.split("-");
|
|
102
|
-
if (parts.length >= 2) {
|
|
103
|
-
const region = parts[parts.length - 1].toUpperCase();
|
|
104
|
-
if (region.length === 2) return region;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
} catch (_) {
|
|
108
|
-
}
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
static trackEvent(endpoint, adId, token) {
|
|
97
|
+
static trackEvent(endpoint, adId, token, viewability) {
|
|
112
98
|
if (!_AdTogether.shared.assertInitialized()) return;
|
|
113
99
|
fetch(`${_AdTogether.shared.baseUrl}${endpoint}`, {
|
|
114
100
|
method: "POST",
|
|
@@ -120,10 +106,11 @@ var AdTogether = class _AdTogether {
|
|
|
120
106
|
token,
|
|
121
107
|
appId: _AdTogether.shared.appId,
|
|
122
108
|
..._AdTogether.shared.bundleId ? { bundleId: _AdTogether.shared.bundleId } : {},
|
|
109
|
+
...endpoint === "/api/ads/impression" && typeof window !== "undefined" ? { pagePath: window.location.pathname } : {},
|
|
110
|
+
...endpoint === "/api/ads/impression" && viewability ? viewability : {},
|
|
123
111
|
// Send platform and environment to match Flutter SDK
|
|
124
112
|
platform: "web",
|
|
125
|
-
environment: typeof process !== "undefined" && process.env?.NODE_ENV === "development" ? "development" : "production"
|
|
126
|
-
country: _AdTogether.detectCountry()
|
|
113
|
+
environment: typeof process !== "undefined" && process.env?.NODE_ENV === "development" ? "development" : "production"
|
|
127
114
|
})
|
|
128
115
|
}).catch(console.error);
|
|
129
116
|
}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
type AdType = 'banner' | 'interstitial';
|
|
2
|
+
interface ViewabilityMetrics {
|
|
3
|
+
visiblePercent: number;
|
|
4
|
+
visibleDurationMs: number;
|
|
5
|
+
}
|
|
2
6
|
interface AdModel {
|
|
3
7
|
id: string;
|
|
4
8
|
title: string;
|
|
@@ -31,15 +35,10 @@ declare class AdTogether {
|
|
|
31
35
|
assertInitialized(): boolean;
|
|
32
36
|
private lastAdId?;
|
|
33
37
|
static fetchAd(adUnitId?: string, adType?: AdType): Promise<AdModel>;
|
|
34
|
-
static
|
|
38
|
+
private static buildServeUrl;
|
|
39
|
+
static trackImpression(adId: string, token?: string, viewability?: ViewabilityMetrics): void;
|
|
35
40
|
static trackClick(adId: string, token?: string): void;
|
|
36
|
-
/**
|
|
37
|
-
* Detect the user's country code from the browser locale.
|
|
38
|
-
* Uses Intl API (most reliable) with navigator.language as fallback.
|
|
39
|
-
* Returns an ISO 3166-1 alpha-2 code like "US", "DE", "JP", or null.
|
|
40
|
-
*/
|
|
41
|
-
private static detectCountry;
|
|
42
41
|
private static trackEvent;
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
export { type AdModel, AdTogether, type AdTogetherOptions, type AdType };
|
|
44
|
+
export { type AdModel, AdTogether, type AdTogetherOptions, type AdType, type ViewabilityMetrics };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
type AdType = 'banner' | 'interstitial';
|
|
2
|
+
interface ViewabilityMetrics {
|
|
3
|
+
visiblePercent: number;
|
|
4
|
+
visibleDurationMs: number;
|
|
5
|
+
}
|
|
2
6
|
interface AdModel {
|
|
3
7
|
id: string;
|
|
4
8
|
title: string;
|
|
@@ -31,15 +35,10 @@ declare class AdTogether {
|
|
|
31
35
|
assertInitialized(): boolean;
|
|
32
36
|
private lastAdId?;
|
|
33
37
|
static fetchAd(adUnitId?: string, adType?: AdType): Promise<AdModel>;
|
|
34
|
-
static
|
|
38
|
+
private static buildServeUrl;
|
|
39
|
+
static trackImpression(adId: string, token?: string, viewability?: ViewabilityMetrics): void;
|
|
35
40
|
static trackClick(adId: string, token?: string): void;
|
|
36
|
-
/**
|
|
37
|
-
* Detect the user's country code from the browser locale.
|
|
38
|
-
* Uses Intl API (most reliable) with navigator.language as fallback.
|
|
39
|
-
* Returns an ISO 3166-1 alpha-2 code like "US", "DE", "JP", or null.
|
|
40
|
-
*/
|
|
41
|
-
private static detectCountry;
|
|
42
41
|
private static trackEvent;
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
export { type AdModel, AdTogether, type AdTogetherOptions, type AdType };
|
|
44
|
+
export { type AdModel, AdTogether, type AdTogetherOptions, type AdType, type ViewabilityMetrics };
|
package/dist/index.js
CHANGED
|
@@ -72,23 +72,13 @@ var AdTogether = class _AdTogether {
|
|
|
72
72
|
}
|
|
73
73
|
try {
|
|
74
74
|
const sdk = _AdTogether.shared;
|
|
75
|
-
|
|
76
|
-
if (adType) {
|
|
77
|
-
url += `&adType=${adType}`;
|
|
78
|
-
}
|
|
79
|
-
if (sdk.lastAdId) {
|
|
80
|
-
url += `&exclude=${sdk.lastAdId}`;
|
|
81
|
-
}
|
|
82
|
-
if (sdk.bundleId) {
|
|
83
|
-
url += `&bundleId=${sdk.bundleId}`;
|
|
84
|
-
}
|
|
85
|
-
url += `&allowSelfAds=${sdk.allowSelfAds}`;
|
|
86
|
-
if (typeof window !== "undefined") {
|
|
87
|
-
url += `&sourceUrl=${encodeURIComponent(window.location.href)}`;
|
|
88
|
-
}
|
|
75
|
+
const url = _AdTogether.buildServeUrl(sdk, adUnitId, adType);
|
|
89
76
|
const response = await fetch(url);
|
|
90
77
|
if (response.ok) {
|
|
91
78
|
const ad = await response.json();
|
|
79
|
+
if (adType && ad.adType !== adType) {
|
|
80
|
+
throw new Error("Received an ad with a different type than requested: " + (ad.adType ?? "missing"));
|
|
81
|
+
}
|
|
92
82
|
sdk.lastAdId = ad.id;
|
|
93
83
|
return ad;
|
|
94
84
|
} else if (response.status === 401 || response.status === 403) {
|
|
@@ -100,41 +90,37 @@ var AdTogether = class _AdTogether {
|
|
|
100
90
|
throw err;
|
|
101
91
|
}
|
|
102
92
|
}
|
|
103
|
-
static
|
|
104
|
-
|
|
93
|
+
static buildServeUrl(sdk, adUnitId, adType) {
|
|
94
|
+
const fallbackBaseUrl = typeof window !== "undefined" ? window.location.origin : "https://www.ad-together.org";
|
|
95
|
+
const url = new URL(sdk.baseUrl || fallbackBaseUrl, fallbackBaseUrl);
|
|
96
|
+
url.pathname = url.pathname.replace(/\/+$/, "") + "/api/ads/serve";
|
|
97
|
+
url.search = "";
|
|
98
|
+
url.hash = "";
|
|
99
|
+
url.searchParams.set("adUnitId", adUnitId);
|
|
100
|
+
url.searchParams.set("appId", sdk.appId ?? "");
|
|
101
|
+
url.searchParams.set("allowSelfAds", String(sdk.allowSelfAds));
|
|
102
|
+
url.searchParams.set("platform", "web");
|
|
103
|
+
if (adType) {
|
|
104
|
+
url.searchParams.set("adType", adType);
|
|
105
|
+
}
|
|
106
|
+
if (sdk.lastAdId) {
|
|
107
|
+
url.searchParams.set("exclude", sdk.lastAdId);
|
|
108
|
+
}
|
|
109
|
+
if (sdk.bundleId) {
|
|
110
|
+
url.searchParams.set("bundleId", sdk.bundleId);
|
|
111
|
+
}
|
|
112
|
+
if (typeof window !== "undefined") {
|
|
113
|
+
url.searchParams.set("sourceUrl", window.location.href);
|
|
114
|
+
}
|
|
115
|
+
return url.toString();
|
|
116
|
+
}
|
|
117
|
+
static trackImpression(adId, token, viewability) {
|
|
118
|
+
this.trackEvent("/api/ads/impression", adId, token, viewability);
|
|
105
119
|
}
|
|
106
120
|
static trackClick(adId, token) {
|
|
107
121
|
this.trackEvent("/api/ads/click", adId, token);
|
|
108
122
|
}
|
|
109
|
-
|
|
110
|
-
* Detect the user's country code from the browser locale.
|
|
111
|
-
* Uses Intl API (most reliable) with navigator.language as fallback.
|
|
112
|
-
* Returns an ISO 3166-1 alpha-2 code like "US", "DE", "JP", or null.
|
|
113
|
-
*/
|
|
114
|
-
static detectCountry() {
|
|
115
|
-
try {
|
|
116
|
-
if (typeof Intl !== "undefined" && Intl.DateTimeFormat) {
|
|
117
|
-
const resolved = Intl.DateTimeFormat().resolvedOptions();
|
|
118
|
-
if (resolved.locale) {
|
|
119
|
-
const parts = resolved.locale.split("-");
|
|
120
|
-
if (parts.length >= 2) {
|
|
121
|
-
const region = parts[parts.length - 1].toUpperCase();
|
|
122
|
-
if (region.length === 2) return region;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
if (typeof navigator !== "undefined" && navigator.language) {
|
|
127
|
-
const parts = navigator.language.split("-");
|
|
128
|
-
if (parts.length >= 2) {
|
|
129
|
-
const region = parts[parts.length - 1].toUpperCase();
|
|
130
|
-
if (region.length === 2) return region;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
} catch (_) {
|
|
134
|
-
}
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
static trackEvent(endpoint, adId, token) {
|
|
123
|
+
static trackEvent(endpoint, adId, token, viewability) {
|
|
138
124
|
if (!_AdTogether.shared.assertInitialized()) return;
|
|
139
125
|
fetch(`${_AdTogether.shared.baseUrl}${endpoint}`, {
|
|
140
126
|
method: "POST",
|
|
@@ -146,10 +132,11 @@ var AdTogether = class _AdTogether {
|
|
|
146
132
|
token,
|
|
147
133
|
appId: _AdTogether.shared.appId,
|
|
148
134
|
..._AdTogether.shared.bundleId ? { bundleId: _AdTogether.shared.bundleId } : {},
|
|
135
|
+
...endpoint === "/api/ads/impression" && typeof window !== "undefined" ? { pagePath: window.location.pathname } : {},
|
|
136
|
+
...endpoint === "/api/ads/impression" && viewability ? viewability : {},
|
|
149
137
|
// Send platform and environment to match Flutter SDK
|
|
150
138
|
platform: "web",
|
|
151
|
-
environment: typeof process !== "undefined" && process.env?.NODE_ENV === "development" ? "development" : "production"
|
|
152
|
-
country: _AdTogether.detectCountry()
|
|
139
|
+
environment: typeof process !== "undefined" && process.env?.NODE_ENV === "development" ? "development" : "production"
|
|
153
140
|
})
|
|
154
141
|
}).catch(console.error);
|
|
155
142
|
}
|
package/dist/index.mjs
CHANGED
package/dist/react/index.js
CHANGED
|
@@ -76,23 +76,13 @@ var AdTogether = class _AdTogether {
|
|
|
76
76
|
}
|
|
77
77
|
try {
|
|
78
78
|
const sdk = _AdTogether.shared;
|
|
79
|
-
|
|
80
|
-
if (adType) {
|
|
81
|
-
url += `&adType=${adType}`;
|
|
82
|
-
}
|
|
83
|
-
if (sdk.lastAdId) {
|
|
84
|
-
url += `&exclude=${sdk.lastAdId}`;
|
|
85
|
-
}
|
|
86
|
-
if (sdk.bundleId) {
|
|
87
|
-
url += `&bundleId=${sdk.bundleId}`;
|
|
88
|
-
}
|
|
89
|
-
url += `&allowSelfAds=${sdk.allowSelfAds}`;
|
|
90
|
-
if (typeof window !== "undefined") {
|
|
91
|
-
url += `&sourceUrl=${encodeURIComponent(window.location.href)}`;
|
|
92
|
-
}
|
|
79
|
+
const url = _AdTogether.buildServeUrl(sdk, adUnitId, adType);
|
|
93
80
|
const response = await fetch(url);
|
|
94
81
|
if (response.ok) {
|
|
95
82
|
const ad = await response.json();
|
|
83
|
+
if (adType && ad.adType !== adType) {
|
|
84
|
+
throw new Error("Received an ad with a different type than requested: " + (ad.adType ?? "missing"));
|
|
85
|
+
}
|
|
96
86
|
sdk.lastAdId = ad.id;
|
|
97
87
|
return ad;
|
|
98
88
|
} else if (response.status === 401 || response.status === 403) {
|
|
@@ -104,41 +94,37 @@ var AdTogether = class _AdTogether {
|
|
|
104
94
|
throw err;
|
|
105
95
|
}
|
|
106
96
|
}
|
|
107
|
-
static
|
|
108
|
-
|
|
97
|
+
static buildServeUrl(sdk, adUnitId, adType) {
|
|
98
|
+
const fallbackBaseUrl = typeof window !== "undefined" ? window.location.origin : "https://www.ad-together.org";
|
|
99
|
+
const url = new URL(sdk.baseUrl || fallbackBaseUrl, fallbackBaseUrl);
|
|
100
|
+
url.pathname = url.pathname.replace(/\/+$/, "") + "/api/ads/serve";
|
|
101
|
+
url.search = "";
|
|
102
|
+
url.hash = "";
|
|
103
|
+
url.searchParams.set("adUnitId", adUnitId);
|
|
104
|
+
url.searchParams.set("appId", sdk.appId ?? "");
|
|
105
|
+
url.searchParams.set("allowSelfAds", String(sdk.allowSelfAds));
|
|
106
|
+
url.searchParams.set("platform", "web");
|
|
107
|
+
if (adType) {
|
|
108
|
+
url.searchParams.set("adType", adType);
|
|
109
|
+
}
|
|
110
|
+
if (sdk.lastAdId) {
|
|
111
|
+
url.searchParams.set("exclude", sdk.lastAdId);
|
|
112
|
+
}
|
|
113
|
+
if (sdk.bundleId) {
|
|
114
|
+
url.searchParams.set("bundleId", sdk.bundleId);
|
|
115
|
+
}
|
|
116
|
+
if (typeof window !== "undefined") {
|
|
117
|
+
url.searchParams.set("sourceUrl", window.location.href);
|
|
118
|
+
}
|
|
119
|
+
return url.toString();
|
|
120
|
+
}
|
|
121
|
+
static trackImpression(adId, token, viewability) {
|
|
122
|
+
this.trackEvent("/api/ads/impression", adId, token, viewability);
|
|
109
123
|
}
|
|
110
124
|
static trackClick(adId, token) {
|
|
111
125
|
this.trackEvent("/api/ads/click", adId, token);
|
|
112
126
|
}
|
|
113
|
-
|
|
114
|
-
* Detect the user's country code from the browser locale.
|
|
115
|
-
* Uses Intl API (most reliable) with navigator.language as fallback.
|
|
116
|
-
* Returns an ISO 3166-1 alpha-2 code like "US", "DE", "JP", or null.
|
|
117
|
-
*/
|
|
118
|
-
static detectCountry() {
|
|
119
|
-
try {
|
|
120
|
-
if (typeof Intl !== "undefined" && Intl.DateTimeFormat) {
|
|
121
|
-
const resolved = Intl.DateTimeFormat().resolvedOptions();
|
|
122
|
-
if (resolved.locale) {
|
|
123
|
-
const parts = resolved.locale.split("-");
|
|
124
|
-
if (parts.length >= 2) {
|
|
125
|
-
const region = parts[parts.length - 1].toUpperCase();
|
|
126
|
-
if (region.length === 2) return region;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
if (typeof navigator !== "undefined" && navigator.language) {
|
|
131
|
-
const parts = navigator.language.split("-");
|
|
132
|
-
if (parts.length >= 2) {
|
|
133
|
-
const region = parts[parts.length - 1].toUpperCase();
|
|
134
|
-
if (region.length === 2) return region;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
} catch (_) {
|
|
138
|
-
}
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
static trackEvent(endpoint, adId, token) {
|
|
127
|
+
static trackEvent(endpoint, adId, token, viewability) {
|
|
142
128
|
if (!_AdTogether.shared.assertInitialized()) return;
|
|
143
129
|
fetch(`${_AdTogether.shared.baseUrl}${endpoint}`, {
|
|
144
130
|
method: "POST",
|
|
@@ -150,10 +136,11 @@ var AdTogether = class _AdTogether {
|
|
|
150
136
|
token,
|
|
151
137
|
appId: _AdTogether.shared.appId,
|
|
152
138
|
..._AdTogether.shared.bundleId ? { bundleId: _AdTogether.shared.bundleId } : {},
|
|
139
|
+
...endpoint === "/api/ads/impression" && typeof window !== "undefined" ? { pagePath: window.location.pathname } : {},
|
|
140
|
+
...endpoint === "/api/ads/impression" && viewability ? viewability : {},
|
|
153
141
|
// Send platform and environment to match Flutter SDK
|
|
154
142
|
platform: "web",
|
|
155
|
-
environment: typeof process !== "undefined" && process.env?.NODE_ENV === "development" ? "development" : "production"
|
|
156
|
-
country: _AdTogether.detectCountry()
|
|
143
|
+
environment: typeof process !== "undefined" && process.env?.NODE_ENV === "development" ? "development" : "production"
|
|
157
144
|
})
|
|
158
145
|
}).catch(console.error);
|
|
159
146
|
}
|
|
@@ -212,22 +199,59 @@ var AdTogetherBanner = ({
|
|
|
212
199
|
};
|
|
213
200
|
}, [adUnitId, onAdLoaded, onAdFailedToLoad]);
|
|
214
201
|
(0, import_react.useEffect)(() => {
|
|
215
|
-
if (!adData || isLoading || hasError || !containerRef.current) return;
|
|
202
|
+
if (!adData || isLoading || hasError || !isVisible || !containerRef.current) return;
|
|
203
|
+
let visibleTimer = null;
|
|
204
|
+
let visibleSince = null;
|
|
205
|
+
let latestVisibleRatio = 0;
|
|
206
|
+
const clearVisibleTimer = () => {
|
|
207
|
+
if (visibleTimer) clearTimeout(visibleTimer);
|
|
208
|
+
visibleTimer = null;
|
|
209
|
+
visibleSince = null;
|
|
210
|
+
};
|
|
211
|
+
const startVisibleTimer = () => {
|
|
212
|
+
if (visibleTimer || impressionTrackedRef.current || document.visibilityState !== "visible") return;
|
|
213
|
+
visibleSince = Date.now();
|
|
214
|
+
visibleTimer = setTimeout(() => {
|
|
215
|
+
const visibleDurationMs = visibleSince === null ? 0 : Date.now() - visibleSince;
|
|
216
|
+
const visiblePercent = Math.round(latestVisibleRatio * 100);
|
|
217
|
+
visibleTimer = null;
|
|
218
|
+
visibleSince = null;
|
|
219
|
+
if (impressionTrackedRef.current || !isVisible || !container.isConnected || document.visibilityState !== "visible" || visiblePercent < 50 || visibleDurationMs < 1e3) return;
|
|
220
|
+
impressionTrackedRef.current = true;
|
|
221
|
+
AdTogether.trackImpression(adData.id, adData.token, { visiblePercent, visibleDurationMs });
|
|
222
|
+
observer.disconnect();
|
|
223
|
+
}, 1e3);
|
|
224
|
+
};
|
|
216
225
|
const observer = new IntersectionObserver(
|
|
217
226
|
(entries) => {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
227
|
+
const entry = entries[0];
|
|
228
|
+
if (!entry) return;
|
|
229
|
+
latestVisibleRatio = entry.intersectionRatio;
|
|
230
|
+
if (entry.isIntersecting && entry.intersectionRatio >= 0.5 && document.visibilityState === "visible") {
|
|
231
|
+
startVisibleTimer();
|
|
232
|
+
} else {
|
|
233
|
+
clearVisibleTimer();
|
|
222
234
|
}
|
|
223
235
|
},
|
|
224
|
-
{ threshold: 0.5 }
|
|
236
|
+
{ threshold: [0, 0.5] }
|
|
225
237
|
);
|
|
226
|
-
|
|
238
|
+
const container = containerRef.current;
|
|
239
|
+
const handleVisibilityChange = () => {
|
|
240
|
+
clearVisibleTimer();
|
|
241
|
+
if (document.visibilityState === "visible") {
|
|
242
|
+
latestVisibleRatio = 0;
|
|
243
|
+
observer.unobserve(container);
|
|
244
|
+
observer.observe(container);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
248
|
+
observer.observe(container);
|
|
227
249
|
return () => {
|
|
250
|
+
clearVisibleTimer();
|
|
251
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
228
252
|
observer.disconnect();
|
|
229
253
|
};
|
|
230
|
-
}, [adData, isLoading, hasError]);
|
|
254
|
+
}, [adData, isLoading, hasError, isVisible]);
|
|
231
255
|
const handleContainerClick = () => {
|
|
232
256
|
if (!adData) return;
|
|
233
257
|
AdTogether.trackClick(adData.id, adData.token);
|
|
@@ -429,8 +453,38 @@ var AdTogetherInterstitial = ({
|
|
|
429
453
|
}, [isOpen, adData, closeDelay]);
|
|
430
454
|
(0, import_react2.useEffect)(() => {
|
|
431
455
|
if (!adData || !isOpen || impressionTrackedRef.current) return;
|
|
432
|
-
|
|
433
|
-
|
|
456
|
+
let visibleTimer = null;
|
|
457
|
+
let visibleSince = null;
|
|
458
|
+
const clearVisibleTimer = () => {
|
|
459
|
+
if (visibleTimer) clearTimeout(visibleTimer);
|
|
460
|
+
visibleTimer = null;
|
|
461
|
+
visibleSince = null;
|
|
462
|
+
};
|
|
463
|
+
const startVisibleTimer = () => {
|
|
464
|
+
if (visibleTimer || impressionTrackedRef.current || document.visibilityState !== "visible") return;
|
|
465
|
+
visibleSince = Date.now();
|
|
466
|
+
visibleTimer = setTimeout(() => {
|
|
467
|
+
const visibleDurationMs = visibleSince === null ? 0 : Date.now() - visibleSince;
|
|
468
|
+
visibleTimer = null;
|
|
469
|
+
visibleSince = null;
|
|
470
|
+
if (impressionTrackedRef.current || document.visibilityState !== "visible" || visibleDurationMs < 1e3) return;
|
|
471
|
+
impressionTrackedRef.current = true;
|
|
472
|
+
AdTogether.trackImpression(adData.id, adData.token, {
|
|
473
|
+
visiblePercent: 100,
|
|
474
|
+
visibleDurationMs
|
|
475
|
+
});
|
|
476
|
+
}, 1e3);
|
|
477
|
+
};
|
|
478
|
+
const handleVisibilityChange = () => {
|
|
479
|
+
if (document.visibilityState === "visible") startVisibleTimer();
|
|
480
|
+
else clearVisibleTimer();
|
|
481
|
+
};
|
|
482
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
483
|
+
startVisibleTimer();
|
|
484
|
+
return () => {
|
|
485
|
+
clearVisibleTimer();
|
|
486
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
487
|
+
};
|
|
434
488
|
}, [adData, isOpen]);
|
|
435
489
|
const handleAdClick = (0, import_react2.useCallback)(() => {
|
|
436
490
|
if (!adData) return;
|
package/dist/react/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AdTogether
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-NXWVGW5H.mjs";
|
|
4
4
|
|
|
5
5
|
// src/react/AdTogetherBanner.tsx
|
|
6
6
|
import { useEffect, useRef, useState } from "react";
|
|
@@ -56,22 +56,59 @@ var AdTogetherBanner = ({
|
|
|
56
56
|
};
|
|
57
57
|
}, [adUnitId, onAdLoaded, onAdFailedToLoad]);
|
|
58
58
|
useEffect(() => {
|
|
59
|
-
if (!adData || isLoading || hasError || !containerRef.current) return;
|
|
59
|
+
if (!adData || isLoading || hasError || !isVisible || !containerRef.current) return;
|
|
60
|
+
let visibleTimer = null;
|
|
61
|
+
let visibleSince = null;
|
|
62
|
+
let latestVisibleRatio = 0;
|
|
63
|
+
const clearVisibleTimer = () => {
|
|
64
|
+
if (visibleTimer) clearTimeout(visibleTimer);
|
|
65
|
+
visibleTimer = null;
|
|
66
|
+
visibleSince = null;
|
|
67
|
+
};
|
|
68
|
+
const startVisibleTimer = () => {
|
|
69
|
+
if (visibleTimer || impressionTrackedRef.current || document.visibilityState !== "visible") return;
|
|
70
|
+
visibleSince = Date.now();
|
|
71
|
+
visibleTimer = setTimeout(() => {
|
|
72
|
+
const visibleDurationMs = visibleSince === null ? 0 : Date.now() - visibleSince;
|
|
73
|
+
const visiblePercent = Math.round(latestVisibleRatio * 100);
|
|
74
|
+
visibleTimer = null;
|
|
75
|
+
visibleSince = null;
|
|
76
|
+
if (impressionTrackedRef.current || !isVisible || !container.isConnected || document.visibilityState !== "visible" || visiblePercent < 50 || visibleDurationMs < 1e3) return;
|
|
77
|
+
impressionTrackedRef.current = true;
|
|
78
|
+
AdTogether.trackImpression(adData.id, adData.token, { visiblePercent, visibleDurationMs });
|
|
79
|
+
observer.disconnect();
|
|
80
|
+
}, 1e3);
|
|
81
|
+
};
|
|
60
82
|
const observer = new IntersectionObserver(
|
|
61
83
|
(entries) => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
84
|
+
const entry = entries[0];
|
|
85
|
+
if (!entry) return;
|
|
86
|
+
latestVisibleRatio = entry.intersectionRatio;
|
|
87
|
+
if (entry.isIntersecting && entry.intersectionRatio >= 0.5 && document.visibilityState === "visible") {
|
|
88
|
+
startVisibleTimer();
|
|
89
|
+
} else {
|
|
90
|
+
clearVisibleTimer();
|
|
66
91
|
}
|
|
67
92
|
},
|
|
68
|
-
{ threshold: 0.5 }
|
|
93
|
+
{ threshold: [0, 0.5] }
|
|
69
94
|
);
|
|
70
|
-
|
|
95
|
+
const container = containerRef.current;
|
|
96
|
+
const handleVisibilityChange = () => {
|
|
97
|
+
clearVisibleTimer();
|
|
98
|
+
if (document.visibilityState === "visible") {
|
|
99
|
+
latestVisibleRatio = 0;
|
|
100
|
+
observer.unobserve(container);
|
|
101
|
+
observer.observe(container);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
105
|
+
observer.observe(container);
|
|
71
106
|
return () => {
|
|
107
|
+
clearVisibleTimer();
|
|
108
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
72
109
|
observer.disconnect();
|
|
73
110
|
};
|
|
74
|
-
}, [adData, isLoading, hasError]);
|
|
111
|
+
}, [adData, isLoading, hasError, isVisible]);
|
|
75
112
|
const handleContainerClick = () => {
|
|
76
113
|
if (!adData) return;
|
|
77
114
|
AdTogether.trackClick(adData.id, adData.token);
|
|
@@ -273,8 +310,38 @@ var AdTogetherInterstitial = ({
|
|
|
273
310
|
}, [isOpen, adData, closeDelay]);
|
|
274
311
|
useEffect2(() => {
|
|
275
312
|
if (!adData || !isOpen || impressionTrackedRef.current) return;
|
|
276
|
-
|
|
277
|
-
|
|
313
|
+
let visibleTimer = null;
|
|
314
|
+
let visibleSince = null;
|
|
315
|
+
const clearVisibleTimer = () => {
|
|
316
|
+
if (visibleTimer) clearTimeout(visibleTimer);
|
|
317
|
+
visibleTimer = null;
|
|
318
|
+
visibleSince = null;
|
|
319
|
+
};
|
|
320
|
+
const startVisibleTimer = () => {
|
|
321
|
+
if (visibleTimer || impressionTrackedRef.current || document.visibilityState !== "visible") return;
|
|
322
|
+
visibleSince = Date.now();
|
|
323
|
+
visibleTimer = setTimeout(() => {
|
|
324
|
+
const visibleDurationMs = visibleSince === null ? 0 : Date.now() - visibleSince;
|
|
325
|
+
visibleTimer = null;
|
|
326
|
+
visibleSince = null;
|
|
327
|
+
if (impressionTrackedRef.current || document.visibilityState !== "visible" || visibleDurationMs < 1e3) return;
|
|
328
|
+
impressionTrackedRef.current = true;
|
|
329
|
+
AdTogether.trackImpression(adData.id, adData.token, {
|
|
330
|
+
visiblePercent: 100,
|
|
331
|
+
visibleDurationMs
|
|
332
|
+
});
|
|
333
|
+
}, 1e3);
|
|
334
|
+
};
|
|
335
|
+
const handleVisibilityChange = () => {
|
|
336
|
+
if (document.visibilityState === "visible") startVisibleTimer();
|
|
337
|
+
else clearVisibleTimer();
|
|
338
|
+
};
|
|
339
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
340
|
+
startVisibleTimer();
|
|
341
|
+
return () => {
|
|
342
|
+
clearVisibleTimer();
|
|
343
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
344
|
+
};
|
|
278
345
|
}, [adData, isOpen]);
|
|
279
346
|
const handleAdClick = useCallback(() => {
|
|
280
347
|
if (!adData) return;
|
package/package.json
CHANGED
package/src/core/AdTogether.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AdModel, AdType, AdTogetherOptions } from './types';
|
|
1
|
+
import { AdModel, AdType, AdTogetherOptions, ViewabilityMetrics } from './types';
|
|
2
2
|
|
|
3
3
|
export class AdTogether {
|
|
4
4
|
private static instance: AdTogether;
|
|
@@ -63,26 +63,14 @@ export class AdTogether {
|
|
|
63
63
|
|
|
64
64
|
try {
|
|
65
65
|
const sdk = AdTogether.shared;
|
|
66
|
-
|
|
67
|
-
if (adType) {
|
|
68
|
-
url += `&adType=${adType}`;
|
|
69
|
-
}
|
|
70
|
-
if (sdk.lastAdId) {
|
|
71
|
-
url += `&exclude=${sdk.lastAdId}`;
|
|
72
|
-
}
|
|
73
|
-
if (sdk.bundleId) {
|
|
74
|
-
url += `&bundleId=${sdk.bundleId}`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Pass allowSelfAds and source URL for smart serving
|
|
78
|
-
url += `&allowSelfAds=${sdk.allowSelfAds}`;
|
|
79
|
-
if (typeof window !== 'undefined') {
|
|
80
|
-
url += `&sourceUrl=${encodeURIComponent(window.location.href)}`;
|
|
81
|
-
}
|
|
66
|
+
const url = AdTogether.buildServeUrl(sdk, adUnitId, adType);
|
|
82
67
|
|
|
83
68
|
const response = await fetch(url);
|
|
84
69
|
if (response.ok) {
|
|
85
|
-
const ad = await response.json();
|
|
70
|
+
const ad = await response.json() as AdModel;
|
|
71
|
+
if (adType && ad.adType !== adType) {
|
|
72
|
+
throw new Error('Received an ad with a different type than requested: ' + (ad.adType ?? 'missing'));
|
|
73
|
+
}
|
|
86
74
|
sdk.lastAdId = ad.id;
|
|
87
75
|
return ad;
|
|
88
76
|
} else if (response.status === 401 || response.status === 403) {
|
|
@@ -96,46 +84,44 @@ export class AdTogether {
|
|
|
96
84
|
}
|
|
97
85
|
}
|
|
98
86
|
|
|
99
|
-
static
|
|
100
|
-
|
|
87
|
+
private static buildServeUrl(sdk: AdTogether, adUnitId: string, adType?: AdType): string {
|
|
88
|
+
const fallbackBaseUrl = typeof window !== 'undefined'
|
|
89
|
+
? window.location.origin
|
|
90
|
+
: 'https://www.ad-together.org';
|
|
91
|
+
const url = new URL(sdk.baseUrl || fallbackBaseUrl, fallbackBaseUrl);
|
|
92
|
+
url.pathname = url.pathname.replace(/\/+$/, '') + '/api/ads/serve';
|
|
93
|
+
url.search = '';
|
|
94
|
+
url.hash = '';
|
|
95
|
+
|
|
96
|
+
url.searchParams.set('adUnitId', adUnitId);
|
|
97
|
+
url.searchParams.set('appId', sdk.appId ?? '');
|
|
98
|
+
url.searchParams.set('allowSelfAds', String(sdk.allowSelfAds));
|
|
99
|
+
url.searchParams.set('platform', 'web');
|
|
100
|
+
if (adType) {
|
|
101
|
+
url.searchParams.set('adType', adType);
|
|
102
|
+
}
|
|
103
|
+
if (sdk.lastAdId) {
|
|
104
|
+
url.searchParams.set('exclude', sdk.lastAdId);
|
|
105
|
+
}
|
|
106
|
+
if (sdk.bundleId) {
|
|
107
|
+
url.searchParams.set('bundleId', sdk.bundleId);
|
|
108
|
+
}
|
|
109
|
+
if (typeof window !== 'undefined') {
|
|
110
|
+
url.searchParams.set('sourceUrl', window.location.href);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return url.toString();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
static trackImpression(adId: string, token?: string, viewability?: ViewabilityMetrics) {
|
|
117
|
+
this.trackEvent('/api/ads/impression', adId, token, viewability);
|
|
101
118
|
}
|
|
102
119
|
|
|
103
120
|
static trackClick(adId: string, token?: string) {
|
|
104
121
|
this.trackEvent('/api/ads/click', adId, token);
|
|
105
122
|
}
|
|
106
123
|
|
|
107
|
-
|
|
108
|
-
* Detect the user's country code from the browser locale.
|
|
109
|
-
* Uses Intl API (most reliable) with navigator.language as fallback.
|
|
110
|
-
* Returns an ISO 3166-1 alpha-2 code like "US", "DE", "JP", or null.
|
|
111
|
-
*/
|
|
112
|
-
private static detectCountry(): string | null {
|
|
113
|
-
try {
|
|
114
|
-
// Intl API gives the most reliable region
|
|
115
|
-
if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) {
|
|
116
|
-
const resolved = Intl.DateTimeFormat().resolvedOptions();
|
|
117
|
-
// timeZone is like "America/New_York" — we can map it, but locale is simpler
|
|
118
|
-
if (resolved.locale) {
|
|
119
|
-
const parts = resolved.locale.split('-');
|
|
120
|
-
if (parts.length >= 2) {
|
|
121
|
-
const region = parts[parts.length - 1].toUpperCase();
|
|
122
|
-
if (region.length === 2) return region;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
// Fallback: navigator.language (e.g. "en-US" -> "US")
|
|
127
|
-
if (typeof navigator !== 'undefined' && navigator.language) {
|
|
128
|
-
const parts = navigator.language.split('-');
|
|
129
|
-
if (parts.length >= 2) {
|
|
130
|
-
const region = parts[parts.length - 1].toUpperCase();
|
|
131
|
-
if (region.length === 2) return region;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
} catch (_) {}
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
private static trackEvent(endpoint: string, adId: string, token?: string) {
|
|
124
|
+
private static trackEvent(endpoint: string, adId: string, token?: string, viewability?: ViewabilityMetrics) {
|
|
139
125
|
if (!AdTogether.shared.assertInitialized()) return;
|
|
140
126
|
|
|
141
127
|
fetch(`${AdTogether.shared.baseUrl}${endpoint}`, {
|
|
@@ -148,10 +134,13 @@ export class AdTogether {
|
|
|
148
134
|
token,
|
|
149
135
|
appId: AdTogether.shared.appId,
|
|
150
136
|
...(AdTogether.shared.bundleId ? { bundleId: AdTogether.shared.bundleId } : {}),
|
|
137
|
+
...(endpoint === '/api/ads/impression' && typeof window !== 'undefined'
|
|
138
|
+
? { pagePath: window.location.pathname }
|
|
139
|
+
: {}),
|
|
140
|
+
...(endpoint === '/api/ads/impression' && viewability ? viewability : {}),
|
|
151
141
|
// Send platform and environment to match Flutter SDK
|
|
152
142
|
platform: 'web',
|
|
153
143
|
environment: typeof process !== 'undefined' && process.env?.NODE_ENV === 'development' ? 'development' : 'production',
|
|
154
|
-
country: AdTogether.detectCountry(),
|
|
155
144
|
}),
|
|
156
145
|
}).catch(console.error);
|
|
157
146
|
}
|
package/src/core/types.ts
CHANGED
|
@@ -83,25 +83,77 @@ export const AdTogetherBanner: React.FC<AdTogetherBannerProps> = ({
|
|
|
83
83
|
|
|
84
84
|
// Impression tracking
|
|
85
85
|
useEffect(() => {
|
|
86
|
-
if (!adData || isLoading || hasError || !containerRef.current) return;
|
|
86
|
+
if (!adData || isLoading || hasError || !isVisible || !containerRef.current) return;
|
|
87
|
+
|
|
88
|
+
let visibleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
89
|
+
let visibleSince: number | null = null;
|
|
90
|
+
let latestVisibleRatio = 0;
|
|
91
|
+
|
|
92
|
+
const clearVisibleTimer = () => {
|
|
93
|
+
if (visibleTimer) clearTimeout(visibleTimer);
|
|
94
|
+
visibleTimer = null;
|
|
95
|
+
visibleSince = null;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const startVisibleTimer = () => {
|
|
99
|
+
if (visibleTimer || impressionTrackedRef.current || document.visibilityState !== 'visible') return;
|
|
100
|
+
|
|
101
|
+
visibleSince = Date.now();
|
|
102
|
+
visibleTimer = setTimeout(() => {
|
|
103
|
+
const visibleDurationMs = visibleSince === null ? 0 : Date.now() - visibleSince;
|
|
104
|
+
const visiblePercent = Math.round(latestVisibleRatio * 100);
|
|
105
|
+
visibleTimer = null;
|
|
106
|
+
visibleSince = null;
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
impressionTrackedRef.current ||
|
|
110
|
+
!isVisible ||
|
|
111
|
+
!container.isConnected ||
|
|
112
|
+
document.visibilityState !== 'visible' ||
|
|
113
|
+
visiblePercent < 50 ||
|
|
114
|
+
visibleDurationMs < 1000
|
|
115
|
+
) return;
|
|
116
|
+
|
|
117
|
+
impressionTrackedRef.current = true;
|
|
118
|
+
AdTogether.trackImpression(adData.id, adData.token, { visiblePercent, visibleDurationMs });
|
|
119
|
+
observer.disconnect();
|
|
120
|
+
}, 1000);
|
|
121
|
+
};
|
|
87
122
|
|
|
88
123
|
const observer = new IntersectionObserver(
|
|
89
124
|
(entries) => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
125
|
+
const entry = entries[0];
|
|
126
|
+
if (!entry) return;
|
|
127
|
+
|
|
128
|
+
latestVisibleRatio = entry.intersectionRatio;
|
|
129
|
+
if (entry.isIntersecting && entry.intersectionRatio >= 0.5 && document.visibilityState === 'visible') {
|
|
130
|
+
startVisibleTimer();
|
|
131
|
+
} else {
|
|
132
|
+
clearVisibleTimer();
|
|
94
133
|
}
|
|
95
134
|
},
|
|
96
|
-
{ threshold: 0.5 }
|
|
135
|
+
{ threshold: [0, 0.5] }
|
|
97
136
|
);
|
|
98
137
|
|
|
99
|
-
|
|
138
|
+
const container = containerRef.current;
|
|
139
|
+
const handleVisibilityChange = () => {
|
|
140
|
+
clearVisibleTimer();
|
|
141
|
+
if (document.visibilityState === 'visible') {
|
|
142
|
+
latestVisibleRatio = 0;
|
|
143
|
+
observer.unobserve(container);
|
|
144
|
+
observer.observe(container);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
149
|
+
observer.observe(container);
|
|
100
150
|
|
|
101
151
|
return () => {
|
|
152
|
+
clearVisibleTimer();
|
|
153
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
102
154
|
observer.disconnect();
|
|
103
155
|
};
|
|
104
|
-
}, [adData, isLoading, hasError]);
|
|
156
|
+
}, [adData, isLoading, hasError, isVisible]);
|
|
105
157
|
|
|
106
158
|
const handleContainerClick = () => {
|
|
107
159
|
if (!adData) return;
|
|
@@ -120,8 +120,46 @@ export const AdTogetherInterstitial: React.FC<AdTogetherInterstitialProps> = ({
|
|
|
120
120
|
// Impression tracking
|
|
121
121
|
useEffect(() => {
|
|
122
122
|
if (!adData || !isOpen || impressionTrackedRef.current) return;
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
|
|
124
|
+
let visibleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
125
|
+
let visibleSince: number | null = null;
|
|
126
|
+
|
|
127
|
+
const clearVisibleTimer = () => {
|
|
128
|
+
if (visibleTimer) clearTimeout(visibleTimer);
|
|
129
|
+
visibleTimer = null;
|
|
130
|
+
visibleSince = null;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const startVisibleTimer = () => {
|
|
134
|
+
if (visibleTimer || impressionTrackedRef.current || document.visibilityState !== 'visible') return;
|
|
135
|
+
|
|
136
|
+
visibleSince = Date.now();
|
|
137
|
+
visibleTimer = setTimeout(() => {
|
|
138
|
+
const visibleDurationMs = visibleSince === null ? 0 : Date.now() - visibleSince;
|
|
139
|
+
visibleTimer = null;
|
|
140
|
+
visibleSince = null;
|
|
141
|
+
|
|
142
|
+
if (impressionTrackedRef.current || document.visibilityState !== 'visible' || visibleDurationMs < 1000) return;
|
|
143
|
+
|
|
144
|
+
impressionTrackedRef.current = true;
|
|
145
|
+
AdTogether.trackImpression(adData.id, adData.token, {
|
|
146
|
+
visiblePercent: 100,
|
|
147
|
+
visibleDurationMs,
|
|
148
|
+
});
|
|
149
|
+
}, 1000);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const handleVisibilityChange = () => {
|
|
153
|
+
if (document.visibilityState === 'visible') startVisibleTimer();
|
|
154
|
+
else clearVisibleTimer();
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
158
|
+
startVisibleTimer();
|
|
159
|
+
return () => {
|
|
160
|
+
clearVisibleTimer();
|
|
161
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
162
|
+
};
|
|
125
163
|
}, [adData, isOpen]);
|
|
126
164
|
|
|
127
165
|
const handleAdClick = useCallback(() => {
|
package/errors.txt
DELETED