This skill defines how to correctly implement Firebase In-App Messaging in Flutter applications, covering setup, programmatic triggers, privacy controls, testing workflows, and campaign management.
Use this skill when:
flutter pub add firebase_in_app_messaging
import 'package:firebase_in_app_messaging/firebase_in_app_messaging.dart';
Firebase.initializeApp() completes before accessing In-App Messaging.Use Google Analytics events to trigger in-app messages without additional code. For programmatic triggering:
// Trigger a message tied to a custom event
await FirebaseInAppMessaging.instance.triggerEvent("purchase_complete");
class PaymentScreen extends StatefulWidget {
@override
State<PaymentScreen> createState() => _PaymentScreenState();
}
class _PaymentScreenState extends State<PaymentScreen> {
@override
void initState() {
super.initState();
// Suppress messages during payment
FirebaseInAppMessaging.instance.setMessagesSuppressed(true);
}
@override
void dispose() {
// Re-enable messages when leaving payment flow
FirebaseInAppMessaging.instance.setMessagesSuppressed(false);
super.dispose();
}
}
By default, In-App Messaging automatically delivers messages to all targeted users.
Disable automatic collection — iOS (Info.plist):
<key>FirebaseInAppMessagingAutomaticDataCollectionEnabled</key>
<false/>
Disable automatic collection — Android (AndroidManifest.xml):
<meta-data
android:name="firebase_inapp_messaging_auto_data_collection_enabled"
android:value="false" />
Enable for users who opt in at runtime:
// Call after user consents to data collection
Future<void> enableMessaging() async {
await FirebaseInAppMessaging.instance.setAutomaticDataCollectionEnabled(true);
}
// Call if user revokes consent
Future<void> disableMessaging() async {
await FirebaseInAppMessaging.instance.setAutomaticDataCollectionEnabled(false);
}
FIAM.Headless — look for Starting InAppMessaging runtime with Installation ID YOUR_INSTALLATION_ID.-FIRDebugEnabled as a runtime argument in Xcode scheme settings. Look for [Firebase/InAppMessaging][I-IAM180017] in the console.app_open, purchase_complete).InAppMessage.data.By default, In-App Messaging draws native templates. To render campaigns with your own Flutter widgets, listen to onMessageDisplay, report the lifecycle back to the SDK, and opt in with setCustomDisplayEnabled(true):
FirebaseInAppMessaging.instance.onMessageDisplay.listen((message) async {
await message.impress();
// Both are null for campaigns published without an action (image-only
// layouts, action-less banners), so resolve the action before using it.
final action = message.primaryAction ?? message.action;
// showCampaign() is your own widget; draw it from message.title, body,
// imageUrl, actions and data, and complete when the user closes it.
await showCampaign(
message,
onActionTap: action == null
? null
: () async {
await message.click(action);
// The plugin does not open action URLs — your app should.
},
);
await message.dismiss();
});
await FirebaseInAppMessaging.instance.setCustomDisplayEnabled(true);
onMessageDisplay alone does not replace native UI — native templates keep showing until setCustomDisplayEnabled(true) is called.impress() when your UI appears, click() only from the user's tap handler, and dismiss() when it closes; onMessageImpression / onMessageClicked still fire.action, primaryAction and secondaryAction are all InAppMessageAction?, while click(InAppMessageAction action) takes a non-nullable action — a campaign published without one has nothing to report and must skip click(). action is the single action for banner, modal and image-only layouts, primaryAction is card-only, and imageOnly carries an optional tap action, so both are null for an image-only campaign with no action.click(), dismiss() and reportError() share one terminal latch — the first call wins and the others return as no-ops. Reporting a click on display therefore also discards the dismiss.click() in the listener body and force-unwraps with message.action!. Verified against the plugin source: for an action-less campaign it throws Null check operator used on a null value, and because the throw sits inside an async stream listener it goes unhandled — only impress() reaches the SDK and no terminal event is ever reported. For a campaign that does carry an action it reports a click the user never made, then silently drops the dismiss.FirebaseInAppMessaging API reference — onMessageDisplay, setCustomDisplayEnabledInAppMessage API reference — action nullability, impress/click/dismiss