diff --git a/WineFix/Patches/CanvaSignInPatch.cs b/WineFix/Patches/CanvaSignInPatch.cs
new file mode 100644
index 0000000..7c05fcb
--- /dev/null
+++ b/WineFix/Patches/CanvaSignInPatch.cs
@@ -0,0 +1,435 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using HarmonyLib;
+using AffinityPluginLoader.Core;
+
+namespace WineFix.Patches
+{
+ ///
+ /// Adds a paste-URL textbox to the Canva sign-in dialog so users can manually
+ /// paste the authentication redirect URL from their browser, bypassing the need
+ /// for the affinity:// protocol handler which doesn't work under Wine.
+ ///
+ public static class CanvaSignInPatch
+ {
+ private static FieldInfo _authHookField;
+ private static object _cloudServicesService;
+ private static Border _overlayPanel;
+
+ public static void ApplyPatches(Harmony harmony)
+ {
+ Logger.Info("Applying CanvaSignIn patch...");
+
+ var serifAffinity = AppDomain.CurrentDomain.GetAssemblies()
+ .FirstOrDefault(a => a.GetName().Name == "Serif.Affinity");
+ var serifInterop = AppDomain.CurrentDomain.GetAssemblies()
+ .FirstOrDefault(a => a.GetName().Name == "Serif.Interop.Persona");
+
+ if (serifAffinity == null || serifInterop == null)
+ {
+ Logger.Error("Required assemblies not found");
+ return;
+ }
+
+ // Resolve the AuthHookReceived backing field on CloudServicesService
+ var cssType = serifInterop.GetType("Serif.Interop.Persona.Services.CloudServicesService");
+ _authHookField = cssType?.GetField("AuthHookReceived",
+ BindingFlags.NonPublic | BindingFlags.Instance);
+
+ if (_authHookField == null)
+ {
+ // Try the mangled name from decompilation
+ _authHookField = cssType?.GetFields(BindingFlags.NonPublic | BindingFlags.Instance)
+ .FirstOrDefault(f => f.Name.Contains("AuthHookReceived") && f.FieldType == typeof(Action));
+ }
+
+ if (_authHookField == null)
+ {
+ Logger.Error("Could not find AuthHookReceived backing field");
+ return;
+ }
+
+ // Patch ProductKeyDialog_Loaded to inject our UI
+ var dialogType = serifAffinity.GetType("Serif.Affinity.UI.ProductKeyDialog");
+ var loadedMethod = dialogType?.GetMethod("ProductKeyDialog_Loaded",
+ BindingFlags.NonPublic | BindingFlags.Instance);
+
+ if (loadedMethod == null)
+ {
+ Logger.Error("ProductKeyDialog_Loaded not found");
+ return;
+ }
+
+ harmony.Patch(loadedMethod,
+ postfix: new HarmonyMethod(typeof(CanvaSignInPatch), nameof(OnDialogLoaded)));
+ Logger.Info("Patched ProductKeyDialog_Loaded");
+ }
+
+ public static void OnDialogLoaded(object __instance)
+ {
+ try
+ {
+ var window = __instance as Window;
+ if (window == null) return;
+
+ // Get CloudServicesService via Application.Current.GetService()
+ var app = Application.Current;
+ var getServiceMethod = app.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)
+ .Where(m => m.Name == "GetService" && m.IsGenericMethod)
+ .FirstOrDefault();
+
+ if (getServiceMethod != null)
+ {
+ var cssType = _authHookField.DeclaringType;
+ var bound = getServiceMethod.MakeGenericMethod(cssType);
+ _cloudServicesService = bound.Invoke(app, null);
+ }
+
+ // Listen for DataContext property changes to detect state transitions
+ var model = ((FrameworkElement)window).DataContext;
+ if (model is System.ComponentModel.INotifyPropertyChanged npc)
+ {
+ npc.PropertyChanged += (s, e) =>
+ {
+ if (e.PropertyName == "State")
+ InjectPasteUI(window);
+ };
+ }
+
+ // Also try immediately in case we're already in SigningIn
+ InjectPasteUI(window);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"OnDialogLoaded failed: {ex.Message}");
+ }
+ }
+
+ private static void InjectPasteUI(Window window)
+ {
+ try
+ {
+ var model = ((FrameworkElement)window).DataContext;
+ var stateProp = model.GetType().GetProperty("State");
+ if (stateProp == null) return;
+
+ var state = stateProp.GetValue(model);
+ if (state.ToString() == "SigningIn")
+ {
+ window.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Loaded,
+ new Action(() => InjectPasteUIImpl(window)));
+ }
+ else
+ {
+ HideOverlay();
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"InjectPasteUI failed: {ex.Message}");
+ }
+ }
+
+ private static void InjectPasteUIImpl(Window window)
+ {
+ try
+ {
+ if (_overlayPanel != null)
+ {
+ _overlayPanel.Visibility = Visibility.Visible;
+ return;
+ }
+
+ var existingContent = window.Content as UIElement;
+ if (existingContent == null) return;
+
+ // Wrap existing window content in a new Grid so we can overlay on top
+ var wrapperGrid = new Grid();
+ window.Content = wrapperGrid;
+ wrapperGrid.Children.Add(existingContent);
+
+ // Build overlay content
+ var panel = new StackPanel
+ {
+ VerticalAlignment = VerticalAlignment.Center,
+ Margin = new Thickness(30)
+ };
+
+ var heading = new TextBlock
+ {
+ Text = "Running under Wine?",
+ FontSize = 14,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = new SolidColorBrush(Color.FromRgb(0x33, 0x33, 0x33)),
+ Margin = new Thickness(0, 0, 0, 8)
+ };
+
+ var instructions = new TextBlock
+ {
+ Text = "After signing in, your browser will show a " +
+ "\"Launching Affinity\" page. Copy the full URL " +
+ "from the address bar and paste it below:",
+ Foreground = new SolidColorBrush(Color.FromRgb(0x65, 0x65, 0x65)),
+ FontSize = 12,
+ TextWrapping = TextWrapping.Wrap,
+ Margin = new Thickness(0, 0, 0, 10)
+ };
+
+ var screenshot = LoadEmbeddedImage("WineFix.Resources.signin.png");
+ Image screenshotImage = null;
+ if (screenshot != null)
+ {
+ screenshotImage = new Image
+ {
+ Source = screenshot,
+ Stretch = Stretch.Uniform,
+ HorizontalAlignment = HorizontalAlignment.Stretch,
+ Margin = new Thickness(0, 0, 0, 12)
+ };
+ }
+
+ var textBox = new TextBox
+ {
+ Height = 28,
+ FontSize = 12,
+ VerticalContentAlignment = VerticalAlignment.Center,
+ HorizontalAlignment = HorizontalAlignment.Stretch
+ };
+
+ var placeholder = new TextBlock
+ {
+ Text = "https://page.service.serif.com/canva-auth-redirect/...",
+ Foreground = new SolidColorBrush(Color.FromRgb(0xAA, 0xAA, 0xAA)),
+ FontSize = 12,
+ IsHitTestVisible = false,
+ VerticalAlignment = VerticalAlignment.Center,
+ Margin = new Thickness(4, 0, 0, 0)
+ };
+ var textBoxContainer = new Grid { Height = 28, HorizontalAlignment = HorizontalAlignment.Stretch };
+ textBoxContainer.Children.Add(textBox);
+ textBoxContainer.Children.Add(placeholder);
+ textBox.TextChanged += (s, e) => placeholder.Visibility =
+ string.IsNullOrEmpty(textBox.Text) ? Visibility.Visible : Visibility.Collapsed;
+ textBox.GotFocus += (s, e) => placeholder.Visibility = Visibility.Collapsed;
+ textBox.LostFocus += (s, e) => placeholder.Visibility =
+ string.IsNullOrEmpty(textBox.Text) ? Visibility.Visible : Visibility.Collapsed;
+
+ var button = new Button
+ {
+ Content = "Submit",
+ Margin = new Thickness(0, 8, 0, 0),
+ Padding = new Thickness(0, 8, 0, 8),
+ HorizontalAlignment = HorizontalAlignment.Stretch
+ };
+
+ var errorText = new TextBlock
+ {
+ Foreground = Brushes.Red,
+ FontSize = 11,
+ TextWrapping = TextWrapping.Wrap,
+ Visibility = Visibility.Collapsed,
+ Margin = new Thickness(0, 4, 0, 0)
+ };
+
+ button.Click += (s, e) => OnSubmitUrl(textBox.Text, errorText);
+
+ panel.Children.Add(heading);
+ panel.Children.Add(instructions);
+ if (screenshotImage != null)
+ panel.Children.Add(screenshotImage);
+ panel.Children.Add(textBoxContainer);
+ panel.Children.Add(button);
+ panel.Children.Add(errorText);
+
+ // Overlay: right-aligned, 50% width, full height, white background
+ _overlayPanel = new Border
+ {
+ Background = new SolidColorBrush(Color.FromArgb(0xF0, 0xFF, 0xFF, 0xFF)),
+ HorizontalAlignment = HorizontalAlignment.Right,
+ VerticalAlignment = VerticalAlignment.Stretch,
+ Child = panel
+ };
+ _overlayPanel.SetBinding(FrameworkElement.WidthProperty,
+ new Binding("ActualWidth")
+ {
+ Source = window,
+ Converter = new HalfWidthConverter()
+ });
+
+ wrapperGrid.Children.Add(_overlayPanel);
+
+ // Nudge the "Back" button left so it's not covered by the overlay
+ var contentControl = FindChild(existingContent,
+ c => c.ContentTemplateSelector != null);
+ if (contentControl != null)
+ {
+ var backButton = FindChild