@affiliateo/web 1.0.0 → 1.1.5
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/package.json +22 -14
- package/src/index.ts +29 -72
- package/src/metadata.ts +57 -0
- package/src/provider.tsx +147 -0
- package/src/types.ts +12 -0
package/package.json
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@affiliateo/web",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Affiliateo web SDK —
|
|
5
|
-
"main": "src/index.ts",
|
|
6
|
-
"types": "src/index.ts",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"build": "tsc",
|
|
9
|
-
"typecheck": "tsc --noEmit"
|
|
10
|
-
},
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@affiliateo/web",
|
|
3
|
+
"version": "1.1.5",
|
|
4
|
+
"description": "Affiliateo web SDK — affiliate attribution and session tracking for web apps",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"typecheck": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"react": ">=18.0.0"
|
|
13
|
+
},
|
|
14
|
+
"peerDependenciesMeta": {
|
|
15
|
+
"react": {
|
|
16
|
+
"optional": true
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"keywords": ["affiliateo", "affiliate", "attribution", "stripe", "web", "session", "tracking"],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"files": ["src"]
|
|
22
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,72 +1,29 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @affiliateo/web —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* ```
|
|
6
|
-
* import {
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// Try Next.js App Router cookies (ReadonlyRequestCookies with .get())
|
|
32
|
-
if (req.cookies && typeof req.cookies === 'object' && 'get' in req.cookies && typeof req.cookies.get === 'function') {
|
|
33
|
-
const visitorId = req.cookies.get('affiliateo_visitor_id')
|
|
34
|
-
const ref = req.cookies.get('affiliateo_ref')
|
|
35
|
-
if (visitorId?.value) metadata.affiliateo_visitor_id = visitorId.value
|
|
36
|
-
if (ref?.value) metadata.affiliateo_ref = ref.value
|
|
37
|
-
return metadata
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Try plain object cookies (Express, Pages Router, etc.)
|
|
41
|
-
if (req.cookies && typeof req.cookies === 'object') {
|
|
42
|
-
const cookies = req.cookies as Record<string, string>
|
|
43
|
-
if (cookies.affiliateo_visitor_id) metadata.affiliateo_visitor_id = cookies.affiliateo_visitor_id
|
|
44
|
-
if (cookies.affiliateo_ref) metadata.affiliateo_ref = cookies.affiliateo_ref
|
|
45
|
-
return metadata
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Fallback: parse cookie header manually
|
|
49
|
-
const cookieHeader = req.headers instanceof Headers
|
|
50
|
-
? req.headers.get('cookie')
|
|
51
|
-
: (req.headers as { cookie?: string })?.cookie
|
|
52
|
-
|
|
53
|
-
if (cookieHeader) {
|
|
54
|
-
const cookies = parseCookies(cookieHeader)
|
|
55
|
-
if (cookies.affiliateo_visitor_id) metadata.affiliateo_visitor_id = cookies.affiliateo_visitor_id
|
|
56
|
-
if (cookies.affiliateo_ref) metadata.affiliateo_ref = cookies.affiliateo_ref
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return metadata
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Parse a cookie header string into key-value pairs.
|
|
64
|
-
*/
|
|
65
|
-
function parseCookies(cookieHeader: string): Record<string, string> {
|
|
66
|
-
const cookies: Record<string, string> = {}
|
|
67
|
-
for (const pair of cookieHeader.split(';')) {
|
|
68
|
-
const [key, ...rest] = pair.trim().split('=')
|
|
69
|
-
if (key) cookies[key.trim()] = rest.join('=').trim()
|
|
70
|
-
}
|
|
71
|
-
return cookies
|
|
72
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @affiliateo/web — Affiliate attribution and session tracking for web apps.
|
|
3
|
+
*
|
|
4
|
+
* React usage:
|
|
5
|
+
* ```tsx
|
|
6
|
+
* import { AffiliateoProvider } from '@affiliateo/web';
|
|
7
|
+
*
|
|
8
|
+
* export default function App() {
|
|
9
|
+
* return (
|
|
10
|
+
* <AffiliateoProvider siteId="YOUR_SITE_ID">
|
|
11
|
+
* <YourApp />
|
|
12
|
+
* </AffiliateoProvider>
|
|
13
|
+
* );
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Server-side (pass ref to Stripe checkout):
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { getMetadata } from '@affiliateo/web';
|
|
20
|
+
*
|
|
21
|
+
* const session = await stripe.checkout.sessions.create({
|
|
22
|
+
* metadata: getMetadata(req),
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export { AffiliateoProvider, useAffiliateRef } from './provider'
|
|
28
|
+
export { getMetadata } from './metadata'
|
|
29
|
+
export type { AffiliateoConfig, AffiliateoContextValue } from './types'
|
package/src/metadata.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract affiliateo cookies from an incoming request.
|
|
3
|
+
* Works with Next.js (App Router & Pages Router), Express, and any framework
|
|
4
|
+
* that exposes cookies on the request object.
|
|
5
|
+
*
|
|
6
|
+
* Returns metadata object to pass to Stripe checkout session.
|
|
7
|
+
*/
|
|
8
|
+
export function getMetadata(
|
|
9
|
+
req: {
|
|
10
|
+
cookies?: Record<string, string> | { get?: (name: string) => { value: string } | undefined }
|
|
11
|
+
headers?: { cookie?: string } | Headers
|
|
12
|
+
}
|
|
13
|
+
): { affiliateo_visitor_id?: string; affiliateo_ref?: string } {
|
|
14
|
+
const metadata: { affiliateo_visitor_id?: string; affiliateo_ref?: string } = {}
|
|
15
|
+
|
|
16
|
+
// Try Next.js App Router cookies (ReadonlyRequestCookies with .get())
|
|
17
|
+
if (req.cookies && typeof req.cookies === 'object' && 'get' in req.cookies && typeof req.cookies.get === 'function') {
|
|
18
|
+
const visitorId = req.cookies.get('affiliateo_visitor_id')
|
|
19
|
+
const ref = req.cookies.get('affiliateo_ref')
|
|
20
|
+
if (visitorId?.value) metadata.affiliateo_visitor_id = visitorId.value
|
|
21
|
+
if (ref?.value) metadata.affiliateo_ref = ref.value
|
|
22
|
+
return metadata
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Try plain object cookies (Express, Pages Router, etc.)
|
|
26
|
+
if (req.cookies && typeof req.cookies === 'object') {
|
|
27
|
+
const cookies = req.cookies as Record<string, string>
|
|
28
|
+
if (cookies.affiliateo_visitor_id) metadata.affiliateo_visitor_id = cookies.affiliateo_visitor_id
|
|
29
|
+
if (cookies.affiliateo_ref) metadata.affiliateo_ref = cookies.affiliateo_ref
|
|
30
|
+
return metadata
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Fallback: parse cookie header manually
|
|
34
|
+
const cookieHeader = req.headers instanceof Headers
|
|
35
|
+
? req.headers.get('cookie')
|
|
36
|
+
: (req.headers as { cookie?: string })?.cookie
|
|
37
|
+
|
|
38
|
+
if (cookieHeader) {
|
|
39
|
+
const cookies = parseCookies(cookieHeader)
|
|
40
|
+
if (cookies.affiliateo_visitor_id) metadata.affiliateo_visitor_id = cookies.affiliateo_visitor_id
|
|
41
|
+
if (cookies.affiliateo_ref) metadata.affiliateo_ref = cookies.affiliateo_ref
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return metadata
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse a cookie header string into key-value pairs.
|
|
49
|
+
*/
|
|
50
|
+
function parseCookies(cookieHeader: string): Record<string, string> {
|
|
51
|
+
const cookies: Record<string, string> = {}
|
|
52
|
+
for (const pair of cookieHeader.split(';')) {
|
|
53
|
+
const [key, ...rest] = pair.trim().split('=')
|
|
54
|
+
if (key) cookies[key.trim()] = rest.join('=').trim()
|
|
55
|
+
}
|
|
56
|
+
return cookies
|
|
57
|
+
}
|
package/src/provider.tsx
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext, useEffect, useRef, useState } from 'react'
|
|
4
|
+
import type { AffiliateoConfig, AffiliateoContextValue } from './types'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_API_URL = 'https://affiliateo.com'
|
|
7
|
+
|
|
8
|
+
// Cookie helpers
|
|
9
|
+
function getCookie(name: string): string | null {
|
|
10
|
+
if (typeof document === 'undefined') return null
|
|
11
|
+
const m = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'))
|
|
12
|
+
return m ? m[2] : null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function setCookie(name: string, value: string, days: number) {
|
|
16
|
+
if (typeof document === 'undefined') return
|
|
17
|
+
const dt = new Date()
|
|
18
|
+
dt.setTime(dt.getTime() + days * 864e5)
|
|
19
|
+
document.cookie = `${name}=${value};expires=${dt.toUTCString()};path=/;SameSite=Lax`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Generate UUID
|
|
23
|
+
function uuid(): string {
|
|
24
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID()
|
|
25
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
26
|
+
const r = (Math.random() * 16) | 0
|
|
27
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8
|
|
28
|
+
return v.toString(16)
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const AffiliateoContext = createContext<AffiliateoContextValue>({
|
|
33
|
+
refCode: null,
|
|
34
|
+
visitorId: null,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns the affiliate ref code and visitor ID.
|
|
39
|
+
*/
|
|
40
|
+
export function useAffiliateRef(): AffiliateoContextValue {
|
|
41
|
+
return useContext(AffiliateoContext)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function AffiliateoProvider({
|
|
45
|
+
siteId,
|
|
46
|
+
apiUrl,
|
|
47
|
+
children,
|
|
48
|
+
}: AffiliateoConfig & { children: React.ReactNode }) {
|
|
49
|
+
const [state, setState] = useState<AffiliateoContextValue>({
|
|
50
|
+
refCode: null,
|
|
51
|
+
visitorId: null,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const apiBase = (apiUrl || DEFAULT_API_URL).replace(/\/$/, '')
|
|
55
|
+
const siteIdRef = useRef(siteId)
|
|
56
|
+
const visitorIdRef = useRef<string | null>(null)
|
|
57
|
+
const refRef = useRef<string | null>(null)
|
|
58
|
+
const sentRef = useRef(false)
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (typeof window === 'undefined') return
|
|
62
|
+
|
|
63
|
+
// Get or create visitor ID
|
|
64
|
+
let vid = getCookie('affiliateo_visitor_id')
|
|
65
|
+
if (!vid) {
|
|
66
|
+
vid = uuid()
|
|
67
|
+
}
|
|
68
|
+
setCookie('affiliateo_visitor_id', vid, 365)
|
|
69
|
+
visitorIdRef.current = vid
|
|
70
|
+
|
|
71
|
+
// Read ref from URL params
|
|
72
|
+
const params = new URLSearchParams(window.location.search)
|
|
73
|
+
const ref = params.get('ref') || params.get('via') || params.get('source') || getCookie('affiliateo_ref') || null
|
|
74
|
+
if (ref) {
|
|
75
|
+
setCookie('affiliateo_ref', ref, 365)
|
|
76
|
+
}
|
|
77
|
+
refRef.current = ref
|
|
78
|
+
|
|
79
|
+
setState({ refCode: ref, visitorId: vid })
|
|
80
|
+
|
|
81
|
+
// Send event helper
|
|
82
|
+
function send(type: string) {
|
|
83
|
+
const payload = {
|
|
84
|
+
type,
|
|
85
|
+
site_id: siteIdRef.current,
|
|
86
|
+
visitor_id: visitorIdRef.current,
|
|
87
|
+
url: window.location.href,
|
|
88
|
+
path: window.location.pathname,
|
|
89
|
+
hostname: window.location.hostname,
|
|
90
|
+
referrer: document.referrer || '',
|
|
91
|
+
ref: refRef.current || '',
|
|
92
|
+
utm_source: params.get('utm_source') || '',
|
|
93
|
+
utm_medium: params.get('utm_medium') || '',
|
|
94
|
+
utm_campaign: params.get('utm_campaign') || '',
|
|
95
|
+
screen_width: window.innerWidth,
|
|
96
|
+
}
|
|
97
|
+
const data = JSON.stringify(payload)
|
|
98
|
+
if (navigator.sendBeacon) {
|
|
99
|
+
navigator.sendBeacon(`${apiBase}/api/v1/event`, new Blob([data], { type: 'application/json' }))
|
|
100
|
+
} else {
|
|
101
|
+
fetch(`${apiBase}/api/v1/event`, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: { 'Content-Type': 'application/json' },
|
|
104
|
+
body: data,
|
|
105
|
+
keepalive: true,
|
|
106
|
+
}).catch(() => {})
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Send initial session_start
|
|
111
|
+
if (!sentRef.current) {
|
|
112
|
+
sentRef.current = true
|
|
113
|
+
send('session_start')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Visibility change — session_start/session_end
|
|
117
|
+
let left = false
|
|
118
|
+
function onLeave() {
|
|
119
|
+
if (left) return
|
|
120
|
+
left = true
|
|
121
|
+
send('session_end')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function onVisibilityChange() {
|
|
125
|
+
if (document.visibilityState === 'hidden') {
|
|
126
|
+
onLeave()
|
|
127
|
+
} else if (document.visibilityState === 'visible') {
|
|
128
|
+
left = false
|
|
129
|
+
send('session_start')
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
document.addEventListener('visibilitychange', onVisibilityChange)
|
|
134
|
+
window.addEventListener('beforeunload', onLeave)
|
|
135
|
+
|
|
136
|
+
return () => {
|
|
137
|
+
document.removeEventListener('visibilitychange', onVisibilityChange)
|
|
138
|
+
window.removeEventListener('beforeunload', onLeave)
|
|
139
|
+
}
|
|
140
|
+
}, [siteId, apiBase])
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<AffiliateoContext.Provider value={state}>
|
|
144
|
+
{children}
|
|
145
|
+
</AffiliateoContext.Provider>
|
|
146
|
+
)
|
|
147
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface AffiliateoConfig {
|
|
2
|
+
siteId: string
|
|
3
|
+
/** Base URL for the API. Defaults to https://affiliateo.com */
|
|
4
|
+
apiUrl?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface AffiliateoContextValue {
|
|
8
|
+
/** The affiliate ref code from the URL (if present) */
|
|
9
|
+
refCode: string | null
|
|
10
|
+
/** The visitor ID stored in the cookie */
|
|
11
|
+
visitorId: string | null
|
|
12
|
+
}
|