diff --git a/.github/scripts/verify-maui-android.sh b/.github/scripts/verify-maui-android.sh
new file mode 100755
index 0000000..b6e9f82
--- /dev/null
+++ b/.github/scripts/verify-maui-android.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+set -e
+
+echo "Installing APK..."
+adb install client-sdk/frameworks/maui/bin/Debug/net8.0-android/com.launchdarkly.hello-Signed.apk
+
+echo "Clearing logcat..."
+adb logcat -c
+
+echo "Launching app..."
+adb shell monkey -p com.launchdarkly.hello -c android.intent.category.LAUNCHER 1
+
+for i in 1 2 3 4; do
+ echo "Waiting 15 seconds (attempt $i of 4)..."
+ sleep 15
+
+ echo "=== Checking if app is running ==="
+ adb shell pidof com.launchdarkly.hello || echo "App process NOT running"
+
+ echo "=== Logcat (crash/mono related) ==="
+ adb logcat -d -t 100 2>&1 | grep -iE "AndroidRuntime|FATAL|crash|mono|dotnet|HelloDotNet|launchdarkly" | tail -50 || true
+
+ echo "=== UI dump ==="
+ adb exec-out uiautomator dump /dev/tty > /tmp/uidump.xml 2>&1 || true
+ cat /tmp/uidump.xml
+
+ if grep -q 'feature flag evaluates to' /tmp/uidump.xml; then
+ echo "SUCCESS: Flag evaluation verified"
+ exit 0
+ fi
+ echo "Attempt $i: text not found yet, retrying..."
+
+ # If app crashed, try relaunching
+ if ! adb shell pidof com.launchdarkly.hello > /dev/null 2>&1; then
+ echo "App not running, relaunching..."
+ adb shell monkey -p com.launchdarkly.hello -c android.intent.category.LAUNCHER 1
+ fi
+done
+
+echo "=== FULL LOGCAT DUMP (last 200 lines) ==="
+adb logcat -d 2>&1 | tail -200 || true
+
+echo "FAILURE: Could not verify flag evaluation after 4 attempts"
+exit 1
diff --git a/.github/workflows/client-sdk.yml b/.github/workflows/client-sdk.yml
new file mode 100644
index 0000000..9029e39
--- /dev/null
+++ b/.github/workflows/client-sdk.yml
@@ -0,0 +1,104 @@
+name: client-sdk
+on:
+ schedule:
+ # * is a special character in YAML so you have to quote this string
+ - cron: '0 9 * * *'
+ push:
+ branches: [ main, 'feat/**' ]
+ paths:
+ - 'client-sdk/**'
+ - '.github/scripts/verify-maui-android.sh'
+ - '.github/workflows/client-sdk.yml'
+ pull_request:
+ branches: [ main, 'feat/**' ]
+ paths:
+ - 'client-sdk/**'
+ - '.github/scripts/verify-maui-android.sh'
+ - '.github/workflows/client-sdk.yml'
+
+jobs:
+ build-and-run:
+ runs-on: ubuntu-latest
+
+ permissions:
+ id-token: write # Needed if using OIDC to get release secrets.
+ contents: read
+
+ steps:
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
+ with:
+ dotnet-version: 8.0.x
+
+ - name: Build console app
+ run: dotnet build client-sdk/getting-started
+
+ - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1
+ name: 'Get mobile key and flag key'
+ with:
+ aws_assume_role: ${{ vars.AWS_ROLE_ARN }}
+ ssm_parameter_pairs: '/sdk/common/hello-apps/mobile-key = LAUNCHDARKLY_MOBILE_KEY,
+ /sdk/common/hello-apps/boolean-flag-key = LAUNCHDARKLY_FLAG_KEY'
+
+ - name: Configure MAUI appsettings
+ run: |
+ echo "{\"MobileKey\": \"$LAUNCHDARKLY_MOBILE_KEY\", \"FlagKey\": \"$LAUNCHDARKLY_FLAG_KEY\"}" > client-sdk/frameworks/maui/Resources/Raw/appsettings.json
+
+ - name: Install MAUI workload
+ working-directory: client-sdk/frameworks/maui
+ run: dotnet workload install maui-android
+
+ - name: Build MAUI app (Android)
+ working-directory: client-sdk/frameworks/maui
+ run: dotnet build -p:EmbedAssembliesIntoApk=true
+
+ - name: Enable KVM group perms (for performance)
+ run: |
+ echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
+ sudo udevadm control --reload-rules
+ sudo udevadm trigger --name-match=kvm
+
+ - name: Free disk space
+ uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
+ with:
+ android: false
+ dotnet: false
+ large-packages: false
+
+ - name: Run MAUI app on emulator
+ uses: reactivecircus/android-emulator-runner@324029e2f414c084d8b15ba075288885e74aef9c # v2.34.0
+ with:
+ api-level: 29
+ arch: x86_64
+ script: bash .github/scripts/verify-maui-android.sh
+
+ - uses: launchdarkly/gh-actions/actions/verify-hello-app@verify-hello-app-v2
+ with:
+ use_mobile_key: true
+ role_arn: ${{ vars.AWS_ROLE_ARN }}
+ command: dotnet run --project client-sdk/getting-started
+
+ build-maui:
+ runs-on: macos-latest
+
+ steps:
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
+ with:
+ dotnet-version: 8.0.x
+
+ - name: Install MAUI workloads
+ working-directory: client-sdk/frameworks/maui
+ run: dotnet workload install maui
+
+ - name: Build MAUI app (Android)
+ working-directory: client-sdk/frameworks/maui
+ run: dotnet build -f net8.0-android
+
+ - name: Build MAUI app (iOS)
+ working-directory: client-sdk/frameworks/maui
+ run: dotnet build -f net8.0-ios
diff --git a/.gitignore b/.gitignore
index d963d49..e944edd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,6 @@
# Local env files
.env
+
+# MAUI appsettings (contains LaunchDarkly keys)
+**/Resources/Raw/appsettings.json
diff --git a/README.md b/README.md
index 2cdc8bc..ad2338d 100644
--- a/README.md
+++ b/README.md
@@ -7,10 +7,11 @@ or the [.NET SDK reference guide](https://docs.launchdarkly.com/sdk/server-side/
## SDKs
-| SDK | Package | Examples |
-|--------------------|-----------------------------|------------------------------------------|
-| .NET Server SDK | `LaunchDarkly.ServerSdk` | [`server-sdk/`](./server-sdk/) |
-| .NET Server AI SDK | `LaunchDarkly.ServerSdk.Ai` | [`server-sdk-ai/`](./server-sdk-ai/) |
+| SDK | Package | Examples |
+|--------------------|-----------------------------|--------------------------------------|
+| .NET Server SDK | `LaunchDarkly.ServerSdk` | [`server-sdk/`](./server-sdk/) |
+| .NET Server AI SDK | `LaunchDarkly.ServerSdk.Ai` | [`server-sdk-ai/`](./server-sdk-ai/) |
+| .NET Client SDK | `LaunchDarkly.ClientSdk` | [`client-sdk/`](./client-sdk/) |
## Requirements
diff --git a/client-sdk/README.md b/client-sdk/README.md
new file mode 100644
index 0000000..5984372
--- /dev/null
+++ b/client-sdk/README.md
@@ -0,0 +1,17 @@
+# LaunchDarkly .NET Client SDK examples
+
+Examples for the [`LaunchDarkly.ClientSdk`](https://www.nuget.org/packages/LaunchDarkly.ClientSdk) package.
+
+For more comprehensive instructions, you can visit your [Quickstart page](https://app.launchdarkly.com/quickstart#/) or the [client-side .NET SDK reference guide](https://docs.launchdarkly.com/sdk/client-side/dotnet).
+
+## Getting Started
+
+| Example | Description |
+|--------------------------------------|-------------------------------------------------------------|
+| [Flag Retrieval](./getting-started/) | Console app that initializes the SDK and evaluates a feature flag |
+
+## Frameworks
+
+| Framework | Example | Description |
+|-----------|----------------------------------|-----------------------------------------------------------------|
+| .NET MAUI | [MAUI](./frameworks/maui/) | Cross-platform (Android & iOS) app evaluating a feature flag |
diff --git a/client-sdk/frameworks/maui/App.xaml b/client-sdk/frameworks/maui/App.xaml
new file mode 100644
index 0000000..73d1e7b
--- /dev/null
+++ b/client-sdk/frameworks/maui/App.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client-sdk/frameworks/maui/App.xaml.cs b/client-sdk/frameworks/maui/App.xaml.cs
new file mode 100644
index 0000000..03f0799
--- /dev/null
+++ b/client-sdk/frameworks/maui/App.xaml.cs
@@ -0,0 +1,11 @@
+namespace HelloDotNetClient;
+
+public partial class App : Application
+{
+ public App()
+ {
+ InitializeComponent();
+
+ MainPage = new AppShell();
+ }
+}
diff --git a/client-sdk/frameworks/maui/AppShell.xaml b/client-sdk/frameworks/maui/AppShell.xaml
new file mode 100644
index 0000000..0629945
--- /dev/null
+++ b/client-sdk/frameworks/maui/AppShell.xaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/client-sdk/frameworks/maui/AppShell.xaml.cs b/client-sdk/frameworks/maui/AppShell.xaml.cs
new file mode 100644
index 0000000..0902f49
--- /dev/null
+++ b/client-sdk/frameworks/maui/AppShell.xaml.cs
@@ -0,0 +1,9 @@
+namespace HelloDotNetClient;
+
+public partial class AppShell : Shell
+{
+ public AppShell()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/client-sdk/frameworks/maui/MainPage.xaml b/client-sdk/frameworks/maui/MainPage.xaml
new file mode 100644
index 0000000..1faa21c
--- /dev/null
+++ b/client-sdk/frameworks/maui/MainPage.xaml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/client-sdk/frameworks/maui/MainPage.xaml.cs b/client-sdk/frameworks/maui/MainPage.xaml.cs
new file mode 100644
index 0000000..080a169
--- /dev/null
+++ b/client-sdk/frameworks/maui/MainPage.xaml.cs
@@ -0,0 +1,141 @@
+using System.Text.Json;
+using LaunchDarkly.Sdk;
+using LaunchDarkly.Sdk.Client;
+using LaunchDarkly.Sdk.Client.Interfaces;
+
+namespace HelloDotNetClient;
+
+public partial class MainPage : ContentPage
+{
+ // Set flagKey to the feature flag key you want to evaluate.
+ const string flagKey = "sample-feature";
+
+ private LdClient? _client;
+
+ public MainPage()
+ {
+ InitializeComponent();
+ InitializeLaunchDarkly();
+ }
+
+ private async void InitializeLaunchDarkly()
+ {
+ var settings = await LoadAppSettings();
+ var resolvedMobileKey = GetMobileKey(settings);
+ var resolvedFlagKey = GetFlagKey(settings);
+
+ if (resolvedMobileKey == null || resolvedFlagKey == null)
+ {
+ return; // Error message already shown
+ }
+
+ try
+ {
+ _client = await LdClient.InitAsync(
+ Configuration.Default(resolvedMobileKey, ConfigurationBuilder.AutoEnvAttributes.Enabled),
+ MakeContext(),
+ TimeSpan.FromSeconds(10)
+ );
+ }
+ catch (Exception ex)
+ {
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ FlagLabel.Text = $"SDK error: {ex.Message}";
+ });
+ return;
+ }
+
+ if (_client.Initialized)
+ {
+ var flagValue = _client.BoolVariation(resolvedFlagKey, false);
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ UpdateUI(resolvedFlagKey, flagValue);
+ });
+
+ _client.FlagTracker.FlagValueChanged += (sender, eventArgs) =>
+ {
+ if (eventArgs.Key == resolvedFlagKey)
+ {
+ var newValue = _client.BoolVariation(resolvedFlagKey, false);
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ UpdateUI(resolvedFlagKey, newValue);
+ });
+ }
+ };
+ }
+ else
+ {
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ FlagLabel.Text = "SDK failed to initialize. Please check your internet connection and SDK credential for any typo.";
+ });
+ }
+ }
+
+ private void UpdateUI(string flagKeyName, bool flagValue)
+ {
+ FlagLabel.Text = $"The {flagKeyName} feature flag evaluates to {flagValue.ToString().ToLowerInvariant()}.";
+ Page.BackgroundColor = flagValue ? Color.FromArgb("#00844B") : Color.FromArgb("#373841");
+ }
+
+ private async Task> LoadAppSettings()
+ {
+ try
+ {
+ using var stream = await FileSystem.OpenAppPackageFileAsync("appsettings.json");
+ using var reader = new StreamReader(stream);
+ var json = await reader.ReadToEndAsync();
+ return JsonSerializer.Deserialize>(json)
+ ?? new Dictionary();
+ }
+ catch
+ {
+ return new Dictionary();
+ }
+ }
+
+ private string? GetMobileKey(Dictionary settings)
+ {
+ if (settings.TryGetValue("MobileKey", out var settingsKey) && !string.IsNullOrEmpty(settingsKey)
+ && settingsKey != "my-mobile-key")
+ {
+ return settingsKey;
+ }
+
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ FlagLabel.Text = "LaunchDarkly mobile key is required.\n\nCopy appsettings.example.json to appsettings.json in Resources/Raw/ and set your mobile key. See the README for details.";
+ });
+ return null;
+ }
+
+ private string? GetFlagKey(Dictionary settings)
+ {
+ if (settings.TryGetValue("FlagKey", out var settingsKey) && !string.IsNullOrEmpty(settingsKey))
+ {
+ return settingsKey;
+ }
+
+ if (!string.IsNullOrEmpty(flagKey))
+ {
+ return flagKey;
+ }
+
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ FlagLabel.Text = "LaunchDarkly flag key is required.\n\nSet the FlagKey in appsettings.json. See the README for details.";
+ });
+ return null;
+ }
+
+ // Set up the evaluation context. This context should appear on your
+ // LaunchDarkly contexts dashboard soon after you run the demo.
+ private static Context MakeContext() =>
+ Context.Builder("example-user-key")
+ .Name("Sandy")
+ .Build();
+}
+
diff --git a/client-sdk/frameworks/maui/MauiApp.csproj b/client-sdk/frameworks/maui/MauiApp.csproj
new file mode 100644
index 0000000..98ce15f
--- /dev/null
+++ b/client-sdk/frameworks/maui/MauiApp.csproj
@@ -0,0 +1,52 @@
+
+
+
+ net8.0-android
+ net8.0-android;net8.0-ios
+
+ Exe
+ HelloDotNetClient
+ true
+ true
+ enable
+ enable
+
+
+ LaunchDarkly Hello
+
+
+ com.launchdarkly.hello
+
+
+ 1.0
+ 1
+
+ 11.0
+ 21.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client-sdk/frameworks/maui/MauiProgram.cs b/client-sdk/frameworks/maui/MauiProgram.cs
new file mode 100644
index 0000000..5d0087b
--- /dev/null
+++ b/client-sdk/frameworks/maui/MauiProgram.cs
@@ -0,0 +1,24 @@
+using Microsoft.Extensions.Logging;
+
+namespace HelloDotNetClient;
+
+public static class MauiProgram
+{
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+ builder
+ .UseMauiApp()
+ .ConfigureFonts(fonts =>
+ {
+ fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
+ fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
+ });
+
+#if DEBUG
+ builder.Logging.AddDebug();
+#endif
+
+ return builder.Build();
+ }
+}
diff --git a/client-sdk/frameworks/maui/NOTICE b/client-sdk/frameworks/maui/NOTICE
new file mode 100644
index 0000000..384d3cb
--- /dev/null
+++ b/client-sdk/frameworks/maui/NOTICE
@@ -0,0 +1,4 @@
+LaunchDarkly .NET Client SDK MAUI example
+Copyright 2023 Catamorphic, Co.
+
+This product includes software developed at LaunchDarkly (https://launchdarkly.com/).
diff --git a/client-sdk/frameworks/maui/Platforms/Android/AndroidManifest.xml b/client-sdk/frameworks/maui/Platforms/Android/AndroidManifest.xml
new file mode 100644
index 0000000..01951e8
--- /dev/null
+++ b/client-sdk/frameworks/maui/Platforms/Android/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client-sdk/frameworks/maui/Platforms/Android/MainActivity.cs b/client-sdk/frameworks/maui/Platforms/Android/MainActivity.cs
new file mode 100644
index 0000000..bbed504
--- /dev/null
+++ b/client-sdk/frameworks/maui/Platforms/Android/MainActivity.cs
@@ -0,0 +1,10 @@
+using Android.App;
+using Android.Content.PM;
+using Android.OS;
+
+namespace HelloDotNetClient;
+
+[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
+public class MainActivity : MauiAppCompatActivity
+{
+}
diff --git a/client-sdk/frameworks/maui/Platforms/Android/MainApplication.cs b/client-sdk/frameworks/maui/Platforms/Android/MainApplication.cs
new file mode 100644
index 0000000..26464f4
--- /dev/null
+++ b/client-sdk/frameworks/maui/Platforms/Android/MainApplication.cs
@@ -0,0 +1,15 @@
+using Android.App;
+using Android.Runtime;
+
+namespace HelloDotNetClient;
+
+[Application]
+public class MainApplication : MauiApplication
+{
+ public MainApplication(IntPtr handle, JniHandleOwnership ownership)
+ : base(handle, ownership)
+ {
+ }
+
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
diff --git a/client-sdk/frameworks/maui/Platforms/Android/Resources/values/colors.xml b/client-sdk/frameworks/maui/Platforms/Android/Resources/values/colors.xml
new file mode 100644
index 0000000..c04d749
--- /dev/null
+++ b/client-sdk/frameworks/maui/Platforms/Android/Resources/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #512BD4
+ #2B0B98
+ #2B0B98
+
\ No newline at end of file
diff --git a/client-sdk/frameworks/maui/Platforms/iOS/AppDelegate.cs b/client-sdk/frameworks/maui/Platforms/iOS/AppDelegate.cs
new file mode 100644
index 0000000..b208490
--- /dev/null
+++ b/client-sdk/frameworks/maui/Platforms/iOS/AppDelegate.cs
@@ -0,0 +1,9 @@
+using Foundation;
+
+namespace HelloDotNetClient;
+
+[Register("AppDelegate")]
+public class AppDelegate : MauiUIApplicationDelegate
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
diff --git a/client-sdk/frameworks/maui/Platforms/iOS/Info.plist b/client-sdk/frameworks/maui/Platforms/iOS/Info.plist
new file mode 100644
index 0000000..0004a4f
--- /dev/null
+++ b/client-sdk/frameworks/maui/Platforms/iOS/Info.plist
@@ -0,0 +1,32 @@
+
+
+
+
+ LSRequiresIPhoneOS
+
+ UIDeviceFamily
+
+ 1
+ 2
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ XSAppIconAssets
+ Assets.xcassets/appicon.appiconset
+
+
diff --git a/client-sdk/frameworks/maui/Platforms/iOS/Program.cs b/client-sdk/frameworks/maui/Platforms/iOS/Program.cs
new file mode 100644
index 0000000..ef536a3
--- /dev/null
+++ b/client-sdk/frameworks/maui/Platforms/iOS/Program.cs
@@ -0,0 +1,15 @@
+using ObjCRuntime;
+using UIKit;
+
+namespace HelloDotNetClient;
+
+public class Program
+{
+ // This is the main entry point of the application.
+ static void Main(string[] args)
+ {
+ // if you want to use a different Application Delegate class from "AppDelegate"
+ // you can specify it here.
+ UIApplication.Main(args, null, typeof(AppDelegate));
+ }
+}
diff --git a/client-sdk/frameworks/maui/Platforms/iOS/Resources/PrivacyInfo.xcprivacy b/client-sdk/frameworks/maui/Platforms/iOS/Resources/PrivacyInfo.xcprivacy
new file mode 100644
index 0000000..24ab3b4
--- /dev/null
+++ b/client-sdk/frameworks/maui/Platforms/iOS/Resources/PrivacyInfo.xcprivacy
@@ -0,0 +1,51 @@
+
+
+
+
+
+ NSPrivacyAccessedAPITypes
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategoryFileTimestamp
+ NSPrivacyAccessedAPITypeReasons
+
+ C617.1
+
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategorySystemBootTime
+ NSPrivacyAccessedAPITypeReasons
+
+ 35F9.1
+
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategoryDiskSpace
+ NSPrivacyAccessedAPITypeReasons
+
+ E174.1
+
+
+
+
+
+
diff --git a/client-sdk/frameworks/maui/Properties/launchSettings.json b/client-sdk/frameworks/maui/Properties/launchSettings.json
new file mode 100644
index 0000000..edf8aad
--- /dev/null
+++ b/client-sdk/frameworks/maui/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "Windows Machine": {
+ "commandName": "MsixPackage",
+ "nativeDebugging": false
+ }
+ }
+}
\ No newline at end of file
diff --git a/client-sdk/frameworks/maui/README.md b/client-sdk/frameworks/maui/README.md
new file mode 100644
index 0000000..0d73bf9
--- /dev/null
+++ b/client-sdk/frameworks/maui/README.md
@@ -0,0 +1,36 @@
+# LaunchDarkly .NET MAUI example
+
+This example demonstrates LaunchDarkly's client-side .NET SDK in a mobile context using [.NET MAUI](https://learn.microsoft.com/dotnet/maui/), targeting both Android and iOS from a single project.
+
+## Build instructions
+
+1. Install the MAUI workload if you haven't already:
+
+ ```bash
+ dotnet workload install maui
+ ```
+
+2. Copy the example settings file and set your mobile key:
+
+ ```bash
+ cp Resources/Raw/appsettings.example.json Resources/Raw/appsettings.json
+ ```
+
+ Then edit `Resources/Raw/appsettings.json` and set your mobile key (and, optionally, a flag key):
+
+ ```json
+ {
+ "MobileKey": "my-mobile-key",
+ "FlagKey": "sample-feature"
+ }
+ ```
+
+ `appsettings.json` is gitignored so your key is never committed.
+
+3. Build and run for a target platform, for example Android:
+
+ ```bash
+ dotnet build -f net8.0-android
+ ```
+
+The app displays the value of the feature flag and reacts to flag changes in LaunchDarkly.
diff --git a/client-sdk/frameworks/maui/Resources/AppIcon/appicon.svg b/client-sdk/frameworks/maui/Resources/AppIcon/appicon.svg
new file mode 100644
index 0000000..9d63b65
--- /dev/null
+++ b/client-sdk/frameworks/maui/Resources/AppIcon/appicon.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/client-sdk/frameworks/maui/Resources/AppIcon/appiconfg.svg b/client-sdk/frameworks/maui/Resources/AppIcon/appiconfg.svg
new file mode 100644
index 0000000..21dfb25
--- /dev/null
+++ b/client-sdk/frameworks/maui/Resources/AppIcon/appiconfg.svg
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/client-sdk/frameworks/maui/Resources/Fonts/OpenSans-Regular.ttf b/client-sdk/frameworks/maui/Resources/Fonts/OpenSans-Regular.ttf
new file mode 100644
index 0000000..ee3f28f
Binary files /dev/null and b/client-sdk/frameworks/maui/Resources/Fonts/OpenSans-Regular.ttf differ
diff --git a/client-sdk/frameworks/maui/Resources/Fonts/OpenSans-Semibold.ttf b/client-sdk/frameworks/maui/Resources/Fonts/OpenSans-Semibold.ttf
new file mode 100644
index 0000000..bc81019
Binary files /dev/null and b/client-sdk/frameworks/maui/Resources/Fonts/OpenSans-Semibold.ttf differ
diff --git a/client-sdk/frameworks/maui/Resources/Images/dotnet_bot.png b/client-sdk/frameworks/maui/Resources/Images/dotnet_bot.png
new file mode 100644
index 0000000..f93ce02
Binary files /dev/null and b/client-sdk/frameworks/maui/Resources/Images/dotnet_bot.png differ
diff --git a/client-sdk/frameworks/maui/Resources/Raw/AboutAssets.txt b/client-sdk/frameworks/maui/Resources/Raw/AboutAssets.txt
new file mode 100644
index 0000000..890d7fa
--- /dev/null
+++ b/client-sdk/frameworks/maui/Resources/Raw/AboutAssets.txt
@@ -0,0 +1,15 @@
+Any raw assets you want to be deployed with your application can be placed in
+this directory (and child directories). Deployment of the asset to your application
+is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
+
+
+
+These files will be deployed with your package and will be accessible using Essentials:
+
+ async Task LoadMauiAsset()
+ {
+ using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
+ using var reader = new StreamReader(stream);
+
+ var contents = reader.ReadToEnd();
+ }
diff --git a/client-sdk/frameworks/maui/Resources/Raw/appsettings.example.json b/client-sdk/frameworks/maui/Resources/Raw/appsettings.example.json
new file mode 100644
index 0000000..05c83af
--- /dev/null
+++ b/client-sdk/frameworks/maui/Resources/Raw/appsettings.example.json
@@ -0,0 +1,4 @@
+{
+ "MobileKey": "my-mobile-key",
+ "FlagKey": "sample-feature"
+}
diff --git a/client-sdk/frameworks/maui/Resources/Splash/splash.svg b/client-sdk/frameworks/maui/Resources/Splash/splash.svg
new file mode 100644
index 0000000..21dfb25
--- /dev/null
+++ b/client-sdk/frameworks/maui/Resources/Splash/splash.svg
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/client-sdk/frameworks/maui/Resources/Styles/Colors.xaml b/client-sdk/frameworks/maui/Resources/Styles/Colors.xaml
new file mode 100644
index 0000000..b6c2e2f
--- /dev/null
+++ b/client-sdk/frameworks/maui/Resources/Styles/Colors.xaml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+ #512BD4
+ #ac99ea
+ #242424
+ #DFD8F7
+ #9880e5
+ #2B0B98
+
+ White
+ Black
+ #D600AA
+ #190649
+ #1f1f1f
+
+ #E1E1E1
+ #C8C8C8
+ #ACACAC
+ #919191
+ #6E6E6E
+ #404040
+ #212121
+ #141414
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client-sdk/frameworks/maui/Resources/Styles/Styles.xaml b/client-sdk/frameworks/maui/Resources/Styles/Styles.xaml
new file mode 100644
index 0000000..dc6b971
--- /dev/null
+++ b/client-sdk/frameworks/maui/Resources/Styles/Styles.xaml
@@ -0,0 +1,427 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client-sdk/frameworks/maui/global.json b/client-sdk/frameworks/maui/global.json
new file mode 100644
index 0000000..3fea262
--- /dev/null
+++ b/client-sdk/frameworks/maui/global.json
@@ -0,0 +1,6 @@
+{
+ "sdk": {
+ "version": "8.0.0",
+ "rollForward": "latestFeature"
+ }
+}
diff --git a/client-sdk/getting-started/DotNetConsoleApp.csproj b/client-sdk/getting-started/DotNetConsoleApp.csproj
new file mode 100644
index 0000000..4fbd379
--- /dev/null
+++ b/client-sdk/getting-started/DotNetConsoleApp.csproj
@@ -0,0 +1,11 @@
+
+
+
+ Exe
+ net8.0
+
+
+
+
+
+
diff --git a/client-sdk/getting-started/NOTICE b/client-sdk/getting-started/NOTICE
new file mode 100644
index 0000000..424fd03
--- /dev/null
+++ b/client-sdk/getting-started/NOTICE
@@ -0,0 +1,4 @@
+LaunchDarkly .NET Client SDK getting-started example
+Copyright 2023 Catamorphic, Co.
+
+This product includes software developed at LaunchDarkly (https://launchdarkly.com/).
diff --git a/client-sdk/getting-started/Program.cs b/client-sdk/getting-started/Program.cs
new file mode 100644
index 0000000..f692ae2
--- /dev/null
+++ b/client-sdk/getting-started/Program.cs
@@ -0,0 +1,137 @@
+using System;
+using System.Threading;
+using LaunchDarkly.Sdk;
+using LaunchDarkly.Sdk.Client;
+using LaunchDarkly.Sdk.Client.Interfaces;
+
+namespace DotNetConsoleApp
+{
+ public class Program
+ {
+ // Set mobileKey to your LaunchDarkly mobile key.
+ const string mobileKey = "";
+
+ // Set flagKey to the feature flag key you want to evaluate.
+ const string flagKey = "sample-feature";
+
+ static void Main(string[] args)
+ {
+ var resolvedMobileKey = GetMobileKey();
+ var resolvedFlagKey = GetFlagKey();
+ var isCi = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"));
+
+ var client = LdClient.Init(
+ Configuration.Default(resolvedMobileKey, ConfigurationBuilder.AutoEnvAttributes.Enabled),
+ MakeContext(),
+ TimeSpan.FromSeconds(10)
+ );
+
+ if (client.Initialized)
+ {
+ ShowMessage("SDK successfully initialized!");
+ }
+ else
+ {
+ ShowMessage("SDK failed to initialize. Please check your internet connection and SDK credential for any typo.");
+ Environment.Exit(1);
+ }
+
+ var flagValue = client.BoolVariation(resolvedFlagKey, false);
+ ShowMessage(string.Format("The {0} feature flag evaluates to {1}.", resolvedFlagKey, flagValue.ToString().ToLowerInvariant()));
+
+ if (flagValue)
+ {
+ ShowAsciiArt();
+ }
+
+ if (!isCi)
+ {
+ client.FlagTracker.FlagValueChanged += (sender, eventArgs) =>
+ {
+ if (eventArgs.Key == resolvedFlagKey)
+ {
+ flagValue = client.BoolVariation(resolvedFlagKey, false);
+ ShowMessage(string.Format("The {0} feature flag evaluates to {1}.", resolvedFlagKey, flagValue.ToString().ToLowerInvariant()));
+
+ if (flagValue)
+ {
+ ShowAsciiArt();
+ }
+ }
+ };
+
+ Thread.Sleep(Timeout.Infinite);
+ }
+
+ // Here we ensure that the SDK shuts down cleanly and has a chance to deliver analytics
+ // events to LaunchDarkly before the program exits. If analytics events are not delivered,
+ // the context properties and flag usage statistics will not appear on your dashboard. In
+ // a normal long-running application, the SDK would continue running and events would be
+ // delivered automatically in the background.
+ client.Dispose();
+ }
+
+ static string GetMobileKey()
+ {
+ if (!string.IsNullOrEmpty(mobileKey))
+ {
+ return mobileKey;
+ }
+
+ var envKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_MOBILE_KEY");
+ if (!string.IsNullOrEmpty(envKey))
+ {
+ return envKey;
+ }
+
+ ShowMessage("LaunchDarkly mobile key is required: set the mobileKey variable in Program.cs, or the LAUNCHDARKLY_MOBILE_KEY environment variable and try again.");
+ Environment.Exit(1);
+ return ""; // unreachable
+ }
+
+ static string GetFlagKey()
+ {
+ var envKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_FLAG_KEY");
+ if (!string.IsNullOrEmpty(envKey))
+ {
+ return envKey;
+ }
+
+ if (!string.IsNullOrEmpty(flagKey))
+ {
+ return flagKey;
+ }
+
+ ShowMessage("LaunchDarkly flag key is required: set the flagKey variable in Program.cs, or the LAUNCHDARKLY_FLAG_KEY environment variable and try again.");
+ Environment.Exit(1);
+ return ""; // unreachable
+ }
+
+ // Set up the evaluation context. This context should appear on your
+ // LaunchDarkly contexts dashboard soon after you run the demo.
+ static Context MakeContext() =>
+ Context.Builder("example-user-key")
+ .Name("Sandy")
+ .Build();
+
+ static void ShowMessage(string s)
+ {
+ Console.WriteLine("*** " + s);
+ Console.WriteLine();
+ }
+
+ static void ShowAsciiArt()
+ {
+ Console.WriteLine(" \u2588\u2588 ");
+ Console.WriteLine(" \u2588\u2588 ");
+ Console.WriteLine(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 ");
+ Console.WriteLine(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588 ");
+ Console.WriteLine("\u2588\u2588 LAUNCHDARKLY \u2588");
+ Console.WriteLine(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588 ");
+ Console.WriteLine(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 ");
+ Console.WriteLine(" \u2588\u2588 ");
+ Console.WriteLine(" \u2588\u2588 ");
+ Console.WriteLine();
+ }
+ }
+}
diff --git a/client-sdk/getting-started/README.md b/client-sdk/getting-started/README.md
new file mode 100644
index 0000000..418f36c
--- /dev/null
+++ b/client-sdk/getting-started/README.md
@@ -0,0 +1,22 @@
+# LaunchDarkly sample .NET client-side application
+
+We've built a simple console application that demonstrates how LaunchDarkly's client-side .NET SDK works.
+
+## Build instructions
+
+1. Set the environment variable `LAUNCHDARKLY_MOBILE_KEY` to your LaunchDarkly mobile key. If there is an existing boolean feature flag in your LaunchDarkly project that you want to evaluate, set `LAUNCHDARKLY_FLAG_KEY` to the flag key; otherwise, a boolean flag of `sample-feature` will be assumed.
+
+ ```bash
+ export LAUNCHDARKLY_MOBILE_KEY="my-mobile-key"
+ export LAUNCHDARKLY_FLAG_KEY="my-boolean-flag"
+ ```
+
+ Alternatively, you can set the `mobileKey` and `flagKey` constants directly in `Program.cs`.
+
+2. Run the application from the command line:
+
+ ```bash
+ dotnet run
+ ```
+
+You should receive the message "The feature flag evaluates to .". The application will run continuously and react to flag changes in LaunchDarkly.