updatify_flutter 1.4.1
updatify_flutter: ^1.4.1 copied to clipboard
A Flutter widget for displaying release notes, product updates, and announcements from the Updatify service.
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,
),
],
),
),
),
);
}
}