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.