From c26185c85f6f66578945ebb97e8aaa7b67dd2ff7 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 9 Apr 2026 15:27:23 -0700 Subject: [PATCH 01/31] chore(demo): use env vars for OneSignal app id --- examples/demo/.env.example | 3 ++- examples/demo/README.md | 15 ++++++++++++++- examples/demo/lib/main.dart | 8 +++----- .../demo/lib/services/preferences_service.dart | 5 ----- examples/demo_spm/.env.example | 2 ++ 5 files changed, 21 insertions(+), 12 deletions(-) create mode 100644 examples/demo_spm/.env.example diff --git a/examples/demo/.env.example b/examples/demo/.env.example index 674a938f..eb491bd5 100644 --- a/examples/demo/.env.example +++ b/examples/demo/.env.example @@ -1 +1,2 @@ -ONESIGNAL_API_KEY=your_rest_api_key +ONESIGNAL_APP_ID=your-onesignal-app-id # default app id: 77e32082-ea27-42e3-a898-c72e141824ef if empty +ONESIGNAL_API_KEY=your-onesignal-api-key diff --git a/examples/demo/README.md b/examples/demo/README.md index 670272f7..4645eab7 100644 --- a/examples/demo/README.md +++ b/examples/demo/README.md @@ -46,7 +46,20 @@ flutter run -d ## Configuration -The app uses a default OneSignal App ID for testing. To use your own, update the `oneSignalAppId` constant in `lib/main.dart`. +Copy the example environment file and fill in your values: + +```bash +cp .env.example .env +``` + +Set your OneSignal credentials in `.env`: + +``` +ONESIGNAL_APP_ID=your-onesignal-app-id +ONESIGNAL_API_KEY=your-onesignal-api-key +``` + +If no `.env` is provided, the app falls back to a built-in default App ID. ## Build Guide diff --git a/examples/demo/lib/main.dart b/examples/demo/lib/main.dart index 1e5544ec..00fb4cc0 100644 --- a/examples/demo/lib/main.dart +++ b/examples/demo/lib/main.dart @@ -13,23 +13,21 @@ import 'services/tooltip_helper.dart'; import 'theme.dart'; import 'viewmodels/app_viewmodel.dart'; -const String oneSignalAppId = '77e32082-ea27-42e3-a898-c72e141824ef'; +const String _defaultAppId = '77e32082-ea27-42e3-a898-c72e141824ef'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - // Load environment variables try { await dotenv.load(fileName: '.env'); } catch (_) { - LogManager().w('App', '.env file not found, continuing without API key'); + LogManager().w('App', '.env file not found, using defaults'); } - // Initialize preferences final prefs = PreferencesService(); await prefs.init(); - final appId = prefs.appId ?? oneSignalAppId; + final appId = dotenv.env['ONESIGNAL_APP_ID'] ?? _defaultAppId; // Initialize OneSignal SDK OneSignal.Debug.setLogLevel(OSLogLevel.verbose); diff --git a/examples/demo/lib/services/preferences_service.dart b/examples/demo/lib/services/preferences_service.dart index 9d4a282b..7fe89283 100644 --- a/examples/demo/lib/services/preferences_service.dart +++ b/examples/demo/lib/services/preferences_service.dart @@ -1,7 +1,6 @@ import 'package:shared_preferences/shared_preferences.dart'; class PreferencesService { - static const _keyAppId = 'app_id'; static const _keyConsentRequired = 'consent_required'; static const _keyPrivacyConsent = 'privacy_consent'; static const _keyExternalUserId = 'external_user_id'; @@ -14,10 +13,6 @@ class PreferencesService { _prefs = await SharedPreferences.getInstance(); } - // App ID - String? get appId => _prefs.getString(_keyAppId); - Future setAppId(String value) => _prefs.setString(_keyAppId, value); - // Consent required bool get consentRequired => _prefs.getBool(_keyConsentRequired) ?? false; Future setConsentRequired(bool value) => diff --git a/examples/demo_spm/.env.example b/examples/demo_spm/.env.example new file mode 100644 index 00000000..f26801b6 --- /dev/null +++ b/examples/demo_spm/.env.example @@ -0,0 +1,2 @@ +ONESIGNAL_APP_ID=your-onesignal-app-id +ONESIGNAL_API_KEY=your-onesignal-api-key From dc0af72bf4eb5cdf9bb62d032594c08ffacaae4d Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 9 Apr 2026 15:53:22 -0700 Subject: [PATCH 02/31] ci: add e2e tests and accessibility labels --- .github/workflows/e2e.yml | 132 ++++++++++++++++++ examples/demo/lib/widgets/action_button.dart | 16 ++- examples/demo/lib/widgets/dialogs.dart | 44 +++--- .../lib/widgets/sections/push_section.dart | 1 + .../lib/widgets/sections/tags_section.dart | 8 +- .../lib/widgets/sections/user_section.dart | 2 + examples/demo/lib/widgets/toggle_row.dart | 16 ++- 7 files changed, 195 insertions(+), 24 deletions(-) create mode 100644 .github/workflows/e2e.yml diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..07da8c13 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,132 @@ +name: E2E Tests + +on: + push: + branches: + - rel/** + workflow_dispatch: + inputs: + platform: + description: 'Platform to test' + required: true + default: 'android' + type: choice + options: + - android + - ios + - both + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-android: + if: >- + github.event_name == 'push' || + github.event.inputs.platform == 'android' || + github.event.inputs.platform == 'both' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Set up Flutter + uses: ./.github/actions/setup-flutter + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + + - name: Create demo .env + working-directory: examples/demo + run: echo "ONESIGNAL_APP_ID=${{ secrets.E2E_ONESIGNAL_APP_ID }}" > .env + + - name: Build release APK + working-directory: examples/demo + run: flutter build apk --release + + - name: Upload APK + uses: actions/upload-artifact@v4 + with: + name: demo-apk + path: examples/demo/build/app/outputs/flutter-apk/app-release.apk + retention-days: 1 + + build-ios: + if: >- + github.event.inputs.platform == 'ios' || + github.event.inputs.platform == 'both' + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Set up Flutter + uses: ./.github/actions/setup-flutter + + - name: Create demo .env + working-directory: examples/demo + run: echo "ONESIGNAL_APP_ID=${{ secrets.E2E_ONESIGNAL_APP_ID }}" > .env + + - name: Install CocoaPods dependencies + working-directory: examples/demo/ios + run: pod install + + - name: Build iOS app + working-directory: examples/demo + run: | + flutter build ios --release --no-codesign + cd build/ios/iphoneos + mkdir Payload + cp -r Runner.app Payload/ + zip -r Runner.ipa Payload + + - name: Upload IPA + uses: actions/upload-artifact@v4 + with: + name: demo-ipa + path: examples/demo/build/ios/iphoneos/Runner.ipa + retention-days: 1 + + e2e-android: + needs: build-android + runs-on: ubuntu-latest + steps: + - name: Download APK + uses: actions/download-artifact@v4 + with: + name: demo-apk + + - name: Run Appium E2E (Android) + uses: OneSignal/sdk-shared/.github/actions/appium-e2e@main + with: + platform: android + app-path: app-release.apk + sdk-type: flutter + browserstack-username: ${{ secrets.BROWSERSTACK_USERNAME }} + browserstack-access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + onesignal-app-id: ${{ secrets.E2E_ONESIGNAL_APP_ID }} + build-name: flutter-android-${{ github.ref_name }}-${{ github.run_number }} + + e2e-ios: + needs: build-ios + runs-on: ubuntu-latest + steps: + - name: Download IPA + uses: actions/download-artifact@v4 + with: + name: demo-ipa + + - name: Run Appium E2E (iOS) + uses: OneSignal/sdk-shared/.github/actions/appium-e2e@main + with: + platform: ios + app-path: Runner.ipa + sdk-type: flutter + browserstack-username: ${{ secrets.BROWSERSTACK_USERNAME }} + browserstack-access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + onesignal-app-id: ${{ secrets.E2E_ONESIGNAL_APP_ID }} + build-name: flutter-ios-${{ github.ref_name }}-${{ github.run_number }} diff --git a/examples/demo/lib/widgets/action_button.dart b/examples/demo/lib/widgets/action_button.dart index 0e657960..67e428b2 100644 --- a/examples/demo/lib/widgets/action_button.dart +++ b/examples/demo/lib/widgets/action_button.dart @@ -6,17 +6,19 @@ class PrimaryButton extends StatelessWidget { final String label; final VoidCallback? onPressed; final IconData? icon; + final String? semanticsLabel; const PrimaryButton({ super.key, required this.label, this.onPressed, this.icon, + this.semanticsLabel, }); @override Widget build(BuildContext context) { - return SizedBox( + Widget button = SizedBox( width: double.infinity, child: ElevatedButton( onPressed: onPressed, @@ -36,22 +38,28 @@ class PrimaryButton extends StatelessWidget { ), ), ); + if (semanticsLabel != null) { + button = Semantics(label: semanticsLabel, child: button); + } + return button; } } class DestructiveButton extends StatelessWidget { final String label; final VoidCallback? onPressed; + final String? semanticsLabel; const DestructiveButton({ super.key, required this.label, this.onPressed, + this.semanticsLabel, }); @override Widget build(BuildContext context) { - return SizedBox( + Widget button = SizedBox( width: double.infinity, child: OutlinedButton( onPressed: onPressed, @@ -62,5 +70,9 @@ class DestructiveButton extends StatelessWidget { child: Text(label), ), ); + if (semanticsLabel != null) { + button = Semantics(label: semanticsLabel, child: button); + } + return button; } } diff --git a/examples/demo/lib/widgets/dialogs.dart b/examples/demo/lib/widgets/dialogs.dart index 9f6e3655..e72c4814 100644 --- a/examples/demo/lib/widgets/dialogs.dart +++ b/examples/demo/lib/widgets/dialogs.dart @@ -72,12 +72,18 @@ class PairInputDialog extends StatefulWidget { final String title; final String keyLabel; final String valueLabel; + final String? keySemanticsLabel; + final String? valueSemanticsLabel; + final String? confirmSemanticsLabel; const PairInputDialog({ super.key, required this.title, this.keyLabel = 'Key', this.valueLabel = 'Value', + this.keySemanticsLabel, + this.valueSemanticsLabel, + this.confirmSemanticsLabel, }); @override @@ -109,7 +115,7 @@ class _PairInputDialogState extends State { children: [ Expanded( child: Semantics( - label: '${widget.keyLabel}_input', + label: widget.keySemanticsLabel ?? '${widget.keyLabel}_input', child: AppTextField( controller: _keyController, decoration: InputDecoration(labelText: widget.keyLabel), @@ -120,7 +126,7 @@ class _PairInputDialogState extends State { const SizedBox(width: 12), Expanded( child: Semantics( - label: '${widget.valueLabel}_input', + label: widget.valueSemanticsLabel ?? '${widget.valueLabel}_input', child: AppTextField( controller: _valueController, decoration: InputDecoration(labelText: widget.valueLabel), @@ -136,14 +142,17 @@ class _PairInputDialogState extends State { onPressed: () => Navigator.pop(context), child: const Text('Cancel'), ), - TextButton( - onPressed: _isValid - ? () => Navigator.pop( - context, - MapEntry(_keyController.text, _valueController.text), - ) - : null, - child: const Text('Add'), + Semantics( + label: widget.confirmSemanticsLabel, + child: TextButton( + onPressed: _isValid + ? () => Navigator.pop( + context, + MapEntry(_keyController.text, _valueController.text), + ) + : null, + child: const Text('Add'), + ), ), ], ); @@ -380,7 +389,7 @@ class _LoginDialogState extends State { content: SizedBox( width: double.maxFinite, child: Semantics( - label: 'external_user_id_input', + label: 'login_user_id_input', child: AppTextField( controller: _controller, decoration: const InputDecoration(labelText: 'External User Id'), @@ -393,11 +402,14 @@ class _LoginDialogState extends State { onPressed: () => Navigator.pop(context), child: const Text('Cancel'), ), - TextButton( - onPressed: _controller.text.isEmpty - ? null - : () => Navigator.pop(context, _controller.text), - child: const Text('Login'), + Semantics( + label: 'login_confirm_button', + child: TextButton( + onPressed: _controller.text.isEmpty + ? null + : () => Navigator.pop(context, _controller.text), + child: const Text('Login'), + ), ), ], ); diff --git a/examples/demo/lib/widgets/sections/push_section.dart b/examples/demo/lib/widgets/sections/push_section.dart index 6d9fe108..e4977a9a 100644 --- a/examples/demo/lib/widgets/sections/push_section.dart +++ b/examples/demo/lib/widgets/sections/push_section.dart @@ -49,6 +49,7 @@ class PushSection extends StatelessWidget { ToggleRow( label: 'Enabled', value: vm.pushEnabled, + semanticsLabel: 'push_enabled_toggle', onChanged: vm.hasNotificationPermission ? vm.togglePush : null, diff --git a/examples/demo/lib/widgets/sections/tags_section.dart b/examples/demo/lib/widgets/sections/tags_section.dart index ec2c7a19..22a263cf 100644 --- a/examples/demo/lib/widgets/sections/tags_section.dart +++ b/examples/demo/lib/widgets/sections/tags_section.dart @@ -36,10 +36,16 @@ class TagsSection extends StatelessWidget { AppSpacing.gapBox, PrimaryButton( label: 'ADD', + semanticsLabel: 'add_tag_button', onPressed: () async { final result = await showDialog>( context: context, - builder: (_) => const PairInputDialog(title: 'Add Tag'), + builder: (_) => const PairInputDialog( + title: 'Add Tag', + keySemanticsLabel: 'multi_pair_key_0', + valueSemanticsLabel: 'multi_pair_value_0', + confirmSemanticsLabel: 'multi_pair_confirm_button', + ), ); if (result != null) { vm.addTag(result.key, result.value); diff --git a/examples/demo/lib/widgets/sections/user_section.dart b/examples/demo/lib/widgets/sections/user_section.dart index 7ef3d0f6..4b40bd3e 100644 --- a/examples/demo/lib/widgets/sections/user_section.dart +++ b/examples/demo/lib/widgets/sections/user_section.dart @@ -66,6 +66,7 @@ class UserSection extends StatelessWidget { AppSpacing.gapBox, PrimaryButton( label: vm.isLoggedIn ? 'SWITCH USER' : 'LOGIN USER', + semanticsLabel: 'login_user_button', onPressed: () async { final result = await showDialog( context: context, @@ -80,6 +81,7 @@ class UserSection extends StatelessWidget { AppSpacing.gapBox, DestructiveButton( label: 'LOGOUT USER', + semanticsLabel: 'logout_user_button', onPressed: vm.logoutUser, ), ], diff --git a/examples/demo/lib/widgets/toggle_row.dart b/examples/demo/lib/widgets/toggle_row.dart index 39ad378c..5381e619 100644 --- a/examples/demo/lib/widgets/toggle_row.dart +++ b/examples/demo/lib/widgets/toggle_row.dart @@ -7,6 +7,7 @@ class ToggleRow extends StatelessWidget { final String? description; final bool value; final ValueChanged? onChanged; + final String? semanticsLabel; const ToggleRow({ super.key, @@ -14,10 +15,19 @@ class ToggleRow extends StatelessWidget { this.description, required this.value, this.onChanged, + this.semanticsLabel, }); @override Widget build(BuildContext context) { + Widget toggle = Switch( + value: value, + onChanged: onChanged, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ); + if (semanticsLabel != null) { + toggle = Semantics(label: semanticsLabel, child: toggle); + } return Row( children: [ Expanded( @@ -35,11 +45,7 @@ class ToggleRow extends StatelessWidget { ], ), ), - Switch( - value: value, - onChanged: onChanged, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), + toggle, ], ); } From c4116675a47209e797c6a006170efceeb36f6b37 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 9 Apr 2026 18:22:24 -0700 Subject: [PATCH 03/31] ci: update E2E tests with new secrets and accessibility --- .github/workflows/e2e.yml | 14 +- examples/demo/lib/widgets/action_button.dart | 4 +- examples/demo/lib/widgets/dialogs.dart | 30 ++- examples/demo/lib/widgets/log_view.dart | 210 +++++++++--------- examples/demo/lib/widgets/section_card.dart | 18 +- .../lib/widgets/sections/push_section.dart | 17 +- .../lib/widgets/sections/user_section.dart | 34 +-- examples/demo/lib/widgets/toggle_row.dart | 2 +- 8 files changed, 180 insertions(+), 149 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 07da8c13..8758f396 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -42,7 +42,9 @@ jobs: - name: Create demo .env working-directory: examples/demo - run: echo "ONESIGNAL_APP_ID=${{ secrets.E2E_ONESIGNAL_APP_ID }}" > .env + run: | + echo "ONESIGNAL_APP_ID=${{ secrets.APPIUM_ONESIGNAL_APP_ID }}" > .env + echo "ONESIGNAL_API_KEY=${{ secrets.APPIUM_ONESIGNAL_API_KEY }}" >> .env - name: Build release APK working-directory: examples/demo @@ -69,7 +71,9 @@ jobs: - name: Create demo .env working-directory: examples/demo - run: echo "ONESIGNAL_APP_ID=${{ secrets.E2E_ONESIGNAL_APP_ID }}" > .env + run: | + echo "ONESIGNAL_APP_ID=${{ secrets.APPIUM_ONESIGNAL_APP_ID }}" > .env + echo "ONESIGNAL_API_KEY=${{ secrets.APPIUM_ONESIGNAL_API_KEY }}" >> .env - name: Install CocoaPods dependencies working-directory: examples/demo/ios @@ -108,7 +112,8 @@ jobs: sdk-type: flutter browserstack-username: ${{ secrets.BROWSERSTACK_USERNAME }} browserstack-access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - onesignal-app-id: ${{ secrets.E2E_ONESIGNAL_APP_ID }} + onesignal-app-id: ${{ secrets.APPIUM_ONESIGNAL_APP_ID }} + onesignal-api-key: ${{ secrets.APPIUM_ONESIGNAL_API_KEY }} build-name: flutter-android-${{ github.ref_name }}-${{ github.run_number }} e2e-ios: @@ -128,5 +133,6 @@ jobs: sdk-type: flutter browserstack-username: ${{ secrets.BROWSERSTACK_USERNAME }} browserstack-access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - onesignal-app-id: ${{ secrets.E2E_ONESIGNAL_APP_ID }} + onesignal-app-id: ${{ secrets.APPIUM_ONESIGNAL_APP_ID }} + onesignal-api-key: ${{ secrets.APPIUM_ONESIGNAL_API_KEY }} build-name: flutter-ios-${{ github.ref_name }}-${{ github.run_number }} diff --git a/examples/demo/lib/widgets/action_button.dart b/examples/demo/lib/widgets/action_button.dart index 67e428b2..9222f81c 100644 --- a/examples/demo/lib/widgets/action_button.dart +++ b/examples/demo/lib/widgets/action_button.dart @@ -39,7 +39,7 @@ class PrimaryButton extends StatelessWidget { ), ); if (semanticsLabel != null) { - button = Semantics(label: semanticsLabel, child: button); + button = Semantics(identifier: semanticsLabel, container: true, child: button); } return button; } @@ -71,7 +71,7 @@ class DestructiveButton extends StatelessWidget { ), ); if (semanticsLabel != null) { - button = Semantics(label: semanticsLabel, child: button); + button = Semantics(identifier: semanticsLabel, container: true, child: button); } return button; } diff --git a/examples/demo/lib/widgets/dialogs.dart b/examples/demo/lib/widgets/dialogs.dart index e72c4814..5d187b32 100644 --- a/examples/demo/lib/widgets/dialogs.dart +++ b/examples/demo/lib/widgets/dialogs.dart @@ -42,7 +42,8 @@ class _SingleInputDialogState extends State { content: SizedBox( width: double.maxFinite, child: Semantics( - label: '${widget.fieldLabel}_input', + identifier: '${widget.fieldLabel}_input', + container: true, child: AppTextField( controller: _controller, decoration: InputDecoration(labelText: widget.fieldLabel), @@ -115,7 +116,8 @@ class _PairInputDialogState extends State { children: [ Expanded( child: Semantics( - label: widget.keySemanticsLabel ?? '${widget.keyLabel}_input', + identifier: widget.keySemanticsLabel ?? '${widget.keyLabel}_input', + container: true, child: AppTextField( controller: _keyController, decoration: InputDecoration(labelText: widget.keyLabel), @@ -126,7 +128,8 @@ class _PairInputDialogState extends State { const SizedBox(width: 12), Expanded( child: Semantics( - label: widget.valueSemanticsLabel ?? '${widget.valueLabel}_input', + identifier: widget.valueSemanticsLabel ?? '${widget.valueLabel}_input', + container: true, child: AppTextField( controller: _valueController, decoration: InputDecoration(labelText: widget.valueLabel), @@ -143,7 +146,8 @@ class _PairInputDialogState extends State { child: const Text('Cancel'), ), Semantics( - label: widget.confirmSemanticsLabel, + identifier: widget.confirmSemanticsLabel, + container: true, child: TextButton( onPressed: _isValid ? () => Navigator.pop( @@ -389,7 +393,8 @@ class _LoginDialogState extends State { content: SizedBox( width: double.maxFinite, child: Semantics( - label: 'login_user_id_input', + identifier: 'login_user_id_input', + container: true, child: AppTextField( controller: _controller, decoration: const InputDecoration(labelText: 'External User Id'), @@ -403,7 +408,8 @@ class _LoginDialogState extends State { child: const Text('Cancel'), ), Semantics( - label: 'login_confirm_button', + identifier: 'login_confirm_button', + container: true, child: TextButton( onPressed: _controller.text.isEmpty ? null @@ -698,7 +704,11 @@ class TooltipDialog extends StatelessWidget { Widget build(BuildContext context) { return AlertDialog( insetPadding: const EdgeInsets.symmetric(horizontal: 16), - title: Text(tooltip.title), + title: Semantics( + identifier: 'tooltip_title', + container: true, + child: Text(tooltip.title), + ), content: SizedBox( width: double.maxFinite, child: SingleChildScrollView( @@ -706,7 +716,11 @@ class TooltipDialog extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(tooltip.description), + Semantics( + identifier: 'tooltip_description', + container: true, + child: Text(tooltip.description), + ), if (tooltip.options != null && tooltip.options!.isNotEmpty) ...[ const SizedBox(height: 16), ...tooltip.options!.map((option) => Padding( diff --git a/examples/demo/lib/widgets/log_view.dart b/examples/demo/lib/widgets/log_view.dart index 1fb43cfe..cd49f5c4 100644 --- a/examples/demo/lib/widgets/log_view.dart +++ b/examples/demo/lib/widgets/log_view.dart @@ -55,139 +55,131 @@ class _LogViewState extends State { const logBackground = AppColors.osLogBackground; return Semantics( - label: 'log_view_container', + identifier: 'log_view_container', + container: true, child: Card( margin: EdgeInsets.zero, color: logBackground, shape: const RoundedRectangleBorder(), child: Column( children: [ - Semantics( - label: 'log_view_header', - child: InkWell( - onTap: () => setState(() => _expanded = !_expanded), - borderRadius: BorderRadius.zero, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - child: Row( - children: [ - Text( - 'LOGS', + InkWell( + onTap: () => setState(() => _expanded = !_expanded), + excludeFromSemantics: true, + borderRadius: BorderRadius.zero, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + child: Row( + children: [ + Text( + 'LOGS', + style: textTheme.labelSmall?.copyWith( + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(width: 8), + Semantics( + identifier: 'log_view_count', + container: true, + child: Text( + '${logs.length}', style: textTheme.labelSmall?.copyWith( - fontWeight: FontWeight.bold, - color: Colors.white, + color: AppColors.osGrey500, ), ), - const SizedBox(width: 8), + ), + const Spacer(), + if (logs.isNotEmpty) Semantics( - label: 'log_view_count', - child: Text( - '(${logs.length})', - style: textTheme.labelSmall?.copyWith( + identifier: 'log_view_clear_button', + container: true, + child: GestureDetector( + excludeFromSemantics: true, + onTap: () => LogManager().clear(), + child: const Icon( + Icons.delete, + size: 18, color: AppColors.osGrey500, ), ), ), - const Spacer(), - if (logs.isNotEmpty) - Semantics( - label: 'log_view_clear_button', - child: GestureDetector( - onTap: () => LogManager().clear(), - child: const Icon( - Icons.delete, - size: 18, - color: AppColors.osGrey500, - ), - ), - ), - const SizedBox(width: 8), - Icon( - _expanded - ? Icons.expand_less - : Icons.expand_more, - size: 18, - color: AppColors.osGrey500, - ), - ], - ), + const SizedBox(width: 8), + Icon( + _expanded + ? Icons.expand_less + : Icons.expand_more, + size: 18, + color: AppColors.osGrey500, + ), + ], ), ), ), if (_expanded) - Semantics( - label: 'log_view_list', - child: SizedBox( - height: 100, - child: logs.isEmpty - ? Semantics( - label: 'log_view_empty', - child: Center( - child: Text( - 'No logs yet', - style: textTheme.bodyMedium?.copyWith( - color: AppColors.osGrey500, - ), - ), + SizedBox( + height: 100, + child: logs.isEmpty + ? Center( + child: Text( + 'No logs yet', + style: textTheme.bodyMedium?.copyWith( + color: AppColors.osGrey500, ), - ) - : ListView.builder( - controller: _scrollController, - itemCount: logs.length, - padding: const EdgeInsets.symmetric(horizontal: 12), - itemBuilder: (context, index) { - final entry = logs[logs.length - 1 - index]; - return Semantics( - label: 'log_entry_$index', - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 1, + ), + ) + : ListView.builder( + controller: _scrollController, + itemCount: logs.length, + padding: const EdgeInsets.symmetric(horizontal: 12), + itemBuilder: (context, index) { + final entry = logs[logs.length - 1 - index]; + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 1, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + entry.formattedTime, + style: logEntryStyle?.copyWith( + color: AppColors.osLogTimestamp, + ), ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Semantics( - label: 'log_entry_${index}_timestamp', - child: Text( - entry.formattedTime, - style: logEntryStyle?.copyWith( - color: AppColors.osLogTimestamp, - ), - ), - ), - const SizedBox(width: 4), - Semantics( - label: 'log_entry_${index}_level', - child: Text( - entry.levelLabel, - style: logEntryStyle?.copyWith( - fontWeight: FontWeight.bold, - color: _levelColor(entry.level), - ), - ), + const SizedBox(width: 4), + Semantics( + identifier: 'log_entry_${index}_level', + container: true, + child: Text( + entry.levelLabel, + style: logEntryStyle?.copyWith( + fontWeight: FontWeight.bold, + color: _levelColor(entry.level), ), - const SizedBox(width: 4), - Expanded( - child: Semantics( - label: 'log_entry_${index}_message', - child: Text( - '${entry.tag}: ${entry.message}', - style: logEntryStyle?.copyWith( - color: Colors.white, - ), - ), + ), + ), + const SizedBox(width: 4), + Expanded( + child: Semantics( + identifier: 'log_entry_${index}_message', + container: true, + child: Text( + '${entry.tag}: ${entry.message}', + style: logEntryStyle?.copyWith( + color: Colors.white, ), ), - ], + ), ), - ), - ); - }, - ), - ), + ], + ), + ); + }, + ), ), ], ), diff --git a/examples/demo/lib/widgets/section_card.dart b/examples/demo/lib/widgets/section_card.dart index 8c5b3c6d..254e28de 100644 --- a/examples/demo/lib/widgets/section_card.dart +++ b/examples/demo/lib/widgets/section_card.dart @@ -5,12 +5,14 @@ import '../theme.dart'; class SectionCard extends StatelessWidget { final String title; final VoidCallback? onInfoTap; + final String? sectionKey; final Widget child; const SectionCard({ super.key, required this.title, this.onInfoTap, + this.sectionKey, required this.child, }); @@ -37,12 +39,16 @@ class SectionCard extends StatelessWidget { ), ), if (onInfoTap != null) - GestureDetector( - onTap: onInfoTap, - child: Icon( - Icons.info_outline, - size: 18, - color: AppColors.osGrey500, + Semantics( + identifier: sectionKey != null ? '${sectionKey}_info_icon' : null, + container: true, + child: GestureDetector( + onTap: onInfoTap, + child: Icon( + Icons.info_outline, + size: 18, + color: AppColors.osGrey500, + ), ), ), ], diff --git a/examples/demo/lib/widgets/sections/push_section.dart b/examples/demo/lib/widgets/sections/push_section.dart index e4977a9a..a02e2c9c 100644 --- a/examples/demo/lib/widgets/sections/push_section.dart +++ b/examples/demo/lib/widgets/sections/push_section.dart @@ -18,6 +18,7 @@ class PushSection extends StatelessWidget { return SectionCard( title: 'Push', + sectionKey: 'push', onInfoTap: onInfoTap, child: Column( children: [ @@ -35,12 +36,16 @@ class PushSection extends StatelessWidget { ), const SizedBox(width: 12), Expanded( - child: SelectableText( - vm.pushSubscriptionId ?? 'N/A', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontFamily: 'monospace', - ), - textAlign: TextAlign.end, + child: Semantics( + identifier: 'push_id_value', + container: true, + child: SelectableText( + vm.pushSubscriptionId ?? 'N/A', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontFamily: 'monospace', + ), + textAlign: TextAlign.end, + ), ), ), ], diff --git a/examples/demo/lib/widgets/sections/user_section.dart b/examples/demo/lib/widgets/sections/user_section.dart index 4b40bd3e..d5649ad5 100644 --- a/examples/demo/lib/widgets/sections/user_section.dart +++ b/examples/demo/lib/widgets/sections/user_section.dart @@ -32,14 +32,18 @@ class UserSection extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Status', style: Theme.of(context).textTheme.bodyMedium), - Text( - vm.isLoggedIn ? 'Logged In' : 'Anonymous', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontFamily: 'monospace', - color: vm.isLoggedIn - ? AppColors.osSuccess - : AppColors.osGrey600, - ), + Semantics( + identifier: 'user_status_value', + container: true, + child: Text( + vm.isLoggedIn ? 'Logged In' : 'Anonymous', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontFamily: 'monospace', + color: vm.isLoggedIn + ? AppColors.osSuccess + : AppColors.osGrey600, + ), + ), ), ], ), @@ -51,11 +55,15 @@ class UserSection extends StatelessWidget { 'External ID', style: Theme.of(context).textTheme.bodyMedium, ), - SelectableText( - vm.isLoggedIn ? (vm.externalUserId ?? '') : '–', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontFamily: 'monospace', - ), + Semantics( + identifier: 'user_external_id_value', + container: true, + child: SelectableText( + vm.isLoggedIn ? (vm.externalUserId ?? '') : '–', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontFamily: 'monospace', + ), + ), ), ], ), diff --git a/examples/demo/lib/widgets/toggle_row.dart b/examples/demo/lib/widgets/toggle_row.dart index 5381e619..2450ca78 100644 --- a/examples/demo/lib/widgets/toggle_row.dart +++ b/examples/demo/lib/widgets/toggle_row.dart @@ -26,7 +26,7 @@ class ToggleRow extends StatelessWidget { materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ); if (semanticsLabel != null) { - toggle = Semantics(label: semanticsLabel, child: toggle); + toggle = Semantics(identifier: semanticsLabel, container: true, child: toggle); } return Row( children: [ From 2d57f81a5a5604f67a4c5be6751fae2e9f8ea44c Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 10 Apr 2026 11:28:12 -0700 Subject: [PATCH 04/31] ci(e2e): migrate to shared Appium workflow --- .github/workflows/e2e.yml | 56 ++++------- examples/demo/lib/screens/home_screen.dart | 11 --- .../demo/lib/viewmodels/app_viewmodel.dart | 92 +++++++++---------- .../widgets/sections/send_push_section.dart | 5 + 4 files changed, 65 insertions(+), 99 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 8758f396..b0d82285 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -43,7 +43,7 @@ jobs: - name: Create demo .env working-directory: examples/demo run: | - echo "ONESIGNAL_APP_ID=${{ secrets.APPIUM_ONESIGNAL_APP_ID }}" > .env + echo "ONESIGNAL_APP_ID=${{ vars.APPIUM_ONESIGNAL_APP_ID }}" > .env echo "ONESIGNAL_API_KEY=${{ secrets.APPIUM_ONESIGNAL_API_KEY }}" >> .env - name: Build release APK @@ -72,7 +72,7 @@ jobs: - name: Create demo .env working-directory: examples/demo run: | - echo "ONESIGNAL_APP_ID=${{ secrets.APPIUM_ONESIGNAL_APP_ID }}" > .env + echo "ONESIGNAL_APP_ID=${{ vars.APPIUM_ONESIGNAL_APP_ID }}" > .env echo "ONESIGNAL_API_KEY=${{ secrets.APPIUM_ONESIGNAL_API_KEY }}" >> .env - name: Install CocoaPods dependencies @@ -97,42 +97,22 @@ jobs: e2e-android: needs: build-android - runs-on: ubuntu-latest - steps: - - name: Download APK - uses: actions/download-artifact@v4 - with: - name: demo-apk - - - name: Run Appium E2E (Android) - uses: OneSignal/sdk-shared/.github/actions/appium-e2e@main - with: - platform: android - app-path: app-release.apk - sdk-type: flutter - browserstack-username: ${{ secrets.BROWSERSTACK_USERNAME }} - browserstack-access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - onesignal-app-id: ${{ secrets.APPIUM_ONESIGNAL_APP_ID }} - onesignal-api-key: ${{ secrets.APPIUM_ONESIGNAL_API_KEY }} - build-name: flutter-android-${{ github.ref_name }}-${{ github.run_number }} + uses: OneSignal/sdk-shared/.github/workflows/appium-e2e.yml@main + secrets: inherit + with: + platform: android + app-artifact: demo-apk + app-filename: app-release.apk + sdk-type: flutter + build-name: flutter-android-${{ github.ref_name }}-${{ github.run_number }} e2e-ios: needs: build-ios - runs-on: ubuntu-latest - steps: - - name: Download IPA - uses: actions/download-artifact@v4 - with: - name: demo-ipa - - - name: Run Appium E2E (iOS) - uses: OneSignal/sdk-shared/.github/actions/appium-e2e@main - with: - platform: ios - app-path: Runner.ipa - sdk-type: flutter - browserstack-username: ${{ secrets.BROWSERSTACK_USERNAME }} - browserstack-access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - onesignal-app-id: ${{ secrets.APPIUM_ONESIGNAL_APP_ID }} - onesignal-api-key: ${{ secrets.APPIUM_ONESIGNAL_API_KEY }} - build-name: flutter-ios-${{ github.ref_name }}-${{ github.run_number }} + uses: OneSignal/sdk-shared/.github/workflows/appium-e2e.yml@main + secrets: inherit + with: + platform: ios + app-artifact: demo-ipa + app-filename: Runner.ipa + sdk-type: flutter + build-name: flutter-ios-${{ github.ref_name }}-${{ github.run_number }} diff --git a/examples/demo/lib/screens/home_screen.dart b/examples/demo/lib/screens/home_screen.dart index e161de2b..c393e64b 100644 --- a/examples/demo/lib/screens/home_screen.dart +++ b/examples/demo/lib/screens/home_screen.dart @@ -57,17 +57,6 @@ class _HomeScreenState extends State { Widget build(BuildContext context) { final vm = context.watch(); - // Listen for snackbar messages - WidgetsBinding.instance.addPostFrameCallback((_) { - final message = vm.snackBarMessage; - if (message != null) { - ScaffoldMessenger.of(context) - ..clearSnackBars() - ..showSnackBar(SnackBar(content: Text(message))); - vm.clearSnackBar(); - } - }); - return Scaffold( appBar: AppBar( centerTitle: true, diff --git a/examples/demo/lib/viewmodels/app_viewmodel.dart b/examples/demo/lib/viewmodels/app_viewmodel.dart index 57f3cfe0..6dd08e06 100644 --- a/examples/demo/lib/viewmodels/app_viewmodel.dart +++ b/examples/demo/lib/viewmodels/app_viewmodel.dart @@ -35,19 +35,6 @@ class AppViewModel extends ChangeNotifier { bool _isLoading = false; bool get isLoading => _isLoading; - // SnackBar message - String? _snackBarMessage; - String? get snackBarMessage => _snackBarMessage; - void clearSnackBar() { - _snackBarMessage = null; - } - - void _showSnackBar(String message) { - _snackBarMessage = message; - LogManager().i('App', message); - notifyListeners(); - } - // App state String _appId = ''; String get appId => _appId; @@ -223,12 +210,12 @@ class AppViewModel extends ChangeNotifier { _isLoading = false; notifyListeners(); - _showSnackBar('Logged in as: $externalUserId'); + LogManager().i('App', 'Logged in as: $externalUserId'); } catch (e) { _isLoading = false; notifyListeners(); LogManager().e('App', 'Login error: $e'); - _showSnackBar('Login failed'); + LogManager().e('App', 'Login failed'); } } @@ -248,7 +235,7 @@ class AppViewModel extends ChangeNotifier { _isLoading = false; notifyListeners(); - _showSnackBar('Logged out'); + LogManager().i('App', 'Logged out'); } catch (e) { _isLoading = false; notifyListeners(); @@ -284,7 +271,7 @@ class AppViewModel extends ChangeNotifier { } _pushEnabled = enabled; notifyListeners(); - _showSnackBar('Push ${enabled ? "enabled" : "disabled"}'); + LogManager().i('App', 'Push ${enabled ? "enabled" : "disabled"}'); } Future promptPush() async { @@ -296,20 +283,25 @@ class AppViewModel extends ChangeNotifier { // Notifications Future sendNotification(NotificationType type) async { final success = await _repository.sendNotification(type); - _showSnackBar(success - ? 'Notification sent: ${type.name}' - : 'Failed to send notification'); + if (success) { + LogManager().i('App', 'Notification sent: ${type.name}'); + } else { + LogManager().e('App', 'Failed to send notification'); + } } Future sendCustomNotification(String title, String body) async { final success = await _repository.sendCustomNotification(title, body); - _showSnackBar( - success ? 'Custom notification sent' : 'Failed to send notification'); + if (success) { + LogManager().i('App', 'Custom notification sent'); + } else { + LogManager().e('App', 'Failed to send notification'); + } } void clearAllNotifications() { _repository.clearAllNotifications(); - _showSnackBar('All notifications cleared'); + LogManager().i('App', 'All notifications cleared'); } // IAM @@ -326,7 +318,7 @@ class AppViewModel extends ChangeNotifier { ..removeWhere((e) => e.key == 'iam_type') ..add(MapEntry('iam_type', type.triggerValue)); notifyListeners(); - _showSnackBar('Sent In-App Message: ${type.label}'); + LogManager().i('App', 'Sent In-App Message: ${type.label}'); } // Aliases @@ -334,14 +326,14 @@ class AppViewModel extends ChangeNotifier { _repository.addAlias(label, id); _aliasesList = List.from(_aliasesList)..add(MapEntry(label, id)); notifyListeners(); - _showSnackBar('Alias added: $label'); + LogManager().i('App', 'Alias added: $label'); } void addAliases(Map aliases) { _repository.addAliases(aliases); _aliasesList = List.from(_aliasesList)..addAll(aliases.entries); notifyListeners(); - _showSnackBar('${aliases.length} alias(es) added'); + LogManager().i('App', '${aliases.length} alias(es) added'); } // Emails @@ -349,14 +341,14 @@ class AppViewModel extends ChangeNotifier { _repository.addEmail(email); _emailsList = List.from(_emailsList)..add(email); notifyListeners(); - _showSnackBar('Email added: $email'); + LogManager().i('App', 'Email added: $email'); } void removeEmail(String email) { _repository.removeEmail(email); _emailsList = List.from(_emailsList)..remove(email); notifyListeners(); - _showSnackBar('Email removed: $email'); + LogManager().i('App', 'Email removed: $email'); } // SMS @@ -364,14 +356,14 @@ class AppViewModel extends ChangeNotifier { _repository.addSms(smsNumber); _smsNumbersList = List.from(_smsNumbersList)..add(smsNumber); notifyListeners(); - _showSnackBar('SMS added: $smsNumber'); + LogManager().i('App', 'SMS added: $smsNumber'); } void removeSms(String smsNumber) { _repository.removeSms(smsNumber); _smsNumbersList = List.from(_smsNumbersList)..remove(smsNumber); notifyListeners(); - _showSnackBar('SMS removed: $smsNumber'); + LogManager().i('App', 'SMS removed: $smsNumber'); } // Tags @@ -379,28 +371,28 @@ class AppViewModel extends ChangeNotifier { _repository.addTag(key, value); _tagsList = List.from(_tagsList)..add(MapEntry(key, value)); notifyListeners(); - _showSnackBar('Tag added: $key'); + LogManager().i('App', 'Tag added: $key'); } void addTags(Map tags) { _repository.addTags(tags); _tagsList = List.from(_tagsList)..addAll(tags.entries); notifyListeners(); - _showSnackBar('${tags.length} tag(s) added'); + LogManager().i('App', '${tags.length} tag(s) added'); } void removeTag(String key) { _repository.removeTag(key); _tagsList = List.from(_tagsList)..removeWhere((e) => e.key == key); notifyListeners(); - _showSnackBar('Tag removed: $key'); + LogManager().i('App', 'Tag removed: $key'); } void removeSelectedTags(List keys) { _repository.removeTags(keys); _tagsList = List.from(_tagsList)..removeWhere((e) => keys.contains(e.key)); notifyListeners(); - _showSnackBar('${keys.length} tag(s) removed'); + LogManager().i('App', '${keys.length} tag(s) removed'); } // Triggers (in-memory only) @@ -408,21 +400,21 @@ class AppViewModel extends ChangeNotifier { _repository.addTrigger(key, value); _triggersList = List.from(_triggersList)..add(MapEntry(key, value)); notifyListeners(); - _showSnackBar('Trigger added: $key'); + LogManager().i('App', 'Trigger added: $key'); } void addTriggers(Map triggers) { _repository.addTriggers(triggers); _triggersList = List.from(_triggersList)..addAll(triggers.entries); notifyListeners(); - _showSnackBar('${triggers.length} trigger(s) added'); + LogManager().i('App', '${triggers.length} trigger(s) added'); } void removeTrigger(String key) { _repository.removeTrigger(key); _triggersList = List.from(_triggersList)..removeWhere((e) => e.key == key); notifyListeners(); - _showSnackBar('Trigger removed: $key'); + LogManager().i('App', 'Trigger removed: $key'); } void removeSelectedTriggers(List keys) { @@ -430,36 +422,36 @@ class AppViewModel extends ChangeNotifier { _triggersList = List.from(_triggersList) ..removeWhere((e) => keys.contains(e.key)); notifyListeners(); - _showSnackBar('${keys.length} trigger(s) removed'); + LogManager().i('App', '${keys.length} trigger(s) removed'); } void clearAllTriggers() { _repository.clearTriggers(); _triggersList = []; notifyListeners(); - _showSnackBar('All triggers cleared'); + LogManager().i('App', 'All triggers cleared'); } // Outcomes void sendOutcome(String name) { _repository.sendOutcome(name); - _showSnackBar('Outcome sent: $name'); + LogManager().i('App', 'Outcome sent: $name'); } void sendUniqueOutcome(String name) { _repository.sendUniqueOutcome(name); - _showSnackBar('Unique outcome sent: $name'); + LogManager().i('App', 'Unique outcome sent: $name'); } void sendOutcomeWithValue(String name, double value) { _repository.sendOutcomeWithValue(name, value); - _showSnackBar('Outcome sent: $name = $value'); + LogManager().i('App', 'Outcome sent: $name = $value'); } // Track Event void trackEvent(String name, Map? properties) { _repository.trackEvent(name, properties); - _showSnackBar('Event tracked: $name'); + LogManager().i('App', 'Event tracked: $name'); } // Live Activities @@ -479,7 +471,7 @@ class AppViewModel extends ChangeNotifier { _statusIndex = 0; await _repository.startDefaultLiveActivity(_activityId, attributes, content); notifyListeners(); - _showSnackBar('Started Live Activity: $_activityId'); + LogManager().i('App', 'Started Live Activity: $_activityId'); } Future updateLiveActivity() async { @@ -494,25 +486,25 @@ class AppViewModel extends ChangeNotifier { _isLaUpdating = false; if (success) { _statusIndex = nextIndex; - _showSnackBar('Updated Live Activity: $_activityId'); + LogManager().i('App', 'Updated Live Activity: $_activityId'); } else { - _showSnackBar('Failed to update Live Activity'); + LogManager().e('App', 'Failed to update Live Activity'); } notifyListeners(); } Future exitLiveActivity() async { await _repository.exitLiveActivity(_activityId); - _showSnackBar('Exited Live Activity: $_activityId'); + LogManager().i('App', 'Exited Live Activity: $_activityId'); } Future endLiveActivity() async { final success = await _repository.endLiveActivity(_activityId); if (success) { _statusIndex = 0; - _showSnackBar('Ended Live Activity: $_activityId'); + LogManager().i('App', 'Ended Live Activity: $_activityId'); } else { - _showSnackBar('Failed to end Live Activity'); + LogManager().e('App', 'Failed to end Live Activity'); } notifyListeners(); } @@ -523,7 +515,7 @@ class AppViewModel extends ChangeNotifier { _repository.setLocationShared(shared); await _prefs.setLocationShared(shared); notifyListeners(); - _showSnackBar('Location sharing ${shared ? "enabled" : "disabled"}'); + LogManager().i('App', 'Location sharing ${shared ? "enabled" : "disabled"}'); } void promptLocation() { diff --git a/examples/demo/lib/widgets/sections/send_push_section.dart b/examples/demo/lib/widgets/sections/send_push_section.dart index ef1b327c..a3d8fce0 100644 --- a/examples/demo/lib/widgets/sections/send_push_section.dart +++ b/examples/demo/lib/widgets/sections/send_push_section.dart @@ -24,21 +24,25 @@ class SendPushSection extends StatelessWidget { children: [ PrimaryButton( label: 'SIMPLE', + semanticsLabel: 'send_simple_button', onPressed: () => vm.sendNotification(NotificationType.simple), ), AppSpacing.gapBox, PrimaryButton( label: 'WITH IMAGE', + semanticsLabel: 'send_image_button', onPressed: () => vm.sendNotification(NotificationType.withImage), ), AppSpacing.gapBox, PrimaryButton( label: 'WITH SOUND', + semanticsLabel: 'send_sound_button', onPressed: () => vm.sendNotification(NotificationType.withSound), ), AppSpacing.gapBox, PrimaryButton( label: 'CUSTOM', + semanticsLabel: 'send_custom_button', onPressed: () async { final result = await showDialog>( context: context, @@ -52,6 +56,7 @@ class SendPushSection extends StatelessWidget { AppSpacing.gapBox, DestructiveButton( label: 'CLEAR ALL', + semanticsLabel: 'clear_all_button', onPressed: vm.clearAllNotifications, ), ], From 8cc894b6e02acb269b7e9d62ddf98393c79818b6 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 10 Apr 2026 11:28:42 -0700 Subject: [PATCH 05/31] refactor(demo): remove log level colors from UI --- examples/demo/lib/widgets/log_view.dart | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/examples/demo/lib/widgets/log_view.dart b/examples/demo/lib/widgets/log_view.dart index cd49f5c4..891eb977 100644 --- a/examples/demo/lib/widgets/log_view.dart +++ b/examples/demo/lib/widgets/log_view.dart @@ -31,19 +31,6 @@ class _LogViewState extends State { setState(() {}); } - Color _levelColor(LogLevel level) { - switch (level) { - case LogLevel.debug: - return AppColors.osLogDebug; - case LogLevel.info: - return AppColors.osLogInfo; - case LogLevel.warn: - return AppColors.osLogWarn; - case LogLevel.error: - return AppColors.osLogError; - } - } - @override Widget build(BuildContext context) { final logs = LogManager().logs; @@ -151,18 +138,6 @@ class _LogViewState extends State { ), ), const SizedBox(width: 4), - Semantics( - identifier: 'log_entry_${index}_level', - container: true, - child: Text( - entry.levelLabel, - style: logEntryStyle?.copyWith( - fontWeight: FontWeight.bold, - color: _levelColor(entry.level), - ), - ), - ), - const SizedBox(width: 4), Expanded( child: Semantics( identifier: 'log_entry_${index}_message', From b099447dc20dbf46cb270532a906b0e46c4f2dbf Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 10 Apr 2026 16:06:33 -0700 Subject: [PATCH 06/31] test(demo): add semantic identifier to main scroll view --- examples/demo/lib/screens/home_screen.dart | 141 +++++++++++---------- 1 file changed, 73 insertions(+), 68 deletions(-) diff --git a/examples/demo/lib/screens/home_screen.dart b/examples/demo/lib/screens/home_screen.dart index c393e64b..8991dc26 100644 --- a/examples/demo/lib/screens/home_screen.dart +++ b/examples/demo/lib/screens/home_screen.dart @@ -87,78 +87,83 @@ class _HomeScreenState extends State { children: [ const LogView(), Expanded( - child: ListView( - padding: const EdgeInsets.only(bottom: 24), - children: [ - const AppSection(), - const UserSection(), - PushSection( - onInfoTap: () => _showTooltipDialog(context, 'push'), - ), - SendPushSection( - onInfoTap: () => - _showTooltipDialog(context, 'sendPushNotification'), - ), - InAppSection( - onInfoTap: () => - _showTooltipDialog(context, 'inAppMessaging'), - ), - SendIamSection( - onInfoTap: () => - _showTooltipDialog(context, 'sendInAppMessage'), - ), - AliasesSection( - onInfoTap: () => _showTooltipDialog(context, 'aliases'), - ), - EmailsSection( - onInfoTap: () => _showTooltipDialog(context, 'emails'), - ), - SmsSection( - onInfoTap: () => _showTooltipDialog(context, 'sms'), - ), - TagsSection( - onInfoTap: () => _showTooltipDialog(context, 'tags'), - ), - OutcomesSection( - onInfoTap: () => - _showTooltipDialog(context, 'outcomes'), - ), - TriggersSection( - onInfoTap: () => _showTooltipDialog(context, 'triggers'), - ), - TrackEventSection( - onInfoTap: () => - _showTooltipDialog(context, 'trackEvent'), - ), - LocationSection( - onInfoTap: () => _showTooltipDialog(context, 'location'), - ), - if (defaultTargetPlatform == TargetPlatform.iOS) - LiveActivitiesSection( + child: Semantics( + identifier: 'main_scroll_view', + child: ListView( + padding: const EdgeInsets.only(bottom: 24), + children: [ + const AppSection(), + const UserSection(), + PushSection( + onInfoTap: () => _showTooltipDialog(context, 'push'), + ), + SendPushSection( + onInfoTap: () => + _showTooltipDialog(context, 'sendPushNotification'), + ), + InAppSection( onInfoTap: () => - _showTooltipDialog(context, 'liveActivities'), + _showTooltipDialog(context, 'inAppMessaging'), ), - const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const SecondaryScreen(), - ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.osPrimary, - foregroundColor: Colors.white, + SendIamSection( + onInfoTap: () => + _showTooltipDialog(context, 'sendInAppMessage'), + ), + AliasesSection( + onInfoTap: () => _showTooltipDialog(context, 'aliases'), + ), + EmailsSection( + onInfoTap: () => _showTooltipDialog(context, 'emails'), + ), + SmsSection( + onInfoTap: () => _showTooltipDialog(context, 'sms'), + ), + TagsSection( + onInfoTap: () => _showTooltipDialog(context, 'tags'), + ), + OutcomesSection( + onInfoTap: () => + _showTooltipDialog(context, 'outcomes'), + ), + TriggersSection( + onInfoTap: () => + _showTooltipDialog(context, 'triggers'), + ), + TrackEventSection( + onInfoTap: () => + _showTooltipDialog(context, 'trackEvent'), + ), + LocationSection( + onInfoTap: () => + _showTooltipDialog(context, 'location'), + ), + if (defaultTargetPlatform == TargetPlatform.iOS) + LiveActivitiesSection( + onInfoTap: () => + _showTooltipDialog(context, 'liveActivities'), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const SecondaryScreen(), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.osPrimary, + foregroundColor: Colors.white, + ), + child: const Text('NEXT ACTIVITY'), ), - child: const Text('NEXT ACTIVITY'), ), - ), - const SizedBox(height: 16), - ], + const SizedBox(height: 16), + ], + ), ), ), ], From 0941cb830be11952a70e7489480cffa393d1ea3a Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 10 Apr 2026 17:46:18 -0700 Subject: [PATCH 07/31] feat(demo): improve info icon accessibility in section cards --- examples/demo/lib/widgets/section_card.dart | 38 +++++++++++++-------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/examples/demo/lib/widgets/section_card.dart b/examples/demo/lib/widgets/section_card.dart index 254e28de..452accaf 100644 --- a/examples/demo/lib/widgets/section_card.dart +++ b/examples/demo/lib/widgets/section_card.dart @@ -25,29 +25,39 @@ class SectionCard extends StatelessWidget { children: [ // Section header (outside card, ALL CAPS like reference) Padding( - padding: EdgeInsets.only(bottom: AppSpacing.gap), + padding: EdgeInsets.only(bottom: onInfoTap != null ? 0 : AppSpacing.gap), child: Row( children: [ Expanded( child: Text( title.toUpperCase(), style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontWeight: FontWeight.bold, - color: AppColors.osGrey700, - letterSpacing: 0.5, - ), + fontWeight: FontWeight.bold, + color: AppColors.osGrey700, + letterSpacing: 0.5, + ), ), ), if (onInfoTap != null) - Semantics( - identifier: sectionKey != null ? '${sectionKey}_info_icon' : null, - container: true, - child: GestureDetector( - onTap: onInfoTap, - child: Icon( - Icons.info_outline, - size: 18, - color: AppColors.osGrey500, + Transform.translate( + offset: const Offset(16, 0), + child: Semantics( + identifier: sectionKey != null + ? '${sectionKey}_info_icon' + : null, + container: true, + child: IconButton( + onPressed: onInfoTap, + icon: Icon( + Icons.info_outline, + size: 18, + color: AppColors.osGrey500, + ), + padding: EdgeInsets.zero, + constraints: const BoxConstraints( + minWidth: 32, + minHeight: 32, + ), ), ), ), From a90a5ce2e5ea93500195eed8f982322a7a0bbde4 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 10 Apr 2026 19:13:20 -0700 Subject: [PATCH 08/31] test(demo): add section keys for Appium testing --- examples/demo/lib/widgets/sections/aliases_section.dart | 1 + examples/demo/lib/widgets/sections/emails_section.dart | 1 + examples/demo/lib/widgets/sections/in_app_section.dart | 2 ++ examples/demo/lib/widgets/sections/live_activities_section.dart | 1 + examples/demo/lib/widgets/sections/location_section.dart | 1 + examples/demo/lib/widgets/sections/outcomes_section.dart | 1 + examples/demo/lib/widgets/sections/send_iam_section.dart | 1 + examples/demo/lib/widgets/sections/send_push_section.dart | 1 + examples/demo/lib/widgets/sections/sms_section.dart | 1 + examples/demo/lib/widgets/sections/tags_section.dart | 1 + examples/demo/lib/widgets/sections/track_event_section.dart | 1 + examples/demo/lib/widgets/sections/triggers_section.dart | 1 + 12 files changed, 13 insertions(+) diff --git a/examples/demo/lib/widgets/sections/aliases_section.dart b/examples/demo/lib/widgets/sections/aliases_section.dart index b17c6b87..a8180d70 100644 --- a/examples/demo/lib/widgets/sections/aliases_section.dart +++ b/examples/demo/lib/widgets/sections/aliases_section.dart @@ -19,6 +19,7 @@ class AliasesSection extends StatelessWidget { return SectionCard( title: 'Aliases', + sectionKey: 'aliases', onInfoTap: onInfoTap, child: Column( children: [ diff --git a/examples/demo/lib/widgets/sections/emails_section.dart b/examples/demo/lib/widgets/sections/emails_section.dart index 95236ae0..fa80a859 100644 --- a/examples/demo/lib/widgets/sections/emails_section.dart +++ b/examples/demo/lib/widgets/sections/emails_section.dart @@ -19,6 +19,7 @@ class EmailsSection extends StatelessWidget { return SectionCard( title: 'Emails', + sectionKey: 'emails', onInfoTap: onInfoTap, child: Column( children: [ diff --git a/examples/demo/lib/widgets/sections/in_app_section.dart b/examples/demo/lib/widgets/sections/in_app_section.dart index acb2ba0a..c8abfed3 100644 --- a/examples/demo/lib/widgets/sections/in_app_section.dart +++ b/examples/demo/lib/widgets/sections/in_app_section.dart @@ -17,6 +17,7 @@ class InAppSection extends StatelessWidget { return SectionCard( title: 'In-App Messaging', + sectionKey: 'iam', onInfoTap: onInfoTap, child: Card( margin: EdgeInsets.zero, @@ -25,6 +26,7 @@ class InAppSection extends StatelessWidget { child: ToggleRow( label: 'Pause In-App Messages', description: 'Toggle in-app message display', + semanticsLabel: 'pause_iam_toggle', value: vm.iamPaused, onChanged: vm.setIamPaused, ), diff --git a/examples/demo/lib/widgets/sections/live_activities_section.dart b/examples/demo/lib/widgets/sections/live_activities_section.dart index 6e5729e4..2f2b9552 100644 --- a/examples/demo/lib/widgets/sections/live_activities_section.dart +++ b/examples/demo/lib/widgets/sections/live_activities_section.dart @@ -42,6 +42,7 @@ class _LiveActivitiesSectionState extends State { return SectionCard( title: 'Live Activities', + sectionKey: 'live_activities', onInfoTap: widget.onInfoTap, child: Column( children: [ diff --git a/examples/demo/lib/widgets/sections/location_section.dart b/examples/demo/lib/widgets/sections/location_section.dart index bdb5d304..dddd0b54 100644 --- a/examples/demo/lib/widgets/sections/location_section.dart +++ b/examples/demo/lib/widgets/sections/location_section.dart @@ -18,6 +18,7 @@ class LocationSection extends StatelessWidget { return SectionCard( title: 'Location', + sectionKey: 'location', onInfoTap: onInfoTap, child: Column( children: [ diff --git a/examples/demo/lib/widgets/sections/outcomes_section.dart b/examples/demo/lib/widgets/sections/outcomes_section.dart index bfa08ac3..961b61ab 100644 --- a/examples/demo/lib/widgets/sections/outcomes_section.dart +++ b/examples/demo/lib/widgets/sections/outcomes_section.dart @@ -17,6 +17,7 @@ class OutcomesSection extends StatelessWidget { return SectionCard( title: 'Outcome Events', + sectionKey: 'outcomes', onInfoTap: onInfoTap, child: PrimaryButton( label: 'SEND OUTCOME', diff --git a/examples/demo/lib/widgets/sections/send_iam_section.dart b/examples/demo/lib/widgets/sections/send_iam_section.dart index 2473a673..9e67a9ea 100644 --- a/examples/demo/lib/widgets/sections/send_iam_section.dart +++ b/examples/demo/lib/widgets/sections/send_iam_section.dart @@ -17,6 +17,7 @@ class SendIamSection extends StatelessWidget { return SectionCard( title: 'Send In-App Message', + sectionKey: 'send_iam', onInfoTap: onInfoTap, child: Column( spacing: AppSpacing.gap, diff --git a/examples/demo/lib/widgets/sections/send_push_section.dart b/examples/demo/lib/widgets/sections/send_push_section.dart index a3d8fce0..3ea7a965 100644 --- a/examples/demo/lib/widgets/sections/send_push_section.dart +++ b/examples/demo/lib/widgets/sections/send_push_section.dart @@ -19,6 +19,7 @@ class SendPushSection extends StatelessWidget { return SectionCard( title: 'Send Push Notification', + sectionKey: 'send_push', onInfoTap: onInfoTap, child: Column( children: [ diff --git a/examples/demo/lib/widgets/sections/sms_section.dart b/examples/demo/lib/widgets/sections/sms_section.dart index 1a0d690e..12b3ee01 100644 --- a/examples/demo/lib/widgets/sections/sms_section.dart +++ b/examples/demo/lib/widgets/sections/sms_section.dart @@ -19,6 +19,7 @@ class SmsSection extends StatelessWidget { return SectionCard( title: 'SMS', + sectionKey: 'sms', onInfoTap: onInfoTap, child: Column( children: [ diff --git a/examples/demo/lib/widgets/sections/tags_section.dart b/examples/demo/lib/widgets/sections/tags_section.dart index 22a263cf..a1431f36 100644 --- a/examples/demo/lib/widgets/sections/tags_section.dart +++ b/examples/demo/lib/widgets/sections/tags_section.dart @@ -19,6 +19,7 @@ class TagsSection extends StatelessWidget { return SectionCard( title: 'Tags', + sectionKey: 'tags', onInfoTap: onInfoTap, child: Column( children: [ diff --git a/examples/demo/lib/widgets/sections/track_event_section.dart b/examples/demo/lib/widgets/sections/track_event_section.dart index 0b53962e..3a296fec 100644 --- a/examples/demo/lib/widgets/sections/track_event_section.dart +++ b/examples/demo/lib/widgets/sections/track_event_section.dart @@ -17,6 +17,7 @@ class TrackEventSection extends StatelessWidget { return SectionCard( title: 'Track Event', + sectionKey: 'track_event', onInfoTap: onInfoTap, child: PrimaryButton( label: 'TRACK EVENT', diff --git a/examples/demo/lib/widgets/sections/triggers_section.dart b/examples/demo/lib/widgets/sections/triggers_section.dart index 0ce8c5aa..fd3aaaf4 100644 --- a/examples/demo/lib/widgets/sections/triggers_section.dart +++ b/examples/demo/lib/widgets/sections/triggers_section.dart @@ -19,6 +19,7 @@ class TriggersSection extends StatelessWidget { return SectionCard( title: 'Triggers', + sectionKey: 'triggers', onInfoTap: onInfoTap, child: Column( children: [ From e6b9c624229f0891595dc24eefb8d212b2ed73e0 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 10 Apr 2026 20:26:59 -0700 Subject: [PATCH 09/31] refactor(demo): replace custom Row with SwitchListTile --- examples/demo/lib/widgets/toggle_row.dart | 46 ++++++++++------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/examples/demo/lib/widgets/toggle_row.dart b/examples/demo/lib/widgets/toggle_row.dart index 2450ca78..53cfa7c5 100644 --- a/examples/demo/lib/widgets/toggle_row.dart +++ b/examples/demo/lib/widgets/toggle_row.dart @@ -20,33 +20,27 @@ class ToggleRow extends StatelessWidget { @override Widget build(BuildContext context) { - Widget toggle = Switch( - value: value, - onChanged: onChanged, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - ); - if (semanticsLabel != null) { - toggle = Semantics(identifier: semanticsLabel, container: true, child: toggle); - } - return Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: Theme.of(context).textTheme.bodyMedium), - if (description != null) - Text( - description!, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: AppColors.osGrey600, - ), - ), - ], - ), + return Theme( + data: Theme.of(context).copyWith( + listTileTheme: const ListTileThemeData( + minVerticalPadding: 0, ), - toggle, - ], + ), + child: SwitchListTile( + title: Text(label, style: Theme.of(context).textTheme.bodyMedium), + subtitle: description != null + ? Text( + description!, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: AppColors.osGrey600, + ), + ) + : null, + value: value, + onChanged: onChanged != null ? (v) => onChanged!(v) : null, + contentPadding: EdgeInsets.zero, + dense: true, + ), ); } } From b4710c7b9cd0384480cee6170718bcb94f2dc5ec Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 13 Apr 2026 10:33:10 -0700 Subject: [PATCH 10/31] test(demo): add semantics and improve button labels --- .../repositories/onesignal_repository.dart | 28 +-------------- examples/demo/lib/widgets/dialogs.dart | 28 +++++++++------ examples/demo/lib/widgets/list_widgets.dart | 10 ++++-- .../lib/widgets/sections/aliases_section.dart | 7 ++-- .../lib/widgets/sections/tags_section.dart | 4 +-- .../widgets/sections/triggers_section.dart | 4 +-- examples/demo/lib/widgets/toggle_row.dart | 36 ++++++++----------- 7 files changed, 50 insertions(+), 67 deletions(-) diff --git a/examples/demo/lib/repositories/onesignal_repository.dart b/examples/demo/lib/repositories/onesignal_repository.dart index 75ca90da..21d9a567 100644 --- a/examples/demo/lib/repositories/onesignal_repository.dart +++ b/examples/demo/lib/repositories/onesignal_repository.dart @@ -12,66 +12,54 @@ class OneSignalRepository { // User operations Future loginUser(String externalUserId) async { - LogManager().i('SDK', 'Login user: $externalUserId'); await OneSignal.login(externalUserId); } Future logoutUser() async { - LogManager().i('SDK', 'Logout user'); await OneSignal.logout(); } // Alias operations void addAlias(String label, String id) { - LogManager().i('SDK', 'Add alias: $label = $id'); OneSignal.User.addAlias(label, id); } void addAliases(Map aliases) { - LogManager().i('SDK', 'Add aliases: $aliases'); OneSignal.User.addAliases(aliases); } // Email operations void addEmail(String email) { - LogManager().i('SDK', 'Add email: $email'); OneSignal.User.addEmail(email); } void removeEmail(String email) { - LogManager().i('SDK', 'Remove email: $email'); OneSignal.User.removeEmail(email); } // SMS operations void addSms(String smsNumber) { - LogManager().i('SDK', 'Add SMS: $smsNumber'); OneSignal.User.addSms(smsNumber); } void removeSms(String smsNumber) { - LogManager().i('SDK', 'Remove SMS: $smsNumber'); OneSignal.User.removeSms(smsNumber); } // Tag operations void addTag(String key, String value) { - LogManager().i('SDK', 'Add tag: $key = $value'); OneSignal.User.addTagWithKey(key, value); } void addTags(Map tags) { - LogManager().i('SDK', 'Add tags: $tags'); OneSignal.User.addTags(tags); } void removeTag(String key) { - LogManager().i('SDK', 'Remove tag: $key'); OneSignal.User.removeTag(key); } void removeTags(List keys) { - LogManager().i('SDK', 'Remove tags: $keys'); OneSignal.User.removeTags(keys); } @@ -81,49 +69,40 @@ class OneSignalRepository { // Trigger operations void addTrigger(String key, String value) { - LogManager().i('SDK', 'Add trigger: $key = $value'); OneSignal.InAppMessages.addTrigger(key, value); } void addTriggers(Map triggers) { - LogManager().i('SDK', 'Add triggers: $triggers'); OneSignal.InAppMessages.addTriggers(triggers); } void removeTrigger(String key) { - LogManager().i('SDK', 'Remove trigger: $key'); OneSignal.InAppMessages.removeTrigger(key); } void removeTriggers(List keys) { - LogManager().i('SDK', 'Remove triggers: $keys'); OneSignal.InAppMessages.removeTriggers(keys); } void clearTriggers() { - LogManager().i('SDK', 'Clear all triggers'); OneSignal.InAppMessages.clearTriggers(); } // Outcome operations void sendOutcome(String name) { - LogManager().i('SDK', 'Send outcome: $name'); OneSignal.Session.addOutcome(name); } void sendUniqueOutcome(String name) { - LogManager().i('SDK', 'Send unique outcome: $name'); OneSignal.Session.addUniqueOutcome(name); } void sendOutcomeWithValue(String name, double value) { - LogManager().i('SDK', 'Send outcome with value: $name = $value'); OneSignal.Session.addOutcomeWithValue(name, value); } // Track event void trackEvent(String name, Map? properties) { - LogManager().i('SDK', 'Track event: $name, properties: $properties'); OneSignal.User.trackEvent(name, properties); } @@ -133,12 +112,10 @@ class OneSignalRepository { bool? isPushOptedIn() => OneSignal.User.pushSubscription.optedIn; void optInPush() { - LogManager().i('SDK', 'Opt in push'); OneSignal.User.pushSubscription.optIn(); } void optOutPush() { - LogManager().i('SDK', 'Opt out push'); OneSignal.User.pushSubscription.optOut(); } @@ -151,7 +128,6 @@ class OneSignalRepository { } void clearAllNotifications() { - LogManager().i('SDK', 'Clear all notifications'); OneSignal.Notifications.clearAll(); } @@ -167,7 +143,6 @@ class OneSignalRepository { // Location void setLocationShared(bool shared) { - LogManager().i('SDK', 'Set location shared: $shared'); OneSignal.Location.setShared(shared); } @@ -188,12 +163,11 @@ class OneSignalRepository { Map attributes, Map content, ) async { - LogManager().i('SDK', 'Start default live activity: $activityId'); await OneSignal.LiveActivities.startDefault(activityId, attributes, content); } Future exitLiveActivity(String activityId) async { - LogManager().i('SDK', 'Exit live activity: $activityId'); + // ignore: deprecated_member_use await OneSignal.LiveActivities.exitLiveActivity(activityId); } diff --git a/examples/demo/lib/widgets/dialogs.dart b/examples/demo/lib/widgets/dialogs.dart index 5d187b32..0e7fe0e2 100644 --- a/examples/demo/lib/widgets/dialogs.dart +++ b/examples/demo/lib/widgets/dialogs.dart @@ -245,21 +245,29 @@ class _MultiPairInputDialogState extends State { Row( children: [ Expanded( - child: AppTextField( - controller: _keyControllers[i], - decoration: InputDecoration( - labelText: widget.keyLabel, - isDense: true, + child: Semantics( + identifier: '${widget.keyLabel}_input_$i', + container: true, + child: AppTextField( + controller: _keyControllers[i], + decoration: InputDecoration( + labelText: widget.keyLabel, + isDense: true, + ), ), ), ), const SizedBox(width: 8), Expanded( - child: AppTextField( - controller: _valueControllers[i], - decoration: InputDecoration( - labelText: widget.valueLabel, - isDense: true, + child: Semantics( + identifier: '${widget.valueLabel}_input_$i', + container: true, + child: AppTextField( + controller: _valueControllers[i], + decoration: InputDecoration( + labelText: widget.valueLabel, + isDense: true, + ), ), ), ), diff --git a/examples/demo/lib/widgets/list_widgets.dart b/examples/demo/lib/widgets/list_widgets.dart index 7b9fbeca..35641190 100644 --- a/examples/demo/lib/widgets/list_widgets.dart +++ b/examples/demo/lib/widgets/list_widgets.dart @@ -66,9 +66,13 @@ class SingleItem extends StatelessWidget { children: [ Expanded(child: Text(text, style: Theme.of(context).textTheme.bodyMedium)), if (onDelete != null) - GestureDetector( - onTap: onDelete, - child: Icon(Icons.close, size: 18, color: AppColors.osPrimary), + Semantics( + identifier: 'remove_$text', + container: true, + child: GestureDetector( + onTap: onDelete, + child: Icon(Icons.close, size: 18, color: AppColors.osPrimary), + ), ), ], ), diff --git a/examples/demo/lib/widgets/sections/aliases_section.dart b/examples/demo/lib/widgets/sections/aliases_section.dart index a8180d70..00fdb61d 100644 --- a/examples/demo/lib/widgets/sections/aliases_section.dart +++ b/examples/demo/lib/widgets/sections/aliases_section.dart @@ -35,7 +35,7 @@ class AliasesSection extends StatelessWidget { ), AppSpacing.gapBox, PrimaryButton( - label: 'ADD', + label: 'ADD ALIAS', onPressed: () async { final result = await showDialog>( context: context, @@ -43,6 +43,9 @@ class AliasesSection extends StatelessWidget { title: 'Add Alias', keyLabel: 'Label', valueLabel: 'ID', + keySemanticsLabel: 'alias_label_input', + valueSemanticsLabel: 'alias_id_input', + confirmSemanticsLabel: 'alias_confirm_button', ), ); if (result != null) { @@ -52,7 +55,7 @@ class AliasesSection extends StatelessWidget { ), AppSpacing.gapBox, PrimaryButton( - label: 'ADD MULTIPLE', + label: 'ADD MULTIPLE ALIASES', onPressed: () async { final result = await showDialog>( context: context, diff --git a/examples/demo/lib/widgets/sections/tags_section.dart b/examples/demo/lib/widgets/sections/tags_section.dart index a1431f36..c87ad7a8 100644 --- a/examples/demo/lib/widgets/sections/tags_section.dart +++ b/examples/demo/lib/widgets/sections/tags_section.dart @@ -36,7 +36,7 @@ class TagsSection extends StatelessWidget { ), AppSpacing.gapBox, PrimaryButton( - label: 'ADD', + label: 'ADD TAG', semanticsLabel: 'add_tag_button', onPressed: () async { final result = await showDialog>( @@ -55,7 +55,7 @@ class TagsSection extends StatelessWidget { ), AppSpacing.gapBox, PrimaryButton( - label: 'ADD MULTIPLE', + label: 'ADD MULTIPLE TAGS', onPressed: () async { final result = await showDialog>( context: context, diff --git a/examples/demo/lib/widgets/sections/triggers_section.dart b/examples/demo/lib/widgets/sections/triggers_section.dart index fd3aaaf4..5035651e 100644 --- a/examples/demo/lib/widgets/sections/triggers_section.dart +++ b/examples/demo/lib/widgets/sections/triggers_section.dart @@ -36,7 +36,7 @@ class TriggersSection extends StatelessWidget { ), AppSpacing.gapBox, PrimaryButton( - label: 'ADD', + label: 'ADD TRIGGER', onPressed: () async { final result = await showDialog>( context: context, @@ -49,7 +49,7 @@ class TriggersSection extends StatelessWidget { ), AppSpacing.gapBox, PrimaryButton( - label: 'ADD MULTIPLE', + label: 'ADD MULTIPLE TRIGGERS', onPressed: () async { final result = await showDialog>( context: context, diff --git a/examples/demo/lib/widgets/toggle_row.dart b/examples/demo/lib/widgets/toggle_row.dart index 53cfa7c5..61ec9ad2 100644 --- a/examples/demo/lib/widgets/toggle_row.dart +++ b/examples/demo/lib/widgets/toggle_row.dart @@ -20,27 +20,21 @@ class ToggleRow extends StatelessWidget { @override Widget build(BuildContext context) { - return Theme( - data: Theme.of(context).copyWith( - listTileTheme: const ListTileThemeData( - minVerticalPadding: 0, - ), - ), - child: SwitchListTile( - title: Text(label, style: Theme.of(context).textTheme.bodyMedium), - subtitle: description != null - ? Text( - description!, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: AppColors.osGrey600, - ), - ) - : null, - value: value, - onChanged: onChanged != null ? (v) => onChanged!(v) : null, - contentPadding: EdgeInsets.zero, - dense: true, - ), + return SwitchListTile( + title: Text(label, style: Theme.of(context).textTheme.bodyMedium), + subtitle: description != null + ? Text( + description!, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: AppColors.osGrey600, + ), + ) + : null, + value: value, + onChanged: onChanged != null ? (v) => onChanged!(v) : null, + contentPadding: EdgeInsets.zero, + dense: true, + visualDensity: VisualDensity.compact, ); } } From bbdf209a58f633f17442cff378885895548d5cc6 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 13 Apr 2026 10:47:55 -0700 Subject: [PATCH 11/31] refactor(demo): remove tag parameter from LogManager --- examples/demo/lib/main.dart | 23 +++-- .../repositories/onesignal_repository.dart | 14 +-- examples/demo/lib/services/log_manager.dart | 15 ++-- .../lib/services/onesignal_api_service.dart | 24 ++---- .../demo/lib/services/tooltip_helper.dart | 4 +- .../demo/lib/viewmodels/app_viewmodel.dart | 85 +++++++++---------- examples/demo/lib/widgets/log_view.dart | 2 +- 7 files changed, 76 insertions(+), 91 deletions(-) diff --git a/examples/demo/lib/main.dart b/examples/demo/lib/main.dart index 00fb4cc0..2446fce3 100644 --- a/examples/demo/lib/main.dart +++ b/examples/demo/lib/main.dart @@ -21,7 +21,7 @@ Future main() async { try { await dotenv.load(fileName: '.env'); } catch (_) { - LogManager().w('App', '.env file not found, using defaults'); + LogManager().w('.env file not found, using defaults'); } final prefs = PreferencesService(); @@ -48,30 +48,27 @@ Future main() async { // Register IAM listeners OneSignal.InAppMessages.addWillDisplayListener((event) { - LogManager().i('IAM', 'Will display: ${event.message.messageId}'); + LogManager().i('IAM will display: ${event.message.messageId}'); }); OneSignal.InAppMessages.addDidDisplayListener((event) { - LogManager().i('IAM', 'Did display: ${event.message.messageId}'); + LogManager().i('IAM did display: ${event.message.messageId}'); }); OneSignal.InAppMessages.addWillDismissListener((event) { - LogManager().i('IAM', 'Will dismiss: ${event.message.messageId}'); + LogManager().i('IAM will dismiss: ${event.message.messageId}'); }); OneSignal.InAppMessages.addDidDismissListener((event) { - LogManager().i('IAM', 'Did dismiss: ${event.message.messageId}'); + LogManager().i('IAM did dismiss: ${event.message.messageId}'); }); OneSignal.InAppMessages.addClickListener((event) { - LogManager().i('IAM', 'Clicked: ${event.result.actionId}'); + LogManager().i('IAM clicked: ${event.result.actionId}'); }); // Register notification listeners OneSignal.Notifications.addClickListener((event) { - LogManager().i('Notification', 'Clicked: ${event.notification.title}'); + LogManager().i('Notification clicked: ${event.notification.title}'); }); OneSignal.Notifications.addForegroundWillDisplayListener((event) { - LogManager().i( - 'Notification', - 'Foreground will display: ${event.notification.title}', - ); + LogManager().i('Notification foreground will display: ${event.notification.title}'); event.notification.display(); }); @@ -80,7 +77,7 @@ Future main() async { try { apiKey = dotenv.env['ONESIGNAL_API_KEY'] ?? ''; } catch (_) { - LogManager().w('App', 'API key not found, continuing without it'); + LogManager().w('API key not found, continuing without it'); } final apiService = OneSignalApiService() ..setAppId(appId) @@ -90,7 +87,7 @@ Future main() async { // Fetch tooltips in background TooltipHelper().init(); - LogManager().i('App', 'OneSignal initialized with app ID: $appId'); + LogManager().i('OneSignal initialized with app ID: $appId'); runApp( ChangeNotifierProvider( diff --git a/examples/demo/lib/repositories/onesignal_repository.dart b/examples/demo/lib/repositories/onesignal_repository.dart index 21d9a567..725166f6 100644 --- a/examples/demo/lib/repositories/onesignal_repository.dart +++ b/examples/demo/lib/repositories/onesignal_repository.dart @@ -123,7 +123,7 @@ class OneSignalRepository { bool hasPermission() => OneSignal.Notifications.permission; Future requestPermission(bool fallbackToSettings) async { - LogManager().i('SDK', 'Request permission (fallback: $fallbackToSettings)'); + LogManager().i('Request permission (fallback: $fallbackToSettings)'); return await OneSignal.Notifications.requestPermission(fallbackToSettings); } @@ -133,7 +133,7 @@ class OneSignalRepository { // In-app messages void setInAppMessagesPaused(bool paused) { - LogManager().i('SDK', 'Set IAM paused: $paused'); + LogManager().i('Set IAM paused: $paused'); OneSignal.InAppMessages.paused(paused); } @@ -151,7 +151,7 @@ class OneSignalRepository { } void requestLocationPermission() { - LogManager().i('SDK', 'Request location permission'); + LogManager().i('Request location permission'); OneSignal.Location.requestPermission(); } @@ -184,12 +184,12 @@ class OneSignalRepository { // Privacy consent void setConsentRequired(bool required) { - LogManager().i('SDK', 'Set consent required: $required'); + LogManager().i('Set consent required: $required'); OneSignal.consentRequired(required); } void setConsentGiven(bool granted) { - LogManager().i('SDK', 'Set consent given: $granted'); + LogManager().i('Set consent given: $granted'); OneSignal.consentGiven(granted); } @@ -206,7 +206,7 @@ class OneSignalRepository { Future sendNotification(NotificationType type) async { final subscriptionId = getPushSubscriptionId(); if (subscriptionId == null) { - LogManager().w('SDK', 'No subscription ID for notification'); + LogManager().w('No subscription ID for notification'); return false; } return _apiService.sendNotification(type, subscriptionId); @@ -215,7 +215,7 @@ class OneSignalRepository { Future sendCustomNotification(String title, String body) async { final subscriptionId = getPushSubscriptionId(); if (subscriptionId == null) { - LogManager().w('SDK', 'No subscription ID for custom notification'); + LogManager().w('No subscription ID for custom notification'); return false; } return _apiService.sendCustomNotification(title, body, subscriptionId); diff --git a/examples/demo/lib/services/log_manager.dart b/examples/demo/lib/services/log_manager.dart index 3e66c7c8..b9a6c8e5 100644 --- a/examples/demo/lib/services/log_manager.dart +++ b/examples/demo/lib/services/log_manager.dart @@ -5,13 +5,11 @@ enum LogLevel { debug, info, warn, error } class LogEntry { final DateTime timestamp; final LogLevel level; - final String tag; final String message; const LogEntry({ required this.timestamp, required this.level, - required this.tag, required this.message, }); @@ -45,22 +43,21 @@ class LogManager extends ChangeNotifier { List get logs => List.unmodifiable(_logs); - void _log(LogLevel level, String tag, String message) { + void _log(LogLevel level, String message) { final entry = LogEntry( timestamp: DateTime.now(), level: level, - tag: tag, message: message, ); _logs.add(entry); - debugPrint('[${entry.levelLabel}] $tag: $message'); + debugPrint('[${entry.levelLabel}] $message'); notifyListeners(); } - void d(String tag, String message) => _log(LogLevel.debug, tag, message); - void i(String tag, String message) => _log(LogLevel.info, tag, message); - void w(String tag, String message) => _log(LogLevel.warn, tag, message); - void e(String tag, String message) => _log(LogLevel.error, tag, message); + void d(String message) => _log(LogLevel.debug, message); + void i(String message) => _log(LogLevel.info, message); + void w(String message) => _log(LogLevel.warn, message); + void e(String message) => _log(LogLevel.error, message); void clear() { _logs.clear(); diff --git a/examples/demo/lib/services/onesignal_api_service.dart b/examples/demo/lib/services/onesignal_api_service.dart index bfc8887f..89561434 100644 --- a/examples/demo/lib/services/onesignal_api_service.dart +++ b/examples/demo/lib/services/onesignal_api_service.dart @@ -49,13 +49,10 @@ class OneSignalApiService { body: jsonEncode(body), ); - LogManager().i( - 'API', - 'Send notification response: ${response.statusCode}', - ); + LogManager().i('Send notification response: ${response.statusCode}'); return response.statusCode == 200; } catch (e) { - LogManager().e('API', 'Send notification error: $e'); + LogManager().e('Send notification error: $e'); return false; } } @@ -82,13 +79,10 @@ class OneSignalApiService { body: jsonEncode(payload), ); - LogManager().i( - 'API', - 'Send custom notification response: ${response.statusCode}', - ); + LogManager().i('Send custom notification response: ${response.statusCode}'); return response.statusCode == 200; } catch (e) { - LogManager().e('API', 'Send custom notification error: $e'); + LogManager().e('Send custom notification error: $e'); return false; } } @@ -115,12 +109,11 @@ class OneSignalApiService { ); LogManager().i( - 'API', 'Update live activity response: ${response.statusCode} ${response.body}', ); return response.statusCode >= 200 && response.statusCode < 300; } catch (e) { - LogManager().e('API', 'Update live activity error: $e'); + LogManager().e('Update live activity error: $e'); return false; } } @@ -147,12 +140,11 @@ class OneSignalApiService { ); LogManager().i( - 'API', 'End live activity response: ${response.statusCode} ${response.body}', ); return response.statusCode >= 200 && response.statusCode < 300; } catch (e) { - LogManager().e('API', 'End live activity error: $e'); + LogManager().e('End live activity error: $e'); return false; } } @@ -170,10 +162,10 @@ class OneSignalApiService { final json = jsonDecode(response.body) as Map; return UserData.fromJson(json); } - LogManager().w('API', 'Fetch user returned ${response.statusCode}'); + LogManager().w('Fetch user returned ${response.statusCode}'); return null; } catch (e) { - LogManager().e('API', 'Fetch user error: $e'); + LogManager().e('Fetch user error: $e'); return null; } } diff --git a/examples/demo/lib/services/tooltip_helper.dart b/examples/demo/lib/services/tooltip_helper.dart index ab05a96f..4a078e2b 100644 --- a/examples/demo/lib/services/tooltip_helper.dart +++ b/examples/demo/lib/services/tooltip_helper.dart @@ -62,10 +62,10 @@ class TooltipHelper { ), ); }); - LogManager().i('Tooltip', 'Loaded ${_tooltips.length} tooltips'); + LogManager().i('Loaded ${_tooltips.length} tooltips'); } } catch (e) { - LogManager().w('Tooltip', 'Failed to load tooltips: $e'); + LogManager().w('Failed to load tooltips: $e'); } _initialized = true; diff --git a/examples/demo/lib/viewmodels/app_viewmodel.dart b/examples/demo/lib/viewmodels/app_viewmodel.dart index 6dd08e06..e34e6d05 100644 --- a/examples/demo/lib/viewmodels/app_viewmodel.dart +++ b/examples/demo/lib/viewmodels/app_viewmodel.dart @@ -141,7 +141,7 @@ class AppViewModel extends ChangeNotifier { } catch (e) { _isLoading = false; notifyListeners(); - LogManager().e('App', 'Error fetching initial user data: $e'); + LogManager().e('Error fetching initial user data: $e'); } } @@ -150,19 +150,19 @@ class AppViewModel extends ChangeNotifier { OneSignal.User.pushSubscription.addObserver((state) { _pushSubscriptionId = state.current.id; _pushEnabled = state.current.optedIn; - LogManager().i('Observer', + LogManager().i( 'Push subscription changed: id=${state.current.id}, optedIn=${state.current.optedIn}'); notifyListeners(); }); OneSignal.Notifications.addPermissionObserver((permission) { _hasNotificationPermission = permission; - LogManager().i('Observer', 'Permission changed: $permission'); + LogManager().i('Permission changed: $permission'); notifyListeners(); }); OneSignal.User.addObserver((state) { - LogManager().i('Observer', 'User state changed'); + LogManager().i('User state changed'); fetchUserDataFromApi(); }); } @@ -188,7 +188,7 @@ class AppViewModel extends ChangeNotifier { notifyListeners(); } } catch (e) { - LogManager().e('App', 'Error fetching user data: $e'); + LogManager().e('Error fetching user data: $e'); } } @@ -210,12 +210,11 @@ class AppViewModel extends ChangeNotifier { _isLoading = false; notifyListeners(); - LogManager().i('App', 'Logged in as: $externalUserId'); + LogManager().i('Logged in as: $externalUserId'); } catch (e) { _isLoading = false; notifyListeners(); - LogManager().e('App', 'Login error: $e'); - LogManager().e('App', 'Login failed'); + LogManager().e('Login error: $e'); } } @@ -235,11 +234,11 @@ class AppViewModel extends ChangeNotifier { _isLoading = false; notifyListeners(); - LogManager().i('App', 'Logged out'); + LogManager().i('Logged out'); } catch (e) { _isLoading = false; notifyListeners(); - LogManager().e('App', 'Logout error: $e'); + LogManager().e('Logout error: $e'); } } @@ -271,7 +270,7 @@ class AppViewModel extends ChangeNotifier { } _pushEnabled = enabled; notifyListeners(); - LogManager().i('App', 'Push ${enabled ? "enabled" : "disabled"}'); + LogManager().i('Push ${enabled ? "enabled" : "disabled"}'); } Future promptPush() async { @@ -284,24 +283,24 @@ class AppViewModel extends ChangeNotifier { Future sendNotification(NotificationType type) async { final success = await _repository.sendNotification(type); if (success) { - LogManager().i('App', 'Notification sent: ${type.name}'); + LogManager().i('Notification sent: ${type.name}'); } else { - LogManager().e('App', 'Failed to send notification'); + LogManager().e('Failed to send notification'); } } Future sendCustomNotification(String title, String body) async { final success = await _repository.sendCustomNotification(title, body); if (success) { - LogManager().i('App', 'Custom notification sent'); + LogManager().i('Custom notification sent'); } else { - LogManager().e('App', 'Failed to send notification'); + LogManager().e('Failed to send notification'); } } void clearAllNotifications() { _repository.clearAllNotifications(); - LogManager().i('App', 'All notifications cleared'); + LogManager().i('All notifications cleared'); } // IAM @@ -318,7 +317,7 @@ class AppViewModel extends ChangeNotifier { ..removeWhere((e) => e.key == 'iam_type') ..add(MapEntry('iam_type', type.triggerValue)); notifyListeners(); - LogManager().i('App', 'Sent In-App Message: ${type.label}'); + LogManager().i('Sent In-App Message: ${type.label}'); } // Aliases @@ -326,14 +325,14 @@ class AppViewModel extends ChangeNotifier { _repository.addAlias(label, id); _aliasesList = List.from(_aliasesList)..add(MapEntry(label, id)); notifyListeners(); - LogManager().i('App', 'Alias added: $label'); + LogManager().i('Alias added: $label'); } void addAliases(Map aliases) { _repository.addAliases(aliases); _aliasesList = List.from(_aliasesList)..addAll(aliases.entries); notifyListeners(); - LogManager().i('App', '${aliases.length} alias(es) added'); + LogManager().i('${aliases.length} alias(es) added'); } // Emails @@ -341,14 +340,14 @@ class AppViewModel extends ChangeNotifier { _repository.addEmail(email); _emailsList = List.from(_emailsList)..add(email); notifyListeners(); - LogManager().i('App', 'Email added: $email'); + LogManager().i('Email added: $email'); } void removeEmail(String email) { _repository.removeEmail(email); _emailsList = List.from(_emailsList)..remove(email); notifyListeners(); - LogManager().i('App', 'Email removed: $email'); + LogManager().i('Email removed: $email'); } // SMS @@ -356,14 +355,14 @@ class AppViewModel extends ChangeNotifier { _repository.addSms(smsNumber); _smsNumbersList = List.from(_smsNumbersList)..add(smsNumber); notifyListeners(); - LogManager().i('App', 'SMS added: $smsNumber'); + LogManager().i('SMS added: $smsNumber'); } void removeSms(String smsNumber) { _repository.removeSms(smsNumber); _smsNumbersList = List.from(_smsNumbersList)..remove(smsNumber); notifyListeners(); - LogManager().i('App', 'SMS removed: $smsNumber'); + LogManager().i('SMS removed: $smsNumber'); } // Tags @@ -371,28 +370,28 @@ class AppViewModel extends ChangeNotifier { _repository.addTag(key, value); _tagsList = List.from(_tagsList)..add(MapEntry(key, value)); notifyListeners(); - LogManager().i('App', 'Tag added: $key'); + LogManager().i('Tag added: $key'); } void addTags(Map tags) { _repository.addTags(tags); _tagsList = List.from(_tagsList)..addAll(tags.entries); notifyListeners(); - LogManager().i('App', '${tags.length} tag(s) added'); + LogManager().i('${tags.length} tag(s) added'); } void removeTag(String key) { _repository.removeTag(key); _tagsList = List.from(_tagsList)..removeWhere((e) => e.key == key); notifyListeners(); - LogManager().i('App', 'Tag removed: $key'); + LogManager().i('Tag removed: $key'); } void removeSelectedTags(List keys) { _repository.removeTags(keys); _tagsList = List.from(_tagsList)..removeWhere((e) => keys.contains(e.key)); notifyListeners(); - LogManager().i('App', '${keys.length} tag(s) removed'); + LogManager().i('${keys.length} tag(s) removed'); } // Triggers (in-memory only) @@ -400,21 +399,21 @@ class AppViewModel extends ChangeNotifier { _repository.addTrigger(key, value); _triggersList = List.from(_triggersList)..add(MapEntry(key, value)); notifyListeners(); - LogManager().i('App', 'Trigger added: $key'); + LogManager().i('Trigger added: $key'); } void addTriggers(Map triggers) { _repository.addTriggers(triggers); _triggersList = List.from(_triggersList)..addAll(triggers.entries); notifyListeners(); - LogManager().i('App', '${triggers.length} trigger(s) added'); + LogManager().i('${triggers.length} trigger(s) added'); } void removeTrigger(String key) { _repository.removeTrigger(key); _triggersList = List.from(_triggersList)..removeWhere((e) => e.key == key); notifyListeners(); - LogManager().i('App', 'Trigger removed: $key'); + LogManager().i('Trigger removed: $key'); } void removeSelectedTriggers(List keys) { @@ -422,36 +421,36 @@ class AppViewModel extends ChangeNotifier { _triggersList = List.from(_triggersList) ..removeWhere((e) => keys.contains(e.key)); notifyListeners(); - LogManager().i('App', '${keys.length} trigger(s) removed'); + LogManager().i('${keys.length} trigger(s) removed'); } void clearAllTriggers() { _repository.clearTriggers(); _triggersList = []; notifyListeners(); - LogManager().i('App', 'All triggers cleared'); + LogManager().i('All triggers cleared'); } // Outcomes void sendOutcome(String name) { _repository.sendOutcome(name); - LogManager().i('App', 'Outcome sent: $name'); + LogManager().i('Outcome sent: $name'); } void sendUniqueOutcome(String name) { _repository.sendUniqueOutcome(name); - LogManager().i('App', 'Unique outcome sent: $name'); + LogManager().i('Unique outcome sent: $name'); } void sendOutcomeWithValue(String name, double value) { _repository.sendOutcomeWithValue(name, value); - LogManager().i('App', 'Outcome sent: $name = $value'); + LogManager().i('Outcome sent: $name = $value'); } // Track Event void trackEvent(String name, Map? properties) { _repository.trackEvent(name, properties); - LogManager().i('App', 'Event tracked: $name'); + LogManager().i('Event tracked: $name'); } // Live Activities @@ -471,7 +470,7 @@ class AppViewModel extends ChangeNotifier { _statusIndex = 0; await _repository.startDefaultLiveActivity(_activityId, attributes, content); notifyListeners(); - LogManager().i('App', 'Started Live Activity: $_activityId'); + LogManager().i('Started Live Activity: $_activityId'); } Future updateLiveActivity() async { @@ -486,25 +485,25 @@ class AppViewModel extends ChangeNotifier { _isLaUpdating = false; if (success) { _statusIndex = nextIndex; - LogManager().i('App', 'Updated Live Activity: $_activityId'); + LogManager().i('Updated Live Activity: $_activityId'); } else { - LogManager().e('App', 'Failed to update Live Activity'); + LogManager().e('Failed to update Live Activity'); } notifyListeners(); } Future exitLiveActivity() async { await _repository.exitLiveActivity(_activityId); - LogManager().i('App', 'Exited Live Activity: $_activityId'); + LogManager().i('Exited Live Activity: $_activityId'); } Future endLiveActivity() async { final success = await _repository.endLiveActivity(_activityId); if (success) { _statusIndex = 0; - LogManager().i('App', 'Ended Live Activity: $_activityId'); + LogManager().i('Ended Live Activity: $_activityId'); } else { - LogManager().e('App', 'Failed to end Live Activity'); + LogManager().e('Failed to end Live Activity'); } notifyListeners(); } @@ -515,7 +514,7 @@ class AppViewModel extends ChangeNotifier { _repository.setLocationShared(shared); await _prefs.setLocationShared(shared); notifyListeners(); - LogManager().i('App', 'Location sharing ${shared ? "enabled" : "disabled"}'); + LogManager().i('Location sharing ${shared ? "enabled" : "disabled"}'); } void promptLocation() { diff --git a/examples/demo/lib/widgets/log_view.dart b/examples/demo/lib/widgets/log_view.dart index 891eb977..137de6ab 100644 --- a/examples/demo/lib/widgets/log_view.dart +++ b/examples/demo/lib/widgets/log_view.dart @@ -143,7 +143,7 @@ class _LogViewState extends State { identifier: 'log_entry_${index}_message', container: true, child: Text( - '${entry.tag}: ${entry.message}', + entry.message, style: logEntryStyle?.copyWith( color: Colors.white, ), From 9aee160109a92ada5a1fc205323b2407033cde51 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 13 Apr 2026 11:25:21 -0700 Subject: [PATCH 12/31] test(demo): add semantics to delete button in PairItem --- examples/demo/lib/widgets/list_widgets.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/demo/lib/widgets/list_widgets.dart b/examples/demo/lib/widgets/list_widgets.dart index 35641190..297d5623 100644 --- a/examples/demo/lib/widgets/list_widgets.dart +++ b/examples/demo/lib/widgets/list_widgets.dart @@ -38,9 +38,13 @@ class PairItem extends StatelessWidget { ), ), if (onDelete != null) - GestureDetector( - onTap: onDelete, - child: Icon(Icons.close, size: 18, color: AppColors.osPrimary), + Semantics( + identifier: 'remove_$keyText', + container: true, + child: GestureDetector( + onTap: onDelete, + child: Icon(Icons.close, size: 18, color: AppColors.osPrimary), + ), ), ], ), From f2be61a451effc6c82e98a4344c3940944383313 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 13 Apr 2026 11:32:48 -0700 Subject: [PATCH 13/31] test(demo): add semantics to SectionCard widget --- examples/demo/lib/widgets/section_card.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/demo/lib/widgets/section_card.dart b/examples/demo/lib/widgets/section_card.dart index 452accaf..5e261f87 100644 --- a/examples/demo/lib/widgets/section_card.dart +++ b/examples/demo/lib/widgets/section_card.dart @@ -18,9 +18,12 @@ class SectionCard extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Column( + return Semantics( + identifier: sectionKey != null ? '${sectionKey}_section' : null, + container: true, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Section header (outside card, ALL CAPS like reference) @@ -68,6 +71,7 @@ class SectionCard extends StatelessWidget { child, ], ), + ), ); } } From 65981b50b518825474c1464c4aae1f446d08c2cf Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 13 Apr 2026 11:41:03 -0700 Subject: [PATCH 14/31] test(demo): add semantics to PairItem text widgets --- examples/demo/lib/widgets/list_widgets.dart | 24 ++++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/examples/demo/lib/widgets/list_widgets.dart b/examples/demo/lib/widgets/list_widgets.dart index 297d5623..920ea847 100644 --- a/examples/demo/lib/widgets/list_widgets.dart +++ b/examples/demo/lib/widgets/list_widgets.dart @@ -24,15 +24,23 @@ class PairItem extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - keyText, - style: Theme.of(context).textTheme.bodyMedium, + Semantics( + identifier: 'pair_key_$keyText', + container: true, + child: Text( + keyText, + style: Theme.of(context).textTheme.bodyMedium, + ), ), - Text( - valueText, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: AppColors.osGrey600, - ), + Semantics( + identifier: 'pair_value_$keyText', + container: true, + child: Text( + valueText, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: AppColors.osGrey600, + ), + ), ), ], ), From f9daaa95cda564a8cacdde04ccd2ecb301625066 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 13 Apr 2026 12:15:30 -0700 Subject: [PATCH 15/31] test(demo): add sectionKey to improve semantic identifiers --- examples/demo/lib/widgets/list_widgets.dart | 11 ++++++++--- .../demo/lib/widgets/sections/aliases_section.dart | 1 + examples/demo/lib/widgets/sections/tags_section.dart | 3 ++- .../demo/lib/widgets/sections/triggers_section.dart | 3 ++- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/examples/demo/lib/widgets/list_widgets.dart b/examples/demo/lib/widgets/list_widgets.dart index 920ea847..17ed4bab 100644 --- a/examples/demo/lib/widgets/list_widgets.dart +++ b/examples/demo/lib/widgets/list_widgets.dart @@ -3,12 +3,14 @@ import 'package:flutter/material.dart'; import '../theme.dart'; class PairItem extends StatelessWidget { + final String sectionKey; final String keyText; final String valueText; final VoidCallback? onDelete; const PairItem({ super.key, + required this.sectionKey, required this.keyText, required this.valueText, this.onDelete, @@ -25,7 +27,7 @@ class PairItem extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Semantics( - identifier: 'pair_key_$keyText', + identifier: '${sectionKey}_pair_key_$keyText', container: true, child: Text( keyText, @@ -33,7 +35,7 @@ class PairItem extends StatelessWidget { ), ), Semantics( - identifier: 'pair_value_$keyText', + identifier: '${sectionKey}_pair_value_$keyText', container: true, child: Text( valueText, @@ -47,7 +49,7 @@ class PairItem extends StatelessWidget { ), if (onDelete != null) Semantics( - identifier: 'remove_$keyText', + identifier: '${sectionKey}_remove_$keyText', container: true, child: GestureDetector( onTap: onDelete, @@ -114,12 +116,14 @@ class EmptyState extends StatelessWidget { } class PairList extends StatelessWidget { + final String sectionKey; final List> items; final String emptyText; final void Function(String key)? onDelete; const PairList({ super.key, + required this.sectionKey, required this.items, required this.emptyText, this.onDelete, @@ -134,6 +138,7 @@ class PairList extends StatelessWidget { for (var i = 0; i < items.length; i++) ...[ PairItem( key: ValueKey('${items[i].key}_${items[i].value}'), + sectionKey: sectionKey, keyText: items[i].key, valueText: items[i].value, onDelete: diff --git a/examples/demo/lib/widgets/sections/aliases_section.dart b/examples/demo/lib/widgets/sections/aliases_section.dart index 00fdb61d..48cba224 100644 --- a/examples/demo/lib/widgets/sections/aliases_section.dart +++ b/examples/demo/lib/widgets/sections/aliases_section.dart @@ -28,6 +28,7 @@ class AliasesSection extends StatelessWidget { child: Padding( padding: AppSpacing.cardPadding, child: PairList( + sectionKey: 'aliases', items: vm.aliasesList, emptyText: 'No aliases added', ), diff --git a/examples/demo/lib/widgets/sections/tags_section.dart b/examples/demo/lib/widgets/sections/tags_section.dart index c87ad7a8..a4eba82c 100644 --- a/examples/demo/lib/widgets/sections/tags_section.dart +++ b/examples/demo/lib/widgets/sections/tags_section.dart @@ -28,6 +28,7 @@ class TagsSection extends StatelessWidget { child: Padding( padding: AppSpacing.cardPadding, child: PairList( + sectionKey: 'tags', items: vm.tagsList, emptyText: 'No tags added', onDelete: vm.removeTag, @@ -70,7 +71,7 @@ class TagsSection extends StatelessWidget { if (vm.tagsList.isNotEmpty) ...[ AppSpacing.gapBox, DestructiveButton( - label: 'REMOVE SELECTED', + label: 'REMOVE TAGS', onPressed: () async { final result = await showDialog>( context: context, diff --git a/examples/demo/lib/widgets/sections/triggers_section.dart b/examples/demo/lib/widgets/sections/triggers_section.dart index 5035651e..95da5a56 100644 --- a/examples/demo/lib/widgets/sections/triggers_section.dart +++ b/examples/demo/lib/widgets/sections/triggers_section.dart @@ -28,6 +28,7 @@ class TriggersSection extends StatelessWidget { child: Padding( padding: AppSpacing.cardPadding, child: PairList( + sectionKey: 'triggers', items: vm.triggersList, emptyText: 'No triggers added', onDelete: vm.removeTrigger, @@ -65,7 +66,7 @@ class TriggersSection extends StatelessWidget { if (vm.triggersList.isNotEmpty) ...[ AppSpacing.gapBox, DestructiveButton( - label: 'REMOVE SELECTED', + label: 'REMOVE TRIGGERS', onPressed: () async { final result = await showDialog>( context: context, From b81921b8a99f99640f4fed645e456ea3c662b640 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 13 Apr 2026 17:22:13 -0700 Subject: [PATCH 16/31] test(demo): add semantics to remove dialog checkboxes --- examples/demo/lib/widgets/dialogs.dart | 32 +++++++++++++++----------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/examples/demo/lib/widgets/dialogs.dart b/examples/demo/lib/widgets/dialogs.dart index 0e7fe0e2..d561cd2c 100644 --- a/examples/demo/lib/widgets/dialogs.dart +++ b/examples/demo/lib/widgets/dialogs.dart @@ -341,20 +341,24 @@ class _MultiSelectRemoveDialogState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: widget.items.map((item) { - return CheckboxListTile( - title: Text(item.key), - value: _selected.contains(item.key), - controlAffinity: ListTileControlAffinity.leading, - onChanged: (checked) { - setState(() { - if (checked == true) { - _selected.add(item.key); - } else { - _selected.remove(item.key); - } - }); - }, - contentPadding: EdgeInsets.zero, + return Semantics( + identifier: 'remove_checkbox_${item.key}', + container: true, + child: CheckboxListTile( + title: Text(item.key), + value: _selected.contains(item.key), + controlAffinity: ListTileControlAffinity.leading, + onChanged: (checked) { + setState(() { + if (checked == true) { + _selected.add(item.key); + } else { + _selected.remove(item.key); + } + }); + }, + contentPadding: EdgeInsets.zero, + ), ); }).toList(), ), From 0bb596f034d8e60b5693a176fa47bbb3626be6b0 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 13 Apr 2026 19:26:06 -0700 Subject: [PATCH 17/31] test(demo): add semantics and rename sections for appium --- examples/demo/lib/screens/home_screen.dart | 4 +- .../demo/lib/viewmodels/app_viewmodel.dart | 2 +- examples/demo/lib/widgets/dialogs.dart | 124 +++++++++++------- .../lib/widgets/sections/app_section.dart | 5 +- ...ection.dart => custom_events_section.dart} | 8 +- .../widgets/sections/outcomes_section.dart | 2 +- .../lib/widgets/sections/push_section.dart | 5 +- .../lib/widgets/sections/user_section.dart | 1 + 8 files changed, 91 insertions(+), 60 deletions(-) rename examples/demo/lib/widgets/sections/{track_event_section.dart => custom_events_section.dart} (83%) diff --git a/examples/demo/lib/screens/home_screen.dart b/examples/demo/lib/screens/home_screen.dart index 8991dc26..aa490927 100644 --- a/examples/demo/lib/screens/home_screen.dart +++ b/examples/demo/lib/screens/home_screen.dart @@ -22,7 +22,7 @@ import '../widgets/sections/send_iam_section.dart'; import '../widgets/sections/send_push_section.dart'; import '../widgets/sections/sms_section.dart'; import '../widgets/sections/tags_section.dart'; -import '../widgets/sections/track_event_section.dart'; +import '../widgets/sections/custom_events_section.dart'; import '../widgets/sections/triggers_section.dart'; import 'secondary_screen.dart'; @@ -129,7 +129,7 @@ class _HomeScreenState extends State { onInfoTap: () => _showTooltipDialog(context, 'triggers'), ), - TrackEventSection( + CustomEventsSection( onInfoTap: () => _showTooltipDialog(context, 'trackEvent'), ), diff --git a/examples/demo/lib/viewmodels/app_viewmodel.dart b/examples/demo/lib/viewmodels/app_viewmodel.dart index e34e6d05..c237b36c 100644 --- a/examples/demo/lib/viewmodels/app_viewmodel.dart +++ b/examples/demo/lib/viewmodels/app_viewmodel.dart @@ -447,7 +447,7 @@ class AppViewModel extends ChangeNotifier { LogManager().i('Outcome sent: $name = $value'); } - // Track Event + // Custom Events void trackEvent(String name, Map? properties) { _repository.trackEvent(name, properties); LogManager().i('Event tracked: $name'); diff --git a/examples/demo/lib/widgets/dialogs.dart b/examples/demo/lib/widgets/dialogs.dart index d561cd2c..799700d7 100644 --- a/examples/demo/lib/widgets/dialogs.dart +++ b/examples/demo/lib/widgets/dialogs.dart @@ -499,19 +499,27 @@ class _OutcomeDialogState extends State { ), ), const SizedBox(height: 8), - AppTextField( - controller: _nameController, - decoration: const InputDecoration(labelText: 'Outcome Name'), - onChanged: (_) => setState(() {}), + Semantics( + identifier: 'outcome_name_input', + container: true, + child: AppTextField( + controller: _nameController, + decoration: const InputDecoration(labelText: 'Outcome Name'), + onChanged: (_) => setState(() {}), + ), ), if (_type == OutcomeType.withValue) ...[ const SizedBox(height: 12), - AppTextField( - controller: _valueController, - decoration: const InputDecoration(labelText: 'Value'), - keyboardType: - const TextInputType.numberWithOptions(decimal: true), - onChanged: (_) => setState(() {}), + Semantics( + identifier: 'outcome_value_input', + container: true, + child: AppTextField( + controller: _valueController, + decoration: const InputDecoration(labelText: 'Value'), + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + onChanged: (_) => setState(() {}), + ), ), ], ], @@ -523,19 +531,23 @@ class _OutcomeDialogState extends State { onPressed: () => Navigator.pop(context), child: const Text('Cancel'), ), - TextButton( - onPressed: _isValid - ? () { - Navigator.pop(context, { - 'type': _type, - 'name': _nameController.text, - 'value': _type == OutcomeType.withValue - ? double.parse(_valueController.text) - : null, - }); - } - : null, - child: const Text('Send'), + Semantics( + identifier: 'outcome_send_button', + container: true, + child: TextButton( + onPressed: _isValid + ? () { + Navigator.pop(context, { + 'type': _type, + 'name': _nameController.text, + 'value': _type == OutcomeType.withValue + ? double.parse(_valueController.text) + : null, + }); + } + : null, + child: const Text('Send'), + ), ), ], ); @@ -587,28 +599,36 @@ class _TrackEventDialogState extends State { Widget build(BuildContext context) { return AlertDialog( insetPadding: const EdgeInsets.symmetric(horizontal: 16), - title: const Text('Track Event'), + title: const Text('Custom Event'), content: SizedBox( width: double.maxFinite, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ - AppTextField( - controller: _nameController, - decoration: const InputDecoration(labelText: 'Event Name'), - onChanged: (_) => setState(() {}), + Semantics( + identifier: 'event_name_input', + container: true, + child: AppTextField( + controller: _nameController, + decoration: const InputDecoration(labelText: 'Event Name'), + onChanged: (_) => setState(() {}), + ), ), const SizedBox(height: 12), - AppTextField( - controller: _propsController, - decoration: InputDecoration( - labelText: 'Properties (optional, JSON)', - hintText: '{"key": "value"}', - errorText: _jsonError, + Semantics( + identifier: 'event_properties_input', + container: true, + child: AppTextField( + controller: _propsController, + decoration: InputDecoration( + labelText: 'Properties (optional, JSON)', + hintText: '{"key": "value"}', + errorText: _jsonError, + ), + maxLines: 3, + onChanged: _validateJson, ), - maxLines: 3, - onChanged: _validateJson, ), ], ), @@ -619,21 +639,25 @@ class _TrackEventDialogState extends State { onPressed: () => Navigator.pop(context), child: const Text('Cancel'), ), - TextButton( - onPressed: _isValid - ? () { - Map? props; - if (_propsController.text.isNotEmpty) { - props = jsonDecode(_propsController.text) - as Map; + Semantics( + identifier: 'event_track_button', + container: true, + child: TextButton( + onPressed: _isValid + ? () { + Map? props; + if (_propsController.text.isNotEmpty) { + props = jsonDecode(_propsController.text) + as Map; + } + Navigator.pop(context, { + 'name': _nameController.text, + 'properties': props, + }); } - Navigator.pop(context, { - 'name': _nameController.text, - 'properties': props, - }); - } - : null, - child: const Text('Track'), + : null, + child: const Text('Track'), + ), ), ], ); diff --git a/examples/demo/lib/widgets/sections/app_section.dart b/examples/demo/lib/widgets/sections/app_section.dart index 7aa07eb8..db5a40d9 100644 --- a/examples/demo/lib/widgets/sections/app_section.dart +++ b/examples/demo/lib/widgets/sections/app_section.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -7,6 +8,8 @@ import '../../viewmodels/app_viewmodel.dart'; import '../section_card.dart'; import '../toggle_row.dart'; +final bool _isE2E = dotenv.env['E2E_MODE'] == 'true'; + class AppSection extends StatelessWidget { final VoidCallback? onInfoTap; @@ -33,7 +36,7 @@ class AppSection extends StatelessWidget { const SizedBox(width: 12), Expanded( child: SelectableText( - vm.appId, + _isE2E ? '••••••••-••••-••••-••••-••••••••••••' : vm.appId, style: Theme.of(context).textTheme.bodySmall?.copyWith( fontFamily: 'monospace', ), diff --git a/examples/demo/lib/widgets/sections/track_event_section.dart b/examples/demo/lib/widgets/sections/custom_events_section.dart similarity index 83% rename from examples/demo/lib/widgets/sections/track_event_section.dart rename to examples/demo/lib/widgets/sections/custom_events_section.dart index 3a296fec..7ab94556 100644 --- a/examples/demo/lib/widgets/sections/track_event_section.dart +++ b/examples/demo/lib/widgets/sections/custom_events_section.dart @@ -6,18 +6,18 @@ import '../action_button.dart'; import '../dialogs.dart'; import '../section_card.dart'; -class TrackEventSection extends StatelessWidget { +class CustomEventsSection extends StatelessWidget { final VoidCallback? onInfoTap; - const TrackEventSection({super.key, this.onInfoTap}); + const CustomEventsSection({super.key, this.onInfoTap}); @override Widget build(BuildContext context) { final vm = context.read(); return SectionCard( - title: 'Track Event', - sectionKey: 'track_event', + title: 'Custom Events', + sectionKey: 'custom_events', onInfoTap: onInfoTap, child: PrimaryButton( label: 'TRACK EVENT', diff --git a/examples/demo/lib/widgets/sections/outcomes_section.dart b/examples/demo/lib/widgets/sections/outcomes_section.dart index 961b61ab..a9b0a6b5 100644 --- a/examples/demo/lib/widgets/sections/outcomes_section.dart +++ b/examples/demo/lib/widgets/sections/outcomes_section.dart @@ -16,7 +16,7 @@ class OutcomesSection extends StatelessWidget { final vm = context.read(); return SectionCard( - title: 'Outcome Events', + title: 'Outcomes', sectionKey: 'outcomes', onInfoTap: onInfoTap, child: PrimaryButton( diff --git a/examples/demo/lib/widgets/sections/push_section.dart b/examples/demo/lib/widgets/sections/push_section.dart index a02e2c9c..331f85b0 100644 --- a/examples/demo/lib/widgets/sections/push_section.dart +++ b/examples/demo/lib/widgets/sections/push_section.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:provider/provider.dart'; import '../../theme.dart'; @@ -7,6 +8,8 @@ import '../action_button.dart'; import '../section_card.dart'; import '../toggle_row.dart'; +final bool _isE2E = dotenv.env['E2E_MODE'] == 'true'; + class PushSection extends StatelessWidget { final VoidCallback? onInfoTap; @@ -40,7 +43,7 @@ class PushSection extends StatelessWidget { identifier: 'push_id_value', container: true, child: SelectableText( - vm.pushSubscriptionId ?? 'N/A', + _isE2E ? '••••••••-••••-••••-••••-••••••••••••' : (vm.pushSubscriptionId ?? 'N/A'), style: Theme.of(context).textTheme.bodySmall?.copyWith( fontFamily: 'monospace', ), diff --git a/examples/demo/lib/widgets/sections/user_section.dart b/examples/demo/lib/widgets/sections/user_section.dart index d5649ad5..3da75f21 100644 --- a/examples/demo/lib/widgets/sections/user_section.dart +++ b/examples/demo/lib/widgets/sections/user_section.dart @@ -18,6 +18,7 @@ class UserSection extends StatelessWidget { return SectionCard( title: 'User', + sectionKey: 'user', onInfoTap: onInfoTap, child: Column( crossAxisAlignment: CrossAxisAlignment.start, From 166ee428921b2a35d336f97284fa0031ee825a12 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 13 Apr 2026 19:54:15 -0700 Subject: [PATCH 18/31] test(demo): add semantic labels to dialog inputs --- examples/demo/lib/widgets/dialogs.dart | 2 +- examples/demo/lib/widgets/sections/tags_section.dart | 6 +++--- examples/demo/lib/widgets/sections/triggers_section.dart | 7 ++++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/demo/lib/widgets/dialogs.dart b/examples/demo/lib/widgets/dialogs.dart index 799700d7..bcf065b6 100644 --- a/examples/demo/lib/widgets/dialogs.dart +++ b/examples/demo/lib/widgets/dialogs.dart @@ -146,7 +146,7 @@ class _PairInputDialogState extends State { child: const Text('Cancel'), ), Semantics( - identifier: widget.confirmSemanticsLabel, + identifier: widget.confirmSemanticsLabel ?? 'confirm_button', container: true, child: TextButton( onPressed: _isValid diff --git a/examples/demo/lib/widgets/sections/tags_section.dart b/examples/demo/lib/widgets/sections/tags_section.dart index a4eba82c..9332ade1 100644 --- a/examples/demo/lib/widgets/sections/tags_section.dart +++ b/examples/demo/lib/widgets/sections/tags_section.dart @@ -44,9 +44,9 @@ class TagsSection extends StatelessWidget { context: context, builder: (_) => const PairInputDialog( title: 'Add Tag', - keySemanticsLabel: 'multi_pair_key_0', - valueSemanticsLabel: 'multi_pair_value_0', - confirmSemanticsLabel: 'multi_pair_confirm_button', + keySemanticsLabel: 'tag_key_input', + valueSemanticsLabel: 'tag_value_input', + confirmSemanticsLabel: 'tag_confirm_button', ), ); if (result != null) { diff --git a/examples/demo/lib/widgets/sections/triggers_section.dart b/examples/demo/lib/widgets/sections/triggers_section.dart index 95da5a56..2017072c 100644 --- a/examples/demo/lib/widgets/sections/triggers_section.dart +++ b/examples/demo/lib/widgets/sections/triggers_section.dart @@ -41,7 +41,12 @@ class TriggersSection extends StatelessWidget { onPressed: () async { final result = await showDialog>( context: context, - builder: (_) => const PairInputDialog(title: 'Add Trigger'), + builder: (_) => const PairInputDialog( + title: 'Add Trigger', + keySemanticsLabel: 'trigger_key_input', + valueSemanticsLabel: 'trigger_value_input', + confirmSemanticsLabel: 'trigger_confirm_button', + ), ); if (result != null) { vm.addTrigger(result.key, result.value); From e88c07b3cce64e03b4c55abd3aa1a09010b16ade Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 13 Apr 2026 20:52:57 -0700 Subject: [PATCH 19/31] test(demo): clarify clear button label for triggers --- examples/demo/lib/widgets/sections/triggers_section.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/demo/lib/widgets/sections/triggers_section.dart b/examples/demo/lib/widgets/sections/triggers_section.dart index 2017072c..2eee5fb2 100644 --- a/examples/demo/lib/widgets/sections/triggers_section.dart +++ b/examples/demo/lib/widgets/sections/triggers_section.dart @@ -87,7 +87,7 @@ class TriggersSection extends StatelessWidget { ), AppSpacing.gapBox, DestructiveButton( - label: 'CLEAR ALL', + label: 'CLEAR ALL TRIGGERS', onPressed: vm.clearAllTriggers, ), ], From 7be7dfc6b0be3b00e9b912b32d923ed348ac84ac Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 13 Apr 2026 20:53:01 -0700 Subject: [PATCH 20/31] refactor(demo): replace LogManager with debugPrint --- examples/build.md | 14 +- examples/demo/lib/main.dart | 23 +-- .../repositories/onesignal_repository.dart | 16 +- examples/demo/lib/screens/home_screen.dart | 152 ++++++++-------- examples/demo/lib/services/log_manager.dart | 66 ------- .../lib/services/onesignal_api_service.dart | 22 +-- .../demo/lib/services/tooltip_helper.dart | 7 +- .../demo/lib/viewmodels/app_viewmodel.dart | 91 +++++----- examples/demo/lib/widgets/log_view.dart | 164 ------------------ 9 files changed, 151 insertions(+), 404 deletions(-) delete mode 100644 examples/demo/lib/services/log_manager.dart delete mode 100644 examples/demo/lib/widgets/log_view.dart diff --git a/examples/build.md b/examples/build.md index a2ef2884..e22d6be3 100644 --- a/examples/build.md +++ b/examples/build.md @@ -135,17 +135,6 @@ OneSignal.User.addObserver(...) - `TextEditingController`s are properly disposed in `StatefulWidget`s - JSON parsing via `jsonDecode` returns `Map` for Track Event -### Accessibility (Appium) -- Use `Semantics` widget with `label` property: - ```dart - Semantics(label: 'log_entry_${index}_message', child: Text(entry.message)) - ``` - -### Log Manager -- Singleton with `ChangeNotifier` for reactive UI updates -- `LogManager().d(tag, message)`, `.i()`, `.w()`, `.e()` -- Also prints via `debugPrint` for development - --- ## File Structure @@ -162,8 +151,7 @@ examples/demo/ │ ├── services/ │ │ ├── onesignal_api_service.dart │ │ ├── preferences_service.dart -│ │ ├── tooltip_helper.dart -│ │ └── log_manager.dart +│ │ └── tooltip_helper.dart │ ├── repositories/ │ │ └── onesignal_repository.dart │ ├── viewmodels/ diff --git a/examples/demo/lib/main.dart b/examples/demo/lib/main.dart index 2446fce3..0936ecf1 100644 --- a/examples/demo/lib/main.dart +++ b/examples/demo/lib/main.dart @@ -6,7 +6,6 @@ import 'package:provider/provider.dart'; import 'repositories/onesignal_repository.dart'; import 'screens/home_screen.dart'; -import 'services/log_manager.dart'; import 'services/onesignal_api_service.dart'; import 'services/preferences_service.dart'; import 'services/tooltip_helper.dart'; @@ -21,7 +20,7 @@ Future main() async { try { await dotenv.load(fileName: '.env'); } catch (_) { - LogManager().w('.env file not found, using defaults'); + debugPrint('.env file not found, using defaults'); } final prefs = PreferencesService(); @@ -48,27 +47,29 @@ Future main() async { // Register IAM listeners OneSignal.InAppMessages.addWillDisplayListener((event) { - LogManager().i('IAM will display: ${event.message.messageId}'); + debugPrint('IAM will display: ${event.message.messageId}'); }); OneSignal.InAppMessages.addDidDisplayListener((event) { - LogManager().i('IAM did display: ${event.message.messageId}'); + debugPrint('IAM did display: ${event.message.messageId}'); }); OneSignal.InAppMessages.addWillDismissListener((event) { - LogManager().i('IAM will dismiss: ${event.message.messageId}'); + debugPrint('IAM will dismiss: ${event.message.messageId}'); }); OneSignal.InAppMessages.addDidDismissListener((event) { - LogManager().i('IAM did dismiss: ${event.message.messageId}'); + debugPrint('IAM did dismiss: ${event.message.messageId}'); }); OneSignal.InAppMessages.addClickListener((event) { - LogManager().i('IAM clicked: ${event.result.actionId}'); + debugPrint('IAM clicked: ${event.result.actionId}'); }); // Register notification listeners OneSignal.Notifications.addClickListener((event) { - LogManager().i('Notification clicked: ${event.notification.title}'); + debugPrint('Notification clicked: ${event.notification.title}'); }); OneSignal.Notifications.addForegroundWillDisplayListener((event) { - LogManager().i('Notification foreground will display: ${event.notification.title}'); + debugPrint( + 'Notification foreground will display: ${event.notification.title}', + ); event.notification.display(); }); @@ -77,7 +78,7 @@ Future main() async { try { apiKey = dotenv.env['ONESIGNAL_API_KEY'] ?? ''; } catch (_) { - LogManager().w('API key not found, continuing without it'); + debugPrint('API key not found, continuing without it'); } final apiService = OneSignalApiService() ..setAppId(appId) @@ -87,7 +88,7 @@ Future main() async { // Fetch tooltips in background TooltipHelper().init(); - LogManager().i('OneSignal initialized with app ID: $appId'); + debugPrint('OneSignal initialized with app ID: $appId'); runApp( ChangeNotifierProvider( diff --git a/examples/demo/lib/repositories/onesignal_repository.dart b/examples/demo/lib/repositories/onesignal_repository.dart index 725166f6..be9397f9 100644 --- a/examples/demo/lib/repositories/onesignal_repository.dart +++ b/examples/demo/lib/repositories/onesignal_repository.dart @@ -1,8 +1,8 @@ +import 'package:flutter/foundation.dart'; import 'package:onesignal_flutter/onesignal_flutter.dart'; import '../models/notification_type.dart'; import '../models/user_data.dart'; -import '../services/log_manager.dart'; import '../services/onesignal_api_service.dart'; class OneSignalRepository { @@ -123,7 +123,7 @@ class OneSignalRepository { bool hasPermission() => OneSignal.Notifications.permission; Future requestPermission(bool fallbackToSettings) async { - LogManager().i('Request permission (fallback: $fallbackToSettings)'); + debugPrint('Request permission (fallback: $fallbackToSettings)'); return await OneSignal.Notifications.requestPermission(fallbackToSettings); } @@ -133,7 +133,7 @@ class OneSignalRepository { // In-app messages void setInAppMessagesPaused(bool paused) { - LogManager().i('Set IAM paused: $paused'); + debugPrint('Set IAM paused: $paused'); OneSignal.InAppMessages.paused(paused); } @@ -151,7 +151,7 @@ class OneSignalRepository { } void requestLocationPermission() { - LogManager().i('Request location permission'); + debugPrint('Request location permission'); OneSignal.Location.requestPermission(); } @@ -184,12 +184,12 @@ class OneSignalRepository { // Privacy consent void setConsentRequired(bool required) { - LogManager().i('Set consent required: $required'); + debugPrint('Set consent required: $required'); OneSignal.consentRequired(required); } void setConsentGiven(bool granted) { - LogManager().i('Set consent given: $granted'); + debugPrint('Set consent given: $granted'); OneSignal.consentGiven(granted); } @@ -206,7 +206,7 @@ class OneSignalRepository { Future sendNotification(NotificationType type) async { final subscriptionId = getPushSubscriptionId(); if (subscriptionId == null) { - LogManager().w('No subscription ID for notification'); + debugPrint('No subscription ID for notification'); return false; } return _apiService.sendNotification(type, subscriptionId); @@ -215,7 +215,7 @@ class OneSignalRepository { Future sendCustomNotification(String title, String body) async { final subscriptionId = getPushSubscriptionId(); if (subscriptionId == null) { - LogManager().w('No subscription ID for custom notification'); + debugPrint('No subscription ID for custom notification'); return false; } return _apiService.sendCustomNotification(title, body, subscriptionId); diff --git a/examples/demo/lib/screens/home_screen.dart b/examples/demo/lib/screens/home_screen.dart index aa490927..f824442b 100644 --- a/examples/demo/lib/screens/home_screen.dart +++ b/examples/demo/lib/screens/home_screen.dart @@ -8,7 +8,6 @@ import '../services/tooltip_helper.dart'; import '../viewmodels/app_viewmodel.dart'; import '../widgets/dialogs.dart'; import '../widgets/loading_overlay.dart'; -import '../widgets/log_view.dart'; import '../widgets/sections/aliases_section.dart'; import '../widgets/sections/app_section.dart'; import '../widgets/sections/user_section.dart'; @@ -83,90 +82,79 @@ class _HomeScreenState extends State { ), body: LoadingOverlay( isLoading: vm.isLoading, - child: Column( - children: [ - const LogView(), - Expanded( - child: Semantics( - identifier: 'main_scroll_view', - child: ListView( - padding: const EdgeInsets.only(bottom: 24), - children: [ - const AppSection(), - const UserSection(), - PushSection( - onInfoTap: () => _showTooltipDialog(context, 'push'), - ), - SendPushSection( - onInfoTap: () => - _showTooltipDialog(context, 'sendPushNotification'), - ), - InAppSection( - onInfoTap: () => - _showTooltipDialog(context, 'inAppMessaging'), - ), - SendIamSection( - onInfoTap: () => - _showTooltipDialog(context, 'sendInAppMessage'), - ), - AliasesSection( - onInfoTap: () => _showTooltipDialog(context, 'aliases'), - ), - EmailsSection( - onInfoTap: () => _showTooltipDialog(context, 'emails'), - ), - SmsSection( - onInfoTap: () => _showTooltipDialog(context, 'sms'), - ), - TagsSection( - onInfoTap: () => _showTooltipDialog(context, 'tags'), - ), - OutcomesSection( - onInfoTap: () => - _showTooltipDialog(context, 'outcomes'), - ), - TriggersSection( - onInfoTap: () => - _showTooltipDialog(context, 'triggers'), - ), - CustomEventsSection( - onInfoTap: () => - _showTooltipDialog(context, 'trackEvent'), - ), - LocationSection( - onInfoTap: () => - _showTooltipDialog(context, 'location'), - ), - if (defaultTargetPlatform == TargetPlatform.iOS) - LiveActivitiesSection( - onInfoTap: () => - _showTooltipDialog(context, 'liveActivities'), - ), - const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const SecondaryScreen(), - ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.osPrimary, - foregroundColor: Colors.white, - ), - child: const Text('NEXT ACTIVITY'), + child: Semantics( + identifier: 'main_scroll_view', + child: ListView( + padding: const EdgeInsets.only(bottom: 24), + children: [ + const AppSection(), + const UserSection(), + PushSection( + onInfoTap: () => _showTooltipDialog(context, 'push'), + ), + SendPushSection( + onInfoTap: () => + _showTooltipDialog(context, 'sendPushNotification'), + ), + InAppSection( + onInfoTap: () => + _showTooltipDialog(context, 'inAppMessaging'), + ), + SendIamSection( + onInfoTap: () => + _showTooltipDialog(context, 'sendInAppMessage'), + ), + AliasesSection( + onInfoTap: () => _showTooltipDialog(context, 'aliases'), + ), + EmailsSection( + onInfoTap: () => _showTooltipDialog(context, 'emails'), + ), + SmsSection( + onInfoTap: () => _showTooltipDialog(context, 'sms'), + ), + TagsSection( + onInfoTap: () => _showTooltipDialog(context, 'tags'), + ), + OutcomesSection( + onInfoTap: () => _showTooltipDialog(context, 'outcomes'), + ), + TriggersSection( + onInfoTap: () => _showTooltipDialog(context, 'triggers'), + ), + CustomEventsSection( + onInfoTap: () => _showTooltipDialog(context, 'trackEvent'), + ), + LocationSection( + onInfoTap: () => _showTooltipDialog(context, 'location'), + ), + if (defaultTargetPlatform == TargetPlatform.iOS) + LiveActivitiesSection( + onInfoTap: () => + _showTooltipDialog(context, 'liveActivities'), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const SecondaryScreen(), ), - ), - const SizedBox(height: 16), - ], + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.osPrimary, + foregroundColor: Colors.white, + ), + child: const Text('NEXT ACTIVITY'), ), ), - ), - ], + const SizedBox(height: 16), + ], + ), ), ), ); diff --git a/examples/demo/lib/services/log_manager.dart b/examples/demo/lib/services/log_manager.dart deleted file mode 100644 index b9a6c8e5..00000000 --- a/examples/demo/lib/services/log_manager.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:flutter/foundation.dart'; - -enum LogLevel { debug, info, warn, error } - -class LogEntry { - final DateTime timestamp; - final LogLevel level; - final String message; - - const LogEntry({ - required this.timestamp, - required this.level, - required this.message, - }); - - String get levelLabel { - switch (level) { - case LogLevel.debug: - return 'D'; - case LogLevel.info: - return 'I'; - case LogLevel.warn: - return 'W'; - case LogLevel.error: - return 'E'; - } - } - - String get formattedTime { - final h = timestamp.hour.toString().padLeft(2, '0'); - final m = timestamp.minute.toString().padLeft(2, '0'); - final s = timestamp.second.toString().padLeft(2, '0'); - return '$h:$m:$s'; - } -} - -class LogManager extends ChangeNotifier { - static final LogManager _instance = LogManager._internal(); - factory LogManager() => _instance; - LogManager._internal(); - - final List _logs = []; - - List get logs => List.unmodifiable(_logs); - - void _log(LogLevel level, String message) { - final entry = LogEntry( - timestamp: DateTime.now(), - level: level, - message: message, - ); - _logs.add(entry); - debugPrint('[${entry.levelLabel}] $message'); - notifyListeners(); - } - - void d(String message) => _log(LogLevel.debug, message); - void i(String message) => _log(LogLevel.info, message); - void w(String message) => _log(LogLevel.warn, message); - void e(String message) => _log(LogLevel.error, message); - - void clear() { - _logs.clear(); - notifyListeners(); - } -} diff --git a/examples/demo/lib/services/onesignal_api_service.dart b/examples/demo/lib/services/onesignal_api_service.dart index 89561434..2c742759 100644 --- a/examples/demo/lib/services/onesignal_api_service.dart +++ b/examples/demo/lib/services/onesignal_api_service.dart @@ -1,10 +1,10 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import '../models/notification_type.dart'; import '../models/user_data.dart'; -import '../services/log_manager.dart'; class OneSignalApiService { String _appId = ''; @@ -49,10 +49,10 @@ class OneSignalApiService { body: jsonEncode(body), ); - LogManager().i('Send notification response: ${response.statusCode}'); + debugPrint('Send notification response: ${response.statusCode}'); return response.statusCode == 200; } catch (e) { - LogManager().e('Send notification error: $e'); + debugPrint('Send notification error: $e'); return false; } } @@ -79,10 +79,10 @@ class OneSignalApiService { body: jsonEncode(payload), ); - LogManager().i('Send custom notification response: ${response.statusCode}'); + debugPrint('Send custom notification response: ${response.statusCode}'); return response.statusCode == 200; } catch (e) { - LogManager().e('Send custom notification error: $e'); + debugPrint('Send custom notification error: $e'); return false; } } @@ -108,12 +108,12 @@ class OneSignalApiService { }), ); - LogManager().i( + debugPrint( 'Update live activity response: ${response.statusCode} ${response.body}', ); return response.statusCode >= 200 && response.statusCode < 300; } catch (e) { - LogManager().e('Update live activity error: $e'); + debugPrint('Update live activity error: $e'); return false; } } @@ -139,12 +139,12 @@ class OneSignalApiService { }), ); - LogManager().i( + debugPrint( 'End live activity response: ${response.statusCode} ${response.body}', ); return response.statusCode >= 200 && response.statusCode < 300; } catch (e) { - LogManager().e('End live activity error: $e'); + debugPrint('End live activity error: $e'); return false; } } @@ -162,10 +162,10 @@ class OneSignalApiService { final json = jsonDecode(response.body) as Map; return UserData.fromJson(json); } - LogManager().w('Fetch user returned ${response.statusCode}'); + debugPrint('Fetch user returned ${response.statusCode}'); return null; } catch (e) { - LogManager().e('Fetch user error: $e'); + debugPrint('Fetch user error: $e'); return null; } } diff --git a/examples/demo/lib/services/tooltip_helper.dart b/examples/demo/lib/services/tooltip_helper.dart index 4a078e2b..664f8121 100644 --- a/examples/demo/lib/services/tooltip_helper.dart +++ b/examples/demo/lib/services/tooltip_helper.dart @@ -1,9 +1,8 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; -import 'log_manager.dart'; - class TooltipData { final String title; final String description; @@ -62,10 +61,10 @@ class TooltipHelper { ), ); }); - LogManager().i('Loaded ${_tooltips.length} tooltips'); + debugPrint('Loaded ${_tooltips.length} tooltips'); } } catch (e) { - LogManager().w('Failed to load tooltips: $e'); + debugPrint('Failed to load tooltips: $e'); } _initialized = true; diff --git a/examples/demo/lib/viewmodels/app_viewmodel.dart b/examples/demo/lib/viewmodels/app_viewmodel.dart index c237b36c..55dc6251 100644 --- a/examples/demo/lib/viewmodels/app_viewmodel.dart +++ b/examples/demo/lib/viewmodels/app_viewmodel.dart @@ -4,7 +4,6 @@ import 'package:onesignal_flutter/onesignal_flutter.dart'; import '../models/in_app_message_type.dart'; import '../models/notification_type.dart'; import '../repositories/onesignal_repository.dart'; -import '../services/log_manager.dart'; import '../services/preferences_service.dart'; class AppViewModel extends ChangeNotifier { @@ -141,7 +140,7 @@ class AppViewModel extends ChangeNotifier { } catch (e) { _isLoading = false; notifyListeners(); - LogManager().e('Error fetching initial user data: $e'); + debugPrint('Error fetching initial user data: $e'); } } @@ -150,19 +149,20 @@ class AppViewModel extends ChangeNotifier { OneSignal.User.pushSubscription.addObserver((state) { _pushSubscriptionId = state.current.id; _pushEnabled = state.current.optedIn; - LogManager().i( - 'Push subscription changed: id=${state.current.id}, optedIn=${state.current.optedIn}'); + debugPrint( + 'Push subscription changed: id=${state.current.id}, optedIn=${state.current.optedIn}', + ); notifyListeners(); }); OneSignal.Notifications.addPermissionObserver((permission) { _hasNotificationPermission = permission; - LogManager().i('Permission changed: $permission'); + debugPrint('Permission changed: $permission'); notifyListeners(); }); OneSignal.User.addObserver((state) { - LogManager().i('User state changed'); + debugPrint('User state changed'); fetchUserDataFromApi(); }); } @@ -188,7 +188,7 @@ class AppViewModel extends ChangeNotifier { notifyListeners(); } } catch (e) { - LogManager().e('Error fetching user data: $e'); + debugPrint('Error fetching user data: $e'); } } @@ -210,11 +210,11 @@ class AppViewModel extends ChangeNotifier { _isLoading = false; notifyListeners(); - LogManager().i('Logged in as: $externalUserId'); + debugPrint('Logged in as: $externalUserId'); } catch (e) { _isLoading = false; notifyListeners(); - LogManager().e('Login error: $e'); + debugPrint('Login error: $e'); } } @@ -234,11 +234,11 @@ class AppViewModel extends ChangeNotifier { _isLoading = false; notifyListeners(); - LogManager().i('Logged out'); + debugPrint('Logged out'); } catch (e) { _isLoading = false; notifyListeners(); - LogManager().e('Logout error: $e'); + debugPrint('Logout error: $e'); } } @@ -270,7 +270,7 @@ class AppViewModel extends ChangeNotifier { } _pushEnabled = enabled; notifyListeners(); - LogManager().i('Push ${enabled ? "enabled" : "disabled"}'); + debugPrint('Push ${enabled ? "enabled" : "disabled"}'); } Future promptPush() async { @@ -283,24 +283,24 @@ class AppViewModel extends ChangeNotifier { Future sendNotification(NotificationType type) async { final success = await _repository.sendNotification(type); if (success) { - LogManager().i('Notification sent: ${type.name}'); + debugPrint('Notification sent: ${type.name}'); } else { - LogManager().e('Failed to send notification'); + debugPrint('Failed to send notification'); } } Future sendCustomNotification(String title, String body) async { final success = await _repository.sendCustomNotification(title, body); if (success) { - LogManager().i('Custom notification sent'); + debugPrint('Custom notification sent'); } else { - LogManager().e('Failed to send notification'); + debugPrint('Failed to send notification'); } } void clearAllNotifications() { _repository.clearAllNotifications(); - LogManager().i('All notifications cleared'); + debugPrint('All notifications cleared'); } // IAM @@ -317,7 +317,7 @@ class AppViewModel extends ChangeNotifier { ..removeWhere((e) => e.key == 'iam_type') ..add(MapEntry('iam_type', type.triggerValue)); notifyListeners(); - LogManager().i('Sent In-App Message: ${type.label}'); + debugPrint('Sent In-App Message: ${type.label}'); } // Aliases @@ -325,14 +325,14 @@ class AppViewModel extends ChangeNotifier { _repository.addAlias(label, id); _aliasesList = List.from(_aliasesList)..add(MapEntry(label, id)); notifyListeners(); - LogManager().i('Alias added: $label'); + debugPrint('Alias added: $label'); } void addAliases(Map aliases) { _repository.addAliases(aliases); _aliasesList = List.from(_aliasesList)..addAll(aliases.entries); notifyListeners(); - LogManager().i('${aliases.length} alias(es) added'); + debugPrint('${aliases.length} alias(es) added'); } // Emails @@ -340,14 +340,14 @@ class AppViewModel extends ChangeNotifier { _repository.addEmail(email); _emailsList = List.from(_emailsList)..add(email); notifyListeners(); - LogManager().i('Email added: $email'); + debugPrint('Email added: $email'); } void removeEmail(String email) { _repository.removeEmail(email); _emailsList = List.from(_emailsList)..remove(email); notifyListeners(); - LogManager().i('Email removed: $email'); + debugPrint('Email removed: $email'); } // SMS @@ -355,14 +355,14 @@ class AppViewModel extends ChangeNotifier { _repository.addSms(smsNumber); _smsNumbersList = List.from(_smsNumbersList)..add(smsNumber); notifyListeners(); - LogManager().i('SMS added: $smsNumber'); + debugPrint('SMS added: $smsNumber'); } void removeSms(String smsNumber) { _repository.removeSms(smsNumber); _smsNumbersList = List.from(_smsNumbersList)..remove(smsNumber); notifyListeners(); - LogManager().i('SMS removed: $smsNumber'); + debugPrint('SMS removed: $smsNumber'); } // Tags @@ -370,28 +370,28 @@ class AppViewModel extends ChangeNotifier { _repository.addTag(key, value); _tagsList = List.from(_tagsList)..add(MapEntry(key, value)); notifyListeners(); - LogManager().i('Tag added: $key'); + debugPrint('Tag added: $key'); } void addTags(Map tags) { _repository.addTags(tags); _tagsList = List.from(_tagsList)..addAll(tags.entries); notifyListeners(); - LogManager().i('${tags.length} tag(s) added'); + debugPrint('${tags.length} tag(s) added'); } void removeTag(String key) { _repository.removeTag(key); _tagsList = List.from(_tagsList)..removeWhere((e) => e.key == key); notifyListeners(); - LogManager().i('Tag removed: $key'); + debugPrint('Tag removed: $key'); } void removeSelectedTags(List keys) { _repository.removeTags(keys); _tagsList = List.from(_tagsList)..removeWhere((e) => keys.contains(e.key)); notifyListeners(); - LogManager().i('${keys.length} tag(s) removed'); + debugPrint('${keys.length} tag(s) removed'); } // Triggers (in-memory only) @@ -399,21 +399,21 @@ class AppViewModel extends ChangeNotifier { _repository.addTrigger(key, value); _triggersList = List.from(_triggersList)..add(MapEntry(key, value)); notifyListeners(); - LogManager().i('Trigger added: $key'); + debugPrint('Trigger added: $key'); } void addTriggers(Map triggers) { _repository.addTriggers(triggers); _triggersList = List.from(_triggersList)..addAll(triggers.entries); notifyListeners(); - LogManager().i('${triggers.length} trigger(s) added'); + debugPrint('${triggers.length} trigger(s) added'); } void removeTrigger(String key) { _repository.removeTrigger(key); _triggersList = List.from(_triggersList)..removeWhere((e) => e.key == key); notifyListeners(); - LogManager().i('Trigger removed: $key'); + debugPrint('Trigger removed: $key'); } void removeSelectedTriggers(List keys) { @@ -421,36 +421,36 @@ class AppViewModel extends ChangeNotifier { _triggersList = List.from(_triggersList) ..removeWhere((e) => keys.contains(e.key)); notifyListeners(); - LogManager().i('${keys.length} trigger(s) removed'); + debugPrint('${keys.length} trigger(s) removed'); } void clearAllTriggers() { _repository.clearTriggers(); _triggersList = []; notifyListeners(); - LogManager().i('All triggers cleared'); + debugPrint('All triggers cleared'); } // Outcomes void sendOutcome(String name) { _repository.sendOutcome(name); - LogManager().i('Outcome sent: $name'); + debugPrint('Outcome sent: $name'); } void sendUniqueOutcome(String name) { _repository.sendUniqueOutcome(name); - LogManager().i('Unique outcome sent: $name'); + debugPrint('Unique outcome sent: $name'); } void sendOutcomeWithValue(String name, double value) { _repository.sendOutcomeWithValue(name, value); - LogManager().i('Outcome sent: $name = $value'); + debugPrint('Outcome sent: $name = $value'); } // Custom Events void trackEvent(String name, Map? properties) { _repository.trackEvent(name, properties); - LogManager().i('Event tracked: $name'); + debugPrint('Event tracked: $name'); } // Live Activities @@ -470,7 +470,7 @@ class AppViewModel extends ChangeNotifier { _statusIndex = 0; await _repository.startDefaultLiveActivity(_activityId, attributes, content); notifyListeners(); - LogManager().i('Started Live Activity: $_activityId'); + debugPrint('Started Live Activity: $_activityId'); } Future updateLiveActivity() async { @@ -480,30 +480,31 @@ class AppViewModel extends ChangeNotifier { final nextIndex = (_statusIndex + 1) % _orderStatuses.length; final content = Map.from(_orderStatuses[nextIndex]); final eventUpdates = {'data': content}; - final success = await _repository.updateLiveActivity(_activityId, eventUpdates); + final success = + await _repository.updateLiveActivity(_activityId, eventUpdates); _isLaUpdating = false; if (success) { _statusIndex = nextIndex; - LogManager().i('Updated Live Activity: $_activityId'); + debugPrint('Updated Live Activity: $_activityId'); } else { - LogManager().e('Failed to update Live Activity'); + debugPrint('Failed to update Live Activity'); } notifyListeners(); } Future exitLiveActivity() async { await _repository.exitLiveActivity(_activityId); - LogManager().i('Exited Live Activity: $_activityId'); + debugPrint('Exited Live Activity: $_activityId'); } Future endLiveActivity() async { final success = await _repository.endLiveActivity(_activityId); if (success) { _statusIndex = 0; - LogManager().i('Ended Live Activity: $_activityId'); + debugPrint('Ended Live Activity: $_activityId'); } else { - LogManager().e('Failed to end Live Activity'); + debugPrint('Failed to end Live Activity'); } notifyListeners(); } @@ -514,7 +515,7 @@ class AppViewModel extends ChangeNotifier { _repository.setLocationShared(shared); await _prefs.setLocationShared(shared); notifyListeners(); - LogManager().i('Location sharing ${shared ? "enabled" : "disabled"}'); + debugPrint('Location sharing ${shared ? "enabled" : "disabled"}'); } void promptLocation() { diff --git a/examples/demo/lib/widgets/log_view.dart b/examples/demo/lib/widgets/log_view.dart deleted file mode 100644 index 137de6ab..00000000 --- a/examples/demo/lib/widgets/log_view.dart +++ /dev/null @@ -1,164 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../services/log_manager.dart'; -import '../theme.dart'; - -class LogView extends StatefulWidget { - const LogView({super.key}); - - @override - State createState() => _LogViewState(); -} - -class _LogViewState extends State { - bool _expanded = true; - final ScrollController _scrollController = ScrollController(); - - @override - void initState() { - super.initState(); - LogManager().addListener(_onLogChanged); - } - - @override - void dispose() { - LogManager().removeListener(_onLogChanged); - _scrollController.dispose(); - super.dispose(); - } - - void _onLogChanged() { - setState(() {}); - } - - @override - Widget build(BuildContext context) { - final logs = LogManager().logs; - final textTheme = Theme.of(context).textTheme; - final logEntryStyle = textTheme.labelSmall?.copyWith( - fontFamily: 'monospace', - ); - - const logBackground = AppColors.osLogBackground; - - return Semantics( - identifier: 'log_view_container', - container: true, - child: Card( - margin: EdgeInsets.zero, - color: logBackground, - shape: const RoundedRectangleBorder(), - child: Column( - children: [ - InkWell( - onTap: () => setState(() => _expanded = !_expanded), - excludeFromSemantics: true, - borderRadius: BorderRadius.zero, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - child: Row( - children: [ - Text( - 'LOGS', - style: textTheme.labelSmall?.copyWith( - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - const SizedBox(width: 8), - Semantics( - identifier: 'log_view_count', - container: true, - child: Text( - '${logs.length}', - style: textTheme.labelSmall?.copyWith( - color: AppColors.osGrey500, - ), - ), - ), - const Spacer(), - if (logs.isNotEmpty) - Semantics( - identifier: 'log_view_clear_button', - container: true, - child: GestureDetector( - excludeFromSemantics: true, - onTap: () => LogManager().clear(), - child: const Icon( - Icons.delete, - size: 18, - color: AppColors.osGrey500, - ), - ), - ), - const SizedBox(width: 8), - Icon( - _expanded - ? Icons.expand_less - : Icons.expand_more, - size: 18, - color: AppColors.osGrey500, - ), - ], - ), - ), - ), - if (_expanded) - SizedBox( - height: 100, - child: logs.isEmpty - ? Center( - child: Text( - 'No logs yet', - style: textTheme.bodyMedium?.copyWith( - color: AppColors.osGrey500, - ), - ), - ) - : ListView.builder( - controller: _scrollController, - itemCount: logs.length, - padding: const EdgeInsets.symmetric(horizontal: 12), - itemBuilder: (context, index) { - final entry = logs[logs.length - 1 - index]; - return Padding( - padding: const EdgeInsets.symmetric( - vertical: 1, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - entry.formattedTime, - style: logEntryStyle?.copyWith( - color: AppColors.osLogTimestamp, - ), - ), - const SizedBox(width: 4), - Expanded( - child: Semantics( - identifier: 'log_entry_${index}_message', - container: true, - child: Text( - entry.message, - style: logEntryStyle?.copyWith( - color: Colors.white, - ), - ), - ), - ), - ], - ), - ); - }, - ), - ), - ], - ), - ), - ); - } -} From 6749f3fe341eb6715be6824485a5b6e1ab97d3bd Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 13 Apr 2026 21:33:09 -0700 Subject: [PATCH 21/31] test(demo): add location shared check button --- examples/demo/lib/viewmodels/app_viewmodel.dart | 4 ++++ .../demo/lib/widgets/sections/location_section.dart | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/examples/demo/lib/viewmodels/app_viewmodel.dart b/examples/demo/lib/viewmodels/app_viewmodel.dart index 55dc6251..c3ce1c9e 100644 --- a/examples/demo/lib/viewmodels/app_viewmodel.dart +++ b/examples/demo/lib/viewmodels/app_viewmodel.dart @@ -522,6 +522,10 @@ class AppViewModel extends ChangeNotifier { _repository.requestLocationPermission(); } + Future checkLocationShared() async { + return await _repository.isLocationShared(); + } + // Dismiss loading (called from user state change observer) void dismissLoading() { if (_isLoading) { diff --git a/examples/demo/lib/widgets/sections/location_section.dart b/examples/demo/lib/widgets/sections/location_section.dart index dddd0b54..8f4e7505 100644 --- a/examples/demo/lib/widgets/sections/location_section.dart +++ b/examples/demo/lib/widgets/sections/location_section.dart @@ -39,6 +39,18 @@ class LocationSection extends StatelessWidget { label: 'PROMPT LOCATION', onPressed: vm.promptLocation, ), + AppSpacing.gapBox, + PrimaryButton( + label: 'CHECK LOCATION SHARED', + onPressed: () async { + final shared = await vm.checkLocationShared(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Location shared: $shared')), + ); + } + }, + ), ], ), ); From 24706536694ff8c658357e03cd61e9e08dd74e7a Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 13 Apr 2026 21:44:26 -0700 Subject: [PATCH 22/31] test(demo): enhance UI elements for appium testing --- examples/demo/lib/theme.dart | 4 +++- .../demo/lib/widgets/sections/location_section.dart | 11 ++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/examples/demo/lib/theme.dart b/examples/demo/lib/theme.dart index 859c0788..eff80108 100644 --- a/examples/demo/lib/theme.dart +++ b/examples/demo/lib/theme.dart @@ -40,7 +40,9 @@ class AppTheme { appBarTheme: const AppBarTheme( backgroundColor: AppColors.osPrimary, foregroundColor: Colors.white, - elevation: 0, + elevation: 2, + scrolledUnderElevation: 2, + shadowColor: Colors.black, ), cardTheme: CardThemeData( color: AppColors.osCardBackground, diff --git a/examples/demo/lib/widgets/sections/location_section.dart b/examples/demo/lib/widgets/sections/location_section.dart index 8f4e7505..00e74a22 100644 --- a/examples/demo/lib/widgets/sections/location_section.dart +++ b/examples/demo/lib/widgets/sections/location_section.dart @@ -45,9 +45,14 @@ class LocationSection extends StatelessWidget { onPressed: () async { final shared = await vm.checkLocationShared(); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Location shared: $shared')), - ); + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text('Location shared: $shared'), + dismissDirection: DismissDirection.horizontal, + ), + ); } }, ), From c2126193ba6151a68faa8a6557e24f3d24264422 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 11:21:08 -0700 Subject: [PATCH 23/31] test(demo): add snackbar feedback and update theme --- examples/demo/lib/theme.dart | 21 ++++++++++++++++++- .../sections/custom_events_section.dart | 7 ++++++- .../widgets/sections/location_section.dart | 9 +------- .../widgets/sections/outcomes_section.dart | 11 +++++++++- 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/examples/demo/lib/theme.dart b/examples/demo/lib/theme.dart index eff80108..1ea9892c 100644 --- a/examples/demo/lib/theme.dart +++ b/examples/demo/lib/theme.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; class AppSpacing { static const double gap = 8; @@ -38,11 +39,16 @@ class AppTheme { ).copyWith(primary: AppColors.osPrimary), scaffoldBackgroundColor: AppColors.osLightBackground, appBarTheme: const AppBarTheme( - backgroundColor: AppColors.osPrimary, + backgroundColor: Colors.black, foregroundColor: Colors.white, elevation: 2, scrolledUnderElevation: 2, shadowColor: Colors.black, + systemOverlayStyle: SystemUiOverlayStyle( + statusBarColor: Colors.black, + statusBarIconBrightness: Brightness.light, + statusBarBrightness: Brightness.dark, + ), ), cardTheme: CardThemeData( color: AppColors.osCardBackground, @@ -107,3 +113,16 @@ class AppTheme { AppTheme._(); } + +extension AppSnackBar on BuildContext { + void showSnackBar(String message) { + ScaffoldMessenger.of(this) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text(message), + dismissDirection: DismissDirection.horizontal, + ), + ); + } +} diff --git a/examples/demo/lib/widgets/sections/custom_events_section.dart b/examples/demo/lib/widgets/sections/custom_events_section.dart index 7ab94556..ae3e21b5 100644 --- a/examples/demo/lib/widgets/sections/custom_events_section.dart +++ b/examples/demo/lib/widgets/sections/custom_events_section.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import '../../theme.dart'; import '../../viewmodels/app_viewmodel.dart'; import '../action_button.dart'; import '../dialogs.dart'; @@ -27,10 +28,14 @@ class CustomEventsSection extends StatelessWidget { builder: (_) => const TrackEventDialog(), ); if (result != null) { + final name = result['name'] as String; vm.trackEvent( - result['name'] as String, + name, result['properties'] as Map?, ); + if (context.mounted) { + context.showSnackBar('Event tracked: $name'); + } } }, ), diff --git a/examples/demo/lib/widgets/sections/location_section.dart b/examples/demo/lib/widgets/sections/location_section.dart index 00e74a22..b5918e2b 100644 --- a/examples/demo/lib/widgets/sections/location_section.dart +++ b/examples/demo/lib/widgets/sections/location_section.dart @@ -45,14 +45,7 @@ class LocationSection extends StatelessWidget { onPressed: () async { final shared = await vm.checkLocationShared(); if (context.mounted) { - ScaffoldMessenger.of(context) - ..hideCurrentSnackBar() - ..showSnackBar( - SnackBar( - content: Text('Location shared: $shared'), - dismissDirection: DismissDirection.horizontal, - ), - ); + context.showSnackBar('Location shared: $shared'); } }, ), diff --git a/examples/demo/lib/widgets/sections/outcomes_section.dart b/examples/demo/lib/widgets/sections/outcomes_section.dart index a9b0a6b5..17e2a7f3 100644 --- a/examples/demo/lib/widgets/sections/outcomes_section.dart +++ b/examples/demo/lib/widgets/sections/outcomes_section.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import '../../theme.dart'; import '../../viewmodels/app_viewmodel.dart'; import '../action_button.dart'; import '../dialogs.dart'; @@ -29,13 +30,21 @@ class OutcomesSection extends StatelessWidget { if (result != null) { final type = result['type'] as OutcomeType; final name = result['name'] as String; + String snackbarMessage; switch (type) { case OutcomeType.normal: vm.sendOutcome(name); + snackbarMessage = 'Outcome sent: $name'; case OutcomeType.unique: vm.sendUniqueOutcome(name); + snackbarMessage = 'Unique outcome sent: $name'; case OutcomeType.withValue: - vm.sendOutcomeWithValue(name, result['value'] as double); + final value = result['value'] as double; + vm.sendOutcomeWithValue(name, value); + snackbarMessage = 'Outcome sent: $name = $value'; + } + if (context.mounted) { + context.showSnackBar(snackbarMessage); } } }, From af4f183e9715de4ec1c79218bd94001214d9c344 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 11:24:52 -0700 Subject: [PATCH 24/31] test(demo): fix tooltip key for custom events --- examples/demo/lib/screens/home_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/demo/lib/screens/home_screen.dart b/examples/demo/lib/screens/home_screen.dart index f824442b..c000d600 100644 --- a/examples/demo/lib/screens/home_screen.dart +++ b/examples/demo/lib/screens/home_screen.dart @@ -123,7 +123,7 @@ class _HomeScreenState extends State { onInfoTap: () => _showTooltipDialog(context, 'triggers'), ), CustomEventsSection( - onInfoTap: () => _showTooltipDialog(context, 'trackEvent'), + onInfoTap: () => _showTooltipDialog(context, 'customEvents'), ), LocationSection( onInfoTap: () => _showTooltipDialog(context, 'location'), From 7e789a29469d707363b57df6debefdcce562f0b8 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 11:28:58 -0700 Subject: [PATCH 25/31] style(demo): update app bar to use primary color --- examples/demo/lib/theme.dart | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/examples/demo/lib/theme.dart b/examples/demo/lib/theme.dart index 1ea9892c..ae0de6cb 100644 --- a/examples/demo/lib/theme.dart +++ b/examples/demo/lib/theme.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; class AppSpacing { static const double gap = 8; @@ -39,16 +38,11 @@ class AppTheme { ).copyWith(primary: AppColors.osPrimary), scaffoldBackgroundColor: AppColors.osLightBackground, appBarTheme: const AppBarTheme( - backgroundColor: Colors.black, + backgroundColor: AppColors.osPrimary, foregroundColor: Colors.white, elevation: 2, scrolledUnderElevation: 2, shadowColor: Colors.black, - systemOverlayStyle: SystemUiOverlayStyle( - statusBarColor: Colors.black, - statusBarIconBrightness: Brightness.light, - statusBarBrightness: Brightness.dark, - ), ), cardTheme: CardThemeData( color: AppColors.osCardBackground, From c069ed8f3a450c41d1bbe41f751b85c72f043267 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 11:47:08 -0700 Subject: [PATCH 26/31] test(demo): rename "activity" to "screen" in UI text --- examples/demo/lib/screens/home_screen.dart | 2 +- examples/demo/lib/screens/secondary_screen.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/demo/lib/screens/home_screen.dart b/examples/demo/lib/screens/home_screen.dart index c000d600..0486a11a 100644 --- a/examples/demo/lib/screens/home_screen.dart +++ b/examples/demo/lib/screens/home_screen.dart @@ -149,7 +149,7 @@ class _HomeScreenState extends State { backgroundColor: AppColors.osPrimary, foregroundColor: Colors.white, ), - child: const Text('NEXT ACTIVITY'), + child: const Text('NEXT SCREEN'), ), ), const SizedBox(height: 16), diff --git a/examples/demo/lib/screens/secondary_screen.dart b/examples/demo/lib/screens/secondary_screen.dart index 9701cec9..d6232e1f 100644 --- a/examples/demo/lib/screens/secondary_screen.dart +++ b/examples/demo/lib/screens/secondary_screen.dart @@ -7,12 +7,12 @@ class SecondaryScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Secondary Activity'), + title: const Text('Secondary Screen'), centerTitle: true, ), body: Center( child: Text( - 'Secondary Activity', + 'Secondary Screen', style: Theme.of(context).textTheme.headlineMedium, ), ), From 02495c2bbc9d412d2fdc019fcea382b510835fd3 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 12:32:46 -0700 Subject: [PATCH 27/31] Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/e2e.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index b0d82285..0464b19e 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -16,6 +16,9 @@ on: - ios - both +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true From b7fd679958ea8da80ac1a257e45e6556e3cb22d2 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 12:33:19 -0700 Subject: [PATCH 28/31] Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/e2e.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 0464b19e..06d4dc87 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -23,6 +23,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: build-android: if: >- From 9f7b8adf9603d1c424a4baba5f22d753431c16c2 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 13:58:12 -0700 Subject: [PATCH 29/31] ci(e2e): add E2E_MODE env var and update test setup --- .github/workflows/e2e.yml | 5 ++--- examples/build.md | 11 ++++++----- examples/demo/lib/widgets/toggle_row.dart | 7 ++++++- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 06d4dc87..7532e03f 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -23,9 +23,6 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -permissions: - contents: read - jobs: build-android: if: >- @@ -51,6 +48,7 @@ jobs: run: | echo "ONESIGNAL_APP_ID=${{ vars.APPIUM_ONESIGNAL_APP_ID }}" > .env echo "ONESIGNAL_API_KEY=${{ secrets.APPIUM_ONESIGNAL_API_KEY }}" >> .env + echo "E2E_MODE=true" >> .env - name: Build release APK working-directory: examples/demo @@ -80,6 +78,7 @@ jobs: run: | echo "ONESIGNAL_APP_ID=${{ vars.APPIUM_ONESIGNAL_APP_ID }}" > .env echo "ONESIGNAL_API_KEY=${{ secrets.APPIUM_ONESIGNAL_API_KEY }}" >> .env + echo "E2E_MODE=true" >> .env - name: Install CocoaPods dependencies working-directory: examples/demo/ios diff --git a/examples/build.md b/examples/build.md index e22d6be3..d81214b7 100644 --- a/examples/build.md +++ b/examples/build.md @@ -119,9 +119,9 @@ OneSignal.User.addObserver(...) - Use `await Future.delayed(const Duration(milliseconds: 100))` after setting state for render delay ### SnackBar Messages -- `AppViewModel` exposes a `snackBarMessage` stream or `ValueNotifier` -- `HomeScreen` shows via `ScaffoldMessenger.of(context).showSnackBar()` -- Clear previous SnackBar with `ScaffoldMessenger.of(context).clearSnackBars()` +- `AppSnackBar` extension on `BuildContext` defined in `theme.dart` +- Call `context.showSnackBar(message)` directly from widget callbacks +- Automatically hides the current SnackBar before showing the new one ### Send In-App Message Icons - TOP BANNER: `Icons.vertical_align_top` @@ -163,9 +163,9 @@ examples/demo/ │ ├── section_card.dart │ ├── toggle_row.dart │ ├── action_button.dart +│ ├── app_text_field.dart │ ├── list_widgets.dart │ ├── loading_overlay.dart -│ ├── log_view.dart │ ├── dialogs.dart │ └── sections/ │ ├── app_section.dart @@ -180,7 +180,8 @@ examples/demo/ │ ├── tags_section.dart │ ├── outcomes_section.dart │ ├── triggers_section.dart -│ ├── track_event_section.dart +│ ├── custom_events_section.dart +│ ├── live_activities_section.dart │ └── location_section.dart ├── android/ ├── ios/ diff --git a/examples/demo/lib/widgets/toggle_row.dart b/examples/demo/lib/widgets/toggle_row.dart index 61ec9ad2..c12e30c0 100644 --- a/examples/demo/lib/widgets/toggle_row.dart +++ b/examples/demo/lib/widgets/toggle_row.dart @@ -20,7 +20,7 @@ class ToggleRow extends StatelessWidget { @override Widget build(BuildContext context) { - return SwitchListTile( + Widget tile = SwitchListTile( title: Text(label, style: Theme.of(context).textTheme.bodyMedium), subtitle: description != null ? Text( @@ -36,5 +36,10 @@ class ToggleRow extends StatelessWidget { dense: true, visualDensity: VisualDensity.compact, ); + if (semanticsLabel != null) { + tile = Semantics( + identifier: semanticsLabel, container: true, child: tile); + } + return tile; } } From 0938d1066e2420bd77399ef7e5e444509135c6d2 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 15:13:38 -0700 Subject: [PATCH 30/31] test(e2e): improve semantic IDs and app config --- .github/workflows/e2e.yml | 1 + examples/demo/lib/main.dart | 3 ++- examples/demo/lib/widgets/list_widgets.dart | 7 ++++++- .../demo/lib/widgets/sections/app_section.dart | 17 +++++++++++------ .../sections/live_activities_section.dart | 17 +++++------------ .../lib/widgets/sections/send_push_section.dart | 1 - .../demo/lib/widgets/sections/user_section.dart | 12 ++++++++++-- 7 files changed, 35 insertions(+), 23 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 7532e03f..e530e239 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -63,6 +63,7 @@ jobs: build-ios: if: >- + github.event_name == 'push' || github.event.inputs.platform == 'ios' || github.event.inputs.platform == 'both' runs-on: macos-latest diff --git a/examples/demo/lib/main.dart b/examples/demo/lib/main.dart index 0936ecf1..d32e57fb 100644 --- a/examples/demo/lib/main.dart +++ b/examples/demo/lib/main.dart @@ -26,7 +26,8 @@ Future main() async { final prefs = PreferencesService(); await prefs.init(); - final appId = dotenv.env['ONESIGNAL_APP_ID'] ?? _defaultAppId; + final envAppId = dotenv.env['ONESIGNAL_APP_ID']; + final appId = (envAppId != null && envAppId.isNotEmpty) ? envAppId : _defaultAppId; // Initialize OneSignal SDK OneSignal.Debug.setLogLevel(OSLogLevel.verbose); diff --git a/examples/demo/lib/widgets/list_widgets.dart b/examples/demo/lib/widgets/list_widgets.dart index 17ed4bab..17fba5fe 100644 --- a/examples/demo/lib/widgets/list_widgets.dart +++ b/examples/demo/lib/widgets/list_widgets.dart @@ -63,11 +63,13 @@ class PairItem extends StatelessWidget { } class SingleItem extends StatelessWidget { + final String sectionKey; final String text; final VoidCallback? onDelete; const SingleItem({ super.key, + required this.sectionKey, required this.text, this.onDelete, }); @@ -81,7 +83,7 @@ class SingleItem extends StatelessWidget { Expanded(child: Text(text, style: Theme.of(context).textTheme.bodyMedium)), if (onDelete != null) Semantics( - identifier: 'remove_$text', + identifier: '${sectionKey}_remove_$text', container: true, child: GestureDetector( onTap: onDelete, @@ -152,6 +154,7 @@ class PairList extends StatelessWidget { } class CollapsibleList extends StatefulWidget { + final String sectionKey; final List items; final String emptyText; final void Function(String item) onDelete; @@ -159,6 +162,7 @@ class CollapsibleList extends StatefulWidget { const CollapsibleList({ super.key, + required this.sectionKey, required this.items, required this.emptyText, required this.onDelete, @@ -186,6 +190,7 @@ class _CollapsibleListState extends State { for (var i = 0; i < visibleItems.length; i++) ...[ SingleItem( key: ValueKey(visibleItems[i]), + sectionKey: widget.sectionKey, text: visibleItems[i], onDelete: () => widget.onDelete(visibleItems[i]), ), diff --git a/examples/demo/lib/widgets/sections/app_section.dart b/examples/demo/lib/widgets/sections/app_section.dart index db5a40d9..06cc5657 100644 --- a/examples/demo/lib/widgets/sections/app_section.dart +++ b/examples/demo/lib/widgets/sections/app_section.dart @@ -21,6 +21,7 @@ class AppSection extends StatelessWidget { return SectionCard( title: 'App', + sectionKey: 'app', onInfoTap: onInfoTap, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -35,12 +36,16 @@ class AppSection extends StatelessWidget { Text('App ID', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(width: 12), Expanded( - child: SelectableText( - _isE2E ? '••••••••-••••-••••-••••-••••••••••••' : vm.appId, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontFamily: 'monospace', - ), - textAlign: TextAlign.end, + child: Semantics( + identifier: 'app_id_value', + container: true, + child: SelectableText( + _isE2E ? '••••••••-••••-••••-••••-••••••••••••' : vm.appId, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontFamily: 'monospace', + ), + textAlign: TextAlign.end, + ), ), ), ], diff --git a/examples/demo/lib/widgets/sections/live_activities_section.dart b/examples/demo/lib/widgets/sections/live_activities_section.dart index 2f2b9552..32f2cb66 100644 --- a/examples/demo/lib/widgets/sections/live_activities_section.dart +++ b/examples/demo/lib/widgets/sections/live_activities_section.dart @@ -83,18 +83,11 @@ class _LiveActivitiesSectionState extends State { : () => vm.updateLiveActivity(), ), AppSpacing.gapBox, - SizedBox( - width: double.infinity, - child: OutlinedButton( - onPressed: activityEmpty || !vm.hasApiKey - ? null - : () => vm.endLiveActivity(), - style: OutlinedButton.styleFrom( - foregroundColor: AppColors.osPrimary, - side: const BorderSide(color: AppColors.osPrimary), - ), - child: const Text('END LIVE ACTIVITY'), - ), + DestructiveButton( + label: 'END LIVE ACTIVITY', + onPressed: activityEmpty || !vm.hasApiKey + ? null + : () => vm.endLiveActivity(), ), ], ), diff --git a/examples/demo/lib/widgets/sections/send_push_section.dart b/examples/demo/lib/widgets/sections/send_push_section.dart index 3ea7a965..81f5cdaa 100644 --- a/examples/demo/lib/widgets/sections/send_push_section.dart +++ b/examples/demo/lib/widgets/sections/send_push_section.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../models/notification_type.dart'; -import '../../theme.dart'; import '../../viewmodels/app_viewmodel.dart'; import '../action_button.dart'; import '../dialogs.dart'; diff --git a/examples/demo/lib/widgets/sections/user_section.dart b/examples/demo/lib/widgets/sections/user_section.dart index 3da75f21..b9331413 100644 --- a/examples/demo/lib/widgets/sections/user_section.dart +++ b/examples/demo/lib/widgets/sections/user_section.dart @@ -82,7 +82,10 @@ class UserSection extends StatelessWidget { builder: (_) => const LoginDialog(), ); if (result != null && context.mounted) { - vm.loginUser(result); + await vm.loginUser(result); + if (context.mounted) { + context.showSnackBar('Logged in as $result'); + } } }, ), @@ -91,7 +94,12 @@ class UserSection extends StatelessWidget { DestructiveButton( label: 'LOGOUT USER', semanticsLabel: 'logout_user_button', - onPressed: vm.logoutUser, + onPressed: () async { + await vm.logoutUser(); + if (context.mounted) { + context.showSnackBar('User logged out'); + } + }, ), ], ], From 6d5ea7828e2ede86c38f57376a5c461d4876e759 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 14 Apr 2026 15:19:10 -0700 Subject: [PATCH 31/31] test(demo): add sectionKey props to CollapsibleList widgets --- examples/demo/lib/widgets/sections/emails_section.dart | 1 + examples/demo/lib/widgets/sections/send_push_section.dart | 1 + examples/demo/lib/widgets/sections/sms_section.dart | 1 + 3 files changed, 3 insertions(+) diff --git a/examples/demo/lib/widgets/sections/emails_section.dart b/examples/demo/lib/widgets/sections/emails_section.dart index fa80a859..2b424cb3 100644 --- a/examples/demo/lib/widgets/sections/emails_section.dart +++ b/examples/demo/lib/widgets/sections/emails_section.dart @@ -28,6 +28,7 @@ class EmailsSection extends StatelessWidget { child: Padding( padding: AppSpacing.cardPadding, child: CollapsibleList( + sectionKey: 'emails', items: vm.emailsList, emptyText: 'No emails added', onDelete: vm.removeEmail, diff --git a/examples/demo/lib/widgets/sections/send_push_section.dart b/examples/demo/lib/widgets/sections/send_push_section.dart index 81f5cdaa..3ea7a965 100644 --- a/examples/demo/lib/widgets/sections/send_push_section.dart +++ b/examples/demo/lib/widgets/sections/send_push_section.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../models/notification_type.dart'; +import '../../theme.dart'; import '../../viewmodels/app_viewmodel.dart'; import '../action_button.dart'; import '../dialogs.dart'; diff --git a/examples/demo/lib/widgets/sections/sms_section.dart b/examples/demo/lib/widgets/sections/sms_section.dart index 12b3ee01..b6040f53 100644 --- a/examples/demo/lib/widgets/sections/sms_section.dart +++ b/examples/demo/lib/widgets/sections/sms_section.dart @@ -28,6 +28,7 @@ class SmsSection extends StatelessWidget { child: Padding( padding: AppSpacing.cardPadding, child: CollapsibleList( + sectionKey: 'sms', items: vm.smsNumbersList, emptyText: 'No SMS added', onDelete: vm.removeSms,