PushEngage — Flutter Spoke
You're working in a Flutter app. This skill walks the customer through integrating PushEngage from zero to verified test push, or debugs a broken integration.
Trigger gating. This spoke fires only when
pubspec.yamlcontains aflutter:SDK block. The presence ofios/Podfileorandroid/build.gradlealone (withoutpubspec.yaml) does NOT route here — that's the native iOS or Android spoke. The hub's detection logic explicitly puts Flutter precedence ABOVE the native checks.
Routing
| Customer intent | Read this file next |
|---|---|
| "Integrate PushEngage" / "Set up PushEngage" (greenfield or partial) | This file's "Integration flow" section below. |
| "PushEngage isn't working" / specific symptom | Run static audit (audit-checks.md) first, then escalate to ../pushengage-debug/SKILL.md for Phase 2/3 (skill: pushengage-debug). |
| "Audit my PushEngage setup" | Read audit-checks.md and run the checks. |
| Concept / terminology question | Read ../pushengage/concepts.md. |
| Best practices / rule lookup | Read best-practices.md (Flutter-specific) or ../pushengage/best-practices.md (cross-platform). |
| API surface / "does method X exist" | Read version-matrix.md first; if not listed, fetch only URLs from references.md. |
Integration flow (greenfield)
This is the happy path. Follow the parts strictly in order — earlier artifacts feed later steps.
Part 1 — Discovery & prereqs
Before touching anything, gather:
- Target platforms — does the customer ship to both stores (Android + iOS), or just one? Look for
ios/andandroid/folders. Confirm with the customer (a Flutter project can have both folders even if they only deploy to one). - Pub version pin — read
pubspec.yaml's currentpushengage_flutter_sdkpin (if any). applicationId(Android) — grepandroid/app/build.gradle*forapplicationId. Needed to register the Firebase app.- iOS Bundle Identifier — confirm with the customer; readable from Xcode's Signing & Capabilities tab on the Runner target.
- PushEngage account state — ask:
- Do you already have a PushEngage account?
- Have you already created a site for this Flutter app? (One per platform — two sites total if shipping both.)
- Firebase project state (Android-only step) — do they have a Firebase project for this app already, or do we create one?
- Apple Developer account state (iOS-only step) — do they have it, and have they enabled Push Notifications on the App ID?
- Compose / UIKit / Material analog — Flutter is the UI framework. No sub-flow split here (unlike Android's Compose-vs-Views split).
Output a one-paragraph integration plan in chat. Wait for the customer to confirm before any edits.
Part 2 — Provider setup (out-of-code, before any code edits)
Do these BEFORE editing code. The dashboard's App ID UUIDs are needed for setAppId(...); Firebase + APNs have real-world setup steps that block code progress.
The work splits by target platform:
- Android target →
firebase-setup.md. Walks the customer through Firebase Console → register Android app → downloadgoogle-services.json→ generate Service Account JSON → capture Sender ID +applicationId. Defers to../pushengage-android/firebase-setup.mdfor the full click-path. - iOS target →
apns-setup.md. Walks the customer through Apple Developer portal → enable Push Notifications capability → Keychain CSR → APNs SSL cert →.p12export → capture Bundle Identifier. Defers to../pushengage-ios/provider-setup.mdfor the full click-path.
If both platforms: do them in parallel-ish — the steps don't conflict.
Part 3 — Dashboard setup
Read dashboard-setup.md. A cross-platform Flutter app needs TWO PushEngage sites (one Android, one iOS) — each generates its own App ID UUID. Capture BOTH UUIDs and confirm them back to the customer before moving on.
Single-platform: just one site, one UUID.
Part 4 — In-code edits
4a — Install + native config
Read install.md. Apply:
pubspec.yaml— bump or addpushengage_flutter_sdk: ^1.0.0. Runflutter pub get.- Android:
android/settings.gradle,android/build.gradle,android/app/build.gradle,AndroidManifest.xml,google-services.json. - iOS:
ios/Podfile(platform + post_install + NSE/NCE targets when applicable),Info.plist,cd ios && pod install.
Auto-edit + announce posture for all config files: pubspec.yaml, Podfile, android/build.gradle*, settings.gradle*, AndroidManifest.xml, Info.plist. Show the diff, then apply.
4b — Dart + Swift init code
Read init-code.md. Apply:
lib/main.dart—PushEngage.setAppId(...)inmain()BEFORErunApp, withPlatform.isIOS ?branching for cross-platform builds.ios/Runner/AppDelegate.swift—swizzleInjection(isEnabled: true)inoverride init(), optionalsetBadgeCount(0)andsetNotificationWillShowInForegroundHandlerindidFinishLaunchingWithOptions.- Deep-link listener —
PushEngage.deepLinkStream.listen(...)in the root widget'sinitState, cancelled indispose. - No Android
Applicationsubclass needed — the Flutter plugin handles native Android init through its plugin lifecycle.
Diff + confirm posture for lib/main.dart and AppDelegate.swift — entry-point code, bad edits break the app.
Insert the App ID UUID(s) captured in Part 3. Per XP-BP-01, recommend the customer later move them out of source (e.g., --dart-define).
4c — Runtime permission flow
Read permission-flow.md. Set up a requestNotificationPermission() call site behind a user action (button, end-of-onboarding) — NOT in main() or app-root initState. Cross-platform via a single Dart call.
For Android: confirm POST_NOTIFICATIONS is declared in the Manifest (Step 4a covers this).
4d — NSE setup (iOS, required for rich notifications)
If the customer is shipping to iOS, read nse-setup.md. Walk them through:
- Adding the NSE target via Xcode UI.
- Setting deployment target to iOS 12.
- Adding
pod 'PushEngageExtension', '1.0.0'to the Podfile NSE target block (NOT to theRunnertarget). - App Groups capability on BOTH targets + matching
PushEngage_App_Group_Keyin both Info.plists.
Skip this step entirely if the customer is Android-only.
4e — NCE setup (optional)
Default: SKIP. Only run if the customer explicitly asks for custom in-notification UI. Read nce-setup.md if they do.
Part 5 — Verification
The success criterion is a real test notification reaches the customer's device (one per platform if cross-platform).
For each target platform:
- Customer connects a real device:
- Android: must have Google Play Services. NOT a Play-Services-less emulator.
- iOS: must be a physical device. APNs does NOT work on the iOS simulator.
- Customer installs and runs the app once. Triggers the in-app permission request (per Part 4c).
- Customer goes to PushEngage dashboard → corresponding site → Campaign → Push Broadcasts → sends to test device (or "All Subscribers" if it's the only one).
- Ask: "Did the notification arrive on
<platform>?"
If yes → go to Part 6.
If no → run static audit first (audit-checks.md), then hand off to pushengage-debug/SKILL.md with the symptom "test push didn't arrive on <platform>."
Part 6 — Post-integration best-practices nudge
Short message. Pick 2–3 rule IDs that actually apply to what you just integrated. Examples:
- XP-BP-01 — move the App ID UUID(s) out of source (
--dart-define,String.fromEnvironment). - FLUTTER-BP-04 — defer
requestNotificationPermission()to a meaningful moment, not on launch. - FLUTTER-BP-05 — wrap
enableLogging(true)inkDebugMode. - FLUTTER-BP-02 — confirm the deep-link listener is in the root widget's
initStateand won't drift to a downstream route during future refactors. - ANDROID-BP-13 (if Android target) — document OEM battery-optimization caveat in the customer's user-facing FAQ.
(Customize to what you actually saw. Don't dump all of them.)
Debugging entry points
If the customer says any of the following — or if Part 5 verification fails — escalate to pushengage-debug/SKILL.md:
- "Notifications aren't arriving" (on one or both platforms).
- "Permission prompt never shows" (Android-specific → check the MERGED manifest for
POST_NOTIFICATIONS; the SDK's library manifest normally supplies it — see FLUTTER-AUDIT-05). - "iOS build fails with 'Application extensions cannot use PushEngage'" → FLUTTER-AUDIT-04.
- "Rich-push image doesn't load" → FLUTTER-AUDIT-03 (stale pod pin) and FLUTTER-AUDIT-09 (App Groups mismatch).
- "Cold-boot deep link is lost" → FLUTTER-AUDIT-12 / KI-F003.
- "Works on Android but not iOS" (or vice versa) → FLUTTER-AUDIT-07 / KI-F006 (hardcoded App ID).
- "
flutter pub getsucceeded butPushEngage.X()says no such method" → FLUTTER-AUDIT-01 / KI-F001 (stale^0.0.1). - "Gradle build fails:
google-services.jsonmissing" → FLUTTER-AUDIT-06.
The debug skill handles audit + known-issues + symptom tree.
Edit posture per file class
- Auto-edit + announce:
pubspec.yaml,Podfile,android/build.gradle*,android/settings.gradle*,android/app/build.gradle*,AndroidManifest.xml,Info.plist, NSEInfo.plist, file placements (google-services.jsonmove). - Diff + confirm:
lib/main.dart,ios/Runner/AppDelegate.swift. Entry-point code — bad edits break the app. - Customer-driven (you instruct only): Xcode UI work — NSE/NCE target creation, App Groups capability toggles, Push Notifications capability, Background Modes. Don't
.pbxproj-edit programmatically. - Never touch: anything outside the integration scope. No formatting passes, no Flutter / Dart / Gradle version bumps beyond what's required, no opportunistic refactoring.
Where the Flutter spoke borrows from other spokes
The Flutter spoke deliberately references content files from the iOS and Android spokes to avoid duplication. These cross-spoke references are by file path only — you read the referenced content file, but does NOT load the other spoke's SKILL.md. The trigger contract (Flutter session loads Flutter skill) is preserved.
| What | Where |
|---|---|
| Firebase Console click-path | ../pushengage-android/firebase-setup.md (referenced from this spoke's firebase-setup.md) |
| Apple Developer portal + Keychain click-path | ../pushengage-ios/provider-setup.md (referenced from this spoke's apns-setup.md) |
| Long-form NCE template + category consistency rules | ../pushengage-ios/nce-setup.md (referenced from this spoke's nce-setup.md) |
| Manual (non-swizzling) iOS forwards template | ../pushengage-ios/init-code.md (referenced from this spoke's init-code.md) |
Never read the other spokes' SKILL.mds. Only their content files.
What you must not do
- Don't invent PushEngage method names. If unsure a Dart method exists, check
version-matrix.md. If still unsure, fetch an allowlisted URL fromreferences.md. If still unsure, say so — don't guess. - Use
version-matrix.mdas the source of truth for version pins (^1.0.0Flutter,1.0.0iOS pods). Older guides may show earlier pins like^0.0.1. - Don't auto-edit
lib/main.dartorAppDelegate.swiftwithout showing the diff and getting confirmation. Entry-point code is diff + confirm. - Don't touch files outside the integration scope. No refactoring, no Dart version bumps, no Flutter SDK upgrades, no Gradle version bumps that weren't strictly required.
- Don't fetch URLs not in
references.md. The allowlist exists so the customer can trust where you got information. - Don't ask the customer to paste Service Account JSON or
.p12contents in chat. Both are secrets. They upload directly to the PushEngage dashboard; you only need to know the local path and confirm the upload succeeded. - Don't add
pod 'PushEngage'to the mainRunnertarget in Podfile — it's pulled transitively. Only add it to NSE / NCE target blocks. (FLUTTER-BP-06.) - Don't create an Android
Applicationsubclass for PushEngage init — the Flutter plugin handles it. If one already exists for other reasons, leave it alone. - Prefer
setAppIdinmain()beforerunAppover the SDK example'sinitStateplacement. The example sets it frominitStatebecause it enters the App ID at runtime — a valid pattern, not a bug — but for a known App ID,main()is cleaner. (FLUTTER-BP-01.)