@@ -70,6 +70,16 @@ public static class ImmutableAudience
7070 // layer; null in pure-C# environments and on non-Android platforms.
7171 internal static volatile Func < string ? > ? MobileInstallReferrerProvider ;
7272
73+ // Returns the current iOS ATT status int (0=notDetermined, 1=restricted,
74+ // 2=denied, 3=authorized). Used by tracking_authorization_changed detection
75+ // on Init and OnResume. Set by the Unity layer on iOS; null elsewhere.
76+ internal static volatile Func < int ? > ? MobileATTStatusProvider ;
77+
78+ // Returns the IDFA string when ATT is authorized. Included in
79+ // tracking_authorization_changed only when transitioning to authorized
80+ // with Full consent. Set by the Unity layer on iOS; null elsewhere.
81+ internal static volatile Func < string ? > ? MobileIDFAProvider ;
82+
7383 // Active session. Created at Init (or on upgrade from None) and disposed
7484 // on Shutdown or SetConsent(None). Volatile so OnPause/OnResume see
7585 // assignments from SetConsent without taking _initLock.
@@ -249,6 +259,8 @@ public static void Init(AudienceConfig config)
249259
250260 FireGameLaunch ( config , consentAtInit , skanRegistered , attributionContext ) ;
251261
262+ CheckAndFireAttStatusChanged ( config , consentAtInit ) ;
263+
252264 // Fires once per install. installReferrer lands asynchronously
253265 // from Google Play Services; on the first launch the cache is
254266 // usually still empty when game_launch fires, so we ship a
@@ -774,6 +786,11 @@ public static async Task<TrackingAuthorizationStatus> RequestTrackingAuthorizati
774786 if ( status < 0 || status > 3 )
775787 return TrackingAuthorizationStatus . NotDetermined ;
776788
789+ // Pass the resolved status directly to avoid a redundant native call.
790+ var config = _config ;
791+ if ( _initialized && config != null )
792+ CheckAndFireAttStatusChanged ( config , _state . Level , status ) ;
793+
777794 return ( TrackingAuthorizationStatus ) status ;
778795 }
779796
@@ -1182,5 +1199,79 @@ private static void FireInstallReferrerReceivedOnce(AudienceConfig config, strin
11821199 Log . Warn ( AudienceLogs . InstallReferrerSentMarkerWriteFailed ( ex ) ) ;
11831200 }
11841201 }
1202+
1203+ // Mirrors AttributionContext.AttStatusToString in the Unity layer; defined
1204+ // here so the Core assembly has no dependency on the Unity assembly.
1205+ private static string AttStatusToString ( int status )
1206+ {
1207+ switch ( status )
1208+ {
1209+ case 0 : return "notDetermined" ;
1210+ case 1 : return "restricted" ;
1211+ case 2 : return "denied" ;
1212+ case 3 : return "authorized" ;
1213+ default : return "unknown" ;
1214+ }
1215+ }
1216+
1217+ // Fires tracking_authorization_changed when the ATT status differs from
1218+ // the last-persisted observation. knownStatus skips the native re-read
1219+ // when the caller already has the resolved value (e.g. after
1220+ // RequestTrackingAuthorizationAsync resolves).
1221+ //
1222+ // First observation (no file): persists the baseline and returns without
1223+ // firing — game_launch already captures the initial state on that Init.
1224+ private static void CheckAndFireAttStatusChanged (
1225+ AudienceConfig config ,
1226+ ConsentLevel consent ,
1227+ int ? knownStatus = null )
1228+ {
1229+ if ( ! config . EnableMobileAttribution ) return ;
1230+ if ( ! consent . CanTrack ( ) ) return ;
1231+
1232+ int currentStatus ;
1233+ if ( knownStatus . HasValue )
1234+ {
1235+ currentStatus = knownStatus . Value ;
1236+ }
1237+ else
1238+ {
1239+ var provider = MobileATTStatusProvider ;
1240+ if ( provider == null ) return ;
1241+ int ? raw ;
1242+ try { raw = provider ( ) ; }
1243+ catch ( Exception ex ) { Log . Warn ( AudienceLogs . ATTStatusProviderThrew ( ex ) ) ; return ; }
1244+ if ( ! raw . HasValue ) return ;
1245+ currentStatus = raw . Value ;
1246+ }
1247+
1248+ var previous = AttStatusStore . Load ( config . PersistentDataPath ! ) ;
1249+
1250+ if ( previous == currentStatus ) return ;
1251+
1252+ AttStatusStore . Save ( config . PersistentDataPath ! , currentStatus ) ;
1253+
1254+ if ( ! previous . HasValue )
1255+ return ; // first observation: no transition to report
1256+
1257+ var props = new Dictionary < string , object >
1258+ {
1259+ [ "previousStatus" ] = AttStatusToString ( previous . Value ) ,
1260+ [ "newStatus" ] = AttStatusToString ( currentStatus ) ,
1261+ } ;
1262+
1263+ if ( currentStatus == 3 && consent . CanIdentify ( ) )
1264+ {
1265+ try
1266+ {
1267+ var idfa = MobileIDFAProvider ? . Invoke ( ) ;
1268+ if ( ! string . IsNullOrEmpty ( idfa ) )
1269+ props [ "idfa" ] = idfa ! ;
1270+ }
1271+ catch ( Exception ex ) { Log . Warn ( AudienceLogs . ATTIDFAProviderThrew ( ex ) ) ; }
1272+ }
1273+
1274+ Track ( "tracking_authorization_changed" , props ) ;
1275+ }
11851276 }
11861277}
0 commit comments