@affiliateo/react-native 0.1.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 +55 -0
- package/package.json +23 -0
- package/src/client.ts +55 -0
- package/src/device.ts +25 -0
- package/src/index.ts +8 -0
- package/src/provider.tsx +119 -0
- package/src/types.ts +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# @affiliateo/react-native
|
|
2
|
+
|
|
3
|
+
Affiliateo SDK for React Native — mobile affiliate attribution and screen tracking.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @affiliateo/react-native
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
Wrap your app with `AffiliateoProvider`:
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { AffiliateoProvider } from '@affiliateo/react-native';
|
|
17
|
+
|
|
18
|
+
export default function App() {
|
|
19
|
+
return (
|
|
20
|
+
<AffiliateoProvider campaignId="YOUR_CAMPAIGN_ID">
|
|
21
|
+
<YourApp />
|
|
22
|
+
</AffiliateoProvider>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Track Screens (optional)
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { useAffiliateoScreen } from '@affiliateo/react-native';
|
|
31
|
+
|
|
32
|
+
function PricingScreen() {
|
|
33
|
+
useAffiliateoScreen('Pricing');
|
|
34
|
+
return <View>...</View>;
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Get Affiliate Ref
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { useAffiliateRef } from '@affiliateo/react-native';
|
|
42
|
+
|
|
43
|
+
function MyComponent() {
|
|
44
|
+
const { refCode, isMatched, isLoading } = useAffiliateRef();
|
|
45
|
+
// ...
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## How It Works
|
|
50
|
+
|
|
51
|
+
1. On first app open, the SDK sends a device fingerprint to Affiliateo
|
|
52
|
+
2. Affiliateo matches it against recent affiliate link clicks using IP + device signals
|
|
53
|
+
3. If matched, the SDK auto-sets the `affiliateo_ref` attribute on RevenueCat (if installed)
|
|
54
|
+
4. Screen views are batched and sent every 30s for funnel analytics
|
|
55
|
+
5. Events are persisted offline and flushed when connectivity returns
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@affiliateo/react-native",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Affiliateo SDK for React Native — mobile affiliate attribution and screen tracking",
|
|
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
|
+
"react-native": ">=0.70.0"
|
|
14
|
+
},
|
|
15
|
+
"peerDependenciesMeta": {
|
|
16
|
+
"react-native-purchases": {
|
|
17
|
+
"optional": true
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"keywords": ["affiliateo", "affiliate", "attribution", "react-native", "mobile"],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"files": ["src"]
|
|
23
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { DeviceInfo, IdentifyResponse, MobileEvent } from './types'
|
|
2
|
+
|
|
3
|
+
const DEFAULT_API_URL = 'https://affiliateo.com'
|
|
4
|
+
|
|
5
|
+
export class AffiliateoClient {
|
|
6
|
+
private apiUrl: string
|
|
7
|
+
|
|
8
|
+
constructor(apiUrl?: string) {
|
|
9
|
+
this.apiUrl = (apiUrl || DEFAULT_API_URL).replace(/\/$/, '')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async identify(
|
|
13
|
+
campaignId: string,
|
|
14
|
+
deviceId: string,
|
|
15
|
+
deviceInfo: DeviceInfo
|
|
16
|
+
): Promise<IdentifyResponse> {
|
|
17
|
+
const res = await fetch(`${this.apiUrl}/api/v1/mobile/identify`, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
body: JSON.stringify({
|
|
21
|
+
campaign_id: campaignId,
|
|
22
|
+
device_id: deviceId,
|
|
23
|
+
...deviceInfo,
|
|
24
|
+
}),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
throw new Error(`Identify failed: ${res.status}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return res.json()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async sendEvents(
|
|
35
|
+
campaignId: string,
|
|
36
|
+
deviceId: string,
|
|
37
|
+
events: MobileEvent[]
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
if (events.length === 0) return
|
|
40
|
+
|
|
41
|
+
const res = await fetch(`${this.apiUrl}/api/v1/mobile/event`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
campaign_id: campaignId,
|
|
46
|
+
device_id: deviceId,
|
|
47
|
+
events,
|
|
48
|
+
}),
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
throw new Error(`Event send failed: ${res.status}`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/device.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Platform, Dimensions } from 'react-native'
|
|
2
|
+
import type { DeviceInfo } from './types'
|
|
3
|
+
|
|
4
|
+
export function getDeviceInfo(): DeviceInfo {
|
|
5
|
+
const { width, height } = Dimensions.get('window')
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
device_model: Platform.select({
|
|
9
|
+
ios: 'iPhone',
|
|
10
|
+
android: 'Android Device',
|
|
11
|
+
default: 'Unknown',
|
|
12
|
+
}) || 'Unknown',
|
|
13
|
+
os: Platform.OS === 'ios' ? 'iOS' : Platform.OS === 'android' ? 'Android' : Platform.OS,
|
|
14
|
+
os_version: String(Platform.Version),
|
|
15
|
+
app_version: '1.0.0', // Override via config if needed
|
|
16
|
+
screen_width: Math.round(width),
|
|
17
|
+
screen_height: Math.round(height),
|
|
18
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'Unknown',
|
|
19
|
+
language: Platform.select({
|
|
20
|
+
ios: 'en',
|
|
21
|
+
android: 'en',
|
|
22
|
+
default: 'en',
|
|
23
|
+
}) || 'en',
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/index.ts
ADDED
package/src/provider.tsx
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import React, { createContext, useContext, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { AppState, Platform } from 'react-native'
|
|
3
|
+
import type { AffiliateoConfig, AffiliateoContextValue } from './types'
|
|
4
|
+
import { AffiliateoClient } from './client'
|
|
5
|
+
import { getDeviceInfo } from './device'
|
|
6
|
+
|
|
7
|
+
// Generate a simple device ID using platform info + random values
|
|
8
|
+
// Persists for the app session, regenerates on reinstall
|
|
9
|
+
let cachedDeviceId: string | null = null
|
|
10
|
+
|
|
11
|
+
function getDeviceId(): string {
|
|
12
|
+
if (cachedDeviceId) return cachedDeviceId
|
|
13
|
+
const random = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
14
|
+
const r = (Math.random() * 16) | 0
|
|
15
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8
|
|
16
|
+
return v.toString(16)
|
|
17
|
+
})
|
|
18
|
+
cachedDeviceId = `${Platform.OS}-${random}`
|
|
19
|
+
return cachedDeviceId
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const AffiliateoContext = createContext<AffiliateoContextValue>({
|
|
23
|
+
refCode: null,
|
|
24
|
+
isMatched: false,
|
|
25
|
+
isLoading: true,
|
|
26
|
+
visitorId: null,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Returns the affiliate ref code and match status.
|
|
31
|
+
*/
|
|
32
|
+
export function useAffiliateRef(): AffiliateoContextValue {
|
|
33
|
+
return useContext(AffiliateoContext)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function AffiliateoProvider({
|
|
37
|
+
campaignId,
|
|
38
|
+
apiUrl,
|
|
39
|
+
children,
|
|
40
|
+
}: AffiliateoConfig & { children: React.ReactNode }) {
|
|
41
|
+
const [state, setState] = useState<AffiliateoContextValue>({
|
|
42
|
+
refCode: null,
|
|
43
|
+
isMatched: false,
|
|
44
|
+
isLoading: true,
|
|
45
|
+
visitorId: null,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const clientRef = useRef(new AffiliateoClient(apiUrl))
|
|
49
|
+
const deviceIdRef = useRef(getDeviceId())
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
let cancelled = false
|
|
53
|
+
const client = clientRef.current
|
|
54
|
+
const deviceId = deviceIdRef.current
|
|
55
|
+
|
|
56
|
+
async function init() {
|
|
57
|
+
// Send session_start immediately
|
|
58
|
+
client.sendEvents(campaignId, deviceId, [
|
|
59
|
+
{ type: 'session_start', timestamp: new Date().toISOString() },
|
|
60
|
+
]).catch(() => {})
|
|
61
|
+
|
|
62
|
+
// Identify
|
|
63
|
+
try {
|
|
64
|
+
const deviceInfo = getDeviceInfo()
|
|
65
|
+
const result = await client.identify(campaignId, deviceId, deviceInfo)
|
|
66
|
+
|
|
67
|
+
if (cancelled) return
|
|
68
|
+
|
|
69
|
+
setState({
|
|
70
|
+
refCode: result.ref_code,
|
|
71
|
+
isMatched: result.matched,
|
|
72
|
+
isLoading: false,
|
|
73
|
+
visitorId: result.visitor_id,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Auto-set RevenueCat attribute if available
|
|
77
|
+
if (result.ref_code) {
|
|
78
|
+
try {
|
|
79
|
+
const Purchases = require('react-native-purchases').default
|
|
80
|
+
if (Purchases?.setAttributes) {
|
|
81
|
+
await Purchases.setAttributes({ affiliateo_ref: result.ref_code })
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// react-native-purchases not installed — that's fine
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
if (cancelled) return
|
|
89
|
+
setState((s) => ({ ...s, isLoading: false }))
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
init()
|
|
94
|
+
|
|
95
|
+
// AppState listener — send session events immediately
|
|
96
|
+
const subscription = AppState.addEventListener('change', (nextState) => {
|
|
97
|
+
if (nextState === 'active') {
|
|
98
|
+
client.sendEvents(campaignId, deviceId, [
|
|
99
|
+
{ type: 'session_start', timestamp: new Date().toISOString() },
|
|
100
|
+
]).catch(() => {})
|
|
101
|
+
} else if (nextState === 'background' || nextState === 'inactive') {
|
|
102
|
+
client.sendEvents(campaignId, deviceId, [
|
|
103
|
+
{ type: 'session_end', timestamp: new Date().toISOString() },
|
|
104
|
+
]).catch(() => {})
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
return () => {
|
|
109
|
+
cancelled = true
|
|
110
|
+
subscription.remove()
|
|
111
|
+
}
|
|
112
|
+
}, [campaignId, apiUrl])
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<AffiliateoContext.Provider value={state}>
|
|
116
|
+
{children}
|
|
117
|
+
</AffiliateoContext.Provider>
|
|
118
|
+
)
|
|
119
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface AffiliateoConfig {
|
|
2
|
+
campaignId: string
|
|
3
|
+
/** Base URL for the API. Defaults to https://affiliateo.com */
|
|
4
|
+
apiUrl?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface DeviceInfo {
|
|
8
|
+
device_model: string
|
|
9
|
+
os: string
|
|
10
|
+
os_version: string
|
|
11
|
+
app_version: string
|
|
12
|
+
screen_width: number
|
|
13
|
+
screen_height: number
|
|
14
|
+
timezone: string
|
|
15
|
+
language: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface IdentifyResponse {
|
|
19
|
+
visitor_id: string
|
|
20
|
+
ref_code: string | null
|
|
21
|
+
matched: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface MobileEvent {
|
|
25
|
+
type: 'screen_view' | 'session_start' | 'session_end' | 'custom'
|
|
26
|
+
screen?: string
|
|
27
|
+
timestamp: string
|
|
28
|
+
metadata?: Record<string, unknown>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AffiliateoContextValue {
|
|
32
|
+
refCode: string | null
|
|
33
|
+
isMatched: boolean
|
|
34
|
+
isLoading: boolean
|
|
35
|
+
visitorId: string | null
|
|
36
|
+
}
|