fireside_flutter 0.4.0
fireside_flutter: ^0.4.0 copied to clipboard
Run a Fireside Interview inside your Flutter app as a full-screen native modal. A thin Dart wrapper over the Fireside Android and iOS SDKs.
fireside_flutter #
Run a Fireside Interview inside your Flutter app.
The plugin opens the interview as a full-screen native screen on top of your Flutter view. It gives you back one final result.
The Dart code is only a thin layer. The real work is done by the Fireside Android SDK, written in Kotlin, and the Fireside iOS SDK, written in Swift. Your Dart code never calls the Fireside server itself.
- On top, not inside. The interview is shown on top of your Flutter view. It
is not a
PlatformView, so it is never placed inside your widget tree. - Four methods, and that is the whole API:
Fireside.init(start the SDK with your key) ·Fireside.identify(link what happens next to your own participant id) ·Fireside.present(open an interview) ·Fireside.fetchInterviewStatuses(read where a participant stands on the studies you name).
You need a Fireside publishable API key and at least one study. Get both from your Fireside representative.
Fireside gives you two keys, not one. One key starts with test_. The other
starts with live_. The same two keys work on Android and on iOS.
Use the test_ key in your debug builds. Use the live_ key in your
release builds.
Both keys reach the same Fireside servers. The difference is where your interview
data is kept. Data that arrives with a test_ key is stored apart from your real
data, so your testing does not mix into your real numbers.
The key is safe to ship inside your app. That is what "publishable" means. It is a key for client code, not a secret, so it is fine that it ends up inside the app you ship.
Still supply it at build time, and keep it out of source control. Not because it is secret, but because you need a different key in debug and in release, and a key hard-coded in your source cannot change between the two. The example below uses a plain string to stay short; a real app passes the key in at build time.
What protects the key is the list of app ids on it. The key works only from an app id that Fireside put on it. So if someone copies the key out of your app, it is useless in their app.
Before you start — send us your app ids #
A Flutter app has two ids, and Fireside needs both:
| Platform | The id | Where to find it |
|---|---|---|
| Android | applicationId |
android/app/build.gradle[.kts], under defaultConfig |
| iOS | bundle identifier | Xcode → the Runner target → Signing & Capabilities → Bundle Identifier (the PRODUCT_BUNDLE_IDENTIFIER build setting) |
Both usually look like com.yourcompany.yourapp. Send them before you start
coding.
Your key works only from the app ids that are on it. Only Fireside can add an app id. "Register" means Fireside puts your app id on the list of app ids stored on your key. There is no web page where you can do this yourself. Send your ids to your Fireside representative. Then wait until they tell you the ids are on your key. Ask them how long that usually takes.
Two things here are easy to get wrong:
- Each platform needs its own registration. You may use the same id on Android and on iOS. That still counts as two registrations. Send the id once for each platform you ship.
- Each build variant is a different id. A build variant is one build of your
app, like debug or staging. If a variant adds a suffix such as
.debugor.staging, its id is not the id you registered. Send us every id you ship.
If the id of the running app is not on your key, the build still works. Nothing
looks wrong. But the SDK makes one call of its own, started by init, and that
call checks your key and your app id together. The server answers that call with
an HTTP 403 error. Your code never sees it. What you do see is this: every
later call to present and fetchInterviewStatuses gives back an integration
error.
A wrong key gives you the same integration error. So does a key that Fireside turned off. The only difference is that the server answers with a 401 error, not a 403. You cannot see that either.
So the error does not tell you which problem you have. Do this: first check the app id of the exact build variant you are running. If that id is right, then check the key.
Requirements #
| Requirement | Value |
|---|---|
| Flutter | 3.27 or newer. You cannot go lower. The iOS side is installed by Swift Package Manager, and 3.27 is the first Flutter that can do that. On 3.27 to 3.43 you must turn it on once — see the note under this table. From 3.44 it is already on |
| Dart | 3.4 or newer |
Android minSdk |
26. You cannot go lower — this comes from the native SDK |
Android compileSdk |
36 recommended |
| Android Gradle Plugin | 8.10 or newer. The plugin is built at compileSdk 36, and 8.10 is the first version Google supports at API level 36. 8.9 stops at API level 35 |
| Gradle | 8.11.1 or newer, because Android Gradle Plugin 8.10 needs it. Set it in android/gradle/wrapper/gradle-wrapper.properties |
| Java toolchain | 17 |
| iOS deployment target | 15.0. You cannot go lower — this comes from the native SDK |
| Android host activity | The standard Flutter MainActivity works. You do not need a different base class |
On iOS this plugin works only through Swift Package Manager. It ships no podspec, so CocoaPods cannot install it. Android is not affected by any of this — it installs normally on every supported Flutter.
What you have to do depends on your Flutter version:
-
Flutter 3.44 or newer — nothing. Swift Package Manager is already on.
-
Flutter 3.27 to 3.43 — run this once, then build as usual:
flutter config --enable-swift-package-manager -
Flutter older than 3.27 — not supported. That Flutter cannot use Swift Package Manager for plugins at all, so there is no setting to turn on.
If you skip that one command, the build stops with a message that names the plugin and the fix:
Plugin fireside_flutter is only Swift Package Manager compatible. Try enabling
Swift Package Manager by running "flutter config --enable-swift-package-manager"
or remove the plugin as a dependency.
flutter pub get still succeeds, so the message appears at the iOS build step,
not at dependency resolution.
On an older Flutter, also check your iOS deployment target. Flutter 3.27 scaffolds a new app at iOS 12.0, and this plugin needs 15.0 (see the table above). If yours is still on the old default the build stops with:
Target Integrity (Xcode): The package product 'fireside-flutter' requires
minimum platform version 15.0 for the iOS platform, but this target supports 12.0
Set IPHONEOS_DEPLOYMENT_TARGET to 15.0 in ios/Runner.xcodeproj (Xcode:
Runner target → General → Minimum Deployments → iOS 15.0) and build again. This
is not specific to Flutter 3.27 — any app below iOS 15.0 hits it.
What enters your app #
On both platforms you ship compiled binaries, which means files that are already built. No SDK source code goes into your app.
- Android — a release AAR. An AAR is the normal file type for an Android
library. It brings three AndroidX libraries with it, and most apps already have
all three:
androidx.core-ktx,androidx.activity-ktx, andandroidx.webkit. It brings no other library. So there is nothing that can conflict with the libraries your app already uses. There is no OkHttp, no RxJava, and no Reactive streams. You can check this yourself with./gradlew :app:dependencies.
Both files are size-capped, and a build that goes over its cap does not ship. So the SDK cannot grow into your app between releases without that being a deliberate decision on our side.
- iOS — a dynamic
FiresideSDK.xcframework. An XCFramework is the normal file type for an iOS library. It holds two builds: one for real devices and one for the Simulator. Your app bundle gets a copy of it. Swift Package Manager installs it, so it brings in no pods and no other frameworks.
Install #
flutter pub add fireside_flutter
Permissions your own app has to declare #
The plugin declares no runtime permissions. A runtime permission is one the user has to allow, like camera. The plugin also adds no network-security exceptions and no App Transport Security exceptions, which are the Android and iOS rules about unsafe connections.
You declare all of those yourself. Your users see them, and the app stores see them, so the choice has to be yours.
On Android the SDK does declare two permissions of its own: INTERNET and
ACCESS_NETWORK_STATE. They are copied into your manifest when you build.
Neither one shows a prompt to the user, and you add nothing for them.
Android — android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
iOS — ios/Runner/Info.plist:
<key>NSCameraUsageDescription</key>
<string>Used to record your video answers during an interview.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Used to record your spoken answers during an interview.</string>
You only need camera and microphone if your studies ask video or audio questions.
Use it #
import 'package:fireside_flutter/fireside_flutter.dart';
import 'package:flutter/material.dart';
void main() {
Fireside.init('test_your_key');
// Once you know who the user is. Use an id that means nothing on its own —
// never an email, a phone number, or a name.
Fireside.identify('your-opaque-user-id');
runApp(const MyApp());
}
Future<void> startInterview() async {
final result = await Fireside.present('your-study-id');
switch (result) {
case InterviewCompletedNoReviewNeeded():
case InterviewCompletedPendingReview():
showThanks();
case InterviewScreenedOut():
case InterviewUserExited():
case InterviewStudyUnavailable():
break;
case InterviewPermissionDenied():
showPermissionHelp();
case InterviewFailed(:final reason):
showRetry(reason.name);
case InterviewTechRejected(:final reason):
showRetry(reason.name);
}
}
init and identify return straight away. They give you nothing back.
You must call identify before present. It links the interview to your own
participant id. That id still counts as personal data, even though it does not
name anyone. So pass an id that means nothing on its own. Never pass an email, a
phone number, or a name.
present returns once, and it returns only an InterviewResult. It never
throws.
Only one interview can be open at a time. If you call present while one is
already open, the second call returns
InterviewTechRejected(TechRejectedReason.alreadyPresenting). The interview that
is already running is left alone.
Extra values for one interview #
await Fireside.present(
'your-study-id',
params: {'utm_source': 'email', 'cohort': 'a8f3k2'},
);
params does the same job as the query string on a study web link. It attaches
extra values to one interview.
The SDK percent-encodes every key and every value for you, which means it makes them safe to put in a web address. So pass them exactly as they are.
The Fireside server keeps only the keys your study lists as allowed. It drops the rest. Keys and values must mean nothing on their own, and they must never hold personal data.
Limits on what you pass in #
If you pass a bad value, the SDK does not throw an error. It ignores the value. Then it keeps running.
For almost every row below, the Fireside SDK writes nothing to its own log, on any platform, in any build. A blank user id is the one exception. The SDK does write a line about it. You see that line only while your own app is a development build, so your users never see it.
Flutter adds one line of its own. In a debug build of your app, the Dart layer prints one line for an empty API key. A release build of your app prints nothing.
So for the other rows the only sign you get is what the next call gives back. That is almost always an integration error, which means "your setup is wrong". Handle that error in your code. Do not assume it cannot happen.
For some rows below you get no sign at all. That is why it is worth reading them before you call:
| Value | Limit | What happens past it |
|---|---|---|
The user id you pass to identify |
128 bytes (a UUID is 36) | The SDK drops the id. It does not cut it short. It also clears any id you set before. After that, every call to present or fetchInterviewStatuses gives back an integration error. It is the same as if you never called identify. |
The user id you pass to identify |
must not be blank | An empty id, or an id that is only spaces, is dropped the same way. It is never stored, and it clears any id you set before. Every later call to present or fetchInterviewStatuses gives back an integration error, exactly as if you never called identify. Pass a real id before you call present. |
The keys of the present params map |
fs_ctx and live are reserved, which means the SDK owns them. Any key the SDK already puts in the interview web address is reserved too |
The SDK removes that key. It writes nothing about it. The interview never gets that key. fs_ctx is a key the SDK sets itself, and it holds the SDK's own data. live is a key the SDK never sets, but the interview page reads ?live=1, so your app must not add it. Put your own prefix in front of your own keys, like utm_source or acme_cohort. Then you will never hit a reserved key. |
| The API key you start the SDK with | must be a real key, and not empty | The Fireside server never accepts an empty key or a wrong key. Nothing throws. Instead, every call to present or fetchInterviewStatuses gives back an integration error. |
You can start the SDK only once while your app is running. If you call it a second time with a different key, nothing happens. You get no error.
There is one other kind of mistake, and the SDK reports it the same way: calling
present or fetchInterviewStatuses before identify. It is not in the table
above.
You see the blank-id line and this one only while your own app is a development build. A store release build of your app shows nothing, so your users never see it either. This page has notes about reading the SDK's log, and they say exactly which builds show it.
Reading interview state #
final fetched = await Fireside.fetchInterviewStatuses(['study-a', 'study-b']);
switch (fetched) {
case FetchStatuses(:final statuses):
render(statuses['study-a']!);
case FetchFailed(:final reason):
showRetry(reason.name);
}
statuses has one entry for every id you asked for. An id that is not a valid
study for your key comes back as InterviewStatusUnknownStudy. It is never just
missing from the map. This call never throws either.
Every result #
InterviewResult is a sealed type. That means the compiler knows every possible
case, so it can check that your Dart switch handles all of them.
| Result | When |
|---|---|
InterviewCompletedNoReviewNeeded |
Submitted, and the study needs no review. This is final |
InterviewCompletedPendingReview |
Submitted, and a reviewer still has to decide |
InterviewScreenedOut |
The participant did not qualify during the screening questions. This is a normal outcome, not an error |
InterviewUserExited |
The participant closed the interview before finishing |
InterviewStudyUnavailable |
They started too late: the study is paused, has ended, or has reached its limit of participants |
InterviewPermissionDenied |
Camera or microphone was refused, and there was no other way to continue |
InterviewFailed(FailureReason.networkIssue) |
Could not reach Fireside, even after retrying. Worth trying again |
InterviewFailed(FailureReason.integrationError) |
Something in your integration is wrong: a bad key, present called before init or before identify, or a study id Fireside does not know |
InterviewFailed(FailureReason.unexpected) |
Any other error that ends the interview |
InterviewTechRejected(reason) |
Fireside cancelled the interview itself. The reason is alreadyParticipated, safety, generalError, or alreadyPresenting, and that last one is harmless |
What the SDK stores on the device #
The SDK keeps two things on the device. Both sit in your app's own private storage, so no other app can read them.
- An install id. The SDK makes this the first time it runs and stores it. It is a random id for one install of your app. It is not tied to the person, and it is not the device id.
- A short queue of status reports that have not been sent yet. This is only the SDK's own delivery state. It holds no interview content.
Both are deleted when your app is uninstalled, and also when the user clears your app's data. The install id is then made again the next time the app runs. So after a reinstall, the same person looks like a new install to Fireside. The SDK gives you no method to reset it yourself.
Store submission #
The SDK uses the camera and microphone permissions you declared under Permissions your own app has to declare. It declares none of its own. They are in your app, so you are the one who answers for them when you upload.
App Store — the SDK's privacy manifest is already done for you. A privacy
manifest is a small file that says what data an SDK collects. Apple asks for one
from every SDK that is not Apple's own. The iOS SDK inside this plugin already
has that file, called PrivacyInfo.xcprivacy, in both of its builds: the one for
real devices and the one for the Simulator. So you add nothing. The file says the
SDK does no tracking. That means the SDK never causes an App Tracking
Transparency prompt.
Both stores — what the SDK adds to your declarations. Neither store can read this out of the file, so you have to write it in yourself:
| What | App Store (App Privacy) | Google Play (Data Safety) |
|---|---|---|
| Spoken answers | Audio Data | Audio → voice or sound recordings |
| Video answers | Photos or Videos | Photos and videos |
| Typed answers | Other User Content | Other user-generated content |
The user reference you pass to identify |
User ID | Personal info → User IDs |
| The SDK's own install identifier | Device ID | Device or other IDs |
The last two rows are separate entries on both forms. You have to get the split
right. Play says a User ID points to a person you can name — "an account id,
account number, or account name". Play says a Device or other ID points to a
device, or to one install of an app. The id you pass to identify is the first
kind. The SDK's install id is the second kind.
Every row gets the same three answers. It is linked to the user. It is not used for tracking. Its purpose is app functionality.
Play asks two more questions on each row. For processed ephemerally, answer no on every row. For required or optional: the three interview rows are optional, because the person chooses whether to start an interview; the two id rows are required.
Play also asks two questions about the whole app. Answer encrypted in transit — yes. Answer users can request deletion — yes, because participants ask Fireside to delete their data.
App Store — camera and microphone. Answer the App Privacy questions for the
permissions you declared yourself. Your NS*UsageDescription strings are the
text the user reads.
Anything you put in params is yours to declare. The SDK passes that map to
the interview and never reads it. So the SDK adds no row of its own for it. But a
campaign tag or a cohort tag that you put in there is your app sending your own
data. So it belongs in your own answers. Keep it non-personal, which the
present contract requires anyway. Then it counts as app functionality, not
marketing.
What the SDK never collects: it collects no advertising id, no location (not exact and not approximate), and it never follows the user into other apps or websites.
Read this section again every time you upgrade the SDK. These answers change if the SDK starts collecting something new. So an answer that is right today can be wrong after you move to a newer SDK version.
This is guidance. It is not legal advice. Check it against what Play Console and App Store Connect ask for at the time you release.
Logging #
This package holds the Fireside SDK inside it. The two of them write logs in different ways.
The plugin's own messages appear in debug builds only, on all three sides:
- Dart:
developer.log(name: 'FiresideFlutter') - Android: Logcat tag
FiresideFlutter - iOS:
os.Loggersubsystemai.fireside.flutter
A release build of your app has none of them.
The SDK inside still writes some messages in a release build. But only one kind:
integration guidance. Those are the messages that tell you about a setup
mistake, such as calling present before identify.
You see them only while your own app is a development build. On Android that means a debuggable build. On iOS it means a Simulator build, or a build signed with your own development or ad-hoc profile. You do not need a debugger attached.
They come out under Logcat tag Fireside on Android, and os.Logger subsystem
ai.fireside on iOS.
Everything else the SDK writes is Fireside's own internal detail, and it is removed from the released build.
No side ever logs auth tokens, session secrets, HTTP headers, or anything the interview web page sends back.
Troubleshooting #
- The Android build stops with a message about the minimum supported Gradle
version — your Gradle wrapper is older than this version of the plugin needs.
A newer plugin version can ask for a higher Gradle than the one before it. Set
Gradle to 8.11.1 or newer in
android/gradle/wrapper/gradle-wrapper.properties, and raise your Android Gradle Plugin to 8.10 or newer. See Requirements. InterviewFailed(unexpected)straight away — the plugin could not find an AndroidActivity, or on iOS aUIViewControllerit can present from. This usually means you calledpresentbefore the Flutter view was attached.InterviewTechRejected(alreadyPresenting)— an interview is already open. The firstpresentstill owns it and will return its own result. Ignore this second result.- The interview screen opens and shows a loading spinner forever — the
interview never finished loading. After the load timeout you get
InterviewFailed(FailureReason.networkIssue). Check that the device can reach the network. - No camera or microphone prompt appears — declare the permissions above in your own app. The plugin passes the request through, but it does not declare the permissions for you.
InterviewFailed(integrationError)on the very first call — most often the app id of the exact build variant you are running is not on your key. See Before you start. If the app id is right, then check the API key itself, check thatinitran beforeidentify, and check thatidentifyran beforepresent.
License #
Commercial software, licensed under an agreement with Fireside — the full text is on this package's License tab. Questions? Contact your Fireside representative.