JOIN Stories Flutter Plugin

A Flutter plugin to integrate JOIN Stories widgets and standalone player in Flutter apps.

🚀 Features

  • Bubble, Card List, Card Grid widgets
  • Unified standalone player API (JOINStories.startPlayer)
  • Centralized event routing (triggers, player, analytics)
  • Refresh support via controllers (Bubble/Card)
  • Custom fonts on Android and iOS
  • iOS and Android support

Compatibility

  • Flutter: 3.0.0+
  • Dart SDK: 2.17.0+
  • iOS: 12.0+
  • Android: API 21+

See COMPATIBILITY.md and COMPATIBILITY_SUMMARY.md for details.

Installation

Add to your app’s pubspec.yaml:

dependencies:
  join_stories_flutter: ^0.0.1

iOS

  • Set platform to 12.0+ in ios/Podfile:
platform :ios, '12.0'
  • Then run in ios/: pod repo update && pod install

Android

  • No special setup required.

Initialization

import 'package:join_stories_flutter/join_stories_flutter.dart';

await JOINStories.initialize(teamId: 'your-team-id');

// optional if provided by JOIN
await JOINStories.initialize(teamId: 'your-team-id', apiKey: 'your-api-key');

Widgets

import 'package:flutter/material.dart';
import 'package:join_stories_flutter/join_stories_flutter.dart';

class MyStoriesPage extends StatefulWidget {
  const MyStoriesPage({super.key});
  @override
  State<MyStoriesPage> createState() => _MyStoriesPageState();
}

class _MyStoriesPageState extends State<MyStoriesPage> {
  final BubbleController _bubbleCtrl = BubbleController();
  final CardController _cardListCtrl = CardController();
  final CardController _cardGridCtrl = CardController();

  Future<void> _pullToRefresh() async {
    await _bubbleCtrl.refresh();
    await _cardListCtrl.refresh();
    await _cardGridCtrl.refresh();
  }

  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: _pullToRefresh,
      child: ListView(
        children: [
          BubbleWidget(
            alias: 'bubble-alias',
            controller: _bubbleCtrl,
            configuration: const BubbleConfiguration(
              showLabel: true,
              labelFontSize: 14,
              fontName: 'Avenir',
              // player customization available here as well
            ),
            onTriggerFetchSuccess: (count) {},
            onTriggerFetchEmpty: () {},
            onTriggerFetchError: (msg) {},
            onPlayerFetchSuccess: () {},
            onPlayerLoaded: () {},
            onPlayerFetchError: (msg) {},
            onPlayerDismissed: (type) {}, // 'auto' | 'manual'
            onContentLinkClick: (link) {},
            onAnalyticsEvent: (name, payload) {},
          ),
          CardListWidget(
            alias: 'card-list-alias',
            controller: _cardListCtrl,
            configuration: const CardConfiguration(
              showLabel: true,
              cardRadius: 12,
            ),
          ),
          CardGridWidget(
            alias: 'card-grid-alias',
            controller: _cardGridCtrl,
            configuration: const CardConfiguration(
              numberOfColumns: 2,
              cardSize: 150,
            ),
          ),
        ],
      ),
    );
  }
}

Standalone Player

Single unified method (customization + callbacks):

await JOINStories.startPlayer(
  'widget-alias',
  standaloneOrigin: 'top', // 'top'|'topLeft'|'topRight'|'bottom'|'bottomLeft'|'bottomRight'
  playerBackgroundColor: 0xFF2C3E50,
  playerVerticalAnchor: 'center',
  playerHorizontalMargins: 20.0,
  playerCornerRadius: 12.0,
  playerProgressBarDefaultColor: 0xFF7F8C8D,
  playerProgressBarFillColor: 0xFF3498DB,
  playerProgressBarThickness: 4.0,
  playerProgressBarRadius: 8.0,
  onPlayerFetchSuccess: () {},
  onPlayerLoaded: () {},
  onPlayerFetchError: (msg) {},
  onPlayerDismissed: (type) {},
  onContentLinkClick: (link) {},
  onAnalyticsEvent: (eventName, payload) {},
);

Analytics & Identification

await JOINStories.setTrackingUserId('user_123');
await JOINStories.sendConversion('purchase', 'ecommerce');
await JOINStories.setSegmentationKey('premium_user');

All analytics events are routed to Flutter via:

onAnalyticsEvent(String eventName, Map<String, dynamic> payload)

Note (iOS widgets): until per-widget IDs are available, analytics include viewId: -1 (global). The plugin broadcasts to mounted widgets so callbacks still fire.

Custom Fonts

JOIN widgets are native views. Make the font available on both Flutter and native sides.

Flutter

Add fonts under assets/fonts/ and declare in pubspec.yaml:

flutter:
  uses-material-design: true
  fonts:
    - family: Parisienne
      fonts:
        - asset: assets/fonts/Parisienne-Regular.ttf

Optional global theme:

MaterialApp(theme: ThemeData(fontFamily: 'Parisienne'))

iOS

  • Copy the font into ios/Runner/Fonts/Parisienne-Regular.ttf (Target Membership: Runner)
  • Add in ios/Runner/Info.plist under UIAppFonts:
<string>Fonts/Parisienne-Regular.ttf</string>
  • Use the exact PostScript name in JOIN configs: fontName: 'Parisienne-Regular'

Android

  • Either put the font under android/app/src/main/res/font/ or keep it in Flutter assets.
  • Pass the same fontName as iOS; the bridge will try assets first, then res/font, then system font.

Refresh Widgets

Recommended: use controllers and call refresh() (see example above).

Advanced: call the API directly if you have a viewId:

await JOINStories.refreshWidget(viewId: someViewId, type: 'bubble'); // or 'card'

Example

See the example/ directory for a complete example application.

License

This project is licensed under the MIT License - see the LICENSE file for details.