Files

909 lines
25 KiB
Markdown

# MASpotlightTour
A powerful, declarative spotlight tour and onboarding library for .NET MAUI applications. Create beautiful feature tours with spotlight overlays, callout cards, and step-by-step user guidance using simple XAML attached properties.
[![NuGet Version](https://img.shields.io/nuget/v/MarketAlly.SpotlightTour.Maui.svg?style=flat)](https://www.nuget.org/packages/MarketAlly.SpotlightTour.Maui/)
[![NuGet Downloads](https://img.shields.io/nuget/dt/MarketAlly.SpotlightTour.Maui.svg)](https://www.nuget.org/packages/MarketAlly.SpotlightTour.Maui/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![.NET](https://img.shields.io/badge/.NET-9.0-512BD4)](https://dotnet.microsoft.com/download)
[![Platform](https://img.shields.io/badge/Platform-iOS%20%7C%20Android%20%7C%20Windows%20%7C%20macOS-lightgray)](https://dotnet.microsoft.com/apps/maui)
## Features
- **Declarative XAML Syntax** - Tag UI elements with attached properties; no code-behind required for basic tours
- **Multiple Display Modes** - Spotlight with callout, callout-only (no dimming), or spotlight with inline labels
- **Flexible Callout Positioning** - Following (relative to element), fixed corner, or auto-corner placement
- **Customizable Spotlights** - Rectangle, rounded rectangle, or circle shapes with configurable padding
- **Corner Navigator** - Compact navigation control with step indicators and Previous/Next/Skip buttons
- **Animation Effects** - Entrance/exit animations, step transitions, and spotlight effects with presets (Subtle, Playful, Elegant, Snappy)
- **Intro Views** - Show custom welcome screens before the tour begins
- **Auto-Advance** - Automatically progress through steps on a timer
- **Tour Looping** - Repeat tours automatically for kiosk/demo modes
- **Theme Support** - Light, Dark, and System theme modes with automatic switching
- **RTL Support** - Full right-to-left layout support for Arabic, Hebrew, and other RTL languages
- **Awaitable Tours** - Chain actions after tour completion using async/await
- **Tour Groups** - Organize multiple tours on the same page
- **Localization** - Built-in support for 8 languages (EN, ES, FR, DE, ZH, JA, PT, IT) with easy extensibility
- **Cross-Platform** - iOS, Android, Windows, and macOS
## Installation
```bash
dotnet add package MarketAlly.SpotlightTour.Maui
```
Or via the NuGet Package Manager:
```
Install-Package MarketAlly.SpotlightTour.Maui
```
## Quick Start
### 1. Add the namespace to your XAML
```xml
xmlns:tour="clr-namespace:MarketAlly.SpotlightTour.Maui;assembly=MarketAlly.SpotlightTour.Maui"
```
### 2. Tag elements you want to highlight
```xml
<Button
Text="Click Me"
tour:Onboarding.StepKey="welcome"
tour:Onboarding.Title="Welcome!"
tour:Onboarding.Description="This button does something amazing."
tour:Onboarding.Order="1" />
<Entry
Placeholder="Enter your name"
tour:Onboarding.StepKey="name-input"
tour:Onboarding.Title="Your Name"
tour:Onboarding.Description="Type your name here to get started."
tour:Onboarding.Order="2" />
```
### 3. Add the OnboardingHost overlay
```xml
<Grid>
<!-- Your page content -->
<VerticalStackLayout>
<!-- ... your UI elements ... -->
</VerticalStackLayout>
<!-- Add at the end, as an overlay -->
<tour:OnboardingHost x:Name="TourHost" />
</Grid>
```
### 4. Start the tour
```csharp
// Start the tour (scans for tagged elements automatically)
await TourHost.StartTourAsync(this.Content);
```
## Display Modes
### SpotlightWithCallout (Default)
Full experience with dimmed overlay, spotlight cutout, and callout card with navigation.
```xml
<tour:OnboardingHost DisplayMode="SpotlightWithCallout" />
```
### CalloutOnly
Floating callout cards without dimming - ideal for light-touch guidance that doesn't block interaction.
```xml
<tour:OnboardingHost DisplayMode="CalloutOnly" />
```
### SpotlightWithInlineLabel
Dimmed overlay with spotlight, but uses compact inline labels instead of callout cards. Best used with the corner navigator.
```xml
<tour:OnboardingHost
DisplayMode="SpotlightWithInlineLabel"
ShowCornerNavigator="True" />
```
## Callout Positioning
### Following Mode (Default)
Callout positions relative to the highlighted element (Top, Bottom, Left, Right, or Auto).
```xml
<tour:OnboardingHost CalloutPositionMode="Following" />
<!-- Per-element placement override -->
<Button tour:Onboarding.Placement="Top" ... />
```
### Fixed Corner Mode
Callout stays in a fixed screen corner while spotlights move.
```xml
<tour:OnboardingHost
CalloutPositionMode="FixedCorner"
CalloutCorner="BottomLeft" />
```
### Auto Corner Mode
Callout automatically positions in the corner that least interferes with the highlighted element.
```xml
<tour:OnboardingHost CalloutPositionMode="AutoCorner" />
```
## Corner Navigator
A compact navigation control that sits in a screen corner with step indicators and navigation buttons.
```xml
<tour:OnboardingHost
ShowCornerNavigator="True"
CornerNavigatorPlacement="BottomRight"
ShowSkipButton="True" />
```
When set to `Auto`, the navigator automatically positions to avoid overlapping with the spotlight and callout card.
```xml
<tour:OnboardingHost
ShowCornerNavigator="True"
CornerNavigatorPlacement="Auto"
CalloutPositionMode="AutoCorner" />
```
## Intro Views
Show a custom welcome screen before the tour begins:
```xml
<tour:OnboardingHost x:Name="TourHost">
<tour:OnboardingHost.IntroView>
<Frame BackgroundColor="White" Padding="30" CornerRadius="16">
<VerticalStackLayout Spacing="20">
<Label Text="Welcome!" FontSize="28" FontAttributes="Bold" />
<Label Text="Let us show you around." />
<Button Text="Start Tour" Clicked="OnStartTourClicked" />
</VerticalStackLayout>
</Frame>
</tour:OnboardingHost.IntroView>
</tour:OnboardingHost>
```
```csharp
private async void OnStartTourClicked(object sender, EventArgs e)
{
TourHost.DismissIntro(); // Dismisses intro and continues to tour
}
// Start with intro
await TourHost.StartTourWithIntroAsync(this.Content);
```
## Spotlight Customization
### Shapes
```xml
<!-- Rounded rectangle (default) -->
<Button tour:Onboarding.SpotlightShape="RoundedRectangle"
tour:Onboarding.SpotlightCornerRadius="12" ... />
<!-- Circle -->
<Image tour:Onboarding.SpotlightShape="Circle" ... />
<!-- Rectangle -->
<Frame tour:Onboarding.SpotlightShape="Rectangle" ... />
```
### Padding
```xml
<!-- Uniform padding -->
<Button tour:Onboarding.SpotlightPadding="16" ... />
<!-- Per-side padding -->
<Button tour:Onboarding.SpotlightPadding="8,16,8,16" ... />
```
### Tap Behavior
```xml
<!-- Tap spotlight to advance -->
<Button tour:Onboarding.TapBehavior="Advance" ... />
<!-- Tap spotlight to close tour -->
<Button tour:Onboarding.TapBehavior="Close" ... />
<!-- Allow interaction with underlying element -->
<Button tour:Onboarding.TapBehavior="AllowInteraction" ... />
```
## Theming
```xml
<!-- Light theme -->
<tour:OnboardingHost Theme="Light" />
<!-- Dark theme -->
<tour:OnboardingHost Theme="Dark" />
<!-- Follow system theme -->
<tour:OnboardingHost Theme="System" />
```
### Overlay Customization
```xml
<tour:OnboardingHost
DimOpacity="0.7"
DimColor="Black" />
```
## Tour Groups
Organize multiple tours on the same page:
```xml
<!-- Basic features tour -->
<Button tour:Onboarding.Group="basics" tour:Onboarding.Order="1" ... />
<Entry tour:Onboarding.Group="basics" tour:Onboarding.Order="2" ... />
<!-- Advanced features tour -->
<Button tour:Onboarding.Group="advanced" tour:Onboarding.Order="1" ... />
<Slider tour:Onboarding.Group="advanced" tour:Onboarding.Order="2" ... />
```
```csharp
// Start specific group
await TourHost.StartTourAsync(this.Content, group: "basics");
```
## Auto-Advance & Looping
### Auto-Advance
Automatically progress through steps:
```xml
<tour:OnboardingHost AutoAdvanceDelay="3000" /> <!-- 3 seconds per step -->
```
### Auto-Loop
Repeat the tour automatically (great for kiosk/demo modes):
```xml
<tour:OnboardingHost
AutoAdvanceDelay="2000"
AutoLoop="3" /> <!-- Repeat 3 times -->
```
### Auto-Start
Start the tour automatically when the page loads:
```xml
<tour:OnboardingHost AutoStartDelay="500" /> <!-- Start after 500ms -->
```
## Awaitable Tours
Chain actions after tour completion:
```csharp
var result = await TourHost.StartTourAsync(this.Content);
switch (result)
{
case TourResult.Completed:
await DisplayAlert("Done!", "You completed the tour.", "OK");
break;
case TourResult.Skipped:
// User skipped the tour
break;
case TourResult.Cancelled:
// Tour was cancelled programmatically
break;
case TourResult.NoSteps:
// No tour steps were found
break;
}
```
## Programmatic Navigation
```csharp
// Navigate to specific step
await TourHost.GoToStepAsync(2);
// Navigate by step key
var step = OnboardingScanner.FindStepByKey(this.Content, "welcome");
// Go to next/previous (async methods preferred)
await TourHost.GoToNextStepAsync();
await TourHost.GoToPreviousStepAsync();
// End tour
await TourHost.CompleteTourAsync();
TourHost.SkipTour();
TourHost.StopTour();
```
## Step Actions
Execute custom code when entering or leaving tour steps. Perfect for UI preparation like switching tabs, opening drawers, or triggering animations before the spotlight appears.
### Attached Property (Per-Element)
```csharp
// In code-behind, attach action directly to the element
Onboarding.SetOnEntering(SettingsTab, async (step, cancellationToken) =>
{
// Switch to the settings tab before spotlight appears
MainTabView.SelectedIndex = 2;
await Task.Delay(100); // Wait for tab transition
});
Onboarding.SetOnLeaving(SettingsTab, async (step, cancellationToken) =>
{
// Cleanup when leaving this step
MainTabView.SelectedIndex = 0;
});
```
### Centralized Registration (By StepKey)
```csharp
// Register actions by step key - keeps tour logic organized
TourHost.RegisterStepEnteringAction("drawer-step", async (step, token) =>
{
Shell.Current.FlyoutIsPresented = true;
await Task.Delay(250); // Wait for drawer animation
});
TourHost.RegisterStepEnteringAction("settings-tab", async (step, token) =>
{
SettingsTabButton.IsSelected = true;
});
// Remove registered actions when no longer needed
TourHost.UnregisterStepEnteringAction("drawer-step");
TourHost.ClearStepActions(); // Clear all
```
### Step Action Events
```csharp
// Global event - fires for every step (useful for analytics, logging, conditional skipping)
TourHost.StepEntering += async (sender, e) =>
{
// Skip premium features for non-premium users
if (e.Step.StepKey == "premium-feature" && !currentUser.IsPremium)
{
e.Skip = true; // Skip this step, move to next
return;
}
// Log analytics
Analytics.Track("tour_step_entering", e.Step.StepKey);
};
// Fires after step is fully visible (animations complete)
TourHost.StepEntered += (sender, e) =>
{
Console.WriteLine($"Now viewing step: {e.Step.Title}");
};
// Fires before leaving current step
TourHost.StepLeaving += async (sender, e) =>
{
// Perform cleanup before navigating away
await SaveStepProgressAsync(e.StepIndex);
};
```
### Execution Order
Actions execute in this order when navigating to a step:
1. `StepLeaving` event + actions (previous step)
2. `StepEntering` event (can set `Skip = true`)
3. `OnEntering` attached property action
4. Registered entering action (by StepKey)
5. Scroll target into view & animate spotlight
6. `StepEntered` event
## Events
```csharp
// Tour lifecycle events
TourHost.TourStarted += (s, e) => { /* Tour began */ };
TourHost.TourCompleted += (s, e) => { /* User finished all steps */ };
TourHost.TourSkipped += (s, e) => { /* User skipped */ };
TourHost.TourEnded += (s, e) => { /* Tour ended (any reason) */ };
// Step navigation events
TourHost.StepEntering += async (s, e) => { /* Before entering step - can set e.Skip = true */ };
TourHost.StepEntered += (s, e) => { /* After step is visible */ };
TourHost.StepLeaving += async (s, e) => { /* Before leaving step */ };
TourHost.StepChanged += (s, e) =>
{
Console.WriteLine($"Step {e.StepIndex + 1}/{e.TotalSteps}: {e.Step.Title}");
};
// Intro events
TourHost.IntroShown += (s, e) => { /* Intro view displayed */ };
TourHost.IntroDismissed += (s, e) => { /* Intro view dismissed */ };
```
## Animation Effects
The library provides a comprehensive animation system with entrance/exit animations, step transitions, and spotlight effects.
### Animation Configuration
Configure animations using the `AnimationConfiguration` class:
```csharp
using MarketAlly.SpotlightTour.Maui.Animations;
// Use a preset
var playful = AnimationConfiguration.Playful;
var elegant = AnimationConfiguration.Elegant;
var snappy = AnimationConfiguration.Snappy;
var subtle = AnimationConfiguration.Subtle;
// Or customize your own
var custom = new AnimationConfiguration
{
// Entrance animations
CalloutEntrance = EntranceAnimation.SlideUpFade,
OverlayEntrance = EntranceAnimation.FadeIn,
NavigatorEntrance = EntranceAnimation.SlideFromRight,
InlineLabelEntrance = EntranceAnimation.PopIn,
// Exit animations
CalloutExit = ExitAnimation.FadeOut,
OverlayExit = ExitAnimation.FadeOut,
// Step transitions
StepTransition = StepTransition.Slide,
// Spotlight effects (continuous animations)
SpotlightEffect = SpotlightEffect.Pulse,
// Timing
EntranceDurationMs = 300,
ExitDurationMs = 250,
StepTransitionDurationMs = 200,
// Easing
Easing = AnimationEasing.CubicInOut
};
```
### Using Animations with OnboardingHost
```xml
<tour:OnboardingHost
x:Name="TourHost"
AnimationsEnabled="True"
AnimationDuration="300" />
```
```csharp
// Apply animation configuration
TourHost.AnimationConfiguration = AnimationConfiguration.Playful;
// Or use custom configuration
TourHost.AnimationConfiguration = new AnimationConfiguration
{
SpotlightEffect = SpotlightEffect.Breathe,
CalloutEntrance = EntranceAnimation.BounceIn,
StepTransition = StepTransition.Push
};
```
### Available Animation Types
#### Entrance Animations
| Animation | Description |
|-----------|-------------|
| `None` | No animation |
| `FadeIn` | Fade in from transparent |
| `SlideFromLeft` | Slide in from left |
| `SlideFromRight` | Slide in from right |
| `SlideFromTop` | Slide in from top |
| `SlideFromBottom` | Slide in from bottom |
| `ScaleUp` | Scale up from small |
| `BounceIn` | Bounce in with overshoot |
| `PopIn` | Pop in with scale |
| `SlideUpFade` | Slide up while fading in |
| `ZoomIn` | Zoom in from small |
#### Exit Animations
| Animation | Description |
|-----------|-------------|
| `None` | No animation |
| `FadeOut` | Fade out to transparent |
| `SlideToLeft` | Slide out to left |
| `SlideToRight` | Slide out to right |
| `SlideToTop` | Slide out to top |
| `SlideToBottom` | Slide out to bottom |
| `ScaleDown` | Scale down to small |
| `ZoomOut` | Zoom out |
| `PopOut` | Pop out with scale |
#### Spotlight Effects (Continuous)
| Effect | Description |
|--------|-------------|
| `None` | No effect |
| `Pulse` | Gentle pulsing scale |
| `Breathe` | Slow breathing scale |
| `Glow` | Opacity breathing effect |
#### Step Transitions
| Transition | Description |
|------------|-------------|
| `Crossfade` | Fade between steps |
| `Slide` | Slide in direction of navigation |
| `Push` | Push old content out |
### Animation Presets
```csharp
// Subtle - minimal, professional animations
AnimationConfiguration.Subtle
// Playful - bouncy, fun animations
AnimationConfiguration.Playful
// Elegant - smooth, refined animations
AnimationConfiguration.Elegant
// Snappy - fast, responsive animations
AnimationConfiguration.Snappy
// None - no animations
AnimationConfiguration.None
```
### Disable Animations
```xml
<!-- Disable all animations -->
<tour:OnboardingHost AnimationsEnabled="False" />
```
### Legacy Animation Duration
For simple animation timing control:
```xml
<!-- Custom animation duration (milliseconds) -->
<tour:OnboardingHost AnimationDuration="400" />
```
## Configuration
The library provides a `TourConfiguration` class for fine-grained control over timing, sizing, and visual settings:
```csharp
// Create custom configuration
var config = new TourConfiguration
{
// Timing
ScrollTimeoutMs = 1500, // Timeout for scroll operations
LayoutSettleDelayMs = 100, // Delay after scrolling
DefaultAnimationDurationMs = 300, // Animation duration
// Sizing
CalloutMaxWidth = 450, // Maximum callout width
NavigatorEstimatedWidth = 220, // Navigator width for auto-placement
DefaultMargin = 16, // Default margin from edges
// Visual
DisabledButtonOpacity = 0.4, // Opacity for disabled buttons
DefaultDimOpacity = 0.7, // Default overlay opacity
CalloutCornerRadius = 16, // Callout card corner radius
NavButtonSize = 40, // Navigation button size
// Colors
PrimaryAccentColor = "#0078D4", // Primary button color
SuccessColor = "#107C10", // Done button color
DangerColor = "#D13438", // Skip button color
};
// Use configuration with components
var overlay = new SpotlightOverlay(config);
var callout = new CalloutCard(config);
```
## RTL (Right-to-Left) Support
The library includes full RTL support for languages like Arabic, Hebrew, and Persian:
```xml
<!-- Auto-detect from system culture or parent -->
<onboard:OnboardingHost TourFlowDirection="MatchParent" />
<!-- Force RTL layout -->
<onboard:OnboardingHost TourFlowDirection="RightToLeft" />
<!-- Force LTR layout -->
<onboard:OnboardingHost TourFlowDirection="LeftToRight" />
```
When `MatchParent` is set (the default), the library will:
1. Check parent elements for explicit `FlowDirection`
2. Fall back to system culture (`CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft`)
RTL mode automatically:
- Reverses button order in callout cards and navigator
- Adjusts text alignment for titles and descriptions
- Mirrors the overall layout direction
## Interfaces for Testability
The library provides interfaces for dependency injection and unit testing:
```csharp
// IStepScanner - for scanning visual trees
public interface IStepScanner
{
IReadOnlyList<OnboardingStep> FindSteps(Element root, string? group = null);
OnboardingStep? FindStepByKey(Element root, string stepKey);
IReadOnlyList<string> GetGroups(Element root);
int CountSteps(Element root, string? group = null);
}
// Use the default implementation
IStepScanner scanner = DefaultStepScanner.Instance;
var steps = scanner.FindSteps(this.Content);
```
## Resource Management
`OnboardingHost` implements `IDisposable` for proper resource cleanup:
```csharp
// In a page with tour
public partial class MyPage : ContentPage, IDisposable
{
private readonly OnboardingHost _tourHost;
public MyPage()
{
InitializeComponent();
_tourHost = new OnboardingHost();
// ... setup
}
public void Dispose()
{
_tourHost.Dispose();
}
}
```
## Localization
The library includes built-in support for 8 languages with automatic detection based on the device culture:
**Supported Languages:**
- English (en)
- Spanish (es)
- French (fr)
- German (de)
- Chinese (zh)
- Japanese (ja)
- Portuguese (pt)
- Italian (it)
### Automatic Language Detection
By default, the library uses `CultureInfo.CurrentUICulture` to select the appropriate language:
```csharp
// UI text automatically uses device language
await TourHost.StartTourAsync(this.Content);
```
### Manual Language Selection
You can override the language programmatically:
```csharp
using MarketAlly.SpotlightTour.Maui.Localization;
using System.Globalization;
// Set to Spanish
TourStrings.CurrentCulture = new CultureInfo("es");
// Set to Japanese
TourStrings.CurrentCulture = new CultureInfo("ja");
// Reset to system default
TourStrings.CurrentCulture = null;
```
### Custom Translations
Add or override translations for any language:
```csharp
// Add custom translations
TourStrings.RegisterTranslations("es", new Dictionary<string, string>
{
["Next"] = "Siguiente paso",
["Done"] = "Terminado"
});
// Add a new language
TourStrings.RegisterTranslations("ko", new Dictionary<string, string>
{
["Previous"] = "이전",
["Next"] = "다음",
["Done"] = "완료",
["Skip"] = "건너뛰기",
["StepIndicator"] = "{0} / {1}"
});
```
### Available String Keys
| Key | English | Description |
|-----|---------|-------------|
| `Previous` | Previous | Back button text |
| `Next` | Next | Forward button text |
| `Done` | Done | Completion button text |
| `Skip` | Skip | Skip tour button text |
| `Close` | Close | Close button text |
| `Start` | Start | Start tour button text |
| `GotIt` | Got it | Acknowledgment button text |
| `Continue` | Continue | Continue button text |
| `Finish` | Finish | Finish button text |
| `StepIndicator` | {0} / {1} | Step counter format |
### Responding to Language Changes
Components automatically update when the culture changes:
```csharp
// Subscribe to culture changes
TourStrings.CultureChanged += (s, e) =>
{
// Handle language change if needed
Console.WriteLine($"Language changed to: {TourStrings.EffectiveCulture.Name}");
};
```
## Async Best Practices
The library provides proper async methods with cancellation token support:
```csharp
// Cancellation support
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromMinutes(5));
var result = await TourHost.StartTourAsync(this.Content, cancellationToken: cts.Token);
// Proper async navigation
await TourHost.GoToNextStepAsync();
await TourHost.GoToPreviousStepAsync();
await TourHost.CompleteTourAsync();
await TourHost.DismissIntroAsync();
// Legacy sync methods are available but marked obsolete
// TourHost.GoToNextStep(); // Use GoToNextStepAsync() instead
```
## Complete Example
```xml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:tour="clr-namespace:MarketAlly.SpotlightTour.Maui;assembly=MarketAlly.SpotlightTour.Maui"
x:Class="MyApp.MainPage">
<Grid>
<ScrollView>
<VerticalStackLayout Padding="20" Spacing="20">
<Image Source="logo.png"
tour:Onboarding.StepKey="logo"
tour:Onboarding.Title="Welcome to MyApp"
tour:Onboarding.Description="This is our beautiful logo!"
tour:Onboarding.Order="1"
tour:Onboarding.SpotlightShape="Circle" />
<Entry Placeholder="Username"
tour:Onboarding.StepKey="username"
tour:Onboarding.Title="Enter Username"
tour:Onboarding.Description="Type your username to sign in."
tour:Onboarding.Order="2" />
<Entry Placeholder="Password" IsPassword="True"
tour:Onboarding.StepKey="password"
tour:Onboarding.Title="Enter Password"
tour:Onboarding.Description="Your password is secure with us."
tour:Onboarding.Order="3" />
<Button Text="Sign In"
tour:Onboarding.StepKey="signin"
tour:Onboarding.Title="Sign In"
tour:Onboarding.Description="Tap here to access your account."
tour:Onboarding.Order="4"
Clicked="OnSignInClicked" />
</VerticalStackLayout>
</ScrollView>
<tour:OnboardingHost
x:Name="TourHost"
Theme="System"
ShowCornerNavigator="True"
CornerNavigatorPlacement="Auto"
CalloutPositionMode="AutoCorner" />
</Grid>
</ContentPage>
```
```csharp
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
protected override async void OnAppearing()
{
base.OnAppearing();
// Check if user has seen the tour
if (!Preferences.Get("HasSeenTour", false))
{
var result = await TourHost.StartTourAsync(this.Content);
if (result == TourResult.Completed)
{
Preferences.Set("HasSeenTour", true);
}
}
}
}
```
## API Reference
For complete API documentation, see [API_Reference.md](API_Reference.md).
## Platform Support
| Platform | Minimum Version |
|----------|-----------------|
| iOS | 15.0 |
| Android | API 21 (5.0) |
| Windows | 10.0.17763.0 |
| macOS | 15.0 (Catalyst) |
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Repository
[https://git.marketally.com/marketally/MASpotlightTour](https://git.marketally.com/marketally/MASpotlightTour)
---
**Built with precision by [MarketAlly](https://marketally.com)**
*Enterprise-grade onboarding solutions for .NET MAUI applications.*