You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
`assets/onesignal_logo_icon_padded.png`is referenced in `pubspec.yaml` but is not checked in -- it is generated via `flutter_launcher_icons` from `assets/onesignal_logo.svg`. Remove the generated PNG after icon generation if desired.
64
+
65
+
### Environment variables
66
+
67
+
- `flutter_dotenv`loads `.env` from the `pubspec.yaml` `assets:` block (`.env` is listed alongside `assets/onesignal_logo.svg`).
68
+
- `ONESIGNAL_APP_ID`-- OneSignal app id. Falls back to the hard-coded `_defaultAppId` in `main.dart` when unset or empty.
69
+
- `ONESIGNAL_API_KEY`-- REST API key used by `OneSignalApiService` for Live Activity update/end and notification sends.
70
+
- `ONESIGNAL_ANDROID_CHANNEL_ID`-- optional Android notification channel id used by the REST send paths.
71
+
72
+
### Theme types
73
+
74
+
- `lib/theme.dart`exports `AppSpacing`, `AppColors`, and `AppTheme` classes alongside the `AppSnackBar` extension on `BuildContext`.
75
+
- `AppTheme.light`is the Material 3 `ThemeData` applied to `MaterialApp`.
76
+
60
77
---
61
78
62
79
## State Management
@@ -68,9 +85,12 @@ Use **Provider** for dependency injection and **ChangeNotifier** for reactive st
68
85
- Exposes action methods that update state and call `notifyListeners()`
69
86
- Receives `OneSignalApiService` and `PreferencesService` via constructor injection
70
87
- Initialize OneSignal SDK before `runApp()`
71
-
- Use `Consumer`/`Selector` from Provider to scope rebuilds and minimize re-renders
88
+
- Section widgets read the viewmodel via `context.watch<AppViewModel>()` (for rebuilds) or `context.read<AppViewModel>()` (one-shot, for callbacks). No `Consumer`/`Selector` usage anywhere in `lib/`.
72
89
- SDK calls (`OneSignal.User.*`, `OneSignal.Notifications.*`, `OneSignal.InAppMessages.*`, etc.) are invoked directly from `AppViewModel`. There is no repository wrapper.
73
-
- `OneSignalApiService`is a plain Dart class that owns the OneSignal REST API calls (send notification, live activity update/end, fetch user). Not a ChangeNotifier.
90
+
91
+
### REST client
92
+
93
+
- `OneSignalApiService`is a plain Dart class (not a `ChangeNotifier`) that owns the OneSignal REST API calls -- send notification, fetch user, Live Activity update/end.
74
94
75
95
### Persistence
76
96
@@ -83,21 +103,44 @@ Use **Provider** for dependency injection and **ChangeNotifier** for reactive st
83
103
84
104
In `main.dart`, restore SDK state from `SharedPreferences` cache BEFORE `initialize`:
- `TooltipHelper().init()`fetches tooltip content in the background.
128
+
- `appId`falls back to a hard-coded `_defaultAppId` constant when `ONESIGNAL_APP_ID` is not set in `.env`.
129
+
130
+
`PreferencesService` is the source of truth for restored UI state. In `AppViewModel.loadInitialState(appId)`, read UI state from `_prefs` (not from the SDK):
131
+
```dart
132
+
_consentRequired = _prefs.consentRequired;
133
+
_privacyConsentGiven = _prefs.privacyConsent;
134
+
_externalUserId = _prefs.externalUserId;
135
+
_iamPaused = _prefs.iamPaused;
136
+
_locationShared = _prefs.locationShared;
137
+
138
+
if (_externalUserId != null) {
139
+
OneSignal.login(_externalUserId!);
140
+
}
95
141
```
96
142
97
-
In `AppViewModel.loadInitialState()`, read UI state from the SDK (not cached prefs):
98
-
- `OneSignal.InAppMessages.arePaused()`for IAM paused state
99
-
- `OneSignal.Location.isShared()`for location state
100
-
- `OneSignal.User.getExternalId()`for external user ID
143
+
The cached `externalUserId` is the only value that drives an SDK call inside `loadInitialState()` -- it triggers `OneSignal.login(...)` so the SDK identity matches the persisted UI state. Push subscription id, opted-in flag, and permission are then read from the live SDK state, and the OneSignal ID is fetched via `OneSignal.User.getOnesignalId()`.
- Call `viewModel.promptPush()` in `initState()` of `HomeScreen`
159
+
- Wraps the call in `WidgetsBinding.instance.addPostFrameCallback` inside `initState()` (`home_screen.dart` lines ~39-41) so the prompt fires after the first frame.
117
160
118
161
### Loading State
119
-
- No global loading overlay; render an inline `CircularProgressIndicator` inside the section that owns the in-flight work (e.g. user section while `isLoading` is true).
162
+
- No global loading overlay. `vm.isLoading` is passed to `PairList(loading: ...)` in **aliases**, **emails**, **sms**, and **tags** sections only. `user_section.dart` has no loading UI.
120
163
- The viewmodel uses a request-sequence counter (`_fetchSequence`) so stale REST results are dropped when a newer fetch is already in flight.
121
164
122
165
### SnackBar Messages
123
-
- `AppSnackBar`extension on `BuildContext` defined in `theme.dart`
124
-
- Call `context.showSnackBar(message)` directly from widget callbacks
125
-
- Automatically hides the current SnackBar before showing the new one
126
-
127
-
### Send In-App Message Icons
128
-
- TOP BANNER: `Icons.vertical_align_top`
129
-
- BOTTOM BANNER: `Icons.vertical_align_bottom`
130
-
- CENTER MODAL: `Icons.crop_square`
131
-
- FULL SCREEN: `Icons.fullscreen`
166
+
- `AppSnackBar`extension on `BuildContext` defined in `lib/theme.dart` exposes `context.showSnackBar(message)`.
167
+
- The extension calls `ScaffoldMessenger.of(this)..hideCurrentSnackBar()..showSnackBar(SnackBar(content: Text(message), duration: _toastDuration))` for replace-on-show behavior.
168
+
- Duration is the file-private constant `const Duration _toastDuration = Duration(seconds: 3);` in `theme.dart`.
169
+
- Section widgets call `context.showSnackBar(...)` from button callbacks, guarded by `if (context.mounted)`. Only Outcomes, Custom Events, and Location check trigger snackbars; everything else uses `debugPrint(...)`. Outcomes and Custom Events call synchronous viewmodel methods then show the snackbar (no `await` on the SDK). Only Location's `CHECK LOCATION SHARED` button truly awaits (`vm.checkLocationShared()`) before showing the result.
170
+
- The `ChangeNotifier` / viewmodel must not hold snackbar state or expose snackbar messages.
132
171
133
172
### Dialogs
134
-
- All dialogs use `insetPadding: EdgeInsets.symmetric(horizontal: 16)` and `SizedBox(width: double.maxFinite)` on content for full-width layout
- `TextEditingController`s are properly disposed in `StatefulWidget`s
137
-
- JSON parsing via `jsonDecode` returns `Map<String, dynamic>` for Track Event
138
-
- Single-field input prompts (e.g. login, add email/SMS, add trigger) reuse a single `SingleInputDialog` widget that takes `title`, `fieldLabel`, `confirmLabel`, and `semanticsLabel`. Avoid creating a per-screen dialog widget when one input is needed.
173
+
- The home screen widget owns layout + the tooltip dialog only. Tooltip presentation is via a private method `_showTooltipDialog(BuildContext, String key)` on the home state (`home_screen.dart` line ~44), wired to each section through an `onInfoTap` callback. No `_activeTooltipKey` field, no `ChangeNotifier` involvement.
174
+
- Section action dialogs call `showDialog<T>(context: context, builder: ...)` inline inside each button's `onPressed` handler (see `aliases_section.dart`, `outcomes_section.dart`) and `await` the typed result. Only `HomeScreen` has a private `_show*Dialog` method, and only for the tooltip. No `*Open` booleans are required because Flutter presents dialogs imperatively; the awaited result drives the SDK call + optional `context.showSnackBar(...)`.
175
+
- Shared dialog widgets live in `lib/widgets/dialogs.dart` (or `lib/widgets/dialogs/`). Reuse the single `SingleInputDialog` widget for any one-field input (takes `title`, `fieldLabel`, `confirmLabel`, `semanticsLabel`) -- do not create per-screen one-field dialogs.
176
+
- All dialogs use `insetPadding: EdgeInsets.symmetric(horizontal: 16)` and `SizedBox(width: double.maxFinite)` on content for full-width layout. `MultiSelectRemoveDialog` uses `CheckboxListTile`. `TextEditingController`s are disposed in the dialog's `StatefulWidget`. JSON parsing via `jsonDecode` returns `Map<String, dynamic>` for Track Event.
177
+
- `dialogs.dart`also defines `PairInputDialog`, `MultiPairInputDialog`, `OutcomeDialog`, `TrackEventDialog`, `CustomNotificationDialog`, and `TooltipDialog` beyond `SingleInputDialog` and `MultiSelectRemoveDialog`.
178
+
- The viewmodel must not hold dialog visibility flags or dialog input drafts.
179
+
180
+
### Live Activities (iOS only)
181
+
- `LiveActivitiesSection`is rendered only when `defaultTargetPlatform == TargetPlatform.iOS` (`home_screen.dart` line ~118).
182
+
- `OneSignal.LiveActivities.setupDefault(options: LiveActivitySetupOptions(enablePushToStart: true, enablePushToUpdate: true))` is called in `main.dart` after `OneSignal.initialize`.
183
+
- REST update/end is performed via `OneSignalApiService` using `ONESIGNAL_API_KEY`. The update/end buttons are disabled when `vm.hasApiKey` is false.
184
+
- While an update is in flight, `isLaUpdating` disables the update button rather than showing a spinner (`live_activities_section.dart`).
185
+
- Native sources live at `ios/OneSignalWidget/` (Live Activity widget extension) and `ios/OneSignalNotificationServiceExtension/`.
186
+
187
+
---
188
+
189
+
## iOS native config
190
+
191
+
- `ios/Runner/Info.plist` includes:
192
+
- `NSSupportsLiveActivities`(enables ActivityKit-backed Live Activities).
- `UIBackgroundModes`containing `remote-notification` for silent push handling.
139
195
140
196
---
141
197
@@ -182,11 +238,16 @@ examples/demo/
182
238
│ ├── custom_events_section.dart
183
239
│ ├── live_activities_section.dart
184
240
│ └── location_section.dart
241
+
├── assets/
242
+
│ └── onesignal_logo.svg
185
243
├── android/
244
+
│ └── app/
245
+
│ └── build.gradle.kts
186
246
├── ios/
187
-
├── pubspec.yaml
188
-
├── google-services.json
189
-
└── agconnect-services.json
247
+
│ ├── OneSignalWidget/
248
+
│ └── OneSignalNotificationServiceExtension/
249
+
├── .env
250
+
└── pubspec.yaml
190
251
```
191
252
192
253
---
@@ -201,5 +262,11 @@ examples/demo/
201
262
- **Semantics** widgets for accessibility and Appium test automation
202
263
- **Immutable state** where possible; lists exposed as unmodifiable views from the ViewModel
203
264
- **Material 3** theming with `ColorScheme.fromSeed`
204
-
- **Minimal rebuilds** via `Consumer`/`Selector` from Provider
265
+
- **Scoped rebuilds** via `context.watch<AppViewModel>()` in `build`, `context.read<AppViewModel>()` in callbacks
205
266
- **No platform channels needed** since the OneSignal Flutter SDK handles all bridging
267
+
268
+
---
269
+
270
+
## Sibling examples
271
+
272
+
- `examples/demo_pods/` exists alongside the pub-based `examples/demo/` in the SDK repo. It is a separate sample whose dependency wiring differs from `demo/`.
0 commit comments