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