updatify_flutter 1.4.0 copy "updatify_flutter: ^1.4.0" to clipboard
updatify_flutter: ^1.4.0 copied to clipboard

A Flutter widget for displaying release notes, product updates, and announcements from the Updatify service.

example/lib/main.dart

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

void main() {
  runApp(const App());
}

class App extends StatefulWidget {
  const App({super.key});

  @override
  State<App> createState() => _AppState();
}

class _AppState extends State<App> {
  static const _projectId = '3593f6b9-92de-4b32-941f-e152062745bb';

  // A neutral, near-monochrome palette: graphite on off-white in light, a light
  // gray accent on dark gray in dark. No color cast.
  static const _seed = Color(0xFF71717A);

  ThemeMode _themeMode = ThemeMode.light;

  Key _triggerKey = UniqueKey();

  void _toggleTheme() {
    setState(() {
      _themeMode =
          _themeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
    });
  }

  Future<void> _resetViewed() async {
    await UpdatifyTrigger.resetLastViewed(_projectId);
    // Rebuild the trigger so it re-checks for unseen posts and replays the ping.
    setState(() => _triggerKey = UniqueKey());
  }

  Future<void> _onShowDialog(BuildContext context) => showUpdatifyDialog(
        context,
        projectId: _projectId,
        borderRadius: BorderRadius.circular(6),
        width: _dialogWidth(context),
        // Whether voting shows, and the popup title, come from the project's
        // dashboard config and are applied automatically. Embed UpdatifyWidget
        // directly and pass onConfig if you want to read those settings.
      );

  Future<void> _onShowSheet(BuildContext context) => showUpdatifyBottomSheet(
        context,
        projectId: _projectId,
        showDragHandle: true,
      );

