blaugold / iabtcf_consent_info

Flutter plugin for reading IAB TCF v2.0 user consent information, such as made available through CMP SDKs, like Funding Choices's User Messaging Platform (UMP).
https://pub.dev/packages/iabtcf_consent_info
10 stars 8 forks source link

Type cast exception in ConsentInfo.parseRawInfo #3

Closed kaboc closed 3 years ago

kaboc commented 3 years ago

A wrong type cast from Null to int causes an exception in the ConsentInfo.parseRawInfo factory.

sdkVersion: rawInfo[_cmpSdkVersionKey] as int

It looks like it is thrown in this line.

Try-catch like below can't catch it and the app stops there, which is critical...

late final ConsentInfo? info;
try {
  info = await IabtcfConsentInfo.instance.currentConsentInfo();
} on Exception catch (e) {
  info = null;
}

I'm not 100% sure in what condition it occurs, but as far as I can see, there's no problem if a GDPR dialog is shown outside EEA using your "user_messaging_platform" plugin with the debug options (geography: DebugGeography.EEA, testDeviceIds: xxxxx) set, and the exception occurs when currentConsentInfo() is executed after the dialog is skipped without the debug options.

E/flutter (18850): [ERROR:flutter/lib/ui/ui_dart_state.cc(199)] Unhandled Exception: type 'Null' is not a subtype of type 'int' in type cast
E/flutter (18850): #0      new ConsentInfo.parseRawInfo (package:iabtcf_consent_info/iabtcf_consent_info.dart:112:46)
E/flutter (18850): #1      IabtcfConsentInfo._onConsentInfoListen.<anonymous closure> (package:iabtcf_consent_info/iabtcf_consent_info.dart:206:31)
E/flutter (18850): #2      _MapStream._handleData (dart:async/stream_pipe.dart:213:31)
E/flutter (18850): #3      _ForwardingStreamSubscription._handleData (dart:async/stream_pipe.dart:153:13)
E/flutter (18850): #4      _rootRunUnary (dart:async/zone.dart:1362:47)
E/flutter (18850): #5      _CustomZone.runUnary (dart:async/zone.dart:1265:19)
E/flutter (18850): #6      _CustomZone.runUnaryGuarded (dart:async/zone.dart:1170:7)
E/flutter (18850): #7      _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:341:11)
E/flutter (18850): #8      _BufferingStreamSubscription._add (dart:async/stream_impl.dart:271:7)
E/flutter (18850): #9      _ForwardingStreamSubscription._add (dart:async/stream_pipe.dart:123:11)
E/flutter (18850): #10     _MapStream._handleData (dart:async/stream_pipe.dart:218:10)
E/flutter (18850): #11     _ForwardingStreamSubscription._handleData (dart:async/stream_pipe.dart:153:13)
E/flutter (18850): #12     _rootRunUnary (dart:async/zone.dart:1362:47)
E/flutter (18850): #13     _CustomZone.runUnary (dart:async/zone.dart:1265:19)
E/flutter (18850): #14     _CustomZone.runUnaryGuarded (dart:async/zone.dart:1170:7)
E/flutter (18850): #15     _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:341:11)
E/flutter (18850): #16     _DelayedData.perform (dart:async/stream_impl.dart:591:14)
E/flutter (18850): #17     _StreamImplEvents.handleNext (dart:async/stream_impl.dart:706:11)
E/flutter (18850): #18     _PendingEvents.schedule.<anonymous closure> (dart:async/stream_impl.dart:663:7)
E/flutter (18850): #19     _rootRun (dart:async/zone.dart:1346:47)
E/flutter (18850): #20     _CustomZone.run (dart:async/zone.dart:1258:19)
E/flutter (18850): #21     _CustomZone.runGuarded (dart:async/zone.dart:1162:7)
E/flutter (18850): #22     _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:1202:23)
E/flutter (18850): #23     _rootRun (dart:async/zone.dart:1354:13)
E/flutter (18850): #24     _CustomZone.run (dart:async/zone.dart:1258:19)
E/flutter (18850): #25     _CustomZone.runGuarded (dart:async/zone.dart:1162:7)
E/flutter (18850): #26     _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:1202:23)
E/flutter (18850): #27     _microtaskLoop (dart:async/schedule_microtask.dart:40:21)
E/flutter (18850): #28     _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5)
E/flutter (18850): 
blaugold commented 3 years ago

Hi @kaboc, thanks for reporting this issue.

Seams like the library is not properly forwarding exceptions to currentConsentInfo, which causes an Unhandled Exception, instead of throwing.

The other issue is that cmpSdkVersion is null even though cmpSdkID is not.

kaboc commented 3 years ago

@blaugold Is there any workaround I can do on my side before the package is improved?

blaugold commented 3 years ago

Not really, but I'll publish a fix today.

blaugold commented 3 years ago

Fixed by 5cfacff78d01347182f53ffc698b41564aa9af1a.

iabtcf_consent_info: ^1.3.3 has been published with the fix.

kaboc commented 3 years ago

@blaugold Thanks for the amazingly quick fix, but the unhandled exception still occurs on the same line. flutter clean didn't change the result.

Here's a reproducible code. I confirmed the issue on Android 5.1.1 and 10.

import 'package:flutter/material.dart';

import 'package:iabtcf_consent_info/iabtcf_consent_info.dart';
import 'package:user_messaging_platform/user_messaging_platform.dart';

void main() {
  runApp(const App());
}

class App extends StatefulWidget {
  const App();

  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<App> {
  @override
  void initState() {
    super.initState();
    _showConsentDialog();
  }

  Future<void> _showConsentDialog() async {
    // Simulates that you're not in EEA fordebugging
    final settings = ConsentDebugSettings(
      geography: DebugGeography.notEEA,
      testDeviceIds: ['YOUR_TEST_DEVICE_ID'],
    );

    final info = await UserMessagingPlatform.instance.requestConsentInfoUpdate(
      ConsentRequestParameters(debugSettings: settings),
    );
    print(info);  // ConsentInformation(consentStatus: ConsentStatus.notRequired, formStatus: FormStatus.unavailable)

    if (info.consentStatus == ConsentStatus.required) {
      // The consent dialog is not shown because you're outside of EEA
      await UserMessagingPlatform.instance.showConsentForm();
    }

    final newInfo = await IabtcfConsentInfo.instance.currentConsentInfo();
    print(newInfo);
  }

  @override
  Widget build(BuildContext context) {
    return const SizedBox.shrink();
  }
}

It doesn't occur if _showConsentDialog() is as simple as below.

  Future<void> _showConsentDialog() async {
    final newInfo = await IabtcfConsentInfo.instance.currentConsentInfo();
    print(newInfo);  // null
  }
blaugold commented 3 years ago

Thanks for the example. I should have read the TCF spec more closely. When GDPR does not apply, the CMP only sets a few fields. I think an expected behavior would be to return null from currentConsentInfo when GDPR does not apply.

blaugold commented 3 years ago

iabtcf_consent_info: ^2.0.0 is the latest release, which contains a breaking change to properly represent the situation where the GDPR does not apply. When no CMP SDK is active, currentConsentInfo returns null. If GDPR does not apply, or it has not been determined yet whether it does, BasicConsentInfo is returned. When full consent info is available, ConsentInfo is returned.