featurely 0.6.1 copy "featurely: ^0.6.1" to clipboard
featurely: ^0.6.1 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 — init once, show anywhere.
  • 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).

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 chatEnabled in GET /api/v1/config. Against older servers the "Message us" action is hidden and unreadMessageCount() returns 0; gate your own showChat button 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 #

  1. Server (self-hosted): set ANTHROPIC_API_KEY and ASSISTANT_ENABLED=true. Optional: ASSISTANT_MODEL (default claude-opus-5), ASSISTANT_EFFORT (default low) and ASSISTANT_CONCURRENCY (default 4). See the server README.
  2. Dashboard: an admin enables the assistant per environment under Settings → Assistant (Sandbox and Live separately), and sets its name, instructions and knowledge document.
  3. 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 (default "Assistant"). 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.
  • stay keeps the chat open. dismiss closes the sheet that showChat (or show) presented right after the handler returns. To navigate once it is gone, schedule the work as above (or navigate after await 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. registerChatActions replaces the previous list; pass null to setChatActionHandler to 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 default support_id, email, and user_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.
  • login is idempotent and safe on every launch; switching accounts is handled automatically (the SDK rotates its device identity between users).
  • logout starts 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/v1 contract (any server version — the SDK decodes leniently and never breaks on additive changes). In-App Chat needs a server that reports chatEnabled; the AI Support Assistant needs one with the assistant enabled for the environment.
0
likes
160
points
280
downloads

Documentation

API reference

Publisher

verified publishercarpediemcreations.io

Weekly Downloads

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.

Repository (GitHub)
View/report issues

Topics

#feedback #feature-requests #bug-report #widget

License

MIT (license)

Dependencies

device_info_plus, flutter, flutter_localizations, http, http_parser, image_picker, intl, package_info_plus, shared_preferences

More

Packages that depend on featurely