Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,6 @@ private void handleMethodCall(MethodCall call, Result result) {

private void setLanguage(MethodCall call, Result result) {
String language = call.argument("language");
if (language != null && language.length() == 0) {
language = null;
}
OneSignal.getUser().setLanguage(language);
replySuccess(result, null);
}
Expand Down
9 changes: 8 additions & 1 deletion examples/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ if (_externalUserId != null) {
}
```

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()`.
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, token, and permission are then read from the live SDK state, and the OneSignal ID is fetched via `OneSignal.User.getOnesignalId()`.

### Observers

Expand All @@ -162,6 +162,13 @@ OneSignal.User.addObserver(...)
- 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.
- The viewmodel uses a request-sequence counter (`_fetchSequence`) so stale REST results are dropped when a newer fetch is already in flight.

### Push Status
- The Push section shows a read-only **Status** row above the **Opted In** toggle: `Subscribed` when `pushSubscription.optedIn` is true, notification permission is granted, and a push token is present; otherwise `Unsubscribed`. `AppViewModel.isPushSubscribed` derives this from the push subscription and permission observers.

### Language
- The User section has a **Language** dropdown (`Device Default` plus the shared language list) after External ID. The selection is cached in `PreferencesService` and reset to `Device Default` on logout.
- `Device Default` calls `OneSignal.User.setLanguage('')`, which the native SDK resolves to the device language.

### SnackBar Messages
- `AppSnackBar` extension on `BuildContext` defined in `lib/theme.dart` exposes `context.showSnackBar(message)`.
- The extension calls `ScaffoldMessenger.of(this)..hideCurrentSnackBar()..showSnackBar(SnackBar(content: Text(message), duration: _toastDuration))` for replace-on-show behavior.
Expand Down
6 changes: 6 additions & 0 deletions examples/demo-pods/lib/services/preferences_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class PreferencesService {
static const _keyExternalUserId = 'external_user_id';
static const _keyLocationShared = 'location_shared';
static const _keyIamPaused = 'iam_paused';
static const _keyLanguage = 'language';

late final SharedPreferences _prefs;

Expand Down Expand Up @@ -39,4 +40,9 @@ class PreferencesService {
bool get iamPaused => _prefs.getBool(_keyIamPaused) ?? false;
Future<void> setIamPaused(bool value) =>
_prefs.setBool(_keyIamPaused, value);

// Language
String get language => _prefs.getString(_keyLanguage) ?? '';
Future<void> setLanguage(String value) =>
_prefs.setString(_keyLanguage, value);
}
31 changes: 31 additions & 0 deletions examples/demo-pods/lib/viewmodels/app_viewmodel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ class AppViewModel extends ChangeNotifier {
bool _hasNotificationPermission = false;
bool get hasNotificationPermission => _hasNotificationPermission;

String? _pushToken;

bool get isPushSubscribed =>
_pushEnabled &&
_hasNotificationPermission &&
(_pushToken?.isNotEmpty ?? false);

// Empty means device default.
String _language = '';
String get language => _language;

// IAM state
bool _iamPaused = false;
bool get iamPaused => _iamPaused;
Expand Down Expand Up @@ -132,13 +143,15 @@ class AppViewModel extends ChangeNotifier {

_iamPaused = _prefs.iamPaused;
_locationShared = _prefs.locationShared;
_language = _prefs.language;

if (_externalUserId != null) {
OneSignal.login(_externalUserId!);
}

_pushSubscriptionId = OneSignal.User.pushSubscription.id;
_pushEnabled = OneSignal.User.pushSubscription.optedIn ?? false;
_pushToken = OneSignal.User.pushSubscription.token;
_hasNotificationPermission = OneSignal.Notifications.permission;

final onesignalId = await OneSignal.User.getOnesignalId();
Expand All @@ -165,6 +178,7 @@ class AppViewModel extends ChangeNotifier {
OneSignal.User.pushSubscription.addObserver((state) {
_pushSubscriptionId = state.current.id;
_pushEnabled = state.current.optedIn;
_pushToken = state.current.token;

String fmtToken(String? t) {
if (t == null || t.isEmpty) return 'null';
Expand Down Expand Up @@ -255,12 +269,29 @@ class AppViewModel extends ChangeNotifier {
try {
await OneSignal.logout();
await _prefs.setExternalUserId(null);
await setLanguage('');
debugPrint('Logged out');
} catch (e) {
debugPrint('Logout error: $e');
}
}

// Language
Future<void> setLanguage(String language) async {
_language = language;
notifyListeners();

try {
await OneSignal.User.setLanguage(language);
await _prefs.setLanguage(language);
debugPrint(
'Language set to: ${language.isEmpty ? 'device default' : language}',
);
} catch (e) {
debugPrint('Set language error: $e');
}
}

// Consent
Future<void> setConsentRequired(bool value) async {
_consentRequired = value;
Expand Down
20 changes: 19 additions & 1 deletion examples/demo-pods/lib/widgets/sections/push_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,26 @@ class PushSection extends StatelessWidget {
],
),
const Divider(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Status',
style: Theme.of(context).textTheme.bodyMedium,
),
Semantics(
identifier: 'push_subscription_status',
container: true,
child: Text(
vm.isPushSubscribed ? 'Subscribed' : 'Unsubscribed',
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
const Divider(),
ToggleRow(
label: 'Enabled',
label: 'Opted In',
value: vm.pushEnabled,
semanticsLabel: 'push_enabled_toggle',
onChanged: vm.hasNotificationPermission
Expand Down
79 changes: 79 additions & 0 deletions examples/demo-pods/lib/widgets/sections/user_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,53 @@ import '../dialogs.dart';
import '../action_button.dart';
import '../section_card.dart';

const _languageOptions = [
('Device Default', ''),
('English', 'en'),
('Arabic', 'ar'),
('Azerbaijani', 'az'),
('Bosnian', 'bs'),
('Catalan', 'ca'),
('Chinese (Simplified)', 'zh-Hans'),
('Chinese (Traditional)', 'zh-Hant'),
('Croatian', 'hr'),
('Czech', 'cs'),
('Danish', 'da'),
('Dutch', 'nl'),
('Estonian', 'et'),
('Finnish', 'fi'),
('French', 'fr'),
('Georgian', 'ka'),
('Bulgarian', 'bg'),
('German', 'de'),
('Greek', 'el'),
('Hindi', 'hi'),
('Hebrew', 'he'),
('Hungarian', 'hu'),
('Indonesian', 'id'),
('Italian', 'it'),
('Japanese', 'ja'),
('Korean', 'ko'),
('Latvian', 'lv'),
('Lithuanian', 'lt'),
('Malay', 'ms'),
('Norwegian', 'nb'),
('Persian', 'fa'),
('Polish', 'pl'),
('Portuguese', 'pt'),
('Punjabi', 'pa'),
('Romanian', 'ro'),
('Russian', 'ru'),
('Serbian', 'sr'),
('Slovak', 'sk'),
('Spanish', 'es'),
('Swedish', 'sv'),
('Thai', 'th'),
('Turkish', 'tr'),
('Ukrainian', 'uk'),
('Vietnamese', 'vi'),
];

class UserSection extends StatelessWidget {
final VoidCallback? onInfoTap;

Expand Down Expand Up @@ -68,6 +115,38 @@ class UserSection extends StatelessWidget {
),
],
),
const Divider(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Language',
style: Theme.of(context).textTheme.bodyMedium,
),
Semantics(
identifier: 'user_language_dropdown',
container: true,
child: DropdownButton<String>(
value:
_languageOptions.any((o) => o.$2 == vm.language)
? vm.language
: '',
isDense: true,
menuMaxHeight: 320,
alignment: AlignmentDirectional.centerEnd,
underline: const SizedBox.shrink(),
style: Theme.of(context).textTheme.bodySmall,
items: [
for (final (name, code) in _languageOptions)
DropdownMenuItem(value: code, child: Text(name)),
],
onChanged: (code) {
if (code != null) vm.setLanguage(code);
},
),
),
],
),
],
),
),
Expand Down
6 changes: 6 additions & 0 deletions examples/demo/lib/services/preferences_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class PreferencesService {
static const _keyExternalUserId = 'external_user_id';
static const _keyLocationShared = 'location_shared';
static const _keyIamPaused = 'iam_paused';
static const _keyLanguage = 'language';

late final SharedPreferences _prefs;

Expand Down Expand Up @@ -39,4 +40,9 @@ class PreferencesService {
bool get iamPaused => _prefs.getBool(_keyIamPaused) ?? false;
Future<void> setIamPaused(bool value) =>
_prefs.setBool(_keyIamPaused, value);

// Language
String get language => _prefs.getString(_keyLanguage) ?? '';
Future<void> setLanguage(String value) =>
_prefs.setString(_keyLanguage, value);
}
31 changes: 31 additions & 0 deletions examples/demo/lib/viewmodels/app_viewmodel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ class AppViewModel extends ChangeNotifier {
bool _hasNotificationPermission = false;
bool get hasNotificationPermission => _hasNotificationPermission;

String? _pushToken;

bool get isPushSubscribed =>
_pushEnabled &&
_hasNotificationPermission &&
(_pushToken?.isNotEmpty ?? false);

// Empty means device default.
String _language = '';
String get language => _language;

// IAM state
bool _iamPaused = false;
bool get iamPaused => _iamPaused;
Expand Down Expand Up @@ -144,13 +155,15 @@ class AppViewModel extends ChangeNotifier {

_iamPaused = _prefs.iamPaused;
_locationShared = _prefs.locationShared;
_language = _prefs.language;

if (_externalUserId != null) {
OneSignal.login(_externalUserId!);
}

_pushSubscriptionId = OneSignal.User.pushSubscription.id;
_pushEnabled = OneSignal.User.pushSubscription.optedIn ?? false;
_pushToken = OneSignal.User.pushSubscription.token;
_hasNotificationPermission = OneSignal.Notifications.permission;

final onesignalId = await OneSignal.User.getOnesignalId();
Expand All @@ -171,6 +184,7 @@ class AppViewModel extends ChangeNotifier {
OneSignal.User.pushSubscription.addObserver((state) {
_pushSubscriptionId = state.current.id;
_pushEnabled = state.current.optedIn;
_pushToken = state.current.token;

String fmtToken(String? t) {
if (t == null || t.isEmpty) return 'null';
Expand Down Expand Up @@ -291,12 +305,29 @@ class AppViewModel extends ChangeNotifier {
try {
await OneSignal.logout();
await _prefs.setExternalUserId(null);
await setLanguage('');
LogService.info('Logged out');
} catch (e) {
LogService.error('Logout error: $e');
}
}

// Language
Future<void> setLanguage(String language) async {
_language = language;
notifyListeners();

try {
await OneSignal.User.setLanguage(language);
await _prefs.setLanguage(language);
LogService.info(
'Language set to: ${language.isEmpty ? 'device default' : language}',
);
} catch (e) {
LogService.error('Set language error: $e');
}
}

// Consent
Future<void> setConsentRequired(bool value) async {
_consentRequired = value;
Expand Down
20 changes: 19 additions & 1 deletion examples/demo/lib/widgets/sections/push_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,26 @@ class PushSection extends StatelessWidget {
],
),
const Divider(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Status',
style: Theme.of(context).textTheme.bodyMedium,
),
Semantics(
identifier: 'push_subscription_status',
container: true,
child: Text(
vm.isPushSubscribed ? 'Subscribed' : 'Unsubscribed',
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
const Divider(),
ToggleRow(
label: 'Enabled',
label: 'Opted In',
value: vm.pushEnabled,
semanticsLabel: 'push_enabled_toggle',
onChanged: vm.hasNotificationPermission
Expand Down
Loading
Loading