@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 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
@@ -0,0 +1,8 @@
1
+ export { AffiliateoProvider, useAffiliateRef } from './provider'
2
+ export type {
3
+ AffiliateoConfig,
4
+ AffiliateoContextValue,
5
+ DeviceInfo,
6
+ IdentifyResponse,
7
+ MobileEvent,
8
+ } from './types'
@@ -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
+ }