@affiliateo/react-native 1.0.0 → 1.0.2

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.
Files changed (2) hide show
  1. package/package.json +10 -1
  2. package/src/provider.tsx +174 -119
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@affiliateo/react-native",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
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
- // 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
- }
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') {
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
+ }