@affiliateo/web 4.6.1 → 4.8.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 +274 -238
- package/dist/bot-detect.d.ts +4 -4
- package/dist/bot-detect.js +6 -6
- package/dist/provider.js +133 -79
- package/dist/queue.d.ts +7 -3
- package/dist/queue.js +48 -14
- package/dist/turnstile.d.ts +3 -3
- package/dist/turnstile.js +6 -6
- package/dist/types.d.ts +24 -15
- package/dist/user-id.d.ts +18 -0
- package/dist/user-id.js +40 -0
- package/package.json +46 -44
package/README.md
CHANGED
|
@@ -1,238 +1,274 @@
|
|
|
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
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
-
|
|
236
|
-
|
|
237
|
-
|
|
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 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
|
+
The SDK remembers the id and sends it again once per visit, so the server
|
|
212
|
+
stays in step without a request on every page. Ids that mean "nobody"
|
|
213
|
+
(`"null"`, `"undefined"`, `"guest"`, `"0"`, an empty string, RevenueCat's
|
|
214
|
+
`$RCAnonymousID:...`, anything over 128 characters) are skipped, and nothing is
|
|
215
|
+
sent while the visitor has opted out.
|
|
216
|
+
|
|
217
|
+
## Sign out
|
|
218
|
+
|
|
219
|
+
Call `logout()` when the user signs out:
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
const { logout } = useAffiliateRef()
|
|
223
|
+
|
|
224
|
+
async function handleSignOut() {
|
|
225
|
+
await auth.signOut()
|
|
226
|
+
await logout()
|
|
227
|
+
router.push('/')
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The browser then counts as a new anonymous customer of your app, so whoever
|
|
232
|
+
signs in next (or nobody) isn't counted as the person who just left.
|
|
233
|
+
Everything already recorded stays with them. Tracking carries on as before:
|
|
234
|
+
same visitor id, same affiliate. If a different user signs in without a
|
|
235
|
+
sign-out in between, the browser switches to them on its own.
|
|
236
|
+
|
|
237
|
+
## Changelog
|
|
238
|
+
|
|
239
|
+
- **4.8.0:** new `logout()` for sign-out. `reset()` is now the same as
|
|
240
|
+
`logout()`: it used to mint a new visitor id and drop the affiliate ref,
|
|
241
|
+
which split one person into two visitors. `identify()` skips placeholder ids,
|
|
242
|
+
does nothing while opted out, and re-sends the stored id once per visit. A
|
|
243
|
+
different user signing in on the same browser now switches it to them. A
|
|
244
|
+
request the server refuses with a 400 is no longer retried.
|
|
245
|
+
|
|
246
|
+
## API
|
|
247
|
+
|
|
248
|
+
### `<AffiliateoProvider appId="..." apiUrl?="...">`
|
|
249
|
+
|
|
250
|
+
React provider. Wrap your app once. Reads `?ref=` from URL, persists to localStorage, sends pageview events, sets up data-attribute + scroll listeners.
|
|
251
|
+
|
|
252
|
+
Props:
|
|
253
|
+
- `appId` (required): your Affiliateo app ID (`campaignId` still works as a deprecated alias)
|
|
254
|
+
- `apiUrl` (optional): defaults to `https://affiliateo.com`
|
|
255
|
+
|
|
256
|
+
### `useAffiliateRef()`
|
|
257
|
+
|
|
258
|
+
React hook that returns `{ refCode, visitorId, track, identify, logout, ... }`.
|
|
259
|
+
|
|
260
|
+
- `track(eventName, metadata?)`. fire a custom goal
|
|
261
|
+
- `identify(userId)`. link this visitor to a merchant user_id (no PII)
|
|
262
|
+
- `logout()`. the user signed out (`reset()` is the same)
|
|
263
|
+
|
|
264
|
+
### `getAttribution()`
|
|
265
|
+
|
|
266
|
+
Client-side function. Returns an object with `affiliateo_ref`, `affiliateo_visitor_id`, and `affiliateo_app_id` from localStorage. Pass directly as Stripe `metadata`.
|
|
267
|
+
|
|
268
|
+
## Framework support
|
|
269
|
+
|
|
270
|
+
Works with any JavaScript framework or vanilla JS:
|
|
271
|
+
- **Next.js** (App Router & Pages Router)
|
|
272
|
+
- **Remix** / **React Router**
|
|
273
|
+
- **Vite + React**
|
|
274
|
+
- **Vanilla JS/HTML** (use the `<script>` tag instead)
|
package/dist/bot-detect.d.ts
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
* never fire a single network request.
|
|
5
5
|
*
|
|
6
6
|
* Defense in depth:
|
|
7
|
-
* 1. This check
|
|
7
|
+
* 1. This check - catches Selenium / Puppeteer / curl / etc
|
|
8
8
|
* BEFORE anything hits the wire. Saves bandwidth.
|
|
9
9
|
* Can be bypassed by a determined attacker who
|
|
10
10
|
* edits the bundled SDK.
|
|
11
|
-
* 2. Server UA regex
|
|
11
|
+
* 2. Server UA regex - catches the same patterns plus legit crawlers
|
|
12
12
|
* (Googlebot, FB crawler, etc). Server is
|
|
13
13
|
* authoritative. Can't be bypassed.
|
|
14
|
-
* 3. Cloudflare
|
|
14
|
+
* 3. Cloudflare - invisible browser-attestation challenge. Real
|
|
15
15
|
* Turnstile browsers pass in 50-200ms; scripts that don't
|
|
16
16
|
* run real JS can't pass it. Catches sophisticated
|
|
17
17
|
* attackers who spoofed their UA past layer 2.
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
/**
|
|
28
28
|
* Returns true if the current browser context looks like a bot or
|
|
29
29
|
* automation tool. Safe to call in SSR (returns true since there's no
|
|
30
|
-
* real visitor in that context anyway
|
|
30
|
+
* real visitor in that context anyway - a server-rendered page
|
|
31
31
|
* shouldn't be firing analytics).
|
|
32
32
|
*
|
|
33
33
|
* Wraps every check in try/catch so a hostile environment (frozen
|
package/dist/bot-detect.js
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
* never fire a single network request.
|
|
5
5
|
*
|
|
6
6
|
* Defense in depth:
|
|
7
|
-
* 1. This check
|
|
7
|
+
* 1. This check - catches Selenium / Puppeteer / curl / etc
|
|
8
8
|
* BEFORE anything hits the wire. Saves bandwidth.
|
|
9
9
|
* Can be bypassed by a determined attacker who
|
|
10
10
|
* edits the bundled SDK.
|
|
11
|
-
* 2. Server UA regex
|
|
11
|
+
* 2. Server UA regex - catches the same patterns plus legit crawlers
|
|
12
12
|
* (Googlebot, FB crawler, etc). Server is
|
|
13
13
|
* authoritative. Can't be bypassed.
|
|
14
|
-
* 3. Cloudflare
|
|
14
|
+
* 3. Cloudflare - invisible browser-attestation challenge. Real
|
|
15
15
|
* Turnstile browsers pass in 50-200ms; scripts that don't
|
|
16
16
|
* run real JS can't pass it. Catches sophisticated
|
|
17
17
|
* attackers who spoofed their UA past layer 2.
|
|
@@ -66,7 +66,7 @@ const BOT_DOC_ATTRS = ['webdriver', 'selenium', 'driver'];
|
|
|
66
66
|
/**
|
|
67
67
|
* Returns true if the current browser context looks like a bot or
|
|
68
68
|
* automation tool. Safe to call in SSR (returns true since there's no
|
|
69
|
-
* real visitor in that context anyway
|
|
69
|
+
* real visitor in that context anyway - a server-rendered page
|
|
70
70
|
* shouldn't be firing analytics).
|
|
71
71
|
*
|
|
72
72
|
* Wraps every check in try/catch so a hostile environment (frozen
|
|
@@ -83,7 +83,7 @@ export function isLikelyBot() {
|
|
|
83
83
|
const win = window;
|
|
84
84
|
// Selenium / Puppeteer / Playwright signature on navigator.
|
|
85
85
|
// Modern stealth plugins try to override this but the override
|
|
86
|
-
// is detectable too (the property descriptor looks wrong)
|
|
86
|
+
// is detectable too (the property descriptor looks wrong) - we
|
|
87
87
|
// catch the easy case here and let Turnstile catch the rest.
|
|
88
88
|
if (nav.webdriver === true)
|
|
89
89
|
return true;
|
|
@@ -106,7 +106,7 @@ export function isLikelyBot() {
|
|
|
106
106
|
}
|
|
107
107
|
const ua = nav.userAgent.toLowerCase();
|
|
108
108
|
// Substring match on the known automation tokens. We split into a
|
|
109
|
-
// single loop instead of one giant regex for readability
|
|
109
|
+
// single loop instead of one giant regex for readability - the cost
|
|
110
110
|
// difference at <20 patterns is microseconds either way.
|
|
111
111
|
for (const pattern of BOT_UA_PATTERNS) {
|
|
112
112
|
if (ua.includes(pattern))
|