  // Opens the modal with a custom vote control built from heart icons instead
  // of the default thumbs. onVote hides all the toggle/network/persistence
  // logic; the builder only renders and forwards taps.
  Future<void> _onShowCustomVotes(BuildContext context) => showUpdatifyDialog(
        context,
        projectId: _projectId,
        borderRadius: BorderRadius.circular(6),
        width: _dialogWidth(context),
        voteBuilder: (context, currentVote, onVote) => Row(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            IconButton(
              tooltip: 'Love it',
              onPressed: () => onVote(VoteType.up),
              icon: Icon(
                currentVote == VoteType.up
                    ? Icons.favorite
                    : Icons.favorite_border,
              ),
            ),
            IconButton(
              tooltip: 'Not for me',
              onPressed: () => onVote(VoteType.down),
              icon: Icon(
                currentVote == VoteType.down
                    ? Icons.heart_broken
                    : Icons.heart_broken_outlined,
              ),
            ),
          ],
        ),
      );

  // Half the screen on wide/desktop layouts; null keeps the default full-width
  // dialog on narrow/mobile layouts.
  double? _dialogWidth(BuildContext context) {
    final width = MediaQuery.sizeOf(context).width;
    return width >= 600 ? width / 2 : null;
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Updatify Flutter',
      debugShowCheckedModeBanner: false,
      theme: _theme(Brightness.light),
      darkTheme: _theme(Brightness.dark),
      themeMode: _themeMode,
      // Builder so the scaffold builds under MaterialApp's theme.
      home: Builder(builder: _buildHome),
    );
  }

  ThemeData _theme(Brightness brightness) {
    final base = ColorScheme.fromSeed(
      seedColor: _seed,
      brightness: brightness,
      dynamicSchemeVariant: DynamicSchemeVariant.neutral,
    );
    // Pin the tokens the UI actually paints with to flat neutral grays so the
    // result is monochrome in both themes, with a genuine dark gray (not black)
    // in dark mode.
    final scheme = brightness == Brightness.light
        ? base.copyWith(
            surface: const Color(0xFFFAFAFA),
            surfaceContainerLowest: const Color(0xFFFFFFFF),
            surfaceContainerLow: const Color(0xFFF7F7F8),
            surfaceContainer: const Color(0xFFF1F1F2),
            surfaceContainerHigh: const Color(0xFFEBEBED),
            surfaceContainerHighest: const Color(0xFFE5E5E7),
            onSurface: const Color(0xFF18181B),
            onSurfaceVariant: const Color(0xFF6B7280),
            outline: const Color(0xFFC4C4C8),
            outlineVariant: const Color(0xFFE4E4E7),
            primary: const Color(0xFF27272A),
            onPrimary: const Color(0xFFFFFFFF),
            primaryContainer: const Color(0xFFE4E4E7),
            onPrimaryContainer: const Color(0xFF27272A),
          )
        : base.copyWith(
            surface: const Color(0xFF1B1B1E),
            surfaceContainerLowest: const Color(0xFF161618),
            surfaceContainerLow: const Color(0xFF1F1F22),
            surfaceContainer: const Color(0xFF232327),
            surfaceContainerHigh: const Color(0xFF2A2A2E),
            surfaceContainerHighest: const Color(0xFF303036),
            onSurface: const Color(0xFFECECEE),
            onSurfaceVariant: const Color(0xFFA1A1AA),
            outline: const Color(0xFF52525A),
            outlineVariant: const Color(0xFF35353A),
            primary: const Color(0xFFE4E4E7),
            onPrimary: const Color(0xFF1B1B1E),
            primaryContainer: const Color(0xFF303036),
            onPrimaryContainer: const Color(0xFFECECEE),
          );
    return ThemeData(
      useMaterial3: true,
      colorScheme: scheme,
      scaffoldBackgroundColor: scheme.surface,
      appBarTheme: const AppBarTheme(
        backgroundColor: Colors.transparent,
        elevation: 0,
        scrolledUnderElevation: 0,
        centerTitle: false,
      ),
    );
  }

  Widget _buildHome(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    final isDark = Theme.of(context).brightness == Brightness.dark;

    return Scaffold(
      appBar: AppBar(
        actions: [
          IconButton(
            tooltip: isDark ? 'Switch to light mode' : 'Switch to dark mode',
            icon: Icon(
              isDark ? Icons.light_mode_outlined : Icons.dark_mode_outlined,
            ),
            onPressed: _toggleTheme,
          ),
          // The recommended entry point: a live trigger that opens the popup
          // and pings when unseen updates exist.
          Padding(
            padding: const EdgeInsets.only(right: 4),
            child: UpdatifyTrigger(
              key: _triggerKey,
              projectId: _projectId,
              borderRadius: BorderRadius.circular(6),
              width: _dialogWidth(context),
            ),
          ),
        ],
      ),
      body: Stack(
        children: [
          Center(
            child: SingleChildScrollView(
              padding: const EdgeInsets.all(24),
              child: ConstrainedBox(
                constraints: const BoxConstraints(maxWidth: 480),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [
                    Padding(
                      padding: const EdgeInsets.only(bottom: 16),
                      child: Text(
                        'Updatify Flutter',
                        textAlign: TextAlign.center,
                        style: Theme.of(context)
                            .textTheme
                            .titleLarge
                            ?.copyWith(fontWeight: FontWeight.w700),
                      ),
                    ),
                    Container(
                      padding: const EdgeInsets.all(28),
                      decoration: BoxDecoration(
                        color: scheme.surfaceContainerLow,
                        borderRadius: BorderRadius.circular(28),
                        border: Border.all(color: scheme.outlineVariant),
                      ),
                      child: Column(
                        mainAxisSize: MainAxisSize.min,
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          // HERO
                          Container(
                            width: 56,
                            height: 56,
                            decoration: BoxDecoration(
                              color: scheme.primaryContainer,
                              borderRadius: BorderRadius.circular(18),
                            ),
                            child: Icon(
                              Icons.campaign_rounded,
                              size: 30,
                              color: scheme.onPrimaryContainer,
                            ),
                          ),
                          const SizedBox(height: 20),
                          Text(
                            'RELEASE NOTES',
                            style: TextStyle(
                              fontSize: 12,
                              fontWeight: FontWeight.w600,
                              letterSpacing: 1.5,
                              color: scheme.primary,
                            ),
                          ),
                          const SizedBox(height: 6),
                          Text(
                            "What's new, right inside your app",
                            style: Theme.of(context)
                                .textTheme
                                .headlineSmall
                                ?.copyWith(
                                  fontWeight: FontWeight.w700,
                                  height: 1.15,
                                ),
                          ),
                          const SizedBox(height: 8),
                          Text(
                            'Pick a presentation to preview it. Voting and the popup '
                            'title are driven by your Updatify project settings.',
                            style: Theme.of(context)
                                .textTheme
                                .bodyMedium
                                ?.copyWith(
                                  color: scheme.onSurfaceVariant,
                                  height: 1.4,
                                ),
                          ),
                          const SizedBox(height: 24),

                          // ACTIONS
                          _ActionTile(
                            icon: Icons.web_asset_rounded,
                            title: 'Modal dialog',
                            subtitle: 'Centered popup, suited to wide layouts',
                            onTap: () => _onShowDialog(context),
                          ),
                          const SizedBox(height: 10),
                          _ActionTile(
                            icon: Icons.vertical_align_bottom_rounded,
                            title: 'Bottom sheet',
                            subtitle: 'Rises from the bottom, suited to mobile',
                            onTap: () => _onShowSheet(context),
                          ),
                          const SizedBox(height: 10),
                          _ActionTile(
                            icon: Icons.favorite_rounded,
                            title: 'Custom vote control',
                            subtitle:
                                'Same popup with a heart-based voteBuilder',
                            onTap: () => _onShowCustomVotes(context),
                          ),

                          const SizedBox(height: 20),
                          Divider(color: scheme.outlineVariant, height: 1),
                          const SizedBox(height: 12),

                          // RESET
                          Align(
                            alignment: Alignment.centerLeft,
                            child: TextButton.icon(
                              onPressed: _resetViewed,
                              icon: const Icon(Icons.refresh_rounded, size: 18),
                              label: const Text('Reset viewed posts'),
                            ),
                          ),
                          Padding(
                            padding: const EdgeInsets.only(left: 12),
                            child: Text(
                              'Marks every update unseen so the bell above pings again.',
                              style: Theme.of(context)
                                  .textTheme
                                  .bodySmall
                                  ?.copyWith(
                                    color: scheme.onSurfaceVariant,
                                  ),
                            ),
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
          Positioned(
            top: 2,
            right: 31,
            child: IgnorePointer(
              child: _BellHint(color: scheme.onSurfaceVariant),
            ),
          ),
        ],
      ),
    );
  }
}

/// A hand-annotation tucked under the app bar: an italic label tilted up toward
/// the [UpdatifyTrigger] bell, with an arrow pointing at it.
class _BellHint extends StatelessWidget {
  const _BellHint({required this.color});

  final Color color;

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        ConstrainedBox(
          constraints: const BoxConstraints(maxWidth: 150),
          child: Transform.rotate(
            angle: -0.14,
            child: Text(
              'This is how trigger button could look like in your app',
              textAlign: TextAlign.right,
              style: TextStyle(
                fontStyle: FontStyle.italic,
                fontSize: 13,
                height: 1.3,
                color: color,
              ),
            ),
          ),
        ),
        const SizedBox(width: 2),
        Transform.translate(
          offset: const Offset(0, -2),
          child: Icon(Icons.north_east_rounded, size: 26, color: color),
        ),
      ],
    );
  }
}

/// A tappable row: a tinted icon tile, a title with a one-line description, and
/// a trailing chevron. Used for the demo's presentation choices.
class _ActionTile extends StatelessWidget {
  const _ActionTile({
    required this.icon,
    required this.title,
    required this.subtitle,
    required this.onTap,
  });

  final IconData icon;
  final String title;
  final String subtitle;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    final radius = BorderRadius.circular(16);
    return Material(
      color: scheme.surfaceContainerHighest.withValues(alpha: 0.5),
      borderRadius: radius,
      child: InkWell(
        onTap: onTap,
        borderRadius: radius,
        child: Padding(
          padding: const EdgeInsets.all(12),
          child: Row(
            children: [
              Container(
                width: 40,
                height: 40,
                decoration: BoxDecoration(
                  color: scheme.primary.withValues(alpha: 0.12),
                  borderRadius: BorderRadius.circular(12),
                ),
                child: Icon(icon, size: 20, color: scheme.primary),
              ),
              const SizedBox(width: 14),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      title,
                      style: const TextStyle(
                        fontWeight: FontWeight.w600,
                        fontSize: 15,
                      ),
                    ),
                    const SizedBox(height: 2),
                    Text(
                      subtitle,
                      style: Theme.of(context).textTheme.bodySmall?.copyWith(
                            color: scheme.onSurfaceVariant,
                          ),
                    ),
                  ],
                ),
              ),
              Icon(
                Icons.chevron_right_rounded,
                color: scheme.onSurfaceVariant,
              ),
            ],
          ),
        ),
      ),
    );
  }
}
2
likes
150
points
673
downloads
screenshot

Documentation

API reference

Publisher

verified publisherupdatify.io

Weekly Downloads

A Flutter widget for displaying release notes, product updates, and announcements from the Updatify service.

Homepage
Repository (GitHub)
View/report issues

Topics

#updatify #release-notes #changelog #announcements

License

MIT (license)

Dependencies

cached_network_image, flutter, http, intl, markdown_rich_text, shared_preferences, url_launcher, uuid, visibility_detector

More

Packages that depend on updatify_flutter