diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index cbb91e89..86dae011 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -75,6 +75,7 @@ jobs: CHANGE_MESSAGE: ${{ inputs.change_message }} RELEASE_TITLE: ${{ inputs.release_title }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + POSTHOG_PROJECT_TOKEN: ${{ secrets.POSTHOG_PROJECT_TOKEN }} run: | $args = @( "-ExecutionPolicy", "Bypass", diff --git a/.github/workflows/release-store.yml b/.github/workflows/release-store.yml index 5c60e906..3c142c1a 100644 --- a/.github/workflows/release-store.yml +++ b/.github/workflows/release-store.yml @@ -53,6 +53,8 @@ jobs: - name: Build Store Package shell: pwsh + env: + POSTHOG_PROJECT_TOKEN: ${{ secrets.POSTHOG_PROJECT_TOKEN }} run: powershell -ExecutionPolicy Bypass -File scripts/build_store_release.ps1 - name: Upload Store Artifacts diff --git a/docs/analytics.md b/docs/analytics.md new file mode 100644 index 00000000..fad9913a --- /dev/null +++ b/docs/analytics.md @@ -0,0 +1,49 @@ +# Anonymous analytics + +Icarus sends a deliberately small set of anonymous product events to PostHog. +Analytics is enabled by default, can be disabled under **Settings > Privacy**, and +is inactive in builds that do not include a PostHog project token. + +## Build configuration + +Set `POSTHOG_PROJECT_TOKEN` before running `scripts/build_desktop_release.ps1`. +The script passes it to Flutter through a temporary Dart-defines file. For an +EU PostHog project, also set `POSTHOG_HOST=https://eu.i.posthog.com`; the default +is the US host. + +The `release-desktop.yml` and `release-store.yml` GitHub Actions workflows read +the token from the `POSTHOG_PROJECT_TOKEN` repository secret. The build scripts +place it in a temporary Dart-defines file so it is not printed in Actions logs, +then delete the file immediately after the build. + +The project token is a public ingestion token, not a private API key. Never use +a PostHog personal API key here. + +For local development: + +```powershell +fvm flutter run -d windows --dart-define=POSTHOG_PROJECT_TOKEN=phc_your_project_token +``` + +## Event schema + +| Event | Properties | Purpose | +| --- | --- | --- | +| `app_opened` | Common properties only | Anonymous active-user and retention counts | +| `strategy_created` | Common properties only | Core activation | +| `strategy_imported` | Common properties only | Import adoption | +| `lineup_created` | `has_video`, `has_notes`, `has_images` | Lineup feature adoption without collecting content | +| `content_exported` | `content_type` (`strategy`, `folder`, or `library`) | Export and backup adoption | + +Every event has a random installation ID, app version, build number, release +channel, and platform. Events set `$process_person_profile` to `false` and +`$geoip_disable` to `true`. Icarus does not collect names, file paths, strategy +content, URLs, notes, images, screen views, recordings, errors, or device details. +The PostHog project must also keep **Settings > Privacy > Discard client IP +data** enabled so the transport IP is removed after ingestion. This is enabled +for the production Icarus project. + +Development smoke tests should set +`ICARUS_ANALYTICS_ENVIRONMENT=development`. Production builds default to +`analytics_environment=production`, making test events easy to exclude from +insights. diff --git a/lib/const/agents.dart b/lib/const/agents.dart index 00642e60..49089148 100644 --- a/lib/const/agents.dart +++ b/lib/const/agents.dart @@ -1090,7 +1090,7 @@ class AgentData implements DraggableData { agent.abilities.first.abilityData = CircleAbility( iconPath: agent.abilities.first.iconPath, - size: 24, + size: 30, rangeOutlineColor: Colors.lightBlueAccent, hasCenterDot: true, ); diff --git a/lib/const/folder_icons.dart b/lib/const/folder_icons.dart index 7e822f99..37b34049 100644 --- a/lib/const/folder_icons.dart +++ b/lib/const/folder_icons.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:icarus/const/custom_icons.dart'; -const int folderIconRegistryVersion = 92; +const int folderIconRegistryVersion = 94; enum FolderIconRenderKind { material, diff --git a/lib/const/settings.dart b/lib/const/settings.dart index 80c87521..2e86f686 100644 --- a/lib/const/settings.dart +++ b/lib/const/settings.dart @@ -88,8 +88,8 @@ class Settings { static final Uri dicordLink = Uri.parse("https://discord.gg/PN2uKwCqYB"); static const Duration autoSaveOffset = Duration(seconds: 15); - static const int versionNumber = 93; - static const String versionName = "4.5.0"; + static const int versionNumber = 94; + static const String versionName = "4.5.1"; static final Uri desktopUpdaterArchiveUrl = buildDesktopUpdaterArchiveUrl(kResolvedUpdateChannel); diff --git a/lib/hive/hive_adapters.g.yaml b/lib/hive/hive_adapters.g.yaml index efaaef9b..ec24c0d4 100644 --- a/lib/hive/hive_adapters.g.yaml +++ b/lib/hive/hive_adapters.g.yaml @@ -443,7 +443,7 @@ types: index: 4 AppPreferences: typeId: 28 - nextIndex: 16 + nextIndex: 17 fields: defaultThemeProfileIdForNewStrategies: index: 0 diff --git a/lib/main.dart b/lib/main.dart index 9eaf8cc6..a9169cbf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -23,6 +23,7 @@ import 'package:icarus/providers/folder_provider.dart'; import 'package:icarus/providers/user_preferences_provider.dart'; import 'package:icarus/providers/strategy_provider.dart'; import 'package:icarus/services/app_error_reporter.dart'; +import 'package:icarus/services/analytics_service.dart'; import 'package:icarus/strategy_view.dart'; import 'package:icarus/widgets/folder_navigator.dart'; import 'package:icarus/widgets/global_shortcuts.dart'; @@ -80,11 +81,14 @@ Future main(List args) async { await Hive.openBox(HiveBoxNames.appPreferencesBox); await Hive.openBox(HiveBoxNames.favoriteAgentsBox); await Hive.openBox(HiveBoxNames.pinnedItemsBox); + await Hive.openBox(AnalyticsService.storageBoxName); await MapThemeProfilesProvider.bootstrap(); await StrategyProvider.migrateAllStrategies(); + await AnalyticsService.instance.initialize(); + // await Hive.box(HiveBoxNames.strategiesBox).clear(); await _initWebViewEnvironment(); diff --git a/lib/providers/strategy_provider.dart b/lib/providers/strategy_provider.dart index 41cd2218..88f63f81 100644 --- a/lib/providers/strategy_provider.dart +++ b/lib/providers/strategy_provider.dart @@ -36,6 +36,7 @@ import 'package:icarus/providers/strategy_page.dart'; import 'package:icarus/providers/strategy_settings_provider.dart'; import 'package:icarus/providers/text_provider.dart'; import 'package:icarus/services/app_error_reporter.dart'; +import 'package:icarus/services/analytics_service.dart'; import 'package:hive_ce/hive.dart'; import 'package:icarus/const/drawing_element.dart'; import 'package:icarus/const/maps.dart'; @@ -3057,6 +3058,7 @@ class StrategyProvider extends Notifier { await Hive.box(HiveBoxNames.strategiesBox) .put(newStrategy.id, newStrategy); + unawaited(AnalyticsService.instance.capture('strategy_imported')); } finally { if (isZip) { try { @@ -3130,6 +3132,8 @@ class StrategyProvider extends Notifier { await Hive.box(HiveBoxNames.strategiesBox) .put(newStrategy.id, newStrategy); + unawaited(AnalyticsService.instance.capture('strategy_created')); + return newStrategy.id; } @@ -3179,6 +3183,12 @@ class StrategyProvider extends Notifier { encoder.create(outputFile); await encoder.addDirectory(stagingDirectory, includeDirName: false); await encoder.close(); + unawaited( + AnalyticsService.instance.capture( + 'content_exported', + properties: const {'content_type': 'folder'}, + ), + ); } finally { try { await stagingDirectory.delete(recursive: true); @@ -3204,6 +3214,12 @@ class StrategyProvider extends Notifier { encoder.create(outputFile); await encoder.addDirectory(stagingDirectory, includeDirName: false); await encoder.close(); + unawaited( + AnalyticsService.instance.capture( + 'content_exported', + properties: const {'content_type': 'library'}, + ), + ); } finally { try { await stagingDirectory.delete(recursive: true); @@ -3567,6 +3583,12 @@ class StrategyProvider extends Notifier { if (outputFile == null) return; await zipStrategy(id: id, outputFilePath: outputFile); + unawaited( + AnalyticsService.instance.capture( + 'content_exported', + properties: const {'content_type': 'strategy'}, + ), + ); } Future renameStrategy(String strategyID, String newName) async { diff --git a/lib/services/analytics_service.dart b/lib/services/analytics_service.dart new file mode 100644 index 00000000..fc2a184d --- /dev/null +++ b/lib/services/analytics_service.dart @@ -0,0 +1,133 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; +import 'package:http/http.dart' as http; +import 'package:icarus/const/settings.dart'; +import 'package:uuid/uuid.dart'; + +/// Minimal, anonymous product analytics. +/// +/// This intentionally uses PostHog's capture endpoint directly so analytics +/// works on Windows, which the official Flutter SDK does not currently support. +/// It does not collect screen views, exceptions, session replays, user-provided +/// content, or person profiles. +class AnalyticsService { + AnalyticsService._({http.Client? client}) : _client = client ?? http.Client(); + + static final AnalyticsService instance = AnalyticsService._(); + + static const String _projectToken = String.fromEnvironment( + 'POSTHOG_PROJECT_TOKEN', + ); + static const String _host = String.fromEnvironment( + 'POSTHOG_HOST', + defaultValue: 'https://us.i.posthog.com', + ); + static const String _environment = String.fromEnvironment( + 'ICARUS_ANALYTICS_ENVIRONMENT', + defaultValue: 'production', + ); + static const String storageBoxName = 'anonymous_analytics'; + static const String _anonymousIdKey = 'anonymous_id'; + + final http.Client _client; + static const String _enabledKey = 'enabled'; + + Box? _storage; + String? _anonymousId; + bool _enabled = true; + + bool get isConfigured => _projectToken.trim().isNotEmpty; + bool get isEnabled => _enabled; + + Future initialize() async { + _storage = Hive.box(storageBoxName); + _enabled = _storage!.get(_enabledKey, defaultValue: true) as bool; + _anonymousId = _storage!.get(_anonymousIdKey); + if (_anonymousId == null) { + _anonymousId = const Uuid().v4(); + await _storage!.put(_anonymousIdKey, _anonymousId!); + } + + unawaited(capture('app_opened')); + } + + Future setEnabled(bool enabled) async { + _enabled = enabled; + await _storage?.put(_enabledKey, enabled); + if (enabled) unawaited(capture('app_opened')); + } + + Future capture( + String event, { + Map? properties, + }) async { + final anonymousId = _anonymousId; + if (!_enabled || !isConfigured || anonymousId == null) return; + + final payload = { + 'api_key': _projectToken, + 'event': event, + 'uuid': const Uuid().v4(), + 'timestamp': DateTime.now().toUtc().toIso8601String(), + 'properties': { + 'distinct_id': anonymousId, + r'$process_person_profile': false, + r'$geoip_disable': true, + 'app_version': Settings.versionName, + 'build_number': Settings.versionNumber, + 'release_channel': kResolvedUpdateChannel, + 'analytics_environment': _environment, + 'platform': _platformName, + ...?properties, + }, + }; + + try { + await _client + .post( + _captureUri, + headers: const {'Content-Type': 'application/json'}, + body: jsonEncode(payload), + ) + .timeout(const Duration(seconds: 5)); + } catch (_) { + // Analytics must never affect app behavior or surface errors to users. + } + } + + Uri get _captureUri { + final normalizedHost = _host.trim().replaceFirst(RegExp(r'/+$'), ''); + return Uri.parse('$normalizedHost/i/v0/e/'); + } + + String get _platformName { + if (kIsWeb) return 'web'; + return switch (defaultTargetPlatform) { + TargetPlatform.android => 'android', + TargetPlatform.iOS => 'ios', + TargetPlatform.linux => 'linux', + TargetPlatform.macOS => 'macos', + TargetPlatform.windows => 'windows', + TargetPlatform.fuchsia => 'fuchsia', + }; + } +} + +final analyticsEnabledProvider = + NotifierProvider( + AnalyticsEnabledNotifier.new, +); + +class AnalyticsEnabledNotifier extends Notifier { + @override + bool build() => AnalyticsService.instance.isEnabled; + + Future setEnabled(bool enabled) async { + state = enabled; + await AnalyticsService.instance.setEnabled(enabled); + } +} diff --git a/lib/widgets/dialogs/create_lineup_dialog.dart b/lib/widgets/dialogs/create_lineup_dialog.dart index 061ee22b..d9414e83 100644 --- a/lib/widgets/dialogs/create_lineup_dialog.dart +++ b/lib/widgets/dialogs/create_lineup_dialog.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -7,6 +9,7 @@ import 'package:icarus/providers/action_provider.dart'; import 'package:icarus/providers/image_provider.dart'; import 'package:icarus/providers/interaction_state_provider.dart'; import 'package:icarus/services/clipboard_service.dart'; +import 'package:icarus/services/analytics_service.dart'; import 'package:icarus/widgets/dialogs/strategy/line_up_media_page.dart'; import 'package:path/path.dart' as path; import 'package:shadcn_ui/shadcn_ui.dart'; @@ -68,18 +71,18 @@ class _CreateLineupDialogState extends ConsumerState { ); if (existingItem != null) { ref.read(actionProvider.notifier).performTransaction( - groups: const [ActionGroup.lineUp], - mutation: () { - notifier.updateItem( - groupId: widget.lineUpGroupId!, - item: existingItem.copyWith( - youtubeLink: _youtubeLinkController.text, - notes: _notesController.text, - images: _imagePaths, - ), - ); - }, + groups: const [ActionGroup.lineUp], + mutation: () { + notifier.updateItem( + groupId: widget.lineUpGroupId!, + item: existingItem.copyWith( + youtubeLink: _youtubeLinkController.text, + notes: _notesController.text, + images: _imagePaths, + ), ); + }, + ); } } else { final currentAbility = lineUpState.currentAbility; @@ -97,18 +100,18 @@ class _CreateLineupDialogState extends ConsumerState { if (lineUpState.currentGroupId != null) { ref.read(actionProvider.notifier).performTransaction( - groups: const [ActionGroup.lineUp], - mutation: () { - notifier.addItemToGroup( - groupId: lineUpState.currentGroupId!, - item: item.copyWith( - ability: item.ability.copyWith( - lineUpID: lineUpState.currentGroupId, - ), - ), - ); - }, + groups: const [ActionGroup.lineUp], + mutation: () { + notifier.addItemToGroup( + groupId: lineUpState.currentGroupId!, + item: item.copyWith( + ability: item.ability.copyWith( + lineUpID: lineUpState.currentGroupId, + ), + ), ); + }, + ); } else { final currentAgent = lineUpState.currentAgent; if (currentAgent == null) { @@ -128,6 +131,17 @@ class _CreateLineupDialogState extends ConsumerState { ), ); } + + unawaited( + AnalyticsService.instance.capture( + 'lineup_created', + properties: { + 'has_video': _youtubeLinkController.text.trim().isNotEmpty, + 'has_notes': _notesController.text.trim().isNotEmpty, + 'has_images': _imagePaths.isNotEmpty, + }, + ), + ); } ref diff --git a/lib/widgets/settings_tab.dart b/lib/widgets/settings_tab.dart index 9152bb70..cae706e7 100644 --- a/lib/widgets/settings_tab.dart +++ b/lib/widgets/settings_tab.dart @@ -11,6 +11,7 @@ import 'package:icarus/providers/user_preferences_provider.dart'; import 'package:icarus/providers/marker_sizes_sync.dart'; import 'package:icarus/providers/strategy_provider.dart'; import 'package:icarus/providers/strategy_settings_provider.dart'; +import 'package:icarus/services/analytics_service.dart'; import 'package:icarus/widgets/map_theme_settings_section.dart'; import 'package:icarus/widgets/settings_scope_card.dart'; import 'package:icarus/widgets/text_editing_shortcut_scope.dart'; @@ -29,6 +30,7 @@ enum _SettingsSection { globalSaving, globalMapVisibility, globalMapProfiles, + globalPrivacy, shortcuts, } @@ -190,6 +192,7 @@ class _SettingsTabState extends ConsumerState { case _SettingsSection.globalSaving: case _SettingsSection.globalMapVisibility: case _SettingsSection.globalMapProfiles: + case _SettingsSection.globalPrivacy: return _SettingsMode.global; case _SettingsSection.shortcuts: return _SettingsMode.shortcuts; @@ -519,6 +522,27 @@ class _GlobalSettingsSections extends ConsumerWidget { const SizedBox(height: 20), const _SectionDivider(), const SizedBox(height: 20), + SettingsScopeCard( + key: sectionKeys[_SettingsSection.globalPrivacy], + title: "Privacy", + description: + "Control anonymous, privacy-preserving product analytics.", + child: _SettingsToggleTile( + icon: Icons.analytics_outlined, + title: "Anonymous analytics", + description: + "Share app opens and a few feature-use events. Icarus never sends strategy names or content, personal details, screen recordings, or crash reports.", + value: ref.watch(analyticsEnabledProvider), + onChanged: (value) async { + await ref + .read(analyticsEnabledProvider.notifier) + .setEnabled(value); + }, + ), + ), + const SizedBox(height: 20), + const _SectionDivider(), + const SizedBox(height: 20), KeyedSubtree( key: sectionKeys[_SettingsSection.globalMapProfiles], child: const MapThemeSettingsSection( @@ -1169,6 +1193,12 @@ class _SettingsNavigationRail extends StatelessWidget { isSelected: selectedSection == _SettingsSection.globalMapProfiles, onTap: () => onSectionSelected(_SettingsSection.globalMapProfiles), ), + _SettingsNavItem( + icon: Icons.privacy_tip_outlined, + label: "Privacy", + isSelected: selectedSection == _SettingsSection.globalPrivacy, + onTap: () => onSectionSelected(_SettingsSection.globalPrivacy), + ), _SettingsNavItem( icon: Icons.keyboard_alt_outlined, label: "Keybinds", diff --git a/pubspec.yaml b/pubspec.yaml index f0d22e48..2afe3fb1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "A new Flutter project." publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: 4.5.0+93 #version number +version: 4.5.1+94 #version number environment: sdk: ">=3.4.3 <4.0.0" @@ -66,7 +66,7 @@ msix_config: identity_name: DaraA.IcarusValorantStrategiesLineups publisher: CN=39055448-F6AA-42E9-91F6-9CF8828C444C logo_path: windows\runner\resources\app_icon.ico - msix_version: 4.5.0.0 #verion number #last 0 needs to be there for msix to work + msix_version: 4.5.1.0 #verion number #last 0 needs to be there for msix to work file_extension: .ica store: true diff --git a/release/metadata/4.5.1+94.json b/release/metadata/4.5.1+94.json new file mode 100644 index 00000000..2cbb9953 --- /dev/null +++ b/release/metadata/4.5.1+94.json @@ -0,0 +1,26 @@ +{ + "version": "4.5.1+94", + "shortVersion": 94, + "title": "4.5.1 Update", + "description": "Desktop release metadata for Icarus.", + "date": "2026-07-19", + "mandatory": false, + "channels": [ + "desktop", + "prerelease", + "stable" + ], + "platforms": [ + "windows" + ], + "changes": [ + { + "message": "Added anonymous analytics to help improve Icarus. No strategy content or personal information is collected, and analytics can be turned off under Settings > Privacy.", + "type": "feature" + }, + { + "message": "Updated Veto's Crosscut range to match its latest 30-meter in-game range.", + "type": "bugfix" + } + ] +} diff --git a/scripts/build_desktop_release.ps1 b/scripts/build_desktop_release.ps1 index ed7882df..9c1b0613 100644 --- a/scripts/build_desktop_release.ps1 +++ b/scripts/build_desktop_release.ps1 @@ -8,6 +8,8 @@ param( [string]$AppArchiveBaseUrl = "https://sunkenintime.github.io/icarus/updates/windows/stable", [string]$MetadataTitle, [string]$InitialChangeMessage = "Describe this release before publishing.", + [string]$PostHogProjectToken = $env:POSTHOG_PROJECT_TOKEN, + [string]$PostHogHost = $(if ($env:POSTHOG_HOST) { $env:POSTHOG_HOST } else { "https://us.i.posthog.com" }), [switch]$SkipPubGet ) @@ -23,14 +25,31 @@ if (-not $SkipPubGet) { Invoke-RepoCommand -WorkingDirectory $repoRoot -Command "fvm" -Arguments @("flutter", "pub", "get") } -Invoke-RepoCommand -WorkingDirectory $repoRoot -Command "fvm" -Arguments @( - "dart", - "run", - "desktop_updater:release", - "windows", - "--release", - "--dart-define=ICARUS_UPDATE_CHANNEL=$Channel" -) +$dartDefinesPath = $null +try { + $releaseArguments = @( + "dart", + "run", + "desktop_updater:release", + "windows", + "--release", + "--dart-define=ICARUS_UPDATE_CHANNEL=$Channel" + ) + if (-not [string]::IsNullOrWhiteSpace($PostHogProjectToken)) { + $dartDefinesPath = Join-Path ([System.IO.Path]::GetTempPath()) ("icarus-dart-defines-{0}.json" -f [guid]::NewGuid()) + Write-JsonFileUtf8 -Path $dartDefinesPath -Value @{ + POSTHOG_PROJECT_TOKEN = $PostHogProjectToken + POSTHOG_HOST = $PostHogHost + } + $releaseArguments += "--dart-define-from-file=$dartDefinesPath" + } + Invoke-RepoCommand -WorkingDirectory $repoRoot -Command "fvm" -Arguments $releaseArguments +} +finally { + if ($null -ne $dartDefinesPath -and (Test-Path -LiteralPath $dartDefinesPath)) { + Remove-Item -LiteralPath $dartDefinesPath -Force + } +} Invoke-RepoCommand -WorkingDirectory $repoRoot -Command "fvm" -Arguments @("dart", "run", "desktop_updater:archive", "windows") Invoke-RepoCommand -WorkingDirectory $repoRoot -Command "powershell" -Arguments @("-ExecutionPolicy", "Bypass", "-File", "installer/build_installer.ps1", "-Configuration", "Release") diff --git a/scripts/build_store_release.ps1 b/scripts/build_store_release.ps1 index a23bfc5c..55e1d84e 100644 --- a/scripts/build_store_release.ps1 +++ b/scripts/build_store_release.ps1 @@ -1,5 +1,7 @@ param( [string]$OutputDir = "release\out\store", + [string]$PostHogProjectToken = $env:POSTHOG_PROJECT_TOKEN, + [string]$PostHogHost = $(if ($env:POSTHOG_HOST) { $env:POSTHOG_HOST } else { "https://us.i.posthog.com" }), [switch]$SkipPubGet ) @@ -10,12 +12,41 @@ Set-StrictMode -Version Latest $repoRoot = Get-RepoRoot -ScriptDirectory $PSScriptRoot $env:FLUTTER_ROOT = Get-FlutterRoot -RepoRoot $repoRoot +$windowsBuildRoot = Resolve-RepoPath -RepoRoot $repoRoot -RelativePath "build\windows" if (-not $SkipPubGet) { Invoke-RepoCommand -WorkingDirectory $repoRoot -Command "fvm" -Arguments @("flutter", "pub", "get") } -Invoke-RepoCommand -WorkingDirectory $repoRoot -Command "fvm" -Arguments @("dart", "run", "msix:create") +$dartDefinesPath = $null +try { + if (Test-Path -LiteralPath $windowsBuildRoot) { + Remove-Item -LiteralPath $windowsBuildRoot -Recurse -Force + } + + $flutterBuildArguments = @("flutter", "build", "windows", "--release") + if (-not [string]::IsNullOrWhiteSpace($PostHogProjectToken)) { + $dartDefinesPath = Join-Path ([System.IO.Path]::GetTempPath()) ("icarus-dart-defines-{0}.json" -f [guid]::NewGuid()) + Write-JsonFileUtf8 -Path $dartDefinesPath -Value @{ + POSTHOG_PROJECT_TOKEN = $PostHogProjectToken + POSTHOG_HOST = $PostHogHost + } + $flutterBuildArguments += "--dart-define-from-file=$dartDefinesPath" + } + Invoke-RepoCommand -WorkingDirectory $repoRoot -Command "fvm" -Arguments $flutterBuildArguments + Invoke-RepoCommand -WorkingDirectory $repoRoot -Command "fvm" -Arguments @( + "dart", + "run", + "msix:create", + "--build-windows", + "false" + ) +} +finally { + if ($null -ne $dartDefinesPath -and (Test-Path -LiteralPath $dartDefinesPath)) { + Remove-Item -LiteralPath $dartDefinesPath -Force + } +} $outputRoot = Resolve-RepoPath -RepoRoot $repoRoot -RelativePath $OutputDir if (Test-Path $outputRoot) {