featurely 0.6.0
featurely: ^0.6.0 copied to clipboard
In-app feedback sheet for the self-hosted Featurely platform: feature requests, issue reports, voting, and comments — themed by your app and localized in 34 languages.
featurely #
In-app feedback for Flutter apps, backed by a self-hosted Featurely instance: end users browse feature requests and issue reports, vote, comment, and submit new feedback (with an optional screenshot and email), and message your team privately through In-App Chat — your team triages and replies from the Featurely web dashboard.
- Two-line integration —
initonce,showanywhere. - In-App Chat — a private one-to-one thread with your team, as a
standalone sheet (
showChat) or from the feedback sheet's "Message us" action. - Native-feeling — inherits your accent color, corner radius, font, and light/dark mode; iOS and Android adaptive details.
- 34 languages including RTL (
ar,he), resolved independently of the host app's locale. - Automatic sandbox/live separation — one API key; debug builds report to Sandbox (with an unmistakable amber SANDBOX strip), release builds to Live. Test data can never pollute Live.
- Android and iOS only.
Getting started #
dependencies:
featurely: ^0.6.0
Initialize once at startup (idempotent — call it on every launch), then present the sheet from any trigger:
import 'package:featurely/featurely.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Featurely.init(
baseUrl: 'https://feedback.example.com', // your instance, no /api/v1
apiKey: const String.fromEnvironment('FEATURELY_API_KEY'),
);
runApp(const MyApp());
}
// From any button:
await Featurely.show(context);
Environments #
Each project has a single API key (always available in Project Settings);
the SDK declares the environment on every request. By default it follows
the build type — debug builds report to Sandbox, release builds to Live —
so there is nothing to configure. For special flavors (e.g. a staging
release build that should stay in Sandbox), override it at init:
await Featurely.init(
baseUrl: …,
apiKey: …,
environment: FeaturelyEnvironment.sandbox,
);
Sandbox sessions render an amber SANDBOX strip across the top of the sheet so QA always knows which mode they're in. It is never a user-facing runtime toggle.
In-App Chat #
End users can message your team privately; the team answers from the dashboard Inbox. Replies show up in the open chat within a few seconds (the SDK polls every 5 s while the chat is on screen and the app is in the foreground), and are also emailed when the user left an address via the optional "Get replies by email" row.
Two entry points:
// 1. A standalone chat sheet, from your own "Contact us" button:
await Featurely.showChat(context);
// 2. Automatically: the feedback sheet opened by Featurely.show shows a
// "Message us" action that pushes the chat inside the same sheet.
To start the user off, prefill the composer — for example from an order screen:
await Featurely.showChat(
context,
initialMessage: 'I have a question about order #1234',
);
The text is only placed in the composer (cursor at the end, field focused);
it is never sent automatically, and the user can edit or delete it. Blank
text is ignored, it is capped to the 4 000-character message limit, and it
is applied once per showChat call — it doesn't come back after the user
sends or clears it. The "Message us" chat opens with an empty composer.
Chat metadata #
Give your team context with each message — the current screen, the plan, an
order id. Set an app-wide map once (it applies to every message sent from
then on; null clears it), and/or pass a map when you open the chat:
Featurely.setChatMetadata({'plan': 'pro', 'appVersion': '2.4.1'});
await Featurely.showChat(
context,
metadata: {'screen': 'Checkout', 'orderId': '1234'}, // wins on collisions
);
Metadata is shown to your team only — next to the message in the Inbox and in the support alert email. It is never shown to the user and never returned by the API, but don't put secrets in it. Values are strings; keys are trimmed and at most 64 characters, values are truncated to 500 characters, and at most 20 entries are sent (the SDK drops or trims anything else rather than failing the send). A retried message keeps the metadata it was composed with. The "Message us" chat inside the feedback sheet uses the app-wide map only.
Show an unread badge on your own button with unreadMessageCount() — it
never throws and returns 0 before init, when the device has no
conversation, when the server doesn't support chat, or on any error:
final unread = await Featurely.unreadMessageCount(); // e.g. on app resume
For a plain dot badge, hasUnreadMessages() returns
unreadMessageCount() > 0 with the same never-throws semantics (false
wherever the count is 0). Each call is a network request, so refresh it on
demand — for example on app resume and after the chat closes:
Future<bool> _unread = Featurely.hasUnreadMessages();
// In build():
FutureBuilder<bool>(
future: _unread,
builder: (context, snapshot) => Badge(
isLabelVisible: snapshot.data ?? false,
child: IconButton(
icon: const Icon(Icons.chat_bubble_outline),
onPressed: () async {
await Featurely.showChat(context);
setState(() => _unread = Featurely.hasUnreadMessages());
},
),
),
)
Things to know:
- One thread per device. The conversation belongs to the SDK's device
ID, not to
login(userId), so it never follows a user across devices.logout()(and logging in as a different user) rotates the device ID and starts a fresh, empty chat; the old thread stays in your Inbox. - Don't collect secrets in chat. Anyone holding the device ID can read that device's thread, the same trust level as votes.
- Messages are plain text, up to 4 000 characters. Messages that fail to send show "Not sent — Tap to retry" and are kept only while the chat is open. A retry never double-posts.
- Chat needs a Featurely server that reports
chatEnabledinGET /api/v1/config. Against older servers the "Message us" action is hidden andunreadMessageCount()returns0; gate your ownshowChatbutton accordingly, since on those servers the chat screen can only show its failed-load state. - The chat uses the same theming, localization (including RTL) and SANDBOX strip as the feedback sheet.
AI Support Assistant #
A Featurely project can turn on an AI support assistant (powered by Claude) that answers chat messages within seconds, from a knowledge document your team maintains in the dashboard plus a live diagnostics snapshot your app attaches to each message. When it can't help, or the user asks for a person, it hands the conversation to your team, who get the usual alert email. A team reply always takes over from the assistant.
Setup #
- Server (self-hosted): set
ANTHROPIC_API_KEYandASSISTANT_ENABLED=true. Optional:ASSISTANT_MODEL(defaultclaude-opus-5),ASSISTANT_EFFORT(defaultlow) andASSISTANT_CONCURRENCY(default4). See the server README. - Dashboard: an admin enables the assistant per environment under Settings → Assistant (Sandbox and Live separately), and sets its name, instructions and knowledge document.
- App: update to this SDK (0.6.0+). Nothing else is required: every chat send declares that this SDK can show assistant replies, and the assistant only answers conversations whose SDK does. The two hooks below make its answers much better.
Assistant replies show under the assistant's name with a small AI tag. They are plain text; step-by-step help comes as numbered lines. While the assistant is writing, the chat shows "{name} is typing…" and checks for the reply every 1.5 s (for up to 60 s, then back to every 5 s). There is no "talk to a person" button: users just ask, and the assistant escalates.
Diagnostics provider #
Give the assistant the app state it needs to diagnose a problem, so it doesn't have to ask:
Featurely.setChatDiagnosticsProvider(() async => {
'bluetooth': bluetooth.isOn ? 'on' : 'off',
'watchPaired': pairing.isPaired,
'lastSyncMinutesAgo': sync.minutesSinceLast,
'permissions': {'notifications': await notificationsGranted()},
});
The provider runs on every send (including a manual retry) and gets 1
second. Its result must be a JSON object: null, bool, finite numbers,
strings of at most 200 characters, lists of at most 30 items, and maps with
String keys, nested at most 3 levels (the top-level map counts), and at
most 4 096 bytes as UTF-8 JSON. If the provider times out, throws, returns
null, or breaks a limit, the message goes without diagnostics (a debug
build logs why); a send never fails because of them. If the server still
rejects the snapshot, the SDK re-sends the message once without it. Pass
null to remove the provider.
Diagnostics are shown to your team in the Inbox and sent to the assistant; they are never shown to the user or returned by the API. Don't include secrets, identifiers, or personal data — report states and counts, not names, emails, or tokens.
Actions #
Let the assistant suggest one-tap actions in your app. Register the actions with localized titles, and handle taps:
Featurely.registerChatActions([
FeaturelyChatAction(id: 'open_settings', title: l10n.openSettings),
FeaturelyChatAction(id: 'retry_pairing', title: l10n.tryAgain),
]);
Featurely.setChatActionHandler((id) {
switch (id) {
case 'retry_pairing':
pairing.retry();
return FeaturelyChatActionResult.stay; // keep the chat open
case 'open_settings':
// Runs after the chat sheet has closed.
WidgetsBinding.instance.addPostFrameCallback(
(_) => navigatorKey.currentState?.pushNamed('/settings'));
return FeaturelyChatActionResult.dismiss; // close the chat sheet
default:
return FeaturelyChatActionResult.stay;
}
});
- Each send lists the registered ids (as long as a handler is set), and the
assistant can suggest up to 3 of them per reply. They appear as buttons
below the reply, labelled with your
title. Ids you haven't registered are never shown. - A tapped button shows as used for the rest of the app session.
staykeeps the chat open.dismisscloses the sheet thatshowChat(orshow) presented right after the handler returns. To navigate once it is gone, schedule the work as above (or navigate afterawait Featurely.showChat(context)completes).- Ids must match
^[a-z][a-z0-9_]{0,39}$; at most 12 are kept. Invalid, duplicate, or blank-titled entries are dropped with a debug-build log.registerChatActionsreplaces the previous list; passnulltosetChatActionHandlerto remove the buttons.
All three hooks are app-wide, need init first, and survive a later init.
Data sent to Anthropic #
When a project enables the assistant, Anthropic becomes a subprocessor for that project. For each reply, the Featurely server sends Anthropic only:
- the conversation's message bodies, who wrote each (user, team, or assistant) and relative timestamps
- the diagnostics snapshot
- the conversation locale
- the chat metadata (
setChatMetadata/showChat(metadata:)), minus the keys your admin hides (by defaultsupport_id,email, anduser_id) - the registered action ids
- your project's assistant instructions and knowledge document
It never sends the contact email, the device ID, your login user ID, IP
addresses, user agents, or your team members' emails. API data is not used
for model training by default.
Privacy policy wording #
Disclose the assistant in your privacy policy. For example:
In-app support messages, together with technical information about the app's state on your device (for example, connection and permission status), may be processed by an AI assistant provided by Anthropic, PBC, to answer your question. Your email address and device identifiers are not shared with Anthropic. You can always ask to reach a person on our team.
Theming #
await Featurely.init(
baseUrl: …,
apiKey: …,
theme: const FeaturelyTheme(
accentColor: Color(0xFFD9572B), // default: your Theme's colorScheme.primary
cornerRadius: 16, // default: 12
brightness: Brightness.dark, // default: follows the host theme
fontFamily: 'Inter', // default: host font
),
);
Status pill colors and the sandbox strip are fixed by design and are not themed. On-accent text color is computed by contrast, so any accent hue stays legible in light and dark.
Localization #
The sheet ships all 34 Featurely locales and resolves its language from the
device locale (or the locale: override passed to init), independent of
your MaterialApp's locale — fallback chain: language + script → language +
region → base language → English. Chinese uses Traditional (zh-Hant) for a
Hant script or, without a script, for the regions TW, HK and MO
(Locale('zh', 'TW') → zh-Hant); everything else gets Simplified (zh). ar and he render fully right-to-left.
Identity: login / logout #
By default users are pseudonymous per install. If your app has its own accounts, link them so votes follow the user across devices and reinstalls:
await Featurely.init(…, userId: currentUser.id); // or:
await Featurely.login(currentUser.id); // on sign-in
await Featurely.logout(); // on sign-out
- Pass an opaque internal id, never an email or other PII.
loginis idempotent and safe on every launch; switching accounts is handled automatically (the SDK rotates its device identity between users).logoutstarts the next user of the device with a clean slate.
Featurely.setPlan('Pro Monthly') updates the plan label attached to
submissions (drives the PAYING badge in your dashboard).
Observability #
The sheet handles every failure with its own localized UI, so errors are
invisible to the host by default. To log or report them (a rotated key, an
unreachable instance), pass onError — it receives each API operation that
ultimately fails, after retries:
await Featurely.init(
…,
onError: (operation, error) => log.warning('featurely $operation: $error'),
);
error is a FeaturelyApiException (branch on its code) or a
FeaturelyNetworkException; neither ever contains the API key. Exceptions
thrown by the listener are swallowed — they never break the SDK's own
handling.
Screenshots & permissions #
The submit form offers one optional screenshot from the photo library
(no camera). No Info.plist entry is needed on iOS 14+ (PHPicker), and no
runtime permission on Android (Photo Picker on API 33+; older APIs are
handled by image_picker's legacy path). HEIC images are transcoded to PNG
automatically before upload.
Example app #
example/ is a runnable host app for manual QA against a local
featurely-app docker instance:
cd example
flutter run \
--dart-define=FEATURELY_BASE_URL=http://localhost:3000 \
--dart-define=FEATURELY_API_KEY=fk_…
(Use http://10.0.2.2:3000 on the Android emulator.) It exposes theming
knobs, a locale override, login/logout buttons, and a chat button wired to
an AI assistant diagnostics provider and one registered action.
Requirements #
- Flutter
>=3.27.0, Dart^3.6.0 - Android & iOS (no web/desktop)
- A Featurely instance serving the frozen
/api/v1contract (any server version — the SDK decodes leniently and never breaks on additive changes). In-App Chat needs a server that reportschatEnabled; the AI Support Assistant needs one with the assistant enabled for the environment.