Skip to content

Latest commit

 

History

History
341 lines (276 loc) · 10.5 KB

File metadata and controls

341 lines (276 loc) · 10.5 KB

EyeGuard - Claude Code Development Notes

This document contains important learnings and patterns discovered while developing EyeGuard with Claude Code.

Microsoft Store Integration

StoreContext Initialization

The StoreContext API requires proper initialization with a window handle to function correctly, especially for purchase dialogs.

Key Requirements:

  • Must be called on the UI thread
  • Needs a valid window handle via WinRT.Interop.WindowNative.GetWindowHandle(window)
  • Must initialize with WinRT.Interop.InitializeWithWindow.Initialize(storeContext, windowHandle)

Common Issues:

  • RequestPurchaseAsync() throws "Invalid window handle (0x80070578)" if not properly initialized
  • Error message: "This function must be called from a UI thread"

Solution Pattern:

private void EnsureStoreContext()
{
    if (_storeContext == null)
    {
        _storeContext = StoreContext.GetDefault();
        var app = Application.Current as App;
        if (app != null && app.SecretWindow != null)
        {
            IntPtr windowHandle = WinRT.Interop.WindowNative.GetWindowHandle(app.SecretWindow);
            WinRT.Interop.InitializeWithWindow.Initialize(_storeContext, windowHandle);
        }
    }
}

Store ID vs Product ID

When working with Microsoft Store add-ons, there's an important distinction:

  • Product ID (InAppOfferToken): The identifier you configure in Partner Center (e.g., "eyeguardsettingsaddon")
  • Store ID: The actual ID used in the Products dictionary (e.g., "9N6MPSMNK7QD")

When calling RequestPurchaseAsync(), you must use the Store ID, not the Product ID.

Finding the Store ID at runtime:

StoreProductQueryResult addonsResult = await _storeContext.GetAssociatedStoreProductsAsync(
    new string[] { "Durable" });

// Iterate through products to find by InAppOfferToken
foreach (var kvp in addonsResult.Products)
{
    if (kvp.Value.InAppOfferToken == SETTINGS_ADDON_PRODUCT_ID)
    {
        return kvp.Value; // Contains StoreId property
    }
}

License Caching Strategy

Implement a multi-layer caching strategy for license checks to handle network outages gracefully:

  1. In-memory cache: Short-term (5 minutes) to reduce API calls
  2. Persistent cache: Store in ApplicationData.LocalSettings for offline fallback
  3. Fallback logic: On Store API failure, use cached status

Benefits:

  • Users retain access during internet outages if previously verified
  • Reduces unnecessary Store API calls
  • Better user experience with offline support

Pattern:

try
{
    bool isPurchased = await CheckLicenseStatusAsync();
    _cachedLicenseStatus = isPurchased;
    SettingsService.Instance.CachedSettingsAddonPurchased = isPurchased; // Persist
    return isPurchased;
}
catch (Exception ex)
{
    var cachedStatus = SettingsService.Instance.CachedSettingsAddonPurchased;
    return cachedStatus ?? false; // Default to not purchased if never verified
}

WinUI 3 Patterns

Avoiding Reentrancy Errors

WinUI 3 can throw reentrancy errors when doing async work during page load events.

Error:

WinRT originate error - 0x80004005: 'Reentrancy was detected in this XAML application'

Solution - Use DispatcherQueue:

private void HomePage_Loaded(object sender, RoutedEventArgs e)
{
    // Defer async work to avoid reentrancy
    DispatcherQueue.TryEnqueue(async () =>
    {
        await CheckAndShowAddonPromotion();
    });
}

DropDownButton with MenuFlyout

For creating button menus with quick-select options:

<DropDownButton Content="Pause Breaks">
    <DropDownButton.Flyout>
        <MenuFlyout Placement="Bottom">
            <MenuFlyoutItem Text="30 minutes" Click="QuickPause30Minutes_Click"/>
            <MenuFlyoutItem Text="1 hour" Click="QuickPause1Hour_Click"/>
            <MenuFlyoutSeparator/>
            <MenuFlyoutItem Text="Custom..." Click="QuickPauseCustom_Click"/>
        </MenuFlyout>
    </DropDownButton.Flyout>
</DropDownButton>

Slider Interaction Pattern

For better UX, sliders should update display values in real-time but only save settings when the user releases the slider.

Benefits:

  • Prevents excessive setting saves and event triggers while dragging
  • Reduces unnecessary work (e.g., timer resets) during adjustment
  • Provides immediate visual feedback

Implementation:

// In constructor, attach both events
BreakIntervalSlider.ValueChanged += BreakIntervalSlider_ValueChanged;
BreakIntervalSlider.ManipulationCompleted += BreakIntervalSlider_ManipulationCompleted;

// ValueChanged updates display only
private void BreakIntervalSlider_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
{
    int value = (int)e.NewValue;
    BreakIntervalValueText.Text = value.ToString();
}

// ManipulationCompleted saves the setting
private void BreakIntervalSlider_ManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)
{
    int value = (int)BreakIntervalSlider.Value;
    SettingsService.Instance.BreakInterval = value;
}

XAML:

<Slider x:Name="BreakIntervalSlider"
        ManipulationMode="TranslateX"
        ... />

Development Testing

Test Mode Pattern

Use conditional compilation to ensure production builds always use real Store API:

public enum StoreTestMode
{
    DEV_NOT_PURCHASED,  // Simulate no license
    DEV_PURCHASED,      // Simulate has license
    STORE               // Real Store API
}

#if DEBUG
    // Can be changed for testing in Debug builds
    public const StoreTestMode TEST_MODE = StoreTestMode.STORE;
#else
    // Release builds always use real Store
    public const StoreTestMode TEST_MODE = StoreTestMode.STORE;
#endif

This prevents accidentally shipping debug/test modes in production while allowing easy testing during development.

Git Configuration

Line Endings

For Windows development, configure Git to use CRLF:

git config core.autocrlf true

This eliminates warnings about CRLF/LF conversion during commits.

Architecture Patterns

Settings Service Pattern

Centralize app settings in a singleton service with:

  • Property getters/setters backed by ApplicationData.LocalSettings
  • Events for settings changes (e.g., BreakIntervalChanged)
  • Validation and clamping of values
  • Default values for first run

UI License Enforcement

Enforce licensing at the UI visibility level rather than in individual handlers:

  • Hide/show controls based on license status on page load
  • Avoids redundant license checks in every button handler
  • Cleaner, more maintainable code
  • Single source of truth for license enforcement

Smart State Update Pattern

When a setting changes that affects an active state (like a running timer), decide intelligently whether to apply the change immediately.

Pattern: Only update active state when the new value would trigger an earlier or more urgent action.

Example:

private void OnSettingChanged(object sender, int newValue)
{
    // Only update if new value is more restrictive/urgent than current state
    if (newValue < currentStateValue)
    {
        UpdateCurrentState(newValue);
    }
    // Otherwise, let current state complete naturally
}

File Path Setting Validation

When storing file paths in settings, validate they exist before use and provide fallback behavior.

Pattern:

public string FileSetting
{
    get
    {
        if (TryGetStoredValue(out var filePath))
        {
            // Trust app resource URIs
            if (filePath.StartsWith("ms-appx://"))
                return filePath;

            // Validate filesystem paths
            if (System.IO.File.Exists(filePath))
                return filePath;
        }
        return DEFAULT_VALUE;
    }
}

Benefits:

  • Graceful degradation if user deletes files
  • No crashes from missing resources
  • Transparent fallback behavior

Media File Preview with Validation

When allowing users to select media files, provide preview functionality with duration display and validation.

Key components:

  1. Duration extraction: Load media temporarily to read metadata
  2. Play/Stop toggle: Allow users to preview and abort
  3. Validation warnings: Alert if media doesn't meet requirements
  4. Async loading: Use TaskCompletionSource to wait for MediaOpened event

Pattern:

private async void UpdateMediaInfo(string mediaPath)
{
    var tempPlayer = new MediaPlayer { Source = MediaSource.CreateFromUri(new Uri(mediaPath)) };

    var tcs = new TaskCompletionSource<bool>();
    tempPlayer.MediaOpened += (s, e) => tcs.TrySetResult(true);
    tempPlayer.MediaFailed += (s, e) => tcs.TrySetResult(false);

    await tcs.Task;

    var duration = tempPlayer.PlaybackSession.NaturalDuration;
    // Display duration, show warnings if needed

    tempPlayer.Dispose();
}

In-App Messaging Pattern

Use Grid layout to add non-intrusive informational footers to full-screen or centered UI.

Pattern:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>

    <!-- Main content (centered or fullscreen) -->
    <ContentControl Grid.Row="0" />

    <!-- Footer message (bottom-aligned) -->
    <StackPanel Grid.Row="1" Margin="20">
        <TextBlock Style="{StaticResource CaptionTextBlockStyle}" Opacity="0.7"/>
    </StackPanel>
</Grid>

Use cases:

  • Feature announcements
  • Tips and help text
  • Version update notifications
  • Action prompts without dialogs

Timezone Handling

When working with user-selected times:

  • Always display pickers in local timezone
  • Store internally as UTC (DateTime.ToUniversalTime())
  • Convert back to local for display (DateTime.ToLocalTime())
  • Add UI hints: "Select date and time in your local timezone"

Pattern:

// User selects local time
var localDateTime = PauseDatePicker.Date + PauseTimePicker.Time;
var localDateTimeObj = localDateTime.DateTime;

// Convert to UTC for storage
var utcDateTime = localDateTimeObj.ToUniversalTime();
SettingsService.Instance.PauseUntil = utcDateTime;

// Convert back to local for display
var localTime = pauseUntil.Value.ToLocalTime();
PauseDatePicker.Date = new DateTimeOffset(localTime.Date);

Useful Resources