zoontek / react-native-permissions

An unified permissions API for React Native on iOS, Android and Windows.
MIT License
4.04k stars 828 forks source link

App tracking transparency request(PERMISSIONS.IOS.APP_TRACKING_TRANSPARENCY) is returning denied #823

Closed jwoodmansey closed 10 months ago

jwoodmansey commented 11 months ago

Bug summary

We are calling request(PERMISSIONS.IOS.APP_TRACKING_TRANSPARENCY) and immediately logging the result for tracking purposes. Since iOS 17 we are seeing a large increase in these logs returning denied. According to the flowchart in this repo the request call should only return granted/blocked?

image

Library version

3.10.1

Environment info

System:
    OS: macOS 13.5.2
    CPU: (8) arm64 Apple M1 Pro
    Memory: 98.44 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 16.13.0 - /var/folders/w7/87j7w3155m79j7lzh_pxqr8r0000gn/T/yarn--1699288369076-0.3305727072937681/node
    Yarn: 1.22.17 - /var/folders/w7/87j7w3155m79j7lzh_pxqr8r0000gn/T/yarn--1699288369076-0.3305727072937681/yarn
    npm: 8.1.0 - ~/.nvm/versions/node/v16.13.0/bin/npm
    Watchman: 2022.03.21.00 - /opt/homebrew/bin/watchman
  Managers:
    CocoaPods: Not Found
  SDKs:
    iOS SDK:
      Platforms: DriverKit 22.4, iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4
    Android SDK: Not Found
  IDEs:
    Android Studio: 2022.3 AI-223.8836.35.2231.10811636
    Xcode: 14.3/14E222b - /usr/bin/xcodebuild
  Languages:
    Java: 17.0.6 - /usr/bin/javac
  npmPackages:
    @react-native-community/cli: Not Found
    react: 18.2.0 => 18.2.0 
    react-native: 0.71.13 => 0.71.13 
    react-native-macos: Not Found
  npmGlobalPackages:
    *react-native*: Not Found

Steps to reproduce

  1. Install library with set up
    setup_permissions([
    'AppTrackingTransparency',
    # 'BluetoothPeripheral',
    # 'Calendars',
    'Camera',
    'Contacts',
    'FaceID',
    # 'LocationAccuracy',
    # 'LocationAlways',
    # 'LocationWhenInUse',
    # 'MediaLibrary',
    # 'Microphone',
    # 'Motion',
    'Notifications',
    'PhotoLibrary',
    'PhotoLibraryAddOnly',
    # 'Reminders',
    'Siri',
    # 'SpeechRecognition',
    # 'StoreKit',
    ])
  2. Call await request(PERMISSIONS.IOS.APP_TRACKING_TRANSPARENCY);
  3. Response can be 'denied'?

Reproducible sample code

import { check, PERMISSIONS, request } from 'react-native-permissions';

...
          const response = await request(
            PERMISSIONS.IOS.APP_TRACKING_TRANSPARENCY
          );
          //response is logging here as denied
zoontek commented 11 months ago

This indeed makes no sense, as denied is returned when the status is ATTrackingManagerAuthorizationStatusNotDetermined, and it should not be possible after a request.

jwoodmansey commented 10 months ago

My colleague has patched this, which seems to bring our analytics back in line with what we'd expect. After this patch we are now back to only getting blocked or granted returned from request, at the ratio we would expect. I'm not entirely how this fixed it, but sharing their code below...

diff --git a/node_modules/react-native-permissions/ios/AppTrackingTransparency/RNPermissionHandlerAppTrackingTransparency.m b/node_modules/react-native-permissions/ios/AppTrackingTransparency/RNPermissionHandlerAppTrackingTransparency.m
index c492b3f..46f3e8b 100644
--- a/node_modules/react-native-permissions/ios/AppTrackingTransparency/RNPermissionHandlerAppTrackingTransparency.m
+++ b/node_modules/react-native-permissions/ios/AppTrackingTransparency/RNPermissionHandlerAppTrackingTransparency.m
@@ -50,13 +50,22 @@ - (void)requestWithResolver:(void (^ _Nonnull)(RNPermissionStatus))resolve
     }

     if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateActive) {
-      [ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(__unused ATTrackingManagerAuthorizationStatus status) {
-        [self checkWithResolver:resolve rejecter:reject];
+
+      [ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
+          switch (status) {
+            case ATTrackingManagerAuthorizationStatusNotDetermined:
+              return resolve(RNPermissionStatusNotDetermined);
+            case ATTrackingManagerAuthorizationStatusRestricted:
+              return resolve(RNPermissionStatusRestricted);
+            case ATTrackingManagerAuthorizationStatusDenied:
+              return resolve(RNPermissionStatusDenied);
+            case ATTrackingManagerAuthorizationStatusAuthorized:
+              return resolve(RNPermissionStatusAuthorized);
+          }
       }];
     } else {
       _resolve = resolve;
       _reject = reject;
-
       [[NSNotificationCenter defaultCenter] addObserver:self
                                                selector:@selector(onApplicationDidBecomeActive:)
                                                    name:UIApplicationDidBecomeActiveNotification
@@ -71,10 +80,18 @@ - (void)onApplicationDidBecomeActive:(__unused NSNotification *)notification {
   [[NSNotificationCenter defaultCenter] removeObserver:self
                                                   name:UIApplicationDidBecomeActiveNotification
                                                 object:nil];
-
   if (@available(iOS 14.0, *)) {
-    [ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(__unused ATTrackingManagerAuthorizationStatus status) {
-      [self checkWithResolver:self->_resolve rejecter:self->_reject];
+    [ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
+        switch (status) {
+          case ATTrackingManagerAuthorizationStatusNotDetermined:
+            return self->_resolve(RNPermissionStatusNotDetermined);
+          case ATTrackingManagerAuthorizationStatusRestricted:
+            return self->_resolve(RNPermissionStatusRestricted);
+          case ATTrackingManagerAuthorizationStatusDenied:
+            return self->_resolve(RNPermissionStatusDenied);
+          case ATTrackingManagerAuthorizationStatusAuthorized:
+            return self->_resolve(RNPermissionStatusAuthorized);
+        }
     }];
   }
 }
mikehardy commented 10 months ago

For that patch to be effective, either the "ios14 is not available" branch is taken in checkWithResolver resulting in nothing but authorized or denied:

https://github.com/zoontek/react-native-permissions/blob/63b695a609c1fb00b7611d98e7b4658e816d8788/ios/AppTrackingTransparency/RNPermissionHandlerAppTrackingTransparency.m#L36-L41

or the ios14-is-available branch is taken and the dynamic call for tracking status is somehow returning something different then the status sent in via the callback sent in with the request?

Here's the dynamic callback: https://github.com/zoontek/react-native-permissions/blob/63b695a609c1fb00b7611d98e7b4658e816d8788/ios/AppTrackingTransparency/RNPermissionHandlerAppTrackingTransparency.m#L26

...because the result of that callback is doing the same switch you've effectively inlined in the callback definition

It would be very interesting to know which of the ios14-is-available conditional branches were used and to know if the dynamic call for status was somehow different then the status provided as a parameter to the callback.

You have a working solution which you likely don't want to mess with and possibly cause a regression, but it could be possible to maybe call check instead of request in addition to see what check says - emulating the dynamic call. Is it safe to assume ios14-is-available is returning the correct value? I think so

That all seems unexpected though, I'm pretty confused how this is happening, doesn't seem like it should unless the request status vs the dynamic check status is racy somehow in ios17 with check providing a stale value for some unknown amount of time while the callback status is fresh 🤷

zoontek commented 10 months ago

Closed as v4.0.0 is out.