@affiliateo/web 4.4.1 → 4.6.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/README.md +238 -238
- package/dist/provider.d.ts +1 -1
- package/dist/provider.js +69 -23
- package/dist/turnstile.d.ts +40 -0
- package/dist/turnstile.js +105 -0
- package/dist/types.d.ts +10 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,238 +1,238 @@
|
|
|
1
|
-
# @affiliateo/web
|
|
2
|
-
|
|
3
|
-
Affiliateo web SDK. track affiliate visits and pass attribution metadata to your Stripe checkout.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @affiliateo/web
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Setup
|
|
12
|
-
|
|
13
|
-
1. Connect your Stripe account on the Affiliateo dashboard
|
|
14
|
-
2. Wrap your app in `<AffiliateoProvider>` with your
|
|
15
|
-
3. At checkout time, call `getAttribution()` and pass the result to Stripe metadata
|
|
16
|
-
|
|
17
|
-
### Step 1. wrap your app
|
|
18
|
-
|
|
19
|
-
```tsx
|
|
20
|
-
// app/layout.tsx (Next.js App Router) or _app.tsx (Pages Router)
|
|
21
|
-
import { AffiliateoProvider } from '@affiliateo/web';
|
|
22
|
-
|
|
23
|
-
export default function RootLayout({ children }) {
|
|
24
|
-
return (
|
|
25
|
-
<AffiliateoProvider
|
|
26
|
-
{children}
|
|
27
|
-
</AffiliateoProvider>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
The provider:
|
|
33
|
-
- Reads `?ref=` from the URL when a visitor arrives
|
|
34
|
-
- Stores attribution in `localStorage` so it survives navigation
|
|
35
|
-
- Sends pageview events to Affiliateo for click tracking
|
|
36
|
-
|
|
37
|
-
### Step 2. in your checkout button (client-side)
|
|
38
|
-
|
|
39
|
-
```tsx
|
|
40
|
-
'use client'
|
|
41
|
-
import { getAttribution } from '@affiliateo/web';
|
|
42
|
-
|
|
43
|
-
async function handleCheckout() {
|
|
44
|
-
const attribution = getAttribution();
|
|
45
|
-
|
|
46
|
-
const res = await fetch('/api/checkout', {
|
|
47
|
-
method: 'POST',
|
|
48
|
-
body: JSON.stringify({ attribution }),
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const { url } = await res.json();
|
|
52
|
-
window.location.href = url;
|
|
53
|
-
}
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
`getAttribution()` reads
|
|
57
|
-
|
|
58
|
-
### Step 3. in your checkout API route (server-side)
|
|
59
|
-
|
|
60
|
-
```ts
|
|
61
|
-
// app/api/checkout/route.ts
|
|
62
|
-
export async function POST(req) {
|
|
63
|
-
const { attribution } = await req.json();
|
|
64
|
-
|
|
65
|
-
const session = await stripe.checkout.sessions.create({
|
|
66
|
-
line_items: [{ price: 'price_xxx', quantity: 1 }],
|
|
67
|
-
mode: 'payment',
|
|
68
|
-
success_url: 'https://yoursite.com/success',
|
|
69
|
-
cancel_url: 'https://yoursite.com/cancel',
|
|
70
|
-
metadata: attribution,
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
return Response.json({ url: session.url });
|
|
74
|
-
}
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
That's it. Affiliateo's webhook will automatically read the metadata and credit the right affiliate when the payment succeeds.
|
|
78
|
-
|
|
79
|
-
## How it works
|
|
80
|
-
|
|
81
|
-
1. Visitor clicks affiliate link → `yoursite.com?ref=pinkproton`
|
|
82
|
-
2. `<AffiliateoProvider>` reads `?ref=pinkproton` from the URL and saves it in `localStorage`
|
|
83
|
-
3. As the visitor browses, attribution stays in their browser
|
|
84
|
-
4. At checkout, `getAttribution()` reads localStorage and returns the attribution object
|
|
85
|
-
5. Your server passes it as Stripe metadata when creating the checkout session
|
|
86
|
-
6. When Stripe fires the `checkout.session.completed` webhook, Affiliateo reads the metadata and credits the affiliate
|
|
87
|
-
|
|
88
|
-
## Why `localStorage` and not cookies?
|
|
89
|
-
|
|
90
|
-
- **Not blocked by Safari ITP or most privacy tools**
|
|
91
|
-
- **No CSRF concerns**
|
|
92
|
-
|
|
93
|
-
## GDPR / ePrivacy. merchant responsibility
|
|
94
|
-
|
|
95
|
-
The SDK writes persistent identifiers (`affiliateo_visitor_id`, `affiliateo_ref`, `affiliateo_campaign_id`) to the visitor's `localStorage` and sends pageview events that include the visitor's IP, user agent, and approximate geolocation to Affiliateo's servers. Under EU ePrivacy (Article 5(3)) and GDPR, persistent identifiers stored on a visitor's device require prior consent the same way cookies do.
|
|
96
|
-
|
|
97
|
-
If you serve visitors in the EU/EEA, UK, or any jurisdiction with equivalent rules, **you are responsible for obtaining consent before this script loads**, the same way you would for Google Analytics, Meta Pixel, or any other analytics tool. Typical approaches:
|
|
98
|
-
|
|
99
|
-
- Gate the `<AffiliateoProvider>` (or the `<script src="…/t.js">` tag) behind your cookie banner's "accept" callback so it only loads after consent.
|
|
100
|
-
- Document Affiliateo in your privacy policy as a processor (see our [Data Processing Agreement](https://affiliateo.com/dpa)) and list the categories of data collected.
|
|
101
|
-
|
|
102
|
-
Affiliateo acts as your data processor for visitor analytics. You are the controller and decide the lawful basis for processing your visitors' data.
|
|
103
|
-
|
|
104
|
-
## Alternative: vanilla HTML (no React)
|
|
105
|
-
|
|
106
|
-
If you're not using React, use the tracking script directly instead of the provider:
|
|
107
|
-
|
|
108
|
-
```html
|
|
109
|
-
<script defer
|
|
110
|
-
data-
|
|
111
|
-
src="https://affiliateo.com/t.js"
|
|
112
|
-
></script>
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
Then in your checkout flow (vanilla JS):
|
|
116
|
-
|
|
117
|
-
```js
|
|
118
|
-
const attribution = {
|
|
119
|
-
affiliateo_ref: localStorage.getItem('affiliateo_ref'),
|
|
120
|
-
affiliateo_visitor_id: localStorage.getItem('affiliateo_visitor_id'),
|
|
121
|
-
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
// Send to your server with the rest of your checkout payload
|
|
125
|
-
fetch('/api/checkout', {
|
|
126
|
-
method: 'POST',
|
|
127
|
-
body: JSON.stringify({ attribution }),
|
|
128
|
-
});
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
## Track conversion goals
|
|
132
|
-
|
|
133
|
-
Pageviews auto-track. For specific moments that matter to your funnel
|
|
134
|
-
(`signup_completed`, `trial_started`, `initiate_checkout`, etc.), use
|
|
135
|
-
`track()` or HTML data attributes.
|
|
136
|
-
|
|
137
|
-
### From React
|
|
138
|
-
|
|
139
|
-
```tsx
|
|
140
|
-
'use client'
|
|
141
|
-
import { useEffect } from 'react'
|
|
142
|
-
import { useAffiliateRef } from '@affiliateo/web'
|
|
143
|
-
|
|
144
|
-
export default function SignupSuccess() {
|
|
145
|
-
const { track } = useAffiliateRef()
|
|
146
|
-
useEffect(() => {
|
|
147
|
-
track('signup_completed', { plan: 'free' })
|
|
148
|
-
}, [])
|
|
149
|
-
return <div>Welcome!</div>
|
|
150
|
-
}
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
### HTML data attributes (zero JS, works everywhere)
|
|
154
|
-
|
|
155
|
-
```html
|
|
156
|
-
<button data-affiliateo-goal="initiate_checkout"
|
|
157
|
-
data-affiliateo-goal-price="49"
|
|
158
|
-
data-affiliateo-goal-plan="pro">
|
|
159
|
-
Subscribe
|
|
160
|
-
</button>
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
The SDK auto-fires the goal on click. Optional `data-affiliateo-goal-*`
|
|
164
|
-
attributes become metadata (kebab-case → snake_case, up to 10 params).
|
|
165
|
-
|
|
166
|
-
### Scroll-depth tracking
|
|
167
|
-
|
|
168
|
-
```html
|
|
169
|
-
<section data-affiliateo-scroll="pricing_viewed">
|
|
170
|
-
...your pricing content...
|
|
171
|
-
</section>
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
Fires the first time any pixel of the section overlaps the viewport.
|
|
175
|
-
Maximum permissiveness — if the user scrolled enough to see even a sliver,
|
|
176
|
-
it counts. Earlier versions required 50% visibility, which was unreachable
|
|
177
|
-
for tall sections (a pricing page with comparison tables silently dropped
|
|
178
|
-
out of every funnel). Great for landing pages where you want to know
|
|
179
|
-
which sections convert.
|
|
180
|
-
|
|
181
|
-
### Goal naming rules
|
|
182
|
-
|
|
183
|
-
Lowercase letters, digits, underscores, hyphens, colons. Max 64 chars.
|
|
184
|
-
Metadata bounded to 4KB JSON.
|
|
185
|
-
|
|
186
|
-
## Identify signed-in users (cross-device stitching)
|
|
187
|
-
|
|
188
|
-
Without `identify()`, the same person browsing on phone and buying on
|
|
189
|
-
laptop counts as two visitors. Call it once after sign-in:
|
|
190
|
-
|
|
191
|
-
```tsx
|
|
192
|
-
'use client'
|
|
193
|
-
import { useEffect } from 'react'
|
|
194
|
-
import { useAffiliateRef } from '@affiliateo/web'
|
|
195
|
-
import { useUser } from './your-auth'
|
|
196
|
-
|
|
197
|
-
export default function AuthBoundary({ children }) {
|
|
198
|
-
const { identify } = useAffiliateRef()
|
|
199
|
-
const user = useUser()
|
|
200
|
-
useEffect(() => {
|
|
201
|
-
if (user) identify(user.id)
|
|
202
|
-
}, [user])
|
|
203
|
-
return <>{children}</>
|
|
204
|
-
}
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
Idempotent. safe to fire on every page load while a user is signed in.
|
|
208
|
-
**user_id only.** The SDK does not accept, collect, or transmit email
|
|
209
|
-
or any other PII.
|
|
210
|
-
|
|
211
|
-
## API
|
|
212
|
-
|
|
213
|
-
### `<AffiliateoProvider
|
|
214
|
-
|
|
215
|
-
React provider. Wrap your app once. Reads `?ref=` from URL, persists to localStorage, sends pageview events, sets up data-attribute + scroll listeners.
|
|
216
|
-
|
|
217
|
-
Props:
|
|
218
|
-
- `
|
|
219
|
-
- `apiUrl` (optional): defaults to `https://affiliateo.com`
|
|
220
|
-
|
|
221
|
-
### `useAffiliateRef()`
|
|
222
|
-
|
|
223
|
-
React hook that returns `{ refCode, visitorId, track, identify }`.
|
|
224
|
-
|
|
225
|
-
- `track(eventName, metadata?)`. fire a custom goal
|
|
226
|
-
- `identify(userId)`. link this visitor to a merchant user_id (no PII)
|
|
227
|
-
|
|
228
|
-
### `getAttribution()`
|
|
229
|
-
|
|
230
|
-
Client-side function. Returns an object with `affiliateo_ref`, `affiliateo_visitor_id`, and `
|
|
231
|
-
|
|
232
|
-
## Framework support
|
|
233
|
-
|
|
234
|
-
Works with any JavaScript framework or vanilla JS:
|
|
235
|
-
- **Next.js** (App Router & Pages Router)
|
|
236
|
-
- **Remix** / **React Router**
|
|
237
|
-
- **Vite + React**
|
|
238
|
-
- **Vanilla JS/HTML** (use the `<script>` tag instead)
|
|
1
|
+
# @affiliateo/web
|
|
2
|
+
|
|
3
|
+
Affiliateo web SDK. track affiliate visits and pass attribution metadata to your Stripe checkout.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @affiliateo/web
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
1. Connect your Stripe account on the Affiliateo dashboard
|
|
14
|
+
2. Wrap your app in `<AffiliateoProvider>` with your app ID
|
|
15
|
+
3. At checkout time, call `getAttribution()` and pass the result to Stripe metadata
|
|
16
|
+
|
|
17
|
+
### Step 1. wrap your app
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
// app/layout.tsx (Next.js App Router) or _app.tsx (Pages Router)
|
|
21
|
+
import { AffiliateoProvider } from '@affiliateo/web';
|
|
22
|
+
|
|
23
|
+
export default function RootLayout({ children }) {
|
|
24
|
+
return (
|
|
25
|
+
<AffiliateoProvider appId="YOUR_APP_ID">
|
|
26
|
+
{children}
|
|
27
|
+
</AffiliateoProvider>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The provider:
|
|
33
|
+
- Reads `?ref=` from the URL when a visitor arrives
|
|
34
|
+
- Stores attribution in `localStorage` so it survives navigation
|
|
35
|
+
- Sends pageview events to Affiliateo for click tracking
|
|
36
|
+
|
|
37
|
+
### Step 2. in your checkout button (client-side)
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
'use client'
|
|
41
|
+
import { getAttribution } from '@affiliateo/web';
|
|
42
|
+
|
|
43
|
+
async function handleCheckout() {
|
|
44
|
+
const attribution = getAttribution();
|
|
45
|
+
|
|
46
|
+
const res = await fetch('/api/checkout', {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
body: JSON.stringify({ attribution }),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const { url } = await res.json();
|
|
52
|
+
window.location.href = url;
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`getAttribution()` reads the stored attribution from `localStorage` and returns `affiliateo_ref`, `affiliateo_visitor_id`, and `affiliateo_app_id` as an object, ready to pass to Stripe.
|
|
57
|
+
|
|
58
|
+
### Step 3. in your checkout API route (server-side)
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// app/api/checkout/route.ts
|
|
62
|
+
export async function POST(req) {
|
|
63
|
+
const { attribution } = await req.json();
|
|
64
|
+
|
|
65
|
+
const session = await stripe.checkout.sessions.create({
|
|
66
|
+
line_items: [{ price: 'price_xxx', quantity: 1 }],
|
|
67
|
+
mode: 'payment',
|
|
68
|
+
success_url: 'https://yoursite.com/success',
|
|
69
|
+
cancel_url: 'https://yoursite.com/cancel',
|
|
70
|
+
metadata: attribution,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return Response.json({ url: session.url });
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
That's it. Affiliateo's webhook will automatically read the metadata and credit the right affiliate when the payment succeeds.
|
|
78
|
+
|
|
79
|
+
## How it works
|
|
80
|
+
|
|
81
|
+
1. Visitor clicks affiliate link → `yoursite.com?ref=pinkproton`
|
|
82
|
+
2. `<AffiliateoProvider>` reads `?ref=pinkproton` from the URL and saves it in `localStorage`
|
|
83
|
+
3. As the visitor browses, attribution stays in their browser
|
|
84
|
+
4. At checkout, `getAttribution()` reads localStorage and returns the attribution object
|
|
85
|
+
5. Your server passes it as Stripe metadata when creating the checkout session
|
|
86
|
+
6. When Stripe fires the `checkout.session.completed` webhook, Affiliateo reads the metadata and credits the affiliate
|
|
87
|
+
|
|
88
|
+
## Why `localStorage` and not cookies?
|
|
89
|
+
|
|
90
|
+
- **Not blocked by Safari ITP or most privacy tools**
|
|
91
|
+
- **No CSRF concerns**
|
|
92
|
+
|
|
93
|
+
## GDPR / ePrivacy. merchant responsibility
|
|
94
|
+
|
|
95
|
+
The SDK writes persistent identifiers (`affiliateo_visitor_id`, `affiliateo_ref`, `affiliateo_campaign_id`) to the visitor's `localStorage` and sends pageview events that include the visitor's IP, user agent, and approximate geolocation to Affiliateo's servers. Under EU ePrivacy (Article 5(3)) and GDPR, persistent identifiers stored on a visitor's device require prior consent the same way cookies do.
|
|
96
|
+
|
|
97
|
+
If you serve visitors in the EU/EEA, UK, or any jurisdiction with equivalent rules, **you are responsible for obtaining consent before this script loads**, the same way you would for Google Analytics, Meta Pixel, or any other analytics tool. Typical approaches:
|
|
98
|
+
|
|
99
|
+
- Gate the `<AffiliateoProvider>` (or the `<script src="…/t.js">` tag) behind your cookie banner's "accept" callback so it only loads after consent.
|
|
100
|
+
- Document Affiliateo in your privacy policy as a processor (see our [Data Processing Agreement](https://affiliateo.com/dpa)) and list the categories of data collected.
|
|
101
|
+
|
|
102
|
+
Affiliateo acts as your data processor for visitor analytics. You are the controller and decide the lawful basis for processing your visitors' data.
|
|
103
|
+
|
|
104
|
+
## Alternative: vanilla HTML (no React)
|
|
105
|
+
|
|
106
|
+
If you're not using React, use the tracking script directly instead of the provider:
|
|
107
|
+
|
|
108
|
+
```html
|
|
109
|
+
<script defer
|
|
110
|
+
data-app="YOUR_APP_ID"
|
|
111
|
+
src="https://affiliateo.com/t.js"
|
|
112
|
+
></script>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Then in your checkout flow (vanilla JS):
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
const attribution = {
|
|
119
|
+
affiliateo_ref: localStorage.getItem('affiliateo_ref'),
|
|
120
|
+
affiliateo_visitor_id: localStorage.getItem('affiliateo_visitor_id'),
|
|
121
|
+
affiliateo_app_id: localStorage.getItem('affiliateo_campaign_id'), // stored under its legacy key
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Send to your server with the rest of your checkout payload
|
|
125
|
+
fetch('/api/checkout', {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
body: JSON.stringify({ attribution }),
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Track conversion goals
|
|
132
|
+
|
|
133
|
+
Pageviews auto-track. For specific moments that matter to your funnel
|
|
134
|
+
(`signup_completed`, `trial_started`, `initiate_checkout`, etc.), use
|
|
135
|
+
`track()` or HTML data attributes.
|
|
136
|
+
|
|
137
|
+
### From React
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
'use client'
|
|
141
|
+
import { useEffect } from 'react'
|
|
142
|
+
import { useAffiliateRef } from '@affiliateo/web'
|
|
143
|
+
|
|
144
|
+
export default function SignupSuccess() {
|
|
145
|
+
const { track } = useAffiliateRef()
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
track('signup_completed', { plan: 'free' })
|
|
148
|
+
}, [])
|
|
149
|
+
return <div>Welcome!</div>
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### HTML data attributes (zero JS, works everywhere)
|
|
154
|
+
|
|
155
|
+
```html
|
|
156
|
+
<button data-affiliateo-goal="initiate_checkout"
|
|
157
|
+
data-affiliateo-goal-price="49"
|
|
158
|
+
data-affiliateo-goal-plan="pro">
|
|
159
|
+
Subscribe
|
|
160
|
+
</button>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The SDK auto-fires the goal on click. Optional `data-affiliateo-goal-*`
|
|
164
|
+
attributes become metadata (kebab-case → snake_case, up to 10 params).
|
|
165
|
+
|
|
166
|
+
### Scroll-depth tracking
|
|
167
|
+
|
|
168
|
+
```html
|
|
169
|
+
<section data-affiliateo-scroll="pricing_viewed">
|
|
170
|
+
...your pricing content...
|
|
171
|
+
</section>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Fires the first time any pixel of the section overlaps the viewport.
|
|
175
|
+
Maximum permissiveness — if the user scrolled enough to see even a sliver,
|
|
176
|
+
it counts. Earlier versions required 50% visibility, which was unreachable
|
|
177
|
+
for tall sections (a pricing page with comparison tables silently dropped
|
|
178
|
+
out of every funnel). Great for landing pages where you want to know
|
|
179
|
+
which sections convert.
|
|
180
|
+
|
|
181
|
+
### Goal naming rules
|
|
182
|
+
|
|
183
|
+
Lowercase letters, digits, underscores, hyphens, colons. Max 64 chars.
|
|
184
|
+
Metadata bounded to 4KB JSON.
|
|
185
|
+
|
|
186
|
+
## Identify signed-in users (cross-device stitching)
|
|
187
|
+
|
|
188
|
+
Without `identify()`, the same person browsing on phone and buying on
|
|
189
|
+
laptop counts as two visitors. Call it once after sign-in:
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
'use client'
|
|
193
|
+
import { useEffect } from 'react'
|
|
194
|
+
import { useAffiliateRef } from '@affiliateo/web'
|
|
195
|
+
import { useUser } from './your-auth'
|
|
196
|
+
|
|
197
|
+
export default function AuthBoundary({ children }) {
|
|
198
|
+
const { identify } = useAffiliateRef()
|
|
199
|
+
const user = useUser()
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
if (user) identify(user.id)
|
|
202
|
+
}, [user])
|
|
203
|
+
return <>{children}</>
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Idempotent. safe to fire on every page load while a user is signed in.
|
|
208
|
+
**user_id only.** The SDK does not accept, collect, or transmit email
|
|
209
|
+
or any other PII.
|
|
210
|
+
|
|
211
|
+
## API
|
|
212
|
+
|
|
213
|
+
### `<AffiliateoProvider appId="..." apiUrl?="...">`
|
|
214
|
+
|
|
215
|
+
React provider. Wrap your app once. Reads `?ref=` from URL, persists to localStorage, sends pageview events, sets up data-attribute + scroll listeners.
|
|
216
|
+
|
|
217
|
+
Props:
|
|
218
|
+
- `appId` (required): your Affiliateo app ID (`campaignId` still works as a deprecated alias)
|
|
219
|
+
- `apiUrl` (optional): defaults to `https://affiliateo.com`
|
|
220
|
+
|
|
221
|
+
### `useAffiliateRef()`
|
|
222
|
+
|
|
223
|
+
React hook that returns `{ refCode, visitorId, track, identify }`.
|
|
224
|
+
|
|
225
|
+
- `track(eventName, metadata?)`. fire a custom goal
|
|
226
|
+
- `identify(userId)`. link this visitor to a merchant user_id (no PII)
|
|
227
|
+
|
|
228
|
+
### `getAttribution()`
|
|
229
|
+
|
|
230
|
+
Client-side function. Returns an object with `affiliateo_ref`, `affiliateo_visitor_id`, and `affiliateo_app_id` from localStorage. Pass directly as Stripe `metadata`.
|
|
231
|
+
|
|
232
|
+
## Framework support
|
|
233
|
+
|
|
234
|
+
Works with any JavaScript framework or vanilla JS:
|
|
235
|
+
- **Next.js** (App Router & Pages Router)
|
|
236
|
+
- **Remix** / **React Router**
|
|
237
|
+
- **Vite + React**
|
|
238
|
+
- **Vanilla JS/HTML** (use the `<script>` tag instead)
|
package/dist/provider.d.ts
CHANGED
|
@@ -18,6 +18,6 @@ export declare function useAffiliateRef(): AffiliateoContextValue;
|
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
20
|
export declare function getAttribution(): Record<string, string>;
|
|
21
|
-
export declare function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allowLocalhost, allowIframe, allowFileProtocol, debug, flushInterval, maxQueueSize, autoCapturePageviews, children, }: AffiliateoConfig & {
|
|
21
|
+
export declare function AffiliateoProvider({ appId, campaignId, apiUrl, allowedHostnames, allowLocalhost, allowIframe, allowFileProtocol, debug, flushInterval, maxQueueSize, autoCapturePageviews, children, }: AffiliateoConfig & {
|
|
22
22
|
children: React.ReactNode;
|
|
23
23
|
}): import("react/jsx-runtime").JSX.Element;
|
package/dist/provider.js
CHANGED
|
@@ -4,10 +4,14 @@ import { createContext, useCallback, useContext, useEffect, useRef, useState } f
|
|
|
4
4
|
import { EventQueue } from './queue';
|
|
5
5
|
import { isLikelyBot } from './bot-detect';
|
|
6
6
|
import { isDevHost } from './dev-host';
|
|
7
|
+
import { mintTurnstileToken, consumeTurnstileToken } from './turnstile';
|
|
7
8
|
const DEFAULT_API_URL = 'https://affiliateo.com';
|
|
8
9
|
const LS_VISITOR_ID = 'affiliateo_visitor_id';
|
|
9
10
|
const LS_REF = 'affiliateo_ref';
|
|
10
|
-
|
|
11
|
+
// Storage key keeps its historical name: t.js and the WooCommerce tracker
|
|
12
|
+
// write the same key, and renaming it would orphan existing visitors'
|
|
13
|
+
// attribution mid-session.
|
|
14
|
+
const LS_APP_ID = 'affiliateo_campaign_id';
|
|
11
15
|
// Persistent opt-out flag (affiliateo_opt_out) —
|
|
12
16
|
// when set to "true" the SDK silently noops every outbound call. Survives
|
|
13
17
|
// page reloads. removed by optIn(). Checked at init time so a previously
|
|
@@ -149,13 +153,15 @@ export function getAttribution() {
|
|
|
149
153
|
const data = {};
|
|
150
154
|
const vid = lsGet(LS_VISITOR_ID);
|
|
151
155
|
const ref = lsGet(LS_REF);
|
|
152
|
-
const cid = lsGet(
|
|
156
|
+
const cid = lsGet(LS_APP_ID);
|
|
153
157
|
if (vid)
|
|
154
158
|
data.affiliateo_visitor_id = vid;
|
|
155
159
|
if (ref)
|
|
156
160
|
data.affiliateo_ref = ref;
|
|
161
|
+
// Key renamed from affiliateo_campaign_id in 4.5.0 — the Affiliateo
|
|
162
|
+
// webhook accepts both, so checkouts created by older SDKs keep working.
|
|
157
163
|
if (cid)
|
|
158
|
-
data.
|
|
164
|
+
data.affiliateo_app_id = cid;
|
|
159
165
|
return data;
|
|
160
166
|
}
|
|
161
167
|
// Double-mount detection. The host app should mount AffiliateoProvider
|
|
@@ -165,7 +171,7 @@ export function getAttribution() {
|
|
|
165
171
|
// mount (host has the call sites) but we can shout in dev so the
|
|
166
172
|
// mistake is visible.
|
|
167
173
|
let providerMountCount = 0;
|
|
168
|
-
export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allowLocalhost, allowIframe, allowFileProtocol, debug, flushInterval, maxQueueSize, autoCapturePageviews, children, }) {
|
|
174
|
+
export function AffiliateoProvider({ appId, campaignId, apiUrl, allowedHostnames, allowLocalhost, allowIframe, allowFileProtocol, debug, flushInterval, maxQueueSize, autoCapturePageviews, children, }) {
|
|
169
175
|
// Normalize autoCapturePageviews. Three shapes:
|
|
170
176
|
// - undefined / true / object → auto-capture ON (with object overrides)
|
|
171
177
|
// - false → auto-capture OFF entirely (host must call trackPageview)
|
|
@@ -211,12 +217,15 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
211
217
|
console.log('[Affiliateo] ' + msg);
|
|
212
218
|
}, [debug]);
|
|
213
219
|
const apiBase = (apiUrl || DEFAULT_API_URL).replace(/\/$/, '');
|
|
214
|
-
//
|
|
220
|
+
// appId is the documented prop; campaignId is the pre-4.5.0 name kept as
|
|
221
|
+
// a working alias so upgrading the package never breaks an integration.
|
|
222
|
+
const resolvedAppId = appId ?? campaignId ?? '';
|
|
223
|
+
// appIdRef stays current so callbacks defined inside useEffect
|
|
215
224
|
// always read the latest value, even after a prop change. Closures
|
|
216
225
|
// captured by host-side `useRef` no longer drift to a stale id.
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
// Mint visitorId once and put it in a ref so it survives
|
|
226
|
+
const appIdRef = useRef(resolvedAppId);
|
|
227
|
+
appIdRef.current = resolvedAppId;
|
|
228
|
+
// Mint visitorId once and put it in a ref so it survives appId
|
|
220
229
|
// prop changes without forcing a re-fetch / re-mint cycle. Same UUID
|
|
221
230
|
// for the lifetime of the localStorage entry across the whole tab.
|
|
222
231
|
const visitorIdRef = useRef(null);
|
|
@@ -230,7 +239,7 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
230
239
|
// handlers, which fire after mount.
|
|
231
240
|
const queueRef = useRef(null);
|
|
232
241
|
// track + identify are defined OUTSIDE the route-effect so they
|
|
233
|
-
// close over refs (
|
|
242
|
+
// close over refs (appIdRef, visitorIdRef) rather than the
|
|
234
243
|
// captured-once values from inside the effect. A consumer caching
|
|
235
244
|
// `track` keeps firing under the LATEST (campaignId, visitorId).
|
|
236
245
|
const track = useCallback(async (eventName, metadata) => {
|
|
@@ -246,7 +255,7 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
246
255
|
const name = eventName.trim().toLowerCase();
|
|
247
256
|
if (!name || name.length > 64 || !/^[a-z0-9_:-]+$/.test(name))
|
|
248
257
|
return;
|
|
249
|
-
const cid =
|
|
258
|
+
const cid = appIdRef.current;
|
|
250
259
|
const vid = visitorIdRef.current;
|
|
251
260
|
if (!cid || !vid)
|
|
252
261
|
return;
|
|
@@ -285,7 +294,7 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
285
294
|
// firebase uid, supabase uuid.
|
|
286
295
|
if (!uid || uid.length > 128)
|
|
287
296
|
return;
|
|
288
|
-
const cid =
|
|
297
|
+
const cid = appIdRef.current;
|
|
289
298
|
const vid = visitorIdRef.current;
|
|
290
299
|
if (!cid || !vid)
|
|
291
300
|
return;
|
|
@@ -315,7 +324,7 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
315
324
|
return;
|
|
316
325
|
if (isOptedOut())
|
|
317
326
|
return;
|
|
318
|
-
const cid =
|
|
327
|
+
const cid = appIdRef.current;
|
|
319
328
|
const vid = visitorIdRef.current;
|
|
320
329
|
if (!cid || !vid)
|
|
321
330
|
return;
|
|
@@ -347,6 +356,12 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
347
356
|
gclid: params.get('gclid') || '',
|
|
348
357
|
screen_width: window.innerWidth,
|
|
349
358
|
};
|
|
359
|
+
// Attach the (single-use) Turnstile token if one is still unconsumed.
|
|
360
|
+
// Matters for consent flows where the host disabled auto-capture and
|
|
361
|
+
// this manual call is the session-opening event.
|
|
362
|
+
const tsToken = consumeTurnstileToken();
|
|
363
|
+
if (tsToken)
|
|
364
|
+
payload.cf_turnstile_token = tsToken;
|
|
350
365
|
queueRef.current?.enqueue(`${apiBase}/api/v1/web/event`, payload);
|
|
351
366
|
log('pageview (manual)', { path: pagePath });
|
|
352
367
|
}
|
|
@@ -425,6 +440,13 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
425
440
|
useEffect(() => {
|
|
426
441
|
if (typeof window === 'undefined')
|
|
427
442
|
return;
|
|
443
|
+
// Neither appId nor the legacy campaignId alias was passed — nothing to
|
|
444
|
+
// attribute events to, so stay dormant instead of firing junk.
|
|
445
|
+
if (!appIdRef.current) {
|
|
446
|
+
// eslint-disable-next-line no-console
|
|
447
|
+
console.error('[Affiliateo] Missing appId — pass your app ID to <AffiliateoProvider appId="...">.');
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
428
450
|
// Mount-count warning. Fires only when more than one Provider has
|
|
429
451
|
// ever mounted in the page lifetime. Dev-only; production swallows
|
|
430
452
|
// the warning to avoid noise in real merchant consoles.
|
|
@@ -613,7 +635,7 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
613
635
|
vid = uuid();
|
|
614
636
|
}
|
|
615
637
|
lsSet(LS_VISITOR_ID, vid);
|
|
616
|
-
lsSet(
|
|
638
|
+
lsSet(LS_APP_ID, appIdRef.current);
|
|
617
639
|
visitorIdRef.current = vid;
|
|
618
640
|
// Read ref. Cross-domain inbound wins, then fresh affiliate link
|
|
619
641
|
// params (?ref / ?via / ?source), then stored value.
|
|
@@ -628,7 +650,7 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
628
650
|
visitorId: vid,
|
|
629
651
|
track, identify, trackPageview, reset, optOut, optIn, flush,
|
|
630
652
|
});
|
|
631
|
-
log('init', {
|
|
653
|
+
log('init', { app: appIdRef.current, visitor: vid, ref: ref || null });
|
|
632
654
|
// lastSendTime drives the visibility-refire heuristic below (refire a
|
|
633
655
|
// pageview when a tab returns after the session window). Duplicate
|
|
634
656
|
// suppression itself is the persisted same-URL throttle inside
|
|
@@ -659,9 +681,14 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
659
681
|
ssSet(SS_PV_THROTTLE, JSON.stringify({ url: href, time: now }));
|
|
660
682
|
lastSendTime = now;
|
|
661
683
|
ended = false;
|
|
684
|
+
// Attach the (single-use) Turnstile token to the session-opening
|
|
685
|
+
// pageview. consumeTurnstileToken() returns null after the first
|
|
686
|
+
// consume, so SPA navigations go out bare — the server trusts them
|
|
687
|
+
// because the visitor row was created behind the verified token.
|
|
688
|
+
const tsToken = consumeTurnstileToken();
|
|
662
689
|
queue.enqueue(endpoint, {
|
|
663
690
|
type: 'pageview',
|
|
664
|
-
campaign_id:
|
|
691
|
+
campaign_id: appIdRef.current,
|
|
665
692
|
visitor_id: vid,
|
|
666
693
|
session_id: getSessionId(),
|
|
667
694
|
url: href,
|
|
@@ -677,6 +704,7 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
677
704
|
ttclid: params.get('ttclid') || '',
|
|
678
705
|
gclid: params.get('gclid') || '',
|
|
679
706
|
screen_width: window.innerWidth,
|
|
707
|
+
...(tsToken ? { cf_turnstile_token: tsToken } : {}),
|
|
680
708
|
});
|
|
681
709
|
log('pageview', { path: window.location.pathname });
|
|
682
710
|
}
|
|
@@ -702,7 +730,7 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
702
730
|
queue.flushOnUnload();
|
|
703
731
|
const data = JSON.stringify({
|
|
704
732
|
type: 'session_end',
|
|
705
|
-
campaign_id:
|
|
733
|
+
campaign_id: appIdRef.current,
|
|
706
734
|
visitor_id: vid,
|
|
707
735
|
ref: ref || lsGet(LS_REF) || '',
|
|
708
736
|
});
|
|
@@ -746,13 +774,30 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
746
774
|
}
|
|
747
775
|
}
|
|
748
776
|
}
|
|
749
|
-
//
|
|
750
|
-
//
|
|
751
|
-
//
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
777
|
+
// Kick off the Turnstile mint regardless of auto-capture config so a
|
|
778
|
+
// consent flow's later manual trackPageview() finds a token waiting.
|
|
779
|
+
// Never rejects; resolves with or without a token (see turnstile.ts).
|
|
780
|
+
let cancelled = false;
|
|
781
|
+
void mintTurnstileToken().then(() => {
|
|
782
|
+
// Send pageview on mount, deferred until the Turnstile mint settles so
|
|
783
|
+
// the session-opening event carries the token (SPA navigations reuse
|
|
784
|
+
// the resolved state and don't wait). captureInitialPageview === false
|
|
785
|
+
// skips this for cookie-consent flows where the host wants to defer
|
|
786
|
+
// until the user accepts (then call trackPageview() manually).
|
|
787
|
+
//
|
|
788
|
+
// sentRef is set HERE (not before the mint) so a StrictMode
|
|
789
|
+
// double-mount can't mark the pageview as sent and then tear down the
|
|
790
|
+
// queue before the deferred send ever ran — the surviving mount's
|
|
791
|
+
// callback sees sentRef still false and fires.
|
|
792
|
+
if (cancelled)
|
|
793
|
+
return;
|
|
794
|
+
if (sentRef.current)
|
|
795
|
+
return;
|
|
796
|
+
if (autoCfg.captureInitialPageview && autoCfg.enabled) {
|
|
797
|
+
sentRef.current = true;
|
|
798
|
+
sendPageview();
|
|
799
|
+
}
|
|
800
|
+
});
|
|
756
801
|
// Detect user leaving
|
|
757
802
|
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
758
803
|
window.addEventListener('pagehide', sendEnd);
|
|
@@ -884,6 +929,7 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allow
|
|
|
884
929
|
document.addEventListener('keydown', onKeydownForLinkDecoration, true);
|
|
885
930
|
}
|
|
886
931
|
return () => {
|
|
932
|
+
cancelled = true;
|
|
887
933
|
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
888
934
|
window.removeEventListener('pagehide', sendEnd);
|
|
889
935
|
window.removeEventListener('popstate', onUrlChange);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Turnstile token minting. Mirrors the hosted t.js tracker:
|
|
3
|
+
* mint ONE invisible token at SDK init and attach it to the first pageview.
|
|
4
|
+
* The server verifies the token before creating the visitor row (and before
|
|
5
|
+
* counting a pay-per-click web_traffic click — the billable path REQUIRES a
|
|
6
|
+
* genuine pass when TURNSTILE_SECRET is configured). Subsequent events from
|
|
7
|
+
* the same visitor_id are trusted because the row was created behind a
|
|
8
|
+
* verified token, so the cost is one mint + one verify per session.
|
|
9
|
+
*
|
|
10
|
+
* Tokens are single-use at Cloudflare's siteverify, so consumeToken()
|
|
11
|
+
* hands the token out exactly once — re-sending a used token would get the
|
|
12
|
+
* event rejected server-side, not warn-allowed.
|
|
13
|
+
*
|
|
14
|
+
* Failure modes: if the Turnstile script is blocked (ad blockers, strict
|
|
15
|
+
* CSP), errors, or times out, minting resolves with no token and events go
|
|
16
|
+
* out bare. The server's warn-allow mode (TURNSTILE_REQUIRE unset) lets
|
|
17
|
+
* those through; commission-on-sale (web_affiliate) tracking never depends
|
|
18
|
+
* on the token at all.
|
|
19
|
+
*/
|
|
20
|
+
interface TurnstileApi {
|
|
21
|
+
render: (el: HTMLElement, opts: Record<string, unknown>) => unknown;
|
|
22
|
+
}
|
|
23
|
+
declare global {
|
|
24
|
+
interface Window {
|
|
25
|
+
turnstile?: TurnstileApi;
|
|
26
|
+
__affiliateoTSLoaded?: () => void;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Load the Turnstile script (once) and run the invisible challenge. Resolves
|
|
31
|
+
* when a token is available OR the mint failed/timed out — never rejects, so
|
|
32
|
+
* callers can unconditionally `.then(sendPageview)`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function mintTurnstileToken(): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Hand out the minted token exactly once (single-use at siteverify).
|
|
37
|
+
* Returns null when no token was minted or it was already consumed.
|
|
38
|
+
*/
|
|
39
|
+
export declare function consumeTurnstileToken(): string | null;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Turnstile token minting. Mirrors the hosted t.js tracker:
|
|
3
|
+
* mint ONE invisible token at SDK init and attach it to the first pageview.
|
|
4
|
+
* The server verifies the token before creating the visitor row (and before
|
|
5
|
+
* counting a pay-per-click web_traffic click — the billable path REQUIRES a
|
|
6
|
+
* genuine pass when TURNSTILE_SECRET is configured). Subsequent events from
|
|
7
|
+
* the same visitor_id are trusted because the row was created behind a
|
|
8
|
+
* verified token, so the cost is one mint + one verify per session.
|
|
9
|
+
*
|
|
10
|
+
* Tokens are single-use at Cloudflare's siteverify, so consumeToken()
|
|
11
|
+
* hands the token out exactly once — re-sending a used token would get the
|
|
12
|
+
* event rejected server-side, not warn-allowed.
|
|
13
|
+
*
|
|
14
|
+
* Failure modes: if the Turnstile script is blocked (ad blockers, strict
|
|
15
|
+
* CSP), errors, or times out, minting resolves with no token and events go
|
|
16
|
+
* out bare. The server's warn-allow mode (TURNSTILE_REQUIRE unset) lets
|
|
17
|
+
* those through; commission-on-sale (web_affiliate) tracking never depends
|
|
18
|
+
* on the token at all.
|
|
19
|
+
*/
|
|
20
|
+
// Public sitekey (safe to expose) — the same one public/t.js ships. The
|
|
21
|
+
// matching secret lives in TURNSTILE_SECRET on the server.
|
|
22
|
+
const TURNSTILE_SITEKEY = '0x4AAAAAADR5UW2x6axFd13p';
|
|
23
|
+
const TURNSTILE_SCRIPT_SRC = 'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=__affiliateoTSLoaded&render=explicit';
|
|
24
|
+
// Hard deadline for the whole mint (script load + challenge). t.js relies on
|
|
25
|
+
// Turnstile's own timeout-callback, but a script tag that never loads never
|
|
26
|
+
// fires ANY callback — this timer makes sure the first pageview can't be
|
|
27
|
+
// held hostage by a blocked challenges.cloudflare.com.
|
|
28
|
+
const MINT_TIMEOUT_MS = 3000;
|
|
29
|
+
let token = null;
|
|
30
|
+
let minting = null;
|
|
31
|
+
/**
|
|
32
|
+
* Load the Turnstile script (once) and run the invisible challenge. Resolves
|
|
33
|
+
* when a token is available OR the mint failed/timed out — never rejects, so
|
|
34
|
+
* callers can unconditionally `.then(sendPageview)`.
|
|
35
|
+
*/
|
|
36
|
+
export function mintTurnstileToken() {
|
|
37
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
38
|
+
return Promise.resolve();
|
|
39
|
+
}
|
|
40
|
+
if (minting)
|
|
41
|
+
return minting;
|
|
42
|
+
minting = new Promise((resolve) => {
|
|
43
|
+
let settled = false;
|
|
44
|
+
const settle = () => {
|
|
45
|
+
if (settled)
|
|
46
|
+
return;
|
|
47
|
+
settled = true;
|
|
48
|
+
clearTimeout(deadline);
|
|
49
|
+
resolve();
|
|
50
|
+
};
|
|
51
|
+
const deadline = setTimeout(settle, MINT_TIMEOUT_MS);
|
|
52
|
+
const render = () => {
|
|
53
|
+
if (!window.turnstile) {
|
|
54
|
+
settle();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const holder = document.createElement('div');
|
|
59
|
+
holder.style.display = 'none';
|
|
60
|
+
document.body.appendChild(holder);
|
|
61
|
+
window.turnstile.render(holder, {
|
|
62
|
+
sitekey: TURNSTILE_SITEKEY,
|
|
63
|
+
// Invisible widget — never shows UI. Real browsers pass the
|
|
64
|
+
// challenge in 50-200ms with zero user interaction.
|
|
65
|
+
size: 'invisible',
|
|
66
|
+
callback: (tok) => {
|
|
67
|
+
token = tok;
|
|
68
|
+
settle();
|
|
69
|
+
},
|
|
70
|
+
'error-callback': settle,
|
|
71
|
+
'timeout-callback': settle,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
settle();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
if (window.turnstile) {
|
|
79
|
+
render();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const script = document.createElement('script');
|
|
84
|
+
script.src = TURNSTILE_SCRIPT_SRC;
|
|
85
|
+
script.async = true;
|
|
86
|
+
script.defer = true;
|
|
87
|
+
script.onerror = settle;
|
|
88
|
+
window.__affiliateoTSLoaded = render;
|
|
89
|
+
document.head.appendChild(script);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
settle();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
return minting;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Hand out the minted token exactly once (single-use at siteverify).
|
|
99
|
+
* Returns null when no token was minted or it was already consumed.
|
|
100
|
+
*/
|
|
101
|
+
export function consumeTurnstileToken() {
|
|
102
|
+
const t = token;
|
|
103
|
+
token = null;
|
|
104
|
+
return t;
|
|
105
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export interface AffiliateoConfig {
|
|
2
|
-
|
|
2
|
+
/** Your Affiliateo app ID — shown in the dashboard when you create a
|
|
3
|
+
* Web App Affiliate app. One of `appId` / `campaignId` is required;
|
|
4
|
+
* `appId` wins when both are set. */
|
|
5
|
+
appId?: string;
|
|
6
|
+
/** @deprecated Affiliateo campaigns are now called apps — use `appId`.
|
|
7
|
+
* Kept working so existing integrations don't break. */
|
|
8
|
+
campaignId?: string;
|
|
3
9
|
/** Base URL for the API. Defaults to https://affiliateo.com */
|
|
4
10
|
apiUrl?: string;
|
|
5
11
|
/** Other domains that are part of the same merchant property and should
|
|
@@ -22,7 +28,7 @@ export interface AffiliateoConfig {
|
|
|
22
28
|
*
|
|
23
29
|
* Example:
|
|
24
30
|
* <AffiliateoProvider
|
|
25
|
-
*
|
|
31
|
+
* appId="..."
|
|
26
32
|
* allowedHostnames={['app.merchant.com', 'docs.merchant.com']}
|
|
27
33
|
* > */
|
|
28
34
|
allowedHostnames?: string[];
|
|
@@ -36,7 +42,7 @@ export interface AffiliateoConfig {
|
|
|
36
42
|
* land server-side).
|
|
37
43
|
*
|
|
38
44
|
* Example:
|
|
39
|
-
* <AffiliateoProvider
|
|
45
|
+
* <AffiliateoProvider appId="..." allowLocalhost> */
|
|
40
46
|
allowLocalhost?: boolean;
|
|
41
47
|
/** Allow tracking when the page is rendered inside an iframe. Defaults
|
|
42
48
|
* to false so scrapers, preview tools, and other sites embedding yours
|
|
@@ -55,7 +61,7 @@ export interface AffiliateoConfig {
|
|
|
55
61
|
* showing up?". Leave off in production.
|
|
56
62
|
*
|
|
57
63
|
* Example:
|
|
58
|
-
* <AffiliateoProvider
|
|
64
|
+
* <AffiliateoProvider appId="..." debug> */
|
|
59
65
|
debug?: boolean;
|
|
60
66
|
/** Allow tracking from pages served via the `file://` protocol (someone
|
|
61
67
|
* double-clicking a built HTML file instead of running a server).
|