반응형

앱을 간단하게 버그를 고치고 다시 올렸는데 이번에는 이상하게 한방에 통과를 안시켜주더라.

 

AppTrackingTransparency 문제 발생

요런 문제가 있다고 한다.

 

바이너리가 거부됨

2020년 11월 7일

If your app integrates AppTrackingTransparency, please indicate where in your app we can find the AppTrackingTransparency permission


Guideline 2.1 - Information Needed



We're looking forward to completing the review of your app, but we need more information to continue. Specifically, we noticed that your app uses the AppTrackingTransparency framework, but we haven't been able to locate the relevant AppTrackingTransparency permission requests.

While it is not required to implement AppTrackingTransparency at this time, we check to make sure the implementation is compliant with our guidelines when we detect the framework in an app.

Next Steps

If your app integrates AppTrackingTransparency, please indicate where in your app we can find the AppTrackingTransparency permission request.

If your app does not integrate AppTrackingTransparency, please indicate this information in the Review Notes section for each version of your app in App Store Connect when submitting for review.

Resources
See the app privacy question update.
Learn more about how AppTrackingTransparency protects user's privacy and data.

 

AppTrackingTransparency Pemission requests 를 넣은 기억이 없어서 다시 검색해봤다. 

 

AppTrackingTransparency Pemission requests란? 

iOS 14 부터는 앱에 포함된 광고 등에서 타겟팅 광고를 위한 디바이스 구분을 위한 식별값 IDFA를 사용해도 되는지 사용자에게 묻는 동의다. 딱 한번 요청을 받을 수 있다. 앱에 구현할 경우 아래와 같이 보인다.

이때 사용자가 직접 수정할 수 있는 문구는 저기 빨간 동그라미 안쪽 문구 뿐이다.

자세한 정보는 Prepare for iOS 14+ 에서 확인 가능하다. 

Flutter 에서는 어떻게 설정하나?

사용하는 패키지에 따라 해당 설정이 필요한 경우가 있는데, 내 경우에는 admob_flutter 플러그인 적용 중 포함되었다.

ios/Runner/info.plist  파일을 열어서 아래와 같이 추가되어있을 경우 해당 프레임워크가 포함된다고 한다.

	<key>NSUserTrackingUsageDescription</key>
	<string>This identifier will be used to deliver personalized ads to you.</string>

 

앱 심사는 어떻게??

일단 적용된 스크린샷과 위치를 알려주는 답장을 쓰고 기다리고 있다.

과연 어떻게 될런지?? 

 

-> 하루 뒤 Pass! 

반응형
반응형

구글 admob 을 적용시켜서 테스트를 하다 보면

Failed to load ad: 0 라는 메세지를 받게 된다.

 

원인은 안드로이드/iOS 폰에서 직접 테스트 할 때 정식으로 배포된 앱이 아닌 경우 광고를 불러오지 못해서다.

이때 test로 사용할 장치 device id 를 admob 에 미리 넣어놓으면 해당 id를 가진 장치에서는 test ad가 뜬다.

 

 

Java / Kotlin

device id 를 얻는 방법은 java 와 kotlin 관련 문서가 많다.

 

해당 장치에서 앱을 실행하고 logCat 에서 adRequest 를 검색해보면 아래처럼 나온다고 한다. 여기서 보이는 Device ID를 복사해서 쓰면 된다.

 

 12-29 11:18:25.615: I/Ads(2132): To get test ads on this device, call adRequest.addTestDevice("D9XXXXXXXXXXXXXXXXXXXXXXXXXXXXX");".

 

 

 

Flutter

 

Flutter / 안드로이드 스튜디오 사용중인데 logCat 에서 잘 안보여서 다른 방법을 찾아봤다.

device_id 를 얻어오는 많은 plugin 이 있는데 그중 하나를 골라서 사용하면 된다.

 

아래 plugin을 사용해봤다.

 

 

 

플러그인 설치

패키지를 pubspec.yaml 파일에 넣어주고 pubget 을 해준다.

dependencies:
  device_id: ^0.2.0

 

사용을 원하는 파일에 import 

import 'package:device_id/device_id.dart';

 

예제 코드

아래와 같이 DeviceId.getID 로 deviceID를 얻어와서 사용하면 된다.

import 'package:flutter/material.dart';
import 'package:device_id/device_id.dart';

void main() => runApp(new MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => new _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String deviceID;

  void getDeviceID() async {
    String deviceId = await DeviceId.getID;
    deviceID = 'Device ID is $deviceId';
    print('Device ID is $deviceId');
  }

  @override
  Widget build(BuildContext context) {
    getDeviceID();
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Text('$deviceID'),
        ),
      ),
    );
  }
}

 

반응형
반응형

안드로이드 / iOS 앱을 만들다보면 여러가지 권한을 사용해야 할 때가 있다.

카메라, 저장장치, 위치정보, 메세지, 연락처 등등 여러 정보를 기기에서 받아서 활용하는 경우가 많다.

 

만약 적절한 권한이 없는 경우 디버그 모드에서는 동작하는데 컴파일 후 장치에서 실행하면 오류와 함께 종료되는 경우가 많다.

 

다행히도 flutter 에 적절한 패키지가 있다. 패키지가 있을 경우에는 정말 사용하기 편하다.

 

Permission Handler 5.0.1+1

현시점에서 버전은 5.0.1+1 이다. 

pub.dev/packages/permission_handler

 

permission_handler | Flutter Package

Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions.

pub.dev

 

사용전 설정

안드로이드 

  • pre 1.12 android project 로 업그레이드 후 사용
  • gladdle.properties 파일에 아래와 같이 추가
android.useAndroidX=true
android.enableJetifier=true
  • 'android/app/build.gradle' 파일에서 android sdk 버전 29로 설정
android {
  compileSdkVersion 28
  ...
}

 

iOS

Info.plist 에 권한 추가

아래 링크에서 모든 종류의 권한 예시 확인 가능

github.com/Baseflow/flutter-permission-handler/blob/develop/permission_handler/example/ios/Runner/Info.plist

 

 

사용법

pubspec.yaml 파일 depency 에 추가해준다.

  permission_handler: ^5.0.1+1

사용하려는 dart 파일에 import 추가 후 사용

import 'package:permission_handler/permission_handler.dart';

 

권한 상태 확인(및 종류)

권한 상태는 undetermined, granted, denied, restricted 가 있으며 안드로이드는 추가로 permanentlyDenied 가 있다.

예를 들어 카메라 권한 상태가 궁금하면 Permission.camera.status 로 확인하면 된다.

var status = await Permission.camera.status;
if (status.isUndetermined) {
  // We didn't ask for permission yet.
}

// You can can also directly ask the permission about its status.
if (await Permission.location.isRestricted) {
  // The OS restricts access, for example because of parental controls.
}

 

권한 요청

아래와 같이 Permission.contacts.request() 로 요청하면 된다. 

return 값은 undetermined, granted, denied, restricted 와 같은 값이다.

if (await Permission.contacts.request().isGranted) {
  // Either the permission was already granted before or the user just granted it.
}

// You can request multiple permissions at once.
Map<Permission, PermissionStatus> statuses = await [
  Permission.location,
  Permission.storage,
].request();
print(statuses[Permission.location]);

 

 

사용예시

실제로 사용할 때는 await 가 있기 때문에 async 로 감싸진 클래스(또는 함수) 에서 다뤄야 한다.

위젯 생명주기중 하나인 initState 에서는 Future 클래스가 호출이 안된다. 

이럴때는 WidgetsBinding.instance.addPostFrameCallback((_) { 요기 }  에다가 넣어주면 된다.

 

아래와 같은 함수를 만들어놓고 불러오면 된다.

여기서 카메라 권한 확인용으로 만든 camPermissionIsGranted 변수를 사용했다.

처음엔 false 로 사용 불가능으로 해놓고 권한이 확인되면 true 로 변경하자.

 

bool camPermissionsGranted = false;

Future<bool> CamPermissionIsGranted() async {
  var _status = await Permission.camera.status.isGranted;
  camPermissionsGranted = _status;
  return _status;
}

 

 

권한이 있을때와 없을때 widget 을 어떻게 구현하는지가 처음에 햇갈렸다.

FutureBuilder로 구현해봤는데 뭔가 배보다 배꼽이 큰거같아서 다시 검색해보니 더 쉽고 깔끔한 방법이 있다.

 

예를들어 Scaffold의 body 에 구현한다고 하면 아래 ? : 연산자를 쓰면 쉽다.

그리고 권한 확인이 되면 camPermissionIsGranted 의 값을 바꾸고 동시에 setState로 위젯을 다시 build 하면 된다.

(조건) ? (true인 경우 실행) : (false인 경우 실행) 

return Scaffold(
  body: camPermissionsGranted 
          ? Container(child: 퍼미션 있을경우 실행)
          : Container(child: 퍼미션 없을 경우 실행)

 

아래 예제 맨 아랫줄에서 보면 setState까지 구현된 권한 요청 함수를 볼 수 있다.

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';

void main() => runApp(MyApp());

/// Example Flutter Application demonstrating the functionality of the
/// Permission Handler plugin.
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
          actions: <Widget>[
            IconButton(
              icon: const Icon(Icons.settings),
              onPressed: () async {
                var hasOpened = openAppSettings();
                debugPrint('App Settings opened: ' + hasOpened.toString());
              },
            )
          ],
        ),
        body: Center(
          child: ListView(
              children: Permission.values
                  .where((Permission permission) {
                    if (Platform.isIOS) {
                      return permission != Permission.unknown &&
                          permission != Permission.sms &&
                          //permission != Permission.storage &&
                          permission != Permission.ignoreBatteryOptimizations &&
                          permission != Permission.accessMediaLocation;
                    } else {
                      return permission != Permission.unknown &&
                          permission != Permission.mediaLibrary &&
                          permission != Permission.photos &&
                          permission != Permission.reminders;
                    }
                  })
                  .map((permission) => PermissionWidget(permission))
                  .toList()),
        ),
      ),
    );
  }
}

/// Permission widget which displays a permission and allows users to request
/// the permissions.
class PermissionWidget extends StatefulWidget {
  /// Constructs a [PermissionWidget] for the supplied [Permission].
  const PermissionWidget(this._permission);

  final Permission _permission;

  @override
  _PermissionState createState() => _PermissionState(_permission);
}

class _PermissionState extends State<PermissionWidget> {
  _PermissionState(this._permission);

  final Permission _permission;
  PermissionStatus _permissionStatus = PermissionStatus.undetermined;

  @override
  void initState() {
    super.initState();

    _listenForPermissionStatus();
  }

  void _listenForPermissionStatus() async {
    final status = await _permission.status;
    setState(() => _permissionStatus = status);
  }

  Color getPermissionColor() {
    switch (_permissionStatus) {
      case PermissionStatus.denied:
        return Colors.red;
      case PermissionStatus.granted:
        return Colors.green;
      default:
        return Colors.grey;
    }
  }

  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(_permission.toString()),
      subtitle: Text(
        _permissionStatus.toString(),
        style: TextStyle(color: getPermissionColor()),
      ),
      trailing: IconButton(
          icon: const Icon(Icons.info),
          onPressed: () {
            checkServiceStatus(context, _permission);
          }),
      onTap: () {
        requestPermission(_permission);
      },
    );
  }

  void checkServiceStatus(BuildContext context, Permission permission) async {
    Scaffold.of(context).showSnackBar(SnackBar(
      content: Text((await permission.status).toString()),
    ));
  }

  Future<void> requestPermission(Permission permission) async {
    final status = await permission.request();

    setState(() {
      print(status);
      _permissionStatus = status;
      print(_permissionStatus);
    });
  }
}

 

 

추가로 안드로이드의 경우 권한 수락을 완전히 거부할 경우 있는데 그럴때는 앱정보에 들어가서 직접 권한을 줘야 한다.

아래 루틴을 requestPermission 에 넣어주면 좋다.

if (await Permission.speech.isPermanentlyDenied) {
  // 유저가 완전히 권한을 거부하였을 경우 앱설정으로 진입
  // 유저가 알아보기 쉽게 도움말을 띄우면 더 좋음
  openAppSettings();
}

 

이상 끝.

반응형

+ Recent posts