fz_hms_push_kit 0.0.2
fz_hms_push_kit: ^0.0.2 copied to clipboard
A Flutter package for handling Huawei Mobile Services (HMS) Push Kit — token retrieval, foreground/background message handling, notification tap events, and a server-side helper to obtain OAuth2 acces [...]
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fz_hms_push_kit/fz_hms_push_kit.dart';
// ─────────────────────────────────────────────────────────────────────────────
// Entry point
// ─────────────────────────────────────────────────────────────────────────────
/// Top-level background message handler for HMS Push Kit.
/// Must be a top-level or static function to run in background isolates.
@pragma('vm:entry-point')
void customBackgroundMessageHandler(RemoteMessage message) {
debugPrint('[Example Background] Message received in background: ${message.data}');
}
final HmsNotificationService hmsService=HmsNotificationServiceImpl();
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await hmsService.initialize(
onTokenReceived: (String token) {
debugPrint('[Example] HMS Token: $token');
},
onBackgroundMessage: customBackgroundMessageHandler,
);
runApp(const HmsPushKitExampleApp());
}
// ─────────────────────────────────────────────────────────────────────────────
// App Configuration
// ─────────────────────────────────────────────────────────────────────────────
class HmsPushKitExampleApp extends StatelessWidget {
const HmsPushKitExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'HMS Push Kit',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xFF090D16),
colorScheme: const ColorScheme.dark(
primary: Color(0xFFE11D48), // Vibrant Huawei Rose
secondary: Color(0xFFF43F5E),
surface: Color(0xFF111827),
onSurface: Color(0xFFF3F4F6),
),
fontFamily: 'Roboto',
useMaterial3: true,
),
home: const HomePage(),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Home Page
// ─────────────────────────────────────────────────────────────────────────────
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
String _token = '';
final List<Map<String, dynamic>> _messages = [];
StreamSubscription<String?>? _sub;
// Form controllers
final _clientSecretCtrl = TextEditingController();
final _appIdCtrl = TextEditingController(text: '110577713');
final _titleCtrl = TextEditingController(text: '⚡ HMS Push Notification');
final _bodyCtrl = TextEditingController(
text: 'This is a live test push notification sent via HMS REST API.');
bool _isSecretVisible = false;
bool _isSending = false;
String _sendResult = '';
bool _sendSuccess = false;
bool _copiedToken = false;
@override
void initState() {
super.initState();
_token = hmsService.token;
// Listen for notification taps & incoming messages
hmsService.onNotificationClick.listen((msg) {
if (msg == null) return;
try {
final decoded = jsonDecode(msg) as Map<String, dynamic>;
setState(() {
_messages.insert(0, {
...decoded,
'time': DateTime.now().toIso8601String().substring(11, 19),
});
});
} catch (_) {
setState(() {
_messages.insert(0, {
'title': 'Raw Message',
'body': msg,
'time': DateTime.now().toIso8601String().substring(11, 19),
});
});
}
});
_sub = hmsService.onNotificationClick.listen((_) {
final t = hmsService.token;
if (t.isNotEmpty && t != _token) {
setState(() => _token = t);
}
});
}
@override
void dispose() {
_sub?.cancel();
_clientSecretCtrl.dispose();
_appIdCtrl.dispose();
_titleCtrl.dispose();
_bodyCtrl.dispose();
super.dispose();
}
void _copyToClipboard(String text) {
Clipboard.setData(ClipboardData(text: text));
setState(() => _copiedToken = true);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Row(
children: [
Icon(Icons.check_circle_rounded, color: Color(0xFF10B981), size: 18),
SizedBox(width: 8),
Text('Token copied to clipboard!'),
],
),
backgroundColor: const Color(0xFF1E293B),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
duration: const Duration(seconds: 2),
),
);
Future.delayed(const Duration(seconds: 2), () {
if (mounted) setState(() => _copiedToken = false);
});
}
Future<void> _sendNotification() async {
if (_token.isEmpty || _token == 'Waiting for token...') {
setState(() {
_sendSuccess = false;
_sendResult = 'Device token is not available yet. Make sure HMS Core is configured.';
});
return;
}
final secret = _clientSecretCtrl.text.trim();
if (secret.isEmpty) {
setState(() {
_sendSuccess = false;
_sendResult = 'Please enter your HMS Client Secret from AppGallery Connect.';
});
return;
}
setState(() {
_isSending = true;
_sendResult = '';
});
try {
final appId = _appIdCtrl.text.trim().isEmpty ? '110577713' : _appIdCtrl.text.trim();
final pushClient = HmsPushClient(
appId: appId,
clientSecret: secret,
);
final result = await pushClient.sendNotification(
HmsNotification(
title: _titleCtrl.text.trim(),
body: _bodyCtrl.text.trim(),
tokens: [_token],
data: const {
'id': '42',
'view': 'dashboard',
'type': 'test_push',
},
),
);
setState(() {
_sendSuccess = result.isSuccess;
_sendResult = result.isSuccess
? 'Notification sent successfully!\nRequest ID: ${result.requestId}'
: 'Error [${result.code}]: ${result.msg}';
});
} catch (e) {
setState(() {
_sendSuccess = false;
_sendResult = 'Exception: $e';
});
} finally {
setState(() => _isSending = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFF0F172A),
Color(0xFF090D16),
],
),
),
child: SafeArea(
child: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
// ── Modern Header ──────────────────────────────────────────────
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFFE11D48), Color(0xFF9F1239)],
),
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: const Color(0xFFE11D48).withOpacity(0.35),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: const Icon(
Icons.notifications_active_rounded,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 14),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'HMS Push Kit',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
letterSpacing: 0.3,
color: Colors.white,
),
),
const SizedBox(height: 2),
Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: _token.isNotEmpty ? const Color(0xFF10B981) : const Color(0xFFF59E0B),
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
Text(
_token.isNotEmpty ? 'Service Active' : 'Waiting for Token',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF94A3B8),
),
),
],
),
],
),
],
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF1E293B),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFF334155)),
),
child: const Text(
'v0.0.1',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFFF43F5E),
),
),
),
],
),
),
),
SliverPadding(
padding: const EdgeInsets.all(20),
sliver: SliverList(
delegate: SliverChildListDelegate([
// ── Device Token Card ────────────────────────────────────
_GlassCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Row(
children: [
Icon(Icons.vpn_key_rounded,
color: Color(0xFFF43F5E), size: 18),
SizedBox(width: 8),
Text(
'HMS Device Push Token',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
],
),
if (_token.isNotEmpty)
Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _copyToClipboard(_token),
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
child: Row(
children: [
Icon(
_copiedToken
? Icons.check_rounded
: Icons.copy_rounded,
size: 14,
color: _copiedToken
? const Color(0xFF10B981)
: const Color(0xFF94A3B8),
),
const SizedBox(width: 4),
Text(
_copiedToken ? 'Copied' : 'Copy',
style: TextStyle(
fontSize: 12,
color: _copiedToken
? const Color(0xFF10B981)
: const Color(0xFF94A3B8),
),
),
],
),
),
),
),
],
),
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF1E293B)),
),
child: SelectableText(
_token.isEmpty
? 'Fetching device token from Huawei Push Kit...'
: _token,
style: TextStyle(
fontFamily: 'monospace',
fontSize: 12,
height: 1.4,
color: _token.isEmpty
? const Color(0xFF64748B)
: const Color(0xFF38BDF8),
),
),
),
],
),
),
const SizedBox(height: 20),
// ── Send Test Push Section ──────────────────────────────
_GlassCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.send_rounded,
color: Color(0xFFF43F5E), size: 18),
SizedBox(width: 8),
Text(
'Send Push Notification',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
],
),
const SizedBox(height: 4),
const Text(
'Test sending a push notification via Huawei Push REST API',
style: TextStyle(fontSize: 12, color: Color(0xFF64748B)),
),
const SizedBox(height: 16),
// App ID & Secret Input
Row(
children: [
Expanded(
flex: 2,
child: _StyledTextField(
controller: _appIdCtrl,
label: 'HMS App ID',
prefixIcon: Icons.apps_rounded,
),
),
const SizedBox(width: 12),
Expanded(
flex: 3,
child: _StyledTextField(
controller: _clientSecretCtrl,
label: 'Client Secret',
obscureText: !_isSecretVisible,
prefixIcon: Icons.lock_outline_rounded,
suffixIcon: IconButton(
icon: Icon(
_isSecretVisible
? Icons.visibility_off_rounded
: Icons.visibility_rounded,
size: 18,
color: const Color(0xFF64748B),
),
onPressed: () {
setState(() =>
_isSecretVisible = !_isSecretVisible);
},
),
),
),
],
),
const SizedBox(height: 12),
_StyledTextField(
controller: _titleCtrl,
label: 'Notification Title',
prefixIcon: Icons.title_rounded,
),
const SizedBox(height: 12),
_StyledTextField(
controller: _bodyCtrl,
label: 'Notification Body',
maxLines: 2,
prefixIcon: Icons.notes_rounded,
),
const SizedBox(height: 16),
// Action Button
SizedBox(
width: double.infinity,
height: 48,
child: DecoratedBox(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
Color(0xFFE11D48),
Color(0xFFBE123C),
],
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: const Color(0xFFE11D48).withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: ElevatedButton(
onPressed: _isSending ? null : _sendNotification,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: _isSending
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2.5,
valueColor:
AlwaysStoppedAnimation<Color>(
Colors.white),
),
)
: const Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(Icons.send_rounded,
size: 18, color: Colors.white),
SizedBox(width: 8),
Text(
'Send Notification',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
),
),
// Response banner
if (_sendResult.isNotEmpty) ...[
const SizedBox(height: 14),
AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _sendSuccess
? const Color(0xFF064E3B).withOpacity(0.4)
: const Color(0xFF7F1D1D).withOpacity(0.4),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: _sendSuccess
? const Color(0xFF059669)
: const Color(0xFFDC2626),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
_sendSuccess
? Icons.check_circle_outline_rounded
: Icons.error_outline_rounded,
color: _sendSuccess
? const Color(0xFF34D399)
: const Color(0xFFFCA5A5),
size: 20,
),
const SizedBox(width: 10),
Expanded(
child: SelectableText(
_sendResult,
style: TextStyle(
fontSize: 12,
height: 1.4,
color: _sendSuccess
? const Color(0xFFD1FAE5)
: const Color(0xFFFEE2E2),
),
),
),
],
),
),
],
],
),
),
const SizedBox(height: 20),
// ── Received Messages Feed ──────────────────────────────
_GlassCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Row(
children: [
Icon(Icons.inbox_rounded,
color: Color(0xFFF43F5E), size: 18),
SizedBox(width: 8),
Text(
'Received Messages',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
],
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFF1E293B),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${_messages.length}',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Color(0xFFF43F5E),
),
),
),
],
),
const SizedBox(height: 12),
if (_messages.isEmpty)
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 30),
decoration: BoxDecoration(
color: const Color(0xFF0F172A).withOpacity(0.5),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: const Color(0xFF1E293B)),
),
child: const Column(
children: [
Icon(Icons.notifications_none_rounded,
size: 40, color: Color(0xFF475569)),
SizedBox(height: 8),
Text(
'No notifications received yet',
style: TextStyle(
fontSize: 13,
color: Color(0xFF64748B)),
),
SizedBox(height: 4),
Text(
'Tap a notification or send a test payload above',
style: TextStyle(
fontSize: 11,
color: Color(0xFF475569)),
),
],
),
)
else
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _messages.length,
separatorBuilder: (_, __) =>
const SizedBox(height: 10),
itemBuilder: (context, index) {
final msg = _messages[index];
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: const Color(0xFF1E293B)),
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
msg['title']?.toString() ??
'Notification Payload',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13,
color: Colors.white,
),
),
Text(
msg['time']?.toString() ?? '',
style: const TextStyle(
fontSize: 11,
color: Color(0xFF64748B),
),
),
],
),
const SizedBox(height: 6),
Wrap(
spacing: 6,
runSpacing: 6,
children: msg.entries
.where((e) =>
e.key != 'title' &&
e.key != 'time')
.map((e) => Container(
padding: const EdgeInsets
.symmetric(
horizontal: 8,
vertical: 3),
decoration: BoxDecoration(
color:
const Color(0xFF1E293B),
borderRadius:
BorderRadius.circular(6),
),
child: Text(
'${e.key}: ${e.value}',
style: const TextStyle(
fontSize: 11,
fontFamily: 'monospace',
color: Color(0xFF38BDF8),
),
),
))
.toList(),
),
],
),
);
},
),
],
),
),
const SizedBox(height: 30),
]),
),
),
],
),
),
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Reusable UI Components
// ─────────────────────────────────────────────────────────────────────────────
class _GlassCard extends StatelessWidget {
const _GlassCard({required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF111827).withOpacity(0.85),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: const Color(0xFF1F2937)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 16,
offset: const Offset(0, 6),
),
],
),
child: child,
);
}
}
class _StyledTextField extends StatelessWidget {
const _StyledTextField({
required this.controller,
required this.label,
this.obscureText = false,
this.maxLines = 1,
this.prefixIcon,
this.suffixIcon,
});
final TextEditingController controller;
final String label;
final bool obscureText;
final int maxLines;
final IconData? prefixIcon;
final Widget? suffixIcon;
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
obscureText: obscureText,
maxLines: maxLines,
style: const TextStyle(fontSize: 13, color: Colors.white),
decoration: InputDecoration(
labelText: label,
labelStyle: const TextStyle(fontSize: 12, color: Color(0xFF64748B)),
floatingLabelStyle: const TextStyle(color: Color(0xFFF43F5E)),
isDense: true,
filled: true,
fillColor: const Color(0xFF0F172A),
prefixIcon: prefixIcon != null
? Icon(prefixIcon, size: 18, color: const Color(0xFF64748B))
: null,
suffixIcon: suffixIcon,
contentPadding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Color(0xFF1E293B)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Color(0xFFE11D48)),
),
),
);
}
}