1
- import { getAttributionToken } from '@brigad/react-native-adservices'
2
1
import { asNumber , asObject , asOptional , asString , asValue } from 'cleaners'
3
2
import { makeReactNativeDisklet } from 'disklet'
4
- import { Platform } from 'react-native'
5
3
6
4
import { FIRST_OPEN } from '../constants/constantSettings'
7
5
import { makeUuid } from '../util/rnUtils'
8
- import { snooze } from '../util/utils'
9
6
import { getCountryCodeByIp } from './AccountReferralActions'
10
7
11
8
export const firstOpenDisklet = makeReactNativeDisklet ( )
12
9
13
- const asAppleAdsAttribution = asObject ( {
14
- campaignId : asOptional ( asString ) ,
15
- keywordId : asOptional ( asString )
16
- } )
17
- type AppleAdsAttribution = ReturnType < typeof asAppleAdsAttribution >
18
-
19
- interface FirstOpenInfo {
20
- isFirstOpen : 'true' | 'false'
21
- deviceId : string
22
- firstOpenEpoch : number
23
- countryCode ?: string
24
- appleAdsAttribution ?: AppleAdsAttribution
25
- }
26
- type FirstOpenInfoFile = Omit < FirstOpenInfo , 'appleAdsAttribution' >
27
-
28
- const asFirstOpenInfoFile = asObject < FirstOpenInfoFile > ( {
10
+ const asFirstOpenInfo = asObject ( {
29
11
isFirstOpen : asValue ( 'true' , 'false' ) ,
30
12
deviceId : asString ,
31
13
firstOpenEpoch : asNumber ,
32
14
countryCode : asOptional ( asString )
33
15
} )
16
+ type FirstOpenInfo = ReturnType < typeof asFirstOpenInfo >
34
17
35
18
let firstOpenInfo : FirstOpenInfo
36
19
let firstLoadPromise : Promise < FirstOpenInfo > | undefined
@@ -57,19 +40,11 @@ const readFirstOpenInfoFromDisk = async (): Promise<FirstOpenInfo> => {
57
40
let firstOpenText
58
41
try {
59
42
firstOpenText = await firstOpenDisklet . getText ( FIRST_OPEN )
60
- // Parse the file data using the file-specific cleaner
61
- const fileData = asFirstOpenInfoFile ( JSON . parse ( firstOpenText ) )
62
- // Create the full in-memory object with attribution data
63
- firstOpenInfo = {
64
- ...fileData ,
65
- isFirstOpen : 'false' ,
66
- appleAdsAttribution : await getAppleAdsAttribution ( )
67
- }
43
+ firstOpenInfo = asFirstOpenInfo ( JSON . parse ( firstOpenText ) )
44
+ firstOpenInfo . isFirstOpen = 'false'
68
45
} catch ( error : unknown ) {
69
46
// Generate new values.
70
-
71
- // Create file data object (without attribution)
72
- const fileData : FirstOpenInfoFile = {
47
+ firstOpenInfo = {
73
48
deviceId : await makeUuid ( ) ,
74
49
firstOpenEpoch : Date . now ( ) ,
75
50
countryCode : await getCountryCodeByIp ( ) ,
@@ -78,78 +53,11 @@ const readFirstOpenInfoFromDisk = async (): Promise<FirstOpenInfo> => {
78
53
// date, just created an empty file.
79
54
// Note that 'firstOpenEpoch' won't be accurate in this case, but at
80
55
// least make a starting point.
81
-
82
56
isFirstOpen : firstOpenText != null ? 'false' : 'true'
83
57
}
84
-
85
- // Create the full in-memory object
86
- firstOpenInfo = {
87
- ...fileData ,
88
- appleAdsAttribution : await getAppleAdsAttribution ( )
89
- }
90
-
91
- // Only save the file-specific data to disk
92
- await firstOpenDisklet . setText ( FIRST_OPEN , JSON . stringify ( fileData ) )
58
+ await firstOpenDisklet . setText ( FIRST_OPEN , JSON . stringify ( firstOpenInfo ) )
93
59
}
94
60
}
95
61
96
62
return firstOpenInfo
97
63
}
98
-
99
- /**
100
- * Get Apple Search Ads attribution data using the AdServices framework
101
- * and make an API call to get the actual keywordId.
102
- */
103
- export async function getAppleAdsAttribution ( ) : Promise < AppleAdsAttribution > {
104
- if ( Platform . OS !== 'ios' ) {
105
- return { campaignId : undefined , keywordId : undefined }
106
- }
107
-
108
- // Get the attribution token from the device. This package also handles
109
- // checking for the required iOS version.
110
- const attributionToken = await getAttributionToken ( ) . catch ( error => {
111
- console . log ( 'Apple Ads attribution token unavailable:' , error )
112
- return undefined
113
- } )
114
-
115
- // Send the token to Apple's API to retrieve the campaign and keyword IDs.
116
- if ( attributionToken != null ) {
117
- // Retry logic as recommended by Apple:
118
- // "A 404 response can occur if you make an API call too quickly after
119
- // receiving a valid token. A best practice is to initiate retries at
120
- // intervals of 5 seconds, with a maximum of three attempts."
121
- // https://developer.apple.com/documentation/adservices/aaattribution/attributiontoken()#Attribution-payload
122
- const maxRetries = 3
123
- const retryDelay = 5000 // 5 seconds
124
-
125
- for ( let attempt = 1 ; attempt <= maxRetries ; attempt ++ ) {
126
- try {
127
- // Get the attribution data from Apple for the token
128
- const response = await fetch ( 'https://api-adservices.apple.com/api/v1/' , {
129
- method : 'POST' ,
130
- headers : {
131
- 'Content-Type' : 'text/plain'
132
- } ,
133
- body : attributionToken
134
- } )
135
-
136
- // If we get a 404, wait and retry as per Apple's recommendation
137
- if ( response . status === 404 && attempt < maxRetries ) {
138
- console . log ( `Apple Ads attribution API returned 404, retrying in ${ retryDelay } ms (attempt ${ attempt } /${ maxRetries } )` )
139
- await snooze ( retryDelay )
140
- continue
141
- }
142
-
143
- if ( ! response . ok ) throw new Error ( `API call failed with status: ${ response . status } ` )
144
-
145
- const data = await response . json ( )
146
- return asAppleAdsAttribution ( data )
147
- } catch ( apiError ) {
148
- console . warn ( 'Error fetching Apple Ads attribution data:' , apiError )
149
- break
150
- }
151
- }
152
- }
153
-
154
- return { campaignId : undefined , keywordId : undefined }
155
- }
0 commit comments