@affiliateo/react-native 0.1.0 → 1.0.1
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 +10 -1
- package/src/provider.tsx +174 -119
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@affiliateo/react-native",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Affiliateo SDK for React Native — mobile affiliate attribution and screen tracking",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -15,6 +15,15 @@
|
|
|
15
15
|
"peerDependenciesMeta": {
|
|
16
16
|
"react-native-purchases": {
|
|
17
17
|
"optional": true
|
|
18
|
+
},
|
|
19
|
+
"expo-application": {
|
|
20
|
+
"optional": true
|
|
21
|
+
},
|
|
22
|
+
"react-native-device-info": {
|
|
23
|
+
"optional": true
|
|
24
|
+
},
|
|
25
|
+
"@react-native-async-storage/async-storage": {
|
|
26
|
+
"optional": true
|
|
18
27
|
}
|
|
19
28
|
},
|
|
20
29
|
"keywords": ["affiliateo", "affiliate", "attribution", "react-native", "mobile"],
|
package/src/provider.tsx
CHANGED
|
@@ -1,119 +1,174 @@
|
|
|
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
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
+
// Get a stable device ID that persists across app launches.
|
|
8
|
+
// Tries expo-application first (IDFV on iOS, androidId on Android),
|
|
9
|
+
// then falls back to a random UUID saved via AsyncStorage.
|
|
10
|
+
let cachedDeviceId: string | null = null
|
|
11
|
+
|
|
12
|
+
async function getStableDeviceId(): Promise<string> {
|
|
13
|
+
if (cachedDeviceId) return cachedDeviceId
|
|
14
|
+
|
|
15
|
+
// Try expo-application (built into Expo, no extra install needed)
|
|
16
|
+
try {
|
|
17
|
+
const ExpoApp = require('expo-application')
|
|
18
|
+
if (Platform.OS === 'ios' && ExpoApp.getIosIdForVendorAsync) {
|
|
19
|
+
const idfv = await ExpoApp.getIosIdForVendorAsync()
|
|
20
|
+
if (idfv) {
|
|
21
|
+
cachedDeviceId = `ios-${idfv}`
|
|
22
|
+
return cachedDeviceId
|
|
23
|
+
}
|
|
24
|
+
} else if (Platform.OS === 'android' && ExpoApp.androidId) {
|
|
25
|
+
cachedDeviceId = `android-${ExpoApp.androidId}`
|
|
26
|
+
return cachedDeviceId
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
// expo-application not available
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Try react-native-device-info (for bare RN apps)
|
|
33
|
+
try {
|
|
34
|
+
const DeviceInfo = require('react-native-device-info')
|
|
35
|
+
const uniqueId = await DeviceInfo.getUniqueId()
|
|
36
|
+
if (uniqueId) {
|
|
37
|
+
cachedDeviceId = `${Platform.OS}-${uniqueId}`
|
|
38
|
+
return cachedDeviceId
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// react-native-device-info not available
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Fallback: generate a UUID and persist it via AsyncStorage
|
|
45
|
+
try {
|
|
46
|
+
const AsyncStorage = require('@react-native-async-storage/async-storage').default
|
|
47
|
+
const stored = await AsyncStorage.getItem('@affiliateo_device_id')
|
|
48
|
+
if (stored) {
|
|
49
|
+
cachedDeviceId = stored
|
|
50
|
+
return cachedDeviceId
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const random = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c: string) => {
|
|
54
|
+
const r = (Math.random() * 16) | 0
|
|
55
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8
|
|
56
|
+
return v.toString(16)
|
|
57
|
+
})
|
|
58
|
+
const newId = `${Platform.OS}-${random}`
|
|
59
|
+
await AsyncStorage.setItem('@affiliateo_device_id', newId)
|
|
60
|
+
cachedDeviceId = newId
|
|
61
|
+
return cachedDeviceId
|
|
62
|
+
} catch {
|
|
63
|
+
// AsyncStorage not available
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Last resort: in-memory random (not ideal — changes on each app restart)
|
|
67
|
+
const random = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c: string) => {
|
|
68
|
+
const r = (Math.random() * 16) | 0
|
|
69
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8
|
|
70
|
+
return v.toString(16)
|
|
71
|
+
})
|
|
72
|
+
cachedDeviceId = `${Platform.OS}-${random}`
|
|
73
|
+
return cachedDeviceId
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const AffiliateoContext = createContext<AffiliateoContextValue>({
|
|
77
|
+
refCode: null,
|
|
78
|
+
isMatched: false,
|
|
79
|
+
isLoading: true,
|
|
80
|
+
visitorId: null,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Returns the affiliate ref code and match status.
|
|
85
|
+
*/
|
|
86
|
+
export function useAffiliateRef(): AffiliateoContextValue {
|
|
87
|
+
return useContext(AffiliateoContext)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function AffiliateoProvider({
|
|
91
|
+
campaignId,
|
|
92
|
+
apiUrl,
|
|
93
|
+
children,
|
|
94
|
+
}: AffiliateoConfig & { children: React.ReactNode }) {
|
|
95
|
+
const [state, setState] = useState<AffiliateoContextValue>({
|
|
96
|
+
refCode: null,
|
|
97
|
+
isMatched: false,
|
|
98
|
+
isLoading: true,
|
|
99
|
+
visitorId: null,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const clientRef = useRef(new AffiliateoClient(apiUrl))
|
|
103
|
+
const deviceIdRef = useRef<string | null>(null)
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
let cancelled = false
|
|
107
|
+
const client = clientRef.current
|
|
108
|
+
|
|
109
|
+
async function init() {
|
|
110
|
+
// Get stable device ID (persists across app launches)
|
|
111
|
+
const deviceId = await getStableDeviceId()
|
|
112
|
+
deviceIdRef.current = deviceId
|
|
113
|
+
|
|
114
|
+
// Identify (also inserts session_start on the backend)
|
|
115
|
+
try {
|
|
116
|
+
const deviceInfo = getDeviceInfo()
|
|
117
|
+
const result = await client.identify(campaignId, deviceId, deviceInfo)
|
|
118
|
+
|
|
119
|
+
if (cancelled) return
|
|
120
|
+
|
|
121
|
+
setState({
|
|
122
|
+
refCode: result.ref_code,
|
|
123
|
+
isMatched: result.matched,
|
|
124
|
+
isLoading: false,
|
|
125
|
+
visitorId: result.visitor_id,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// Auto-set RevenueCat attribute if available
|
|
129
|
+
if (result.ref_code) {
|
|
130
|
+
try {
|
|
131
|
+
const Purchases = require('react-native-purchases').default
|
|
132
|
+
if (Purchases?.setAttributes) {
|
|
133
|
+
await Purchases.setAttributes({ affiliateo_ref: result.ref_code })
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// react-native-purchases not installed — that's fine
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
if (cancelled) return
|
|
141
|
+
setState((s) => ({ ...s, isLoading: false }))
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
init()
|
|
146
|
+
|
|
147
|
+
// AppState listener — send session events
|
|
148
|
+
const subscription = AppState.addEventListener('change', (nextState) => {
|
|
149
|
+
const deviceId = deviceIdRef.current
|
|
150
|
+
if (!deviceId) return
|
|
151
|
+
|
|
152
|
+
if (nextState === 'active') {
|
|
153
|
+
client.sendEvents(campaignId, deviceId, [
|
|
154
|
+
{ type: 'session_start', timestamp: new Date().toISOString() },
|
|
155
|
+
]).catch(() => {})
|
|
156
|
+
} else if (nextState === 'background' || nextState === 'inactive') {
|
|
157
|
+
client.sendEvents(campaignId, deviceId, [
|
|
158
|
+
{ type: 'session_end', timestamp: new Date().toISOString() },
|
|
159
|
+
]).catch(() => {})
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
return () => {
|
|
164
|
+
cancelled = true
|
|
165
|
+
subscription.remove()
|
|
166
|
+
}
|
|
167
|
+
}, [campaignId, apiUrl])
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<AffiliateoContext.Provider value={state}>
|
|
171
|
+
{children}
|
|
172
|
+
</AffiliateoContext.Provider>
|
|
173
|
+
)
|
|
174
|
+
}
|