@rashidee/co2 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +25449 -0
- package/drizzle/0000_remarkable_charles_xavier.sql +210 -0
- package/drizzle/0001_wise_wolf_cub.sql +13 -0
- package/drizzle/0002_secret_spyke.sql +16 -0
- package/drizzle/0003_workspace_config_singleton.sql +16 -0
- package/drizzle/0004_terminal_claim_v120.sql +11 -0
- package/drizzle/0005_brainstorm_v124.sql +10 -0
- package/drizzle/0006_project_v140.sql +6 -0
- package/drizzle/0007_prd_wizard_v124.sql +14 -0
- package/drizzle/0008_workflow_artifact_v126.sql +24 -0
- package/drizzle/0009_project_import_v150.sql +7 -0
- package/drizzle/0010_quality_v151.sql +3 -0
- package/drizzle/meta/0000_snapshot.json +1335 -0
- package/drizzle/meta/0001_snapshot.json +1416 -0
- package/drizzle/meta/0002_snapshot.json +1510 -0
- package/drizzle/meta/_journal.json +83 -0
- package/package.json +41 -0
- package/plugin/.claude-plugin/marketplace.json +36 -0
- package/plugin/.claude-plugin/plugin.json +22 -0
- package/plugin/README.md +625 -0
- package/plugin/SKILLS.md +350 -0
- package/plugin/co2.svg +5 -0
- package/plugin/commands/brainstorm-loop.md +129 -0
- package/plugin/skills/conductor-defect/SKILL.md +898 -0
- package/plugin/skills/conductor-feature-develop/SKILL.md +1383 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-domain-template.md +131 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-master-template.md +89 -0
- package/plugin/skills/conductor-feature-develop/references/playwright-setup.md +224 -0
- package/plugin/skills/conductor-feature-prepare/SKILL.md +604 -0
- package/plugin/skills/depgen-k8s/SKILL.md +468 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-laravel.md +197 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-nodejs.md +163 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-spring-boot.md +174 -0
- package/plugin/skills/depgen-k8s/references/k8s-patterns.md +869 -0
- package/plugin/skills/mockgen-shadcn/SKILL.md +1067 -0
- package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +1467 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-index-template.md +216 -0
- package/plugin/skills/mockgen-tailwind/SKILL.md +904 -0
- package/plugin/skills/mockgen-tailwind/references/admin-layout-template.md +720 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-index-template.md +190 -0
- package/plugin/skills/modelgen-nosql/SKILL.md +617 -0
- package/plugin/skills/modelgen-nosql/references/model-extraction-methodology.md +497 -0
- package/plugin/skills/modelgen-nosql/references/nosql-design-guide.md +258 -0
- package/plugin/skills/modelgen-relational/SKILL.md +707 -0
- package/plugin/skills/modelgen-relational/references/model-extraction-methodology.md +499 -0
- package/plugin/skills/specgen-flutter-riverpod/SKILL.md +898 -0
- package/plugin/skills/specgen-flutter-riverpod/references/component-patterns.md +971 -0
- package/plugin/skills/specgen-flutter-riverpod/references/notification-patterns.md +457 -0
- package/plugin/skills/specgen-flutter-riverpod/references/routing-patterns.md +362 -0
- package/plugin/skills/specgen-flutter-riverpod/references/security-patterns.md +709 -0
- package/plugin/skills/specgen-flutter-riverpod/references/spec-template.md +1566 -0
- package/plugin/skills/specgen-flutter-riverpod/references/state-patterns.md +518 -0
- package/plugin/skills/specgen-flutter-riverpod/references/storage-patterns.md +370 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/SKILL.md +984 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/batch-patterns.md +359 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/messaging-patterns.md +468 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/modulith-patterns.md +461 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/reporting-patterns.md +803 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/security-patterns.md +501 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/spec-template.md +1268 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/web-patterns.md +301 -0
- package/plugin/skills/specgen-node-cli-web/SKILL.md +561 -0
- package/plugin/skills/specgen-node-cli-web/references/auth-patterns.md +293 -0
- package/plugin/skills/specgen-node-cli-web/references/cli-packaging-patterns.md +250 -0
- package/plugin/skills/specgen-node-cli-web/references/server-patterns.md +247 -0
- package/plugin/skills/specgen-node-cli-web/references/spec-template.md +401 -0
- package/plugin/skills/specgen-node-cli-web/references/testing-patterns.md +174 -0
- package/plugin/skills/specgen-react-mui/SKILL.md +789 -0
- package/plugin/skills/specgen-react-mui/references/component-patterns.md +1077 -0
- package/plugin/skills/specgen-react-mui/references/reporting-patterns.md +1998 -0
- package/plugin/skills/specgen-react-mui/references/routing-patterns.md +461 -0
- package/plugin/skills/specgen-react-mui/references/security-patterns.md +599 -0
- package/plugin/skills/specgen-react-mui/references/spec-template.md +1030 -0
- package/plugin/skills/specgen-react-mui/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-react-tailwind/SKILL.md +859 -0
- package/plugin/skills/specgen-react-tailwind/references/component-patterns.md +1212 -0
- package/plugin/skills/specgen-react-tailwind/references/reporting-patterns.md +1529 -0
- package/plugin/skills/specgen-react-tailwind/references/routing-patterns.md +669 -0
- package/plugin/skills/specgen-react-tailwind/references/security-patterns.md +656 -0
- package/plugin/skills/specgen-react-tailwind/references/spec-template.md +1237 -0
- package/plugin/skills/specgen-react-tailwind/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-sdk-java/SKILL.md +933 -0
- package/plugin/skills/specgen-sdk-java/references/api-extraction-patterns.md +247 -0
- package/plugin/skills/specgen-sdk-java/references/http-client-patterns.md +495 -0
- package/plugin/skills/specgen-sdk-java/references/packaging-patterns.md +366 -0
- package/plugin/skills/specgen-sdk-java/references/spec-template.md +956 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/SKILL.md +1284 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/jasper-patterns.md +1638 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/modulith-patterns.md +354 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/security-patterns.md +551 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/spec-template.md +2502 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/web-patterns.md +503 -0
- package/plugin/skills/specgen-spring-jpa-restapi/SKILL.md +990 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/modulith-patterns.md +279 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/reporting-patterns.md +1536 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/restapi-patterns.md +722 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/security-patterns.md +351 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/spec-template.md +553 -0
- package/plugin/skills/specgen-ts-cli/SKILL.md +861 -0
- package/plugin/skills/specgen-ts-cli/references/async-patterns.md +1101 -0
- package/plugin/skills/specgen-ts-cli/references/command-patterns.md +383 -0
- package/plugin/skills/specgen-ts-cli/references/config-patterns.md +430 -0
- package/plugin/skills/specgen-ts-cli/references/database-patterns.md +750 -0
- package/plugin/skills/specgen-ts-cli/references/packaging-patterns.md +426 -0
- package/plugin/skills/specgen-ts-cli/references/spec-template.md +1391 -0
- package/plugin/skills/specgen-ts-cli/references/testing-patterns.md +909 -0
- package/plugin/skills/testgen-functional/SKILL.md +1381 -0
- package/plugin/skills/tracegen-matrix/SKILL.md +243 -0
- package/plugin/skills/util-preparek8senv/SKILL.md +422 -0
- package/plugin/skills/util-projectinit/SKILL.md +307 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-loop.md +129 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-protocol.md +144 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-setup-guide.md +153 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-template-appdev.md +87 -0
- package/plugin/skills/util-projectinit/reference/co2-context-generation-guide.md +761 -0
- package/plugin/skills/util-projectsync/SKILL.md +501 -0
- package/plugin/skills/util-updprd/SKILL.md +324 -0
- package/plugin/skills/util-usanalyzer/SKILL.md +307 -0
- package/plugin/skills/util-ustagger/SKILL.md +154 -0
- package/static/assets/abnfDiagram-VRR7QNED-DvPuwyCV.js +1 -0
- package/static/assets/arc-DRKw7sjs.js +1 -0
- package/static/assets/architectureDiagram-ZJ3FMSHR-CaNcxUbC.js +36 -0
- package/static/assets/blockDiagram-677ZJIJ3-Demd4AOK.js +132 -0
- package/static/assets/c4Diagram-LMCZKHZV-PVKUfc2x.js +10 -0
- package/static/assets/channel-C2nVbuwv.js +1 -0
- package/static/assets/chunk-2Q5K7J3B-CExf8ww_.js +1 -0
- package/static/assets/chunk-32BRIVSS-CFPaoONF.js +1 -0
- package/static/assets/chunk-5VM5RSS4-DE2esM67.js +15 -0
- package/static/assets/chunk-EX3LRPZG-SON_YdDG.js +231 -0
- package/static/assets/chunk-JWPE2WC7-Qxah8LNr.js +1 -0
- package/static/assets/chunk-MOJQB5TN-BcWGQV3j.js +88 -0
- package/static/assets/chunk-RYQCIY6F-DkDj-OR5.js +1 -0
- package/static/assets/chunk-V7JOEXUC-BlI1Mrah.js +206 -0
- package/static/assets/chunk-VR4S4FIN-Agf__duj.js +1 -0
- package/static/assets/chunk-XXDRQBXY-B5CfAh85.js +1 -0
- package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +1 -0
- package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +1 -0
- package/static/assets/cose-bilkent-JH36ORCC-DO0cQkcy.js +1 -0
- package/static/assets/cynefin-VYW2F7L2-CfOnsnXc.js +178 -0
- package/static/assets/cynefinDiagram-TSTJHNR4-BJ8tULAv.js +62 -0
- package/static/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
- package/static/assets/dagre-VKFMJZFB-DfjwzHqP.js +4 -0
- package/static/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/static/assets/diagram-FQU43EPY-CndSOIIM.js +3 -0
- package/static/assets/diagram-G47NLZAW-Czf5hN0Z.js +24 -0
- package/static/assets/diagram-NH7WQ7WH-COAyojGh.js +24 -0
- package/static/assets/diagram-OA4YK3LP-H8PoiN2m.js +30 -0
- package/static/assets/diagram-WEI45ONY-DzcaVzjp.js +41 -0
- package/static/assets/ebnfDiagram-CCIWWBDH-DPazGYLJ.js +1 -0
- package/static/assets/erDiagram-Q63AITRT-Bo-fubgF.js +85 -0
- package/static/assets/flowDiagram-23GEKE2U-CSvxUDwh.js +156 -0
- package/static/assets/ganttDiagram-NO4QXBWP-DnBveWX6.js +292 -0
- package/static/assets/gitGraphDiagram-IHSO6WYX-B3rVL8c-.js +106 -0
- package/static/assets/graph-C9eacEi8.js +1 -0
- package/static/assets/index-LZSQkLE2.css +41 -0
- package/static/assets/index-y7XDSgMG.js +495 -0
- package/static/assets/infoDiagram-FWYZ7A6U-CvWV9yy2.js +2 -0
- package/static/assets/init-Gi6I4Gst.js +1 -0
- package/static/assets/ishikawaDiagram-FXEZZL3T-DF28a1N6.js +70 -0
- package/static/assets/journeyDiagram-5HDEW3XC-CB6CugYk.js +139 -0
- package/static/assets/kanban-definition-HUTT4EX6-ResSLF6d.js +89 -0
- package/static/assets/katex-C5jXJg4s.js +257 -0
- package/static/assets/layout-DEXfKzaS.js +1 -0
- package/static/assets/linear-CLO--ryk.js +1 -0
- package/static/assets/map-Czzmt4hB.js +1 -0
- package/static/assets/mindmap-definition-LN4V7U3C-BvW3EbxX.js +96 -0
- package/static/assets/ordinal-Cboi1Yqb.js +1 -0
- package/static/assets/pegDiagram-2B236MQR-BHCgqrrM.js +1 -0
- package/static/assets/pieDiagram-ENE6RG2P-DVmCQ5FA.js +39 -0
- package/static/assets/quadrantDiagram-ABIIQ3AL-B3e5KR52.js +7 -0
- package/static/assets/railroadDiagram-RFXS5EU6-Csv5ysLc.js +1 -0
- package/static/assets/requirementDiagram-TGXJPOKE-DOMVzvnB.js +84 -0
- package/static/assets/sankeyDiagram-HTMAVEWB-B1dcTOvy.js +40 -0
- package/static/assets/sequenceDiagram-DBY2YBRQ-BY414yeO.js +162 -0
- package/static/assets/sizeCapture-X5ZJPWSS-CuJfrfBN.js +1 -0
- package/static/assets/stateDiagram-2N3HPSRC-FX4rJkzV.js +1 -0
- package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +1 -0
- package/static/assets/swimlanes-5IMT3BWC-Sfy58mmv.js +2 -0
- package/static/assets/swimlanesDiagram-G3AALYLV-wDCDw08H.js +8 -0
- package/static/assets/timeline-definition-FHXFAJF6-CmV7by8p.js +120 -0
- package/static/assets/vennDiagram-L72KCM5P-FqIyFZFt.js +34 -0
- package/static/assets/wardleyDiagram-EHGQE667-BZ06O45i.js +78 -0
- package/static/assets/xychartDiagram-FW5EYKEG-BOkffR06.js +7 -0
- package/static/index.html +24 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
# Notification Patterns — FCM + flutter_local_notifications
|
|
2
|
+
|
|
3
|
+
This reference describes how Firebase Cloud Messaging (push) and
|
|
4
|
+
`flutter_local_notifications` (local/foreground/scheduled) are wired together. Include
|
|
5
|
+
this content in Section 16 of the generated specification.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why Both Libraries?
|
|
10
|
+
|
|
11
|
+
- **Firebase Messaging (FCM)** delivers messages from the server. It shows system
|
|
12
|
+
notifications automatically when the app is **backgrounded** or **terminated** (data
|
|
13
|
+
+ notification payload).
|
|
14
|
+
- **Foreground delivery does NOT auto-display.** On Android and iOS, FCM messages
|
|
15
|
+
received while the app is in the foreground only fire the `onMessage` callback —
|
|
16
|
+
the developer must render a notification (or in-app UI) themselves.
|
|
17
|
+
- **`flutter_local_notifications`** is the rendering library for those foreground FCM
|
|
18
|
+
payloads and for any in-app scheduled reminders (alarms, follow-ups).
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Initialization
|
|
23
|
+
|
|
24
|
+
```dart
|
|
25
|
+
// lib/core/notification/notification_service.dart
|
|
26
|
+
import 'package:firebase_messaging/firebase_messaging.dart';
|
|
27
|
+
import 'package:flutter/foundation.dart';
|
|
28
|
+
import 'package:flutter/material.dart';
|
|
29
|
+
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|
30
|
+
|
|
31
|
+
import 'notification_handler.dart';
|
|
32
|
+
|
|
33
|
+
class NotificationService {
|
|
34
|
+
NotificationService._();
|
|
35
|
+
static final NotificationService instance = NotificationService._();
|
|
36
|
+
|
|
37
|
+
final FlutterLocalNotificationsPlugin _local = FlutterLocalNotificationsPlugin();
|
|
38
|
+
final FirebaseMessaging _fcm = FirebaseMessaging.instance;
|
|
39
|
+
|
|
40
|
+
/// Android notification channel for default notifications. Must match the channel
|
|
41
|
+
/// id referenced in AndroidManifest.xml (`default_notification_channel_id`).
|
|
42
|
+
static const AndroidNotificationChannel _defaultChannel = AndroidNotificationChannel(
|
|
43
|
+
'default',
|
|
44
|
+
'General Notifications',
|
|
45
|
+
description: 'Default channel for app notifications',
|
|
46
|
+
importance: Importance.high,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
Future<void> init() async {
|
|
50
|
+
await _initLocal();
|
|
51
|
+
await _initFcm();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Future<void> _initLocal() async {
|
|
55
|
+
const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher');
|
|
56
|
+
const iosInit = DarwinInitializationSettings(
|
|
57
|
+
requestAlertPermission: false, // request explicitly via permission flow
|
|
58
|
+
requestBadgePermission: false,
|
|
59
|
+
requestSoundPermission: false,
|
|
60
|
+
);
|
|
61
|
+
const initSettings = InitializationSettings(android: androidInit, iOS: iosInit);
|
|
62
|
+
|
|
63
|
+
await _local.initialize(
|
|
64
|
+
initSettings,
|
|
65
|
+
onDidReceiveNotificationResponse: NotificationHandler.onLocalTap,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
final androidPlugin =
|
|
69
|
+
_local.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
|
|
70
|
+
await androidPlugin?.createNotificationChannel(_defaultChannel);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Future<void> _initFcm() async {
|
|
74
|
+
// Request permission (iOS + Android 13+)
|
|
75
|
+
await _fcm.requestPermission(
|
|
76
|
+
alert: true,
|
|
77
|
+
announcement: false,
|
|
78
|
+
badge: true,
|
|
79
|
+
carPlay: false,
|
|
80
|
+
criticalAlert: false,
|
|
81
|
+
provisional: false,
|
|
82
|
+
sound: true,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Foreground presentation options for iOS (banner + sound while app is open)
|
|
86
|
+
await _fcm.setForegroundNotificationPresentationOptions(
|
|
87
|
+
alert: true,
|
|
88
|
+
badge: true,
|
|
89
|
+
sound: true,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Foreground message handler — show as local notification
|
|
93
|
+
FirebaseMessaging.onMessage.listen(NotificationHandler.onForegroundMessage);
|
|
94
|
+
|
|
95
|
+
// Notification tap (from background → foreground)
|
|
96
|
+
FirebaseMessaging.onMessageOpenedApp.listen(NotificationHandler.onMessageOpenedApp);
|
|
97
|
+
|
|
98
|
+
// Cold start (terminated → foreground via notification tap)
|
|
99
|
+
final initialMessage = await _fcm.getInitialMessage();
|
|
100
|
+
if (initialMessage != null) {
|
|
101
|
+
// Defer until router is mounted
|
|
102
|
+
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
103
|
+
NotificationHandler.onMessageOpenedApp(initialMessage);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
Future<String?> fcmToken() => _fcm.getToken();
|
|
109
|
+
|
|
110
|
+
Stream<String> onTokenRefresh() => _fcm.onTokenRefresh;
|
|
111
|
+
|
|
112
|
+
/// Subscribe to a topic (e.g., for role-based broadcasts)
|
|
113
|
+
Future<void> subscribeToTopic(String topic) => _fcm.subscribeToTopic(topic);
|
|
114
|
+
|
|
115
|
+
Future<void> unsubscribeFromTopic(String topic) => _fcm.unsubscribeFromTopic(topic);
|
|
116
|
+
|
|
117
|
+
/// Show a local notification immediately. Used to display foreground FCM payloads.
|
|
118
|
+
Future<void> showLocal({
|
|
119
|
+
required String title,
|
|
120
|
+
required String body,
|
|
121
|
+
String? payload,
|
|
122
|
+
}) async {
|
|
123
|
+
const details = NotificationDetails(
|
|
124
|
+
android: AndroidNotificationDetails(
|
|
125
|
+
'default',
|
|
126
|
+
'General Notifications',
|
|
127
|
+
importance: Importance.high,
|
|
128
|
+
priority: Priority.high,
|
|
129
|
+
),
|
|
130
|
+
iOS: DarwinNotificationDetails(
|
|
131
|
+
presentAlert: true,
|
|
132
|
+
presentBadge: true,
|
|
133
|
+
presentSound: true,
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
await _local.show(
|
|
137
|
+
DateTime.now().millisecondsSinceEpoch.remainder(1 << 31),
|
|
138
|
+
title,
|
|
139
|
+
body,
|
|
140
|
+
details,
|
|
141
|
+
payload: payload,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Schedule a local notification at a specific time (e.g., reminder).
|
|
146
|
+
Future<void> schedule({
|
|
147
|
+
required int id,
|
|
148
|
+
required String title,
|
|
149
|
+
required String body,
|
|
150
|
+
required DateTime scheduledAt,
|
|
151
|
+
String? payload,
|
|
152
|
+
}) async {
|
|
153
|
+
const details = NotificationDetails(
|
|
154
|
+
android: AndroidNotificationDetails(
|
|
155
|
+
'default',
|
|
156
|
+
'General Notifications',
|
|
157
|
+
importance: Importance.high,
|
|
158
|
+
priority: Priority.high,
|
|
159
|
+
),
|
|
160
|
+
iOS: DarwinNotificationDetails(),
|
|
161
|
+
);
|
|
162
|
+
// Note: `tz.TZDateTime` from package:timezone is preferred; for brevity use
|
|
163
|
+
// local DateTime here. In a production app, initialise timezones in main().
|
|
164
|
+
await _local.zonedSchedule(
|
|
165
|
+
id,
|
|
166
|
+
title,
|
|
167
|
+
body,
|
|
168
|
+
// Convert to tz.TZDateTime.from(scheduledAt, tz.local) in production
|
|
169
|
+
scheduledAt as dynamic,
|
|
170
|
+
details,
|
|
171
|
+
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
|
|
172
|
+
payload: payload,
|
|
173
|
+
uiLocalNotificationDateInterpretation:
|
|
174
|
+
UILocalNotificationDateInterpretation.absoluteTime,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
Future<void> cancel(int id) => _local.cancel(id);
|
|
179
|
+
Future<void> cancelAll() => _local.cancelAll();
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Handlers
|
|
186
|
+
|
|
187
|
+
```dart
|
|
188
|
+
// lib/core/notification/notification_handler.dart
|
|
189
|
+
import 'package:firebase_messaging/firebase_messaging.dart';
|
|
190
|
+
import 'package:flutter/foundation.dart';
|
|
191
|
+
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|
192
|
+
|
|
193
|
+
import 'notification_navigation.dart';
|
|
194
|
+
import 'notification_service.dart';
|
|
195
|
+
|
|
196
|
+
class NotificationHandler {
|
|
197
|
+
NotificationHandler._();
|
|
198
|
+
|
|
199
|
+
/// Called when an FCM message is received while the app is in the foreground.
|
|
200
|
+
/// FCM does NOT auto-display these — render a local notification instead.
|
|
201
|
+
static Future<void> onForegroundMessage(RemoteMessage message) async {
|
|
202
|
+
final notification = message.notification;
|
|
203
|
+
if (notification == null) return;
|
|
204
|
+
|
|
205
|
+
await NotificationService.instance.showLocal(
|
|
206
|
+
title: notification.title ?? '',
|
|
207
|
+
body: notification.body ?? '',
|
|
208
|
+
payload: message.data['deeplink'] as String?,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/// Called when the user taps an FCM notification (background or terminated).
|
|
213
|
+
static Future<void> onMessageOpenedApp(RemoteMessage message) async {
|
|
214
|
+
final deeplink = message.data['deeplink'] as String?;
|
|
215
|
+
if (deeplink != null) {
|
|
216
|
+
NotificationNavigator.go(deeplink);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/// Called when the user taps a local notification (in-app reminders or
|
|
221
|
+
/// foreground-rendered FCM messages).
|
|
222
|
+
static void onLocalTap(NotificationResponse response) {
|
|
223
|
+
final deeplink = response.payload;
|
|
224
|
+
if (deeplink != null && deeplink.isNotEmpty) {
|
|
225
|
+
NotificationNavigator.go(deeplink);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Background Message Handler (Top-level)
|
|
234
|
+
|
|
235
|
+
The background handler MUST be a top-level (not class member) function annotated with
|
|
236
|
+
`@pragma('vm:entry-point')` so it survives release-mode tree shaking. It is registered
|
|
237
|
+
in `main.dart` (already shown):
|
|
238
|
+
|
|
239
|
+
```dart
|
|
240
|
+
@pragma('vm:entry-point')
|
|
241
|
+
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
|
242
|
+
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
|
243
|
+
// No UI work here — the system displays the notification automatically when the
|
|
244
|
+
// payload includes a `notification:` block. Only do background data processing.
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Deep-Link Navigation From Notifications
|
|
251
|
+
|
|
252
|
+
The router lives inside a Riverpod provider, so we expose the root `ProviderContainer`
|
|
253
|
+
in `app.dart` and reach it from outside the widget tree:
|
|
254
|
+
|
|
255
|
+
```dart
|
|
256
|
+
// lib/core/notification/notification_navigation.dart
|
|
257
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
258
|
+
|
|
259
|
+
import '../../router/app_router.dart';
|
|
260
|
+
|
|
261
|
+
class NotificationNavigator {
|
|
262
|
+
NotificationNavigator._();
|
|
263
|
+
|
|
264
|
+
/// Root container set by the app on startup.
|
|
265
|
+
static ProviderContainer? container;
|
|
266
|
+
|
|
267
|
+
static void go(String location) {
|
|
268
|
+
final c = container;
|
|
269
|
+
if (c == null) return;
|
|
270
|
+
final router = c.read(appRouterProvider);
|
|
271
|
+
router.go(location);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
In `main.dart`, expose the container:
|
|
277
|
+
|
|
278
|
+
```dart
|
|
279
|
+
void main() async {
|
|
280
|
+
// ...existing init...
|
|
281
|
+
final container = ProviderContainer();
|
|
282
|
+
NotificationNavigator.container = container;
|
|
283
|
+
runApp(
|
|
284
|
+
UncontrolledProviderScope(container: container, child: const MyApp()),
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Backend Token Registration
|
|
292
|
+
|
|
293
|
+
Register the FCM token with the backend on first sign-in and on token refresh:
|
|
294
|
+
|
|
295
|
+
```dart
|
|
296
|
+
// inside auth flow, after successful login:
|
|
297
|
+
final token = await NotificationService.instance.fcmToken();
|
|
298
|
+
if (token != null) {
|
|
299
|
+
await dio.post('/users/me/fcm-token', data: {'token': token});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
NotificationService.instance.onTokenRefresh().listen((newToken) async {
|
|
303
|
+
await dio.post('/users/me/fcm-token', data: {'token': newToken});
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Unregister the token on logout:
|
|
308
|
+
|
|
309
|
+
```dart
|
|
310
|
+
await dio.delete('/users/me/fcm-token');
|
|
311
|
+
await FirebaseMessaging.instance.deleteToken();
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Permission Handling
|
|
317
|
+
|
|
318
|
+
| Platform | Behaviour |
|
|
319
|
+
|---|---|
|
|
320
|
+
| iOS | `FirebaseMessaging.requestPermission()` triggers the system prompt |
|
|
321
|
+
| Android 13+ | `POST_NOTIFICATIONS` runtime permission required |
|
|
322
|
+
| Android <13 | Notifications enabled by default |
|
|
323
|
+
|
|
324
|
+
```dart
|
|
325
|
+
// Example Android 13+ permission flow using permission_handler (if added):
|
|
326
|
+
import 'package:permission_handler/permission_handler.dart';
|
|
327
|
+
|
|
328
|
+
final status = await Permission.notification.request();
|
|
329
|
+
if (status.isDenied) {
|
|
330
|
+
// Surface a settings prompt
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Notification Payload Contract
|
|
337
|
+
|
|
338
|
+
The backend should send messages in this shape so the client can route consistently:
|
|
339
|
+
|
|
340
|
+
```json
|
|
341
|
+
{
|
|
342
|
+
"notification": {
|
|
343
|
+
"title": "New task assigned",
|
|
344
|
+
"body": "Inspect site #42"
|
|
345
|
+
},
|
|
346
|
+
"data": {
|
|
347
|
+
"deeplink": "/task/abc-123",
|
|
348
|
+
"category": "task"
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
`deeplink` is read by `NotificationHandler.onMessageOpenedApp` and passed to
|
|
354
|
+
`router.go(...)`.
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Topic Subscription Strategy
|
|
359
|
+
|
|
360
|
+
Topics are useful for role/segment broadcasts (e.g., all workers, all admins):
|
|
361
|
+
|
|
362
|
+
```dart
|
|
363
|
+
// On login, subscribe to role-based topics
|
|
364
|
+
for (final role in user.roles) {
|
|
365
|
+
await NotificationService.instance.subscribeToTopic('role-${role.toLowerCase()}');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// On logout, unsubscribe
|
|
369
|
+
for (final role in user.roles) {
|
|
370
|
+
await NotificationService.instance.unsubscribeFromTopic('role-${role.toLowerCase()}');
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## Scheduled Local Reminders
|
|
377
|
+
|
|
378
|
+
Use `zonedSchedule` for time-based reminders (e.g., "Inspect site at 09:00 tomorrow"):
|
|
379
|
+
|
|
380
|
+
```dart
|
|
381
|
+
await NotificationService.instance.schedule(
|
|
382
|
+
id: task.hashCode,
|
|
383
|
+
title: 'Task reminder',
|
|
384
|
+
body: task.title,
|
|
385
|
+
scheduledAt: task.dueDate.subtract(const Duration(hours: 1)),
|
|
386
|
+
payload: '/task/${task.id}',
|
|
387
|
+
);
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
For production, initialise the `timezone` package in `main()`:
|
|
391
|
+
|
|
392
|
+
```dart
|
|
393
|
+
// pubspec.yaml → dependencies: timezone: ^0.9.4
|
|
394
|
+
import 'package:timezone/data/latest_all.dart' as tz;
|
|
395
|
+
import 'package:timezone/timezone.dart' as tz;
|
|
396
|
+
|
|
397
|
+
tz.initializeTimeZones();
|
|
398
|
+
tz.setLocalLocation(tz.getLocation('Asia/Kuala_Lumpur')); // or detect from device
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## iOS-Specific Notes
|
|
404
|
+
|
|
405
|
+
- Add the **Push Notifications** capability in Xcode (Runner target → Signing &
|
|
406
|
+
Capabilities)
|
|
407
|
+
- Add the **Background Modes → Remote notifications** capability
|
|
408
|
+
- Upload an **APNs Authentication Key** to Firebase Console → Project Settings → Cloud
|
|
409
|
+
Messaging
|
|
410
|
+
- `Info.plist` must include `NSUserNotificationUsageDescription` if you display
|
|
411
|
+
notifications while the device is locked
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Android-Specific Notes
|
|
416
|
+
|
|
417
|
+
- The Android `default` channel MUST be created on app startup (done in
|
|
418
|
+
`_initLocal`). Otherwise high-importance notifications fall back to silent on
|
|
419
|
+
Android 8.0+.
|
|
420
|
+
- `AndroidManifest.xml` includes the channel meta-data so FCM uses the correct channel
|
|
421
|
+
for background-delivered notifications:
|
|
422
|
+
|
|
423
|
+
```xml
|
|
424
|
+
<meta-data
|
|
425
|
+
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
|
426
|
+
android:value="default" />
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
- Add the Android 13+ `POST_NOTIFICATIONS` permission to `AndroidManifest.xml`:
|
|
430
|
+
|
|
431
|
+
```xml
|
|
432
|
+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## Testing Notifications Locally
|
|
438
|
+
|
|
439
|
+
- **FCM from console**: Firebase Console → Cloud Messaging → New campaign → Test on
|
|
440
|
+
device using FCM token printed at startup
|
|
441
|
+
- **Local foreground**: trigger `NotificationService.instance.showLocal(...)` from a
|
|
442
|
+
debug button
|
|
443
|
+
- **Scheduled local**: short-delay schedule (`DateTime.now().add(Duration(seconds: 5))`)
|
|
444
|
+
to verify channel and tap routing
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## Common Pitfalls
|
|
449
|
+
|
|
450
|
+
| Pitfall | Mitigation |
|
|
451
|
+
|---|---|
|
|
452
|
+
| Foreground FCM never shown | You MUST manually render a local notification — FCM does not auto-display |
|
|
453
|
+
| Android 13+ silent | Request `POST_NOTIFICATIONS` runtime permission |
|
|
454
|
+
| iOS background not delivered | Ensure APNs key uploaded to Firebase, Background Modes enabled |
|
|
455
|
+
| Deep-link doesn't navigate on cold start | Read `getInitialMessage()` AFTER router mounts (use `addPostFrameCallback`) |
|
|
456
|
+
| FCM token churns after reinstall | Re-register on every app launch, deduplicate server-side |
|
|
457
|
+
| Scheduled notifications not firing | Confirm `androidScheduleMode: exactAllowWhileIdle` and request `SCHEDULE_EXACT_ALARM` on Android 12+ |
|