diff --git a/.github/workflows/google_mobile_ads.yaml b/.github/workflows/google_mobile_ads.yaml index 9240f9069..3d5f5d1a0 100644 --- a/.github/workflows/google_mobile_ads.yaml +++ b/.github/workflows/google_mobile_ads.yaml @@ -72,7 +72,7 @@ jobs: flutter clean flutter pub get pod install - xcodebuild -configuration Debug -resultBundlePath TestResults VERBOSE_SCRIPT_LOGGING=YES -workspace Runner.xcworkspace -scheme Runner -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 14 Pro,OS=16.2' test + xcodebuild -configuration Debug -resultBundlePath TestResults VERBOSE_SCRIPT_LOGGING=YES -workspace Runner.xcworkspace -scheme Runner -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' test - uses: actions/upload-artifact@v4 if: failure() with: diff --git a/packages/google_mobile_ads/CHANGELOG.md b/packages/google_mobile_ads/CHANGELOG.md index 250ff01a9..e4c934d40 100644 --- a/packages/google_mobile_ads/CHANGELOG.md +++ b/packages/google_mobile_ads/CHANGELOG.md @@ -1,3 +1,7 @@ +## Next Version +* Adds support for APIs from the [Android](https://developers.google.com/admob/android/privacy/release-notes) UMP SDK version 2.2.0. +* Adds support for APIs from the [iOS](https://developers.google.com/admob/ios/privacy/download#release_notes) UMP SDK version 2.4.0. + ## 5.0.0 * Adds `MediationExtras` class to include parameters when using mediation through the implementation of `FlutterMediationExtras` in Android and `FlutterMediationExtras` in iOS. * Deprecates `MediationNetworkExtrasProvider` and `FLTMediationNetworkExtrasProvider`. diff --git a/packages/google_mobile_ads/android/build.gradle b/packages/google_mobile_ads/android/build.gradle index 0b951f9b6..4399d31d9 100644 --- a/packages/google_mobile_ads/android/build.gradle +++ b/packages/google_mobile_ads/android/build.gradle @@ -36,7 +36,7 @@ android { } dependencies { api 'com.google.android.gms:play-services-ads:23.0.0' - implementation 'com.google.android.ump:user-messaging-platform:2.1.0' + implementation 'com.google.android.ump:user-messaging-platform:2.2.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.lifecycle:lifecycle-process:2.6.2' implementation 'com.google.errorprone:error_prone_annotations:2.16' diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/usermessagingplatform/UserMessagingCodec.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/usermessagingplatform/UserMessagingCodec.java index 04f3a6497..88aba3e00 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/usermessagingplatform/UserMessagingCodec.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/usermessagingplatform/UserMessagingCodec.java @@ -17,6 +17,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.ump.ConsentForm; +import com.google.android.ump.FormError; import io.flutter.plugin.common.StandardMessageCodec; import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; @@ -31,6 +32,7 @@ public class UserMessagingCodec extends StandardMessageCodec { private static final byte VALUE_CONSENT_REQUEST_PARAMETERS = (byte) 129; private static final byte VALUE_CONSENT_DEBUG_SETTINGS = (byte) 130; private static final byte VALUE_CONSENT_FORM = (byte) 131; + private static final byte VALUE_FORM_ERROR = (byte) 132; private final Map consentFormMap; @@ -53,6 +55,11 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, @NonNull Object } else if (value instanceof ConsentForm) { stream.write(VALUE_CONSENT_FORM); writeValue(stream, value.hashCode()); + } else if (value instanceof FormError) { + stream.write(VALUE_FORM_ERROR); + FormError formError = (FormError) value; + writeValue(stream, formError.getErrorCode()); + writeValue(stream, formError.getMessage()); } else { super.writeValue(stream, value); } @@ -96,6 +103,12 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { Integer hash = (Integer) readValueOfType(buffer.get(), buffer); return consentFormMap.get(hash); } + case VALUE_FORM_ERROR: + { + Integer errorCode = (Integer) readValueOfType(buffer.get(), buffer); + String errorMessage = (String) readValueOfType(buffer.get(), buffer); + return new FormError(errorCode, errorMessage); + } default: return super.readValueOfType(type, buffer); } diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/usermessagingplatform/UserMessagingPlatformManager.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/usermessagingplatform/UserMessagingPlatformManager.java index 234e2c25d..dde127149 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/usermessagingplatform/UserMessagingPlatformManager.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/usermessagingplatform/UserMessagingPlatformManager.java @@ -131,6 +131,35 @@ public void onConsentInfoUpdateFailure(FormError error) { }); break; } + case "ConsentInformation#canRequestAds": + result.success(getConsentInformation().canRequestAds()); + break; + case "ConsentInformation#getPrivacyOptionsRequirementStatus": + switch (getConsentInformation().getPrivacyOptionsRequirementStatus()) { + case NOT_REQUIRED: + result.success(0); + break; + case REQUIRED: + result.success(1); + break; + default: + result.success(2); + } + break; + case "UserMessagingPlatform#loadAndShowConsentFormIfRequired": + if (activity == null) { + result.error( + INTERNAL_ERROR_CODE, + "UserMessagingPlatform#loadAndShowConsentFormIfRequired called before plugin has been registered to an activity.", + null); + break; + } + UserMessagingPlatform.loadAndShowConsentFormIfRequired( + activity, + loadAndShowError -> { + result.success(loadAndShowError); + }); + break; case "UserMessagingPlatform#loadConsentForm": UserMessagingPlatform.loadConsentForm( context, @@ -149,6 +178,20 @@ public void onConsentFormLoadFailure(FormError formError) { } }); break; + case "UserMessagingPlatform#showPrivacyOptionsForm": + if (activity == null) { + result.error( + INTERNAL_ERROR_CODE, + "UserMessagingPlatform#showPrivacyOptionsForm called before plugin has been registered to an activity.", + null); + break; + } + UserMessagingPlatform.showPrivacyOptionsForm( + activity, + loadAndShowError -> { + result.success(loadAndShowError); + }); + break; case "ConsentInformation#isConsentFormAvailable": { result.success(getConsentInformation().isConsentFormAvailable()); diff --git a/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/usermessagingplatform/UserMessagingCodecTest.java b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/usermessagingplatform/UserMessagingCodecTest.java index 8a03e8f84..ad6e1c751 100644 --- a/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/usermessagingplatform/UserMessagingCodecTest.java +++ b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/usermessagingplatform/UserMessagingCodecTest.java @@ -19,6 +19,7 @@ import static org.mockito.Mockito.mock; import com.google.android.ump.ConsentForm; +import com.google.android.ump.FormError; import java.nio.ByteBuffer; import java.util.Collections; import java.util.List; @@ -130,4 +131,16 @@ public void testConsentForm() { decoded = (ConsentForm) codec.decodeMessage((ByteBuffer) message.position(0)); assertNull(decoded); } + + @Test + public void testFormError() { + FormError formError = new FormError(123, "testMessage"); + + final ByteBuffer message = codec.encodeMessage(formError); + FormError decoded = (FormError) codec.decodeMessage((ByteBuffer) message.position(0)); + + assert decoded != null; + assertEquals(formError.getErrorCode(), decoded.getErrorCode()); + assertEquals(formError.getMessage(), decoded.getMessage()); + } } diff --git a/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/usermessagingplatform/UserMessagingPlatformManagerTest.java b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/usermessagingplatform/UserMessagingPlatformManagerTest.java index 78e959827..7c5782488 100644 --- a/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/usermessagingplatform/UserMessagingPlatformManagerTest.java +++ b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/usermessagingplatform/UserMessagingPlatformManagerTest.java @@ -30,6 +30,7 @@ import com.google.android.ump.ConsentInformation.ConsentStatus; import com.google.android.ump.ConsentInformation.OnConsentInfoUpdateFailureListener; import com.google.android.ump.ConsentInformation.OnConsentInfoUpdateSuccessListener; +import com.google.android.ump.ConsentInformation.PrivacyOptionsRequirementStatus; import com.google.android.ump.ConsentRequestParameters; import com.google.android.ump.FormError; import com.google.android.ump.UserMessagingPlatform; @@ -170,6 +171,59 @@ public void testConsentInformation_isConsentFormAvailable() { verify(result).success(eq(false)); } + @Test + public void testConsentInformation_canRequestAds() { + doReturn(true).when(mockConsentInformation).canRequestAds(); + MethodCall methodCall = new MethodCall("ConsentInformation#canRequestAds", null); + Result result = mock(Result.class); + + manager.onMethodCall(methodCall, result); + + verify(result).success(eq(true)); + } + + @Test + public void testConsentInformation_getPrivacyOptionsRequirementStatus_notRequiredReturns0() { + doReturn(PrivacyOptionsRequirementStatus.NOT_REQUIRED) + .when(mockConsentInformation) + .getPrivacyOptionsRequirementStatus(); + MethodCall methodCall = + new MethodCall("ConsentInformation#getPrivacyOptionsRequirementStatus", null); + Result result = mock(Result.class); + + manager.onMethodCall(methodCall, result); + + verify(result).success(eq(0)); + } + + @Test + public void testConsentInformation_getPrivacyOptionsRequirementStatus_requiredReturns1() { + doReturn(PrivacyOptionsRequirementStatus.REQUIRED) + .when(mockConsentInformation) + .getPrivacyOptionsRequirementStatus(); + MethodCall methodCall = + new MethodCall("ConsentInformation#getPrivacyOptionsRequirementStatus", null); + Result result = mock(Result.class); + + manager.onMethodCall(methodCall, result); + + verify(result).success(eq(1)); + } + + @Test + public void testConsentInformation_getPrivacyOptionsRequirementStatus_requiredReturns2() { + doReturn(PrivacyOptionsRequirementStatus.UNKNOWN) + .when(mockConsentInformation) + .getPrivacyOptionsRequirementStatus(); + MethodCall methodCall = + new MethodCall("ConsentInformation#getPrivacyOptionsRequirementStatus", null); + Result result = mock(Result.class); + + manager.onMethodCall(methodCall, result); + + verify(result).success(eq(2)); + } + @Test public void testUserMessagingPlatform_loadConsentFormAndDispose() { MethodCall methodCall = new MethodCall("UserMessagingPlatform#loadConsentForm", null); @@ -204,6 +258,78 @@ public void testUserMessagingPlatform_loadConsentFormAndDispose() { verify(result).success(null); } + @Test + public void testUserMessagingPlatform_loadAndShowConsentFormIfRequired() { + manager.setActivity(activity); + MethodCall methodCall = + new MethodCall("UserMessagingPlatform#loadAndShowConsentFormIfRequired", null); + Result result = mock(Result.class); + + manager.onMethodCall(methodCall, result); + + ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(OnConsentFormDismissedListener.class); + mockedUmp.verify( + () -> + UserMessagingPlatform.loadAndShowConsentFormIfRequired( + eq(activity), listenerCaptor.capture())); + listenerCaptor.getValue().onConsentFormDismissed(null); + verify(result).success(isNull()); + } + + @Test + public void testUserMessagingPlatform_loadAndShowConsentFormIfRequired_withFormError() { + manager.setActivity(activity); + FormError mockFormError = mock(FormError.class); + MethodCall methodCall = + new MethodCall("UserMessagingPlatform#loadAndShowConsentFormIfRequired", null); + Result result = mock(Result.class); + + manager.onMethodCall(methodCall, result); + + ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(OnConsentFormDismissedListener.class); + mockedUmp.verify( + () -> + UserMessagingPlatform.loadAndShowConsentFormIfRequired( + eq(activity), listenerCaptor.capture())); + listenerCaptor.getValue().onConsentFormDismissed(mockFormError); + verify(result).success(mockFormError); + } + + @Test + public void testUserMessagingPlatform_showPrivacyOptionsForm() { + manager.setActivity(activity); + MethodCall methodCall = new MethodCall("UserMessagingPlatform#showPrivacyOptionsForm", null); + Result result = mock(Result.class); + + manager.onMethodCall(methodCall, result); + + ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(OnConsentFormDismissedListener.class); + mockedUmp.verify( + () -> UserMessagingPlatform.showPrivacyOptionsForm(eq(activity), listenerCaptor.capture())); + listenerCaptor.getValue().onConsentFormDismissed(null); + verify(result).success(isNull()); + } + + @Test + public void testUserMessagingPlatform_showPrivacyOptionsForm_withFormError() { + manager.setActivity(activity); + FormError mockFormError = mock(FormError.class); + MethodCall methodCall = new MethodCall("UserMessagingPlatform#showPrivacyOptionsForm", null); + Result result = mock(Result.class); + + manager.onMethodCall(methodCall, result); + + ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(OnConsentFormDismissedListener.class); + mockedUmp.verify( + () -> UserMessagingPlatform.showPrivacyOptionsForm(eq(activity), listenerCaptor.capture())); + listenerCaptor.getValue().onConsentFormDismissed(mockFormError); + verify(result).success(mockFormError); + } + @Test public void testConsentForm_show() { manager.setActivity(activity); diff --git a/packages/google_mobile_ads/example/ios/RunnerTests/FLTUserMessagingPlatformManagerTest.m b/packages/google_mobile_ads/example/ios/RunnerTests/FLTUserMessagingPlatformManagerTest.m index 020c14a09..d05146a23 100644 --- a/packages/google_mobile_ads/example/ios/RunnerTests/FLTUserMessagingPlatformManagerTest.m +++ b/packages/google_mobile_ads/example/ios/RunnerTests/FLTUserMessagingPlatformManagerTest.m @@ -81,6 +81,75 @@ - (void)testGetConsentStatus { OCMVerify([mockUmpConsentInformation consentStatus]); } +- (void)testCanRequestAds_yes { + FlutterMethodCall *methodCall = [FlutterMethodCall + methodCallWithMethodName:@"ConsentInformation#canRequestAds" + arguments:@{}]; + OCMStub([mockUmpConsentInformation canRequestAds]).andReturn(YES); + + [umpManager handleMethodCall:methodCall result:flutterResult]; + + XCTAssertTrue(resultInvoked); + XCTAssertEqual(returnedResult, [[NSNumber alloc] initWithBool:YES]); +} + +- (void)testCanRequestAds_no { + FlutterMethodCall *methodCall = [FlutterMethodCall + methodCallWithMethodName:@"ConsentInformation#canRequestAds" + arguments:@{}]; + OCMStub([mockUmpConsentInformation canRequestAds]).andReturn(NO); + + [umpManager handleMethodCall:methodCall result:flutterResult]; + + XCTAssertTrue(resultInvoked); + XCTAssertEqual(returnedResult, [[NSNumber alloc] initWithBool:NO]); +} + +- (void)testGetPrivacyOptionsRequirementStatus_notRequiredReturns0 { + FlutterMethodCall *methodCall = [FlutterMethodCall + methodCallWithMethodName: + @"ConsentInformation#getPrivacyOptionsRequirementStatus" + arguments:@{}]; + OCMStub([mockUmpConsentInformation privacyOptionsRequirementStatus]) + .andReturn(UMPPrivacyOptionsRequirementStatusNotRequired); + + [umpManager handleMethodCall:methodCall result:flutterResult]; + + XCTAssertTrue(resultInvoked); + NSNumber *expected = [[NSNumber alloc] initWithInt:0]; + XCTAssertEqual(returnedResult, expected); +} + +- (void)testGetPrivacyOptionsRequirementStatus_requiredReturns1 { + FlutterMethodCall *methodCall = [FlutterMethodCall + methodCallWithMethodName: + @"ConsentInformation#getPrivacyOptionsRequirementStatus" + arguments:@{}]; + OCMStub([mockUmpConsentInformation privacyOptionsRequirementStatus]) + .andReturn(UMPPrivacyOptionsRequirementStatusRequired); + + [umpManager handleMethodCall:methodCall result:flutterResult]; + + XCTAssertTrue(resultInvoked); + NSNumber *expected = [[NSNumber alloc] initWithInt:1]; + XCTAssertEqual(returnedResult, expected); +} + +- (void)testGetPrivacyOptionsRequirementStatus_unknownReturns2 { + FlutterMethodCall *methodCall = [FlutterMethodCall + methodCallWithMethodName: + @"ConsentInformation#getPrivacyOptionsRequirementStatus" + arguments:@{}]; + OCMStub([mockUmpConsentInformation privacyOptionsRequirementStatus]) + .andReturn(UMPPrivacyOptionsRequirementStatusUnknown); + + [umpManager handleMethodCall:methodCall result:flutterResult]; + + XCTAssertTrue(resultInvoked); + NSNumber *expected = [[NSNumber alloc] initWithInt:2]; + XCTAssertEqual(returnedResult, expected); +} + - (void)testRequestConsentInfoUpdate_success { UMPRequestParameters *params = OCMClassMock([UMPRequestParameters class]); @@ -136,6 +205,61 @@ - (void)testRequestConsentInfoUpdate_error { XCTAssertEqualObjects(resultError.message, @"description"); } +- (void)testLoadAndShowConsentFormIfRequired_success { + UMPRequestParameters *params = OCMClassMock([UMPRequestParameters class]); + FlutterMethodCall *methodCall = [FlutterMethodCall + methodCallWithMethodName: + @"UserMessagingPlatform#loadAndShowConsentFormIfRequired" + arguments:@{@"params" : params}]; + id mockUmpConsentForm = OCMClassMock([UMPConsentForm class]); + OCMStub([mockUmpConsentForm + loadAndPresentIfRequiredFromViewController:[OCMArg any] + completionHandler:[OCMArg any]]) + .andDo(^(NSInvocation *invocation) { + void (^completionHandler)(NSError *loadError); + [invocation getArgument:&completionHandler atIndex:3]; + completionHandler(nil); + }); + + [umpManager handleMethodCall:methodCall result:flutterResult]; + + XCTAssertTrue(resultInvoked); + XCTAssertNil(returnedResult); + OCMVerify([mockUmpConsentForm + loadAndPresentIfRequiredFromViewController:[OCMArg any] + completionHandler:[OCMArg any]]); +} + +- (void)testLoadAndShowConsentFormIfRequired_error { + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : @"description"}; + NSError *error = [NSError errorWithDomain:@"domain" code:1 userInfo:userInfo]; + UMPRequestParameters *params = OCMClassMock([UMPRequestParameters class]); + FlutterMethodCall *methodCall = [FlutterMethodCall + methodCallWithMethodName: + @"UserMessagingPlatform#loadAndShowConsentFormIfRequired" + arguments:@{@"params" : params}]; + id mockUmpConsentForm = OCMClassMock([UMPConsentForm class]); + OCMStub([mockUmpConsentForm + loadAndPresentIfRequiredFromViewController:[OCMArg any] + completionHandler:[OCMArg any]]) + .andDo(^(NSInvocation *invocation) { + void (^completionHandler)(NSError *loadError); + [invocation getArgument:&completionHandler atIndex:3]; + completionHandler(error); + }); + + [umpManager handleMethodCall:methodCall result:flutterResult]; + + XCTAssertTrue(resultInvoked); + FlutterError *resultError = (FlutterError *)returnedResult; + XCTAssertEqualObjects(resultError.code, @"1"); + XCTAssertEqualObjects(resultError.details, @"domain"); + XCTAssertEqualObjects(resultError.message, @"description"); + OCMVerify([mockUmpConsentForm + loadAndPresentIfRequiredFromViewController:[OCMArg any] + completionHandler:[OCMArg any]]); +} + - (void)testLoadConsentForm_successAndDispose { FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"UserMessagingPlatform#loadConsentForm" @@ -187,6 +311,59 @@ - (void)testLoadConsentForm_error { XCTAssertEqualObjects(resultError.message, @"description"); } +- (void)testShowPrivacyOptionsForm_success { + UMPRequestParameters *params = OCMClassMock([UMPRequestParameters class]); + FlutterMethodCall *methodCall = [FlutterMethodCall + methodCallWithMethodName:@"UserMessagingPlatform#showPrivacyOptionsForm" + arguments:@{@"params" : params}]; + id mockUmpConsentForm = OCMClassMock([UMPConsentForm class]); + OCMStub([mockUmpConsentForm + presentPrivacyOptionsFormFromViewController:[OCMArg any] + completionHandler:[OCMArg any]]) + .andDo(^(NSInvocation *invocation) { + void (^completionHandler)(NSError *loadError); + [invocation getArgument:&completionHandler atIndex:3]; + completionHandler(nil); + }); + + [umpManager handleMethodCall:methodCall result:flutterResult]; + + XCTAssertTrue(resultInvoked); + XCTAssertNil(returnedResult); + OCMVerify([mockUmpConsentForm + presentPrivacyOptionsFormFromViewController:[OCMArg any] + completionHandler:[OCMArg any]]); +} + +- (void)testShowPrivacyOptionsForm_error { + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : @"description"}; + NSError *error = [NSError errorWithDomain:@"domain" code:1 userInfo:userInfo]; + UMPRequestParameters *params = OCMClassMock([UMPRequestParameters class]); + FlutterMethodCall *methodCall = [FlutterMethodCall + methodCallWithMethodName:@"UserMessagingPlatform#showPrivacyOptionsForm" + arguments:@{@"params" : params}]; + id mockUmpConsentForm = OCMClassMock([UMPConsentForm class]); + OCMStub([mockUmpConsentForm + presentPrivacyOptionsFormFromViewController:[OCMArg any] + completionHandler:[OCMArg any]]) + .andDo(^(NSInvocation *invocation) { + void (^completionHandler)(NSError *loadError); + [invocation getArgument:&completionHandler atIndex:3]; + completionHandler(error); + }); + + [umpManager handleMethodCall:methodCall result:flutterResult]; + + XCTAssertTrue(resultInvoked); + FlutterError *resultError = (FlutterError *)returnedResult; + XCTAssertEqualObjects(resultError.code, @"1"); + XCTAssertEqualObjects(resultError.details, @"domain"); + XCTAssertEqualObjects(resultError.message, @"description"); + OCMVerify([mockUmpConsentForm + presentPrivacyOptionsFormFromViewController:[OCMArg any] + completionHandler:[OCMArg any]]); +} + - (void)testIsConsentFormAvailable_available { FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"ConsentInformation#isConsentFormAvailable" diff --git a/packages/google_mobile_ads/ios/Classes/UserMessagingPlatform/FLTUserMessagingPlatformManager.m b/packages/google_mobile_ads/ios/Classes/UserMessagingPlatform/FLTUserMessagingPlatformManager.m index ab5309680..4201b34d9 100644 --- a/packages/google_mobile_ads/ios/Classes/UserMessagingPlatform/FLTUserMessagingPlatformManager.m +++ b/packages/google_mobile_ads/ios/Classes/UserMessagingPlatform/FLTUserMessagingPlatformManager.m @@ -57,6 +57,25 @@ - (void)handleMethodCall:(FlutterMethodCall *_Nonnull)call UMPConsentStatus status = UMPConsentInformation.sharedInstance.consentStatus; result([[NSNumber alloc] initWithInteger:status]); + } else if ([call.method + isEqualToString:@"ConsentInformation#canRequestAds"]) { + result(@([UMPConsentInformation.sharedInstance canRequestAds])); + } else if ([call.method + isEqualToString:@"ConsentInformation#" + @"getPrivacyOptionsRequirementStatus"]) { + UMPPrivacyOptionsRequirementStatus status = + UMPConsentInformation.sharedInstance.privacyOptionsRequirementStatus; + switch (status) { + case UMPPrivacyOptionsRequirementStatusNotRequired: + result([[NSNumber alloc] initWithInt:0]); + break; + case UMPPrivacyOptionsRequirementStatusRequired: + result([[NSNumber alloc] initWithInt:1]); + break; + default: + result([[NSNumber alloc] initWithInt:2]); + break; + } } else if ([call.method isEqualToString: @"ConsentInformation#requestConsentInfoUpdate"]) { UMPRequestParameters *parameters = call.arguments[@"params"]; @@ -73,6 +92,24 @@ - (void)handleMethodCall:(FlutterMethodCall *_Nonnull)call details:error.domain]); } }]; + } else if ([call.method + isEqualToString:@"UserMessagingPlatform#" + @"loadAndShowConsentFormIfRequired"]) { + [UMPConsentForm + loadAndPresentIfRequiredFromViewController:self.rootController + completionHandler:^(NSError *_Nullable error) { + if ([FLTAdUtil isNull:error]) { + result(nil); + } else { + result([FlutterError + errorWithCode: + [[NSString alloc] + initWithInt:error.code] + message:error + .localizedDescription + details:error.domain]); + } + }]; } else if ([call.method isEqualToString:@"UserMessagingPlatform#loadConsentForm"]) { [UMPConsentForm @@ -87,6 +124,26 @@ - (void)handleMethodCall:(FlutterMethodCall *_Nonnull)call details:loadError.domain]); } }]; + } else if ([call.method + isEqualToString: + @"UserMessagingPlatform#showPrivacyOptionsForm"]) { + [UMPConsentForm + presentPrivacyOptionsFormFromViewController:self.rootController + completionHandler:^( + NSError *_Nullable formError) { + if ([FLTAdUtil isNull:formError]) { + result(nil); + } else { + result([FlutterError + errorWithCode: + [[NSString alloc] + initWithInt:formError.code] + message: + formError + .localizedDescription + details:formError.domain]); + } + }]; } else if ([call.method isEqualToString: @"ConsentInformation#isConsentFormAvailable"]) { BOOL isAvailable = UMPConsentInformation.sharedInstance.formStatus == diff --git a/packages/google_mobile_ads/lib/src/ump/consent_form.dart b/packages/google_mobile_ads/lib/src/ump/consent_form.dart index 9a4f71666..62e0514f4 100644 --- a/packages/google_mobile_ads/lib/src/ump/consent_form.dart +++ b/packages/google_mobile_ads/lib/src/ump/consent_form.dart @@ -32,6 +32,13 @@ abstract class ConsentForm { /// Shows the consent form. void show(OnConsentFormDismissedListener onConsentFormDismissedListener); + /// Presents a privacy options form. + static Future showPrivacyOptionsForm( + OnConsentFormDismissedListener onConsentFormDismissedListener) async { + onConsentFormDismissedListener( + await UserMessagingChannel.instance.showPrivacyOptionsForm()); + } + /// Free platform resources associated with this object. /// /// Returns a future that completes when the platform resources are freed. @@ -46,4 +53,11 @@ abstract class ConsentForm { UserMessagingChannel.instance .loadConsentForm(successListener, failureListener); } + + /// Loads a consent form and immediately shows it. + static Future loadAndShowConsentFormIfRequired( + OnConsentFormDismissedListener onConsentFormDismissedListener) async { + onConsentFormDismissedListener( + await UserMessagingChannel.instance.loadAndShowConsentFormIfRequired()); + } } diff --git a/packages/google_mobile_ads/lib/src/ump/consent_information.dart b/packages/google_mobile_ads/lib/src/ump/consent_information.dart index 47b9944fd..c64f3bf16 100644 --- a/packages/google_mobile_ads/lib/src/ump/consent_information.dart +++ b/packages/google_mobile_ads/lib/src/ump/consent_information.dart @@ -61,6 +61,24 @@ abstract class ConsentInformation { /// the platform API has been called. Future reset(); + /// Indicates whether the app has completed the necessary steps for gathering updated user consent. + Future canRequestAds(); + + /// Indicates the privacy options requirement status as a [PrivacyOptionsRequirementStatus]. + Future getPrivacyOptionsRequirementStatus(); + /// The static [ConsentInformation] instance. static ConsentInformation instance = ConsentInformationImpl(); } + +/// Values indicate whether a privacy options button is required. +enum PrivacyOptionsRequirementStatus { + /// Privacy options entry point is not required. + notRequired, + + /// Privacy options entry point is required. + required, + + /// Privacy options requirement status is unknown. + unknown; +} diff --git a/packages/google_mobile_ads/lib/src/ump/consent_information_impl.dart b/packages/google_mobile_ads/lib/src/ump/consent_information_impl.dart index 5e1f87867..5fb15322e 100644 --- a/packages/google_mobile_ads/lib/src/ump/consent_information_impl.dart +++ b/packages/google_mobile_ads/lib/src/ump/consent_information_impl.dart @@ -44,4 +44,14 @@ class ConsentInformationImpl extends ConsentInformation { Future reset() { return UserMessagingChannel.instance.reset(); } + + @override + Future canRequestAds() { + return UserMessagingChannel.instance.canRequestAds(); + } + + @override + Future getPrivacyOptionsRequirementStatus() { + return UserMessagingChannel.instance.getPrivacyOptionsRequirementStatus(); + } } diff --git a/packages/google_mobile_ads/lib/src/ump/user_messaging_channel.dart b/packages/google_mobile_ads/lib/src/ump/user_messaging_channel.dart index 64ea4bec4..d795b83f6 100644 --- a/packages/google_mobile_ads/lib/src/ump/user_messaging_channel.dart +++ b/packages/google_mobile_ads/lib/src/ump/user_messaging_channel.dart @@ -102,6 +102,27 @@ class UserMessagingChannel { return _methodChannel.invokeMethod('ConsentInformation#reset'); } + /// Returns indicating whether it is ok to request ads. + Future canRequestAds() async { + return (await _methodChannel + .invokeMethod('ConsentInformation#canRequestAds'))!; + } + + /// Indicates the privacy options requirement status as a [PrivacyOptionsRequirementStatus]. + Future + getPrivacyOptionsRequirementStatus() async { + int? privacyOptionsStatusInt = (await _methodChannel.invokeMethod( + 'ConsentInformation#getPrivacyOptionsRequirementStatus')); + switch (privacyOptionsStatusInt) { + case 0: + return PrivacyOptionsRequirementStatus.notRequired; + case 1: + return PrivacyOptionsRequirementStatus.required; + default: + return PrivacyOptionsRequirementStatus.unknown; + } + } + /// Loads a consent form and calls the corresponding listener. void loadConsentForm(OnConsentFormLoadSuccessListener successListener, OnConsentFormLoadFailureListener failureListener) async { @@ -114,6 +135,16 @@ class UserMessagingChannel { } } + /// Loads a consent form and calls the listener afterwards. + Future loadAndShowConsentFormIfRequired() async { + try { + return await _methodChannel.invokeMethod( + 'UserMessagingPlatform#loadAndShowConsentFormIfRequired'); + } on PlatformException catch (e) { + return _formErrorFromPlatformException(e); + } + } + /// Show the consent form. void show(ConsentForm consentForm, OnConsentFormDismissedListener onConsentFormDismissedListener) async { @@ -130,6 +161,16 @@ class UserMessagingChannel { } } + /// Presents a privacy options form. + Future showPrivacyOptionsForm() async { + try { + return await _methodChannel.invokeMethod( + 'UserMessagingPlatform#showPrivacyOptionsForm'); + } on PlatformException catch (e) { + return _formErrorFromPlatformException(e); + } + } + FormError _formErrorFromPlatformException(PlatformException e) { return FormError( errorCode: int.tryParse(e.code) ?? -1, message: e.message ?? ''); diff --git a/packages/google_mobile_ads/lib/src/ump/user_messaging_codec.dart b/packages/google_mobile_ads/lib/src/ump/user_messaging_codec.dart index 22fd870d0..96eb36917 100644 --- a/packages/google_mobile_ads/lib/src/ump/user_messaging_codec.dart +++ b/packages/google_mobile_ads/lib/src/ump/user_messaging_codec.dart @@ -13,6 +13,7 @@ // limitations under the License. import 'package:flutter/services.dart'; +import 'package:google_mobile_ads/src/ump/form_error.dart'; import 'consent_form_impl.dart'; import 'consent_request_parameters.dart'; @@ -22,6 +23,7 @@ class UserMessagingCodec extends StandardMessageCodec { static const int _valueConsentRequestParameters = 129; static const int _valueConsentDebugSettings = 130; static const int _valueConsentForm = 131; + static const int _valueFormError = 132; @override void writeValue(WriteBuffer buffer, dynamic value) { @@ -36,6 +38,10 @@ class UserMessagingCodec extends StandardMessageCodec { } else if (value is ConsentFormImpl) { buffer.putUint8(_valueConsentForm); writeValue(buffer, value.platformHash); + } else if (value is FormError) { + buffer.putUint8(_valueFormError); + writeValue(buffer, value.errorCode); + writeValue(buffer, value.message); } else { super.writeValue(buffer, value); } @@ -64,6 +70,10 @@ class UserMessagingCodec extends StandardMessageCodec { case _valueConsentForm: final int hashCode = readValueOfType(buffer.getUint8(), buffer); return ConsentFormImpl(hashCode); + case _valueFormError: + final int errorCode = readValueOfType(buffer.getUint8(), buffer); + final String errorMessage = readValueOfType(buffer.getUint8(), buffer); + return FormError(errorCode: errorCode, message: errorMessage); default: return super.readValueOfType(type, buffer); } diff --git a/packages/google_mobile_ads/pubspec.yaml b/packages/google_mobile_ads/pubspec.yaml index 2aab67672..86114d22f 100644 --- a/packages/google_mobile_ads/pubspec.yaml +++ b/packages/google_mobile_ads/pubspec.yaml @@ -48,5 +48,5 @@ dev_dependencies: environment: - sdk: ">=2.15.0 <4.0.0" + sdk: ">=2.17.0 <4.0.0" flutter: ">=3.7.0" diff --git a/packages/google_mobile_ads/test/ump/consent_form_test.dart b/packages/google_mobile_ads/test/ump/consent_form_test.dart index 90eb7c23d..d7e664dd5 100644 --- a/packages/google_mobile_ads/test/ump/consent_form_test.dart +++ b/packages/google_mobile_ads/test/ump/consent_form_test.dart @@ -36,6 +36,29 @@ void main() { UserMessagingChannel.instance = mockChannel; }); + test('showPrivacyOptionsForm success', () async { + Completer formCompleter = Completer(); + + await ConsentForm.showPrivacyOptionsForm( + (formError) => formCompleter.complete(formError)); + + FormError? formError = await formCompleter.future; + expect(formError, null); + }); + + test('showPrivacyOptionsForm failure', () async { + FormError? testError = FormError(errorCode: 1, message: 'testErrorMsg'); + when(mockChannel.showPrivacyOptionsForm()) + .thenAnswer((realInvocation) => Future.value(testError)); + Completer formCompleter = Completer(); + + await ConsentForm.showPrivacyOptionsForm( + (formError) => formCompleter.complete(formError)); + + FormError? formError = await formCompleter.future; + expect(formError, testError); + }); + test('loadConsentForm() success', () async { ConsentForm form = ConsentFormImpl(2); @@ -73,5 +96,28 @@ void main() { expect(successCompleter.isCompleted, false); expect(errorCompleter.isCompleted, true); }); + + test('loadAndShowConsentFormIfRequired success', () async { + Completer formCompleter = Completer(); + + await ConsentForm.loadAndShowConsentFormIfRequired( + (formError) => formCompleter.complete(formError)); + + FormError? formError = await formCompleter.future; + expect(formError, null); + }); + + test('loadAndShowConsentFormIfRequired failure', () async { + FormError? testError = FormError(errorCode: 1, message: 'testErrorMsg'); + when(mockChannel.loadAndShowConsentFormIfRequired()) + .thenAnswer((realInvocation) => Future.value(testError)); + Completer formCompleter = Completer(); + + await ConsentForm.loadAndShowConsentFormIfRequired( + (formError) => formCompleter.complete(formError)); + + FormError? formError = await formCompleter.future; + expect(formError, testError); + }); }); } diff --git a/packages/google_mobile_ads/test/ump/consent_form_test.mocks.dart b/packages/google_mobile_ads/test/ump/consent_form_test.mocks.dart index aa468f25f..6cf89d043 100644 --- a/packages/google_mobile_ads/test/ump/consent_form_test.mocks.dart +++ b/packages/google_mobile_ads/test/ump/consent_form_test.mocks.dart @@ -9,6 +9,7 @@ import 'package:google_mobile_ads/src/ump/consent_form.dart' as _i6; import 'package:google_mobile_ads/src/ump/consent_information.dart' as _i4; import 'package:google_mobile_ads/src/ump/consent_request_parameters.dart' as _i3; +import 'package:google_mobile_ads/src/ump/form_error.dart' as _i7; import 'package:google_mobile_ads/src/ump/user_messaging_channel.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; @@ -76,6 +77,24 @@ class MockUserMessagingChannel extends _i1.Mock returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); @override + _i5.Future canRequestAds() => (super.noSuchMethod( + Invocation.method( + #canRequestAds, + [], + ), + returnValueForMissingStub: _i5.Future.value(false), + ) as _i5.Future); + @override + _i5.Future<_i4.PrivacyOptionsRequirementStatus> + getPrivacyOptionsRequirementStatus() => (super.noSuchMethod( + Invocation.method( + #getPrivacyOptionsRequirementStatus, + [], + ), + returnValue: _i5.Future<_i4.PrivacyOptionsRequirementStatus>.value( + _i4.PrivacyOptionsRequirementStatus.unknown), + ) as _i5.Future<_i4.PrivacyOptionsRequirementStatus>); + @override void loadConsentForm( _i6.OnConsentFormLoadSuccessListener? successListener, _i6.OnConsentFormLoadFailureListener? failureListener, @@ -91,6 +110,16 @@ class MockUserMessagingChannel extends _i1.Mock returnValueForMissingStub: null, ); @override + _i5.Future<_i7.FormError?> loadAndShowConsentFormIfRequired() => + (super.noSuchMethod( + Invocation.method( + #loadAndShowConsentFormIfRequired, + [], + ), + returnValue: _i5.Future<_i7.FormError?>.value(), + returnValueForMissingStub: _i5.Future<_i7.FormError?>.value(), + ) as _i5.Future<_i7.FormError?>); + @override void show( _i6.ConsentForm? consentForm, _i6.OnConsentFormDismissedListener? onConsentFormDismissedListener, @@ -106,6 +135,15 @@ class MockUserMessagingChannel extends _i1.Mock returnValueForMissingStub: null, ); @override + _i5.Future<_i7.FormError?> showPrivacyOptionsForm() => (super.noSuchMethod( + Invocation.method( + #showPrivacyOptionsForm, + [], + ), + returnValue: _i5.Future<_i7.FormError?>.value(), + returnValueForMissingStub: _i5.Future<_i7.FormError?>.value(), + ) as _i5.Future<_i7.FormError?>); + @override _i5.Future disposeConsentForm(_i6.ConsentForm? consentForm) => (super.noSuchMethod( Invocation.method( diff --git a/packages/google_mobile_ads/test/ump/consent_information_impl_test.dart b/packages/google_mobile_ads/test/ump/consent_information_impl_test.dart index 9baaa17da..f422665df 100644 --- a/packages/google_mobile_ads/test/ump/consent_information_impl_test.dart +++ b/packages/google_mobile_ads/test/ump/consent_information_impl_test.dart @@ -109,5 +109,25 @@ void main() { expect(errorCompleter.isCompleted, true); expect(responseError, formError); }); + + test('canRequestAds()', () async { + when(mockChannel.canRequestAds()).thenAnswer((_) => Future.value(true)); + + bool canRequestAds = await consentInfo.canRequestAds(); + + verify(mockChannel.canRequestAds()); + expect(canRequestAds, true); + }); + + test('getPrivacyOptionsRequirementStatus()', () async { + when(mockChannel.getPrivacyOptionsRequirementStatus()).thenAnswer( + (_) => Future.value(PrivacyOptionsRequirementStatus.required)); + + PrivacyOptionsRequirementStatus status = + await consentInfo.getPrivacyOptionsRequirementStatus(); + + verify(mockChannel.getPrivacyOptionsRequirementStatus()); + expect(status, PrivacyOptionsRequirementStatus.required); + }); }); } diff --git a/packages/google_mobile_ads/test/ump/consent_information_impl_test.mocks.dart b/packages/google_mobile_ads/test/ump/consent_information_impl_test.mocks.dart index 6b8f13cbb..3c3874680 100644 --- a/packages/google_mobile_ads/test/ump/consent_information_impl_test.mocks.dart +++ b/packages/google_mobile_ads/test/ump/consent_information_impl_test.mocks.dart @@ -115,4 +115,15 @@ class MockUserMessagingChannel extends _i1.Mock returnValue: _i5.Future.value(), returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); + @override + _i5.Future canRequestAds() => (super.noSuchMethod( + Invocation.method(#canRequestAds, []), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i5.Future<_i4.PrivacyOptionsRequirementStatus> + getPrivacyOptionsRequirementStatus() => (super.noSuchMethod( + Invocation.method(#getPrivacyOptionsRequirementStatus, []), + returnValue: _i5.Future<_i4.PrivacyOptionsRequirementStatus>.value( + _i4.PrivacyOptionsRequirementStatus.unknown))); } diff --git a/packages/google_mobile_ads/test/ump/user_messaging_channel_test.dart b/packages/google_mobile_ads/test/ump/user_messaging_channel_test.dart index 2c43a1a75..246c07e1c 100644 --- a/packages/google_mobile_ads/test/ump/user_messaging_channel_test.dart +++ b/packages/google_mobile_ads/test/ump/user_messaging_channel_test.dart @@ -267,5 +267,151 @@ void main() { expect(method, equals('ConsentForm#dispose')); expect(arguments, {'consentForm': consentForm}); }); + + test('canRequestAds()', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (call) async { + expect(call.method, equals('ConsentInformation#canRequestAds')); + expect(call.arguments, null); + return Future.value(true); + }); + + bool canRequestAds = await channel.canRequestAds(); + + expect(canRequestAds, true); + }); + + test('getPrivacyOptionsRequirementStatus() maps 1 to required', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (call) async { + expect(call.method, + equals('ConsentInformation#getPrivacyOptionsRequirementStatus')); + expect(call.arguments, null); + return Future.value(1); + }); + + PrivacyOptionsRequirementStatus privacyStatus = + await channel.getPrivacyOptionsRequirementStatus(); + + expect(privacyStatus, PrivacyOptionsRequirementStatus.required); + }); + + test('getPrivacyOptionsRequirementStatus() maps 0 to notRequired', + () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (call) async { + expect(call.method, + equals('ConsentInformation#getPrivacyOptionsRequirementStatus')); + expect(call.arguments, null); + return Future.value(0); + }); + + PrivacyOptionsRequirementStatus privacyStatus = + await channel.getPrivacyOptionsRequirementStatus(); + + expect(privacyStatus, PrivacyOptionsRequirementStatus.notRequired); + }); + + test('loadAndShowConsentFormIfRequired() success', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (call) async { + expect(call.method, + equals('UserMessagingPlatform#loadAndShowConsentFormIfRequired')); + expect(call.arguments, null); + return Future.value(); + }); + Completer successCompleter = Completer(); + + successCompleter.complete(channel.loadAndShowConsentFormIfRequired()); + + FormError? error = await successCompleter.future; + expect(error, null); + }); + + test('loadAndShowConsentFormIfRequired() failure', () async { + FormError? testError = FormError(errorCode: 55, message: 'msg'); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (call) async { + expect(call.method, + equals('UserMessagingPlatform#loadAndShowConsentFormIfRequired')); + expect(call.arguments, null); + return Future.value(testError); + }); + Completer failureCompleter = Completer(); + + failureCompleter.complete(channel.loadAndShowConsentFormIfRequired()); + + FormError? error = await failureCompleter.future; + expect(failureCompleter.isCompleted, true); + expect(error, testError); + }); + + test('loadAndShowConsentFormIfRequired() error', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (call) async { + expect(call.method, + equals('UserMessagingPlatform#loadAndShowConsentFormIfRequired')); + expect(call.arguments, null); + return Future.error(PlatformException(code: '55', message: 'msg')); + }); + Completer errorCompleter = Completer(); + + errorCompleter.complete(channel.loadAndShowConsentFormIfRequired()); + + FormError? error = await errorCompleter.future; + expect(errorCompleter.isCompleted, true); + expect(error, FormError(errorCode: 55, message: 'msg')); + }); + + test('showPrivacyOptionsForm() success', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (call) async { + expect(call.method, + equals('UserMessagingPlatform#showPrivacyOptionsForm')); + expect(call.arguments, null); + return Future.value(); + }); + Completer successCompleter = Completer(); + + successCompleter.complete(channel.showPrivacyOptionsForm()); + + FormError? error = await successCompleter.future; + expect(error, null); + }); + + test('showPrivacyOptionsForm() failure', () async { + FormError? testError = FormError(errorCode: 55, message: 'msg'); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (call) async { + expect(call.method, + equals('UserMessagingPlatform#showPrivacyOptionsForm')); + expect(call.arguments, null); + return Future.value(testError); + }); + Completer failureCompleter = Completer(); + + failureCompleter.complete(channel.showPrivacyOptionsForm()); + + FormError? error = await failureCompleter.future; + expect(failureCompleter.isCompleted, true); + expect(error, testError); + }); + + test('showPrivacyOptionsForm() error', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (call) async { + expect(call.method, + equals('UserMessagingPlatform#showPrivacyOptionsForm')); + expect(call.arguments, null); + return Future.error(PlatformException(code: '55', message: 'msg')); + }); + Completer errorCompleter = Completer(); + + errorCompleter.complete(channel.showPrivacyOptionsForm()); + + FormError? error = await errorCompleter.future; + expect(errorCompleter.isCompleted, true); + expect(error, FormError(errorCode: 55, message: 'msg')); + }); }); } diff --git a/packages/google_mobile_ads/test/ump/user_messaging_codec_test.dart b/packages/google_mobile_ads/test/ump/user_messaging_codec_test.dart index d4d05287e..d13ea1d07 100644 --- a/packages/google_mobile_ads/test/ump/user_messaging_codec_test.dart +++ b/packages/google_mobile_ads/test/ump/user_messaging_codec_test.dart @@ -70,5 +70,12 @@ void main() { ConsentForm decodedConsentForm = codec.decodeMessage(byteData); expect(decodedConsentForm, equals(consentFormImpl)); }); + + test('encode and decode FormError', () async { + FormError formError = FormError(errorCode: 123, message: 'testError'); + ByteData? byteData = codec.encodeMessage(formError); + FormError decodedFormError = codec.decodeMessage(byteData); + expect(decodedFormError, equals(formError)); + }); }); }