--- phase: 19-app-registration-removal plan: 02 type: execute wave: 2 depends_on: ["19-01"] files_modified: - SharepointToolbox/ViewModels/ProfileManagementViewModel.cs - SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml - SharepointToolbox/Localization/Strings.resx - SharepointToolbox/Localization/Strings.fr.resx - SharepointToolbox/App.xaml.cs - SharepointToolbox.Tests/ViewModels/ProfileManagementViewModelRegistrationTests.cs autonomous: true requirements: [APPREG-01, APPREG-04, APPREG-05] must_haves: truths: - "Register App button visible in profile dialog when a profile is selected and has no AppId" - "Remove App button visible when selected profile has a non-null AppId" - "Clicking Register checks Global Admin first; if not admin, shows fallback instructions panel" - "Clicking Register when admin runs full registration and stores AppId on profile" - "Clicking Remove deletes the app registration and clears AppId + MSAL session" - "Fallback panel shows step-by-step manual registration instructions" - "Status feedback shown during registration/removal (busy indicator + result message)" - "All strings localized in EN and FR" artifacts: - path: "SharepointToolbox/ViewModels/ProfileManagementViewModel.cs" provides: "RegisterAppCommand, RemoveAppCommand, status/fallback properties" contains: "RegisterAppCommand" - path: "SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml" provides: "Register/Remove buttons, fallback instructions panel, status area" contains: "RegisterAppCommand" - path: "SharepointToolbox/Localization/Strings.resx" provides: "EN localization for register/remove/fallback strings" contains: "profile.register" - path: "SharepointToolbox/Localization/Strings.fr.resx" provides: "FR localization for register/remove/fallback strings" contains: "profile.register" - path: "SharepointToolbox.Tests/ViewModels/ProfileManagementViewModelRegistrationTests.cs" provides: "Unit tests for register/remove commands" min_lines: 50 key_links: - from: "SharepointToolbox/ViewModels/ProfileManagementViewModel.cs" to: "IAppRegistrationService" via: "constructor injection" pattern: "IAppRegistrationService" - from: "SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml" to: "ProfileManagementViewModel" via: "data binding" pattern: "RegisterAppCommand" - from: "SharepointToolbox/App.xaml.cs" to: "AppRegistrationService" via: "DI registration" pattern: "IAppRegistrationService" --- Wire RegisterApp and RemoveApp commands into the ProfileManagementViewModel and dialog XAML, with fallback instructions panel and full EN/FR localization. Purpose: This is the user-facing layer — the profile dialog becomes the entry point for tenant onboarding via app registration, with a guided fallback when permissions are insufficient. Output: Working register/remove UI in profile dialog, localized strings, DI wiring, ViewModel unit tests @C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/dev/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/19-app-registration-removal/19-RESEARCH.md @.planning/phases/19-app-registration-removal/19-01-SUMMARY.md From SharepointToolbox/Services/IAppRegistrationService.cs: ```csharp public interface IAppRegistrationService { Task IsGlobalAdminAsync(string clientId, CancellationToken ct); Task RegisterAsync(string clientId, string tenantDisplayName, CancellationToken ct); Task RemoveAsync(string clientId, string appId, CancellationToken ct); Task ClearMsalSessionAsync(string clientId, string tenantUrl); } ``` From SharepointToolbox/Core/Models/AppRegistrationResult.cs: ```csharp public class AppRegistrationResult { public bool IsSuccess { get; } public bool IsFallback { get; } public string? AppId { get; } public string? ErrorMessage { get; } public static AppRegistrationResult Success(string appId); public static AppRegistrationResult Failure(string errorMessage); public static AppRegistrationResult FallbackRequired(); } ``` From SharepointToolbox/Core/Models/TenantProfile.cs: ```csharp public class TenantProfile { public string Name { get; set; } = string.Empty; public string TenantUrl { get; set; } = string.Empty; public string ClientId { get; set; } = string.Empty; public LogoData? ClientLogo { get; set; } public string? AppId { get; set; } // NEW in Plan 01 } ``` From SharepointToolbox/ViewModels/ProfileManagementViewModel.cs (existing): ```csharp public partial class ProfileManagementViewModel : ObservableObject { // Constructor: ProfileService, IBrandingService, GraphClientFactory, ILogger // Commands: AddCommand, RenameCommand, DeleteCommand, BrowseClientLogoCommand, ClearClientLogoCommand, AutoPullClientLogoCommand // Properties: SelectedProfile, NewName, NewTenantUrl, NewClientId, ValidationMessage, ClientLogoPreview, Profiles } ``` From SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml (existing): - 5-row Grid: profiles list, input fields, logo section, buttons - Window size: 500x620 Task 1: ViewModel commands + DI + Localization SharepointToolbox/ViewModels/ProfileManagementViewModel.cs, SharepointToolbox/App.xaml.cs, SharepointToolbox/Localization/Strings.resx, SharepointToolbox/Localization/Strings.fr.resx 1. **Update ProfileManagementViewModel constructor** to accept `IAppRegistrationService` as new last parameter. Store as `_appRegistrationService` field. 2. **Add observable properties:** - `[ObservableProperty] private bool _isRegistering;` — true during async registration/removal - `[ObservableProperty] private bool _showFallbackInstructions;` — true when fallback panel should be visible - `[ObservableProperty] private string _registrationStatus = string.Empty;` — status text for user feedback - Add a computed `HasRegisteredApp` property: `public bool HasRegisteredApp => SelectedProfile?.AppId != null;` - In `OnSelectedProfileChanged`, call `OnPropertyChanged(nameof(HasRegisteredApp))` and notify register/remove commands 3. **Add commands:** - `public IAsyncRelayCommand RegisterAppCommand { get; }` — initialized in constructor as `new AsyncRelayCommand(RegisterAppAsync, CanRegisterApp)` - `public IAsyncRelayCommand RemoveAppCommand { get; }` — initialized as `new AsyncRelayCommand(RemoveAppAsync, CanRemoveApp)` - `CanRegisterApp()`: `SelectedProfile != null && SelectedProfile.AppId == null && !IsRegistering` - `CanRemoveApp()`: `SelectedProfile != null && SelectedProfile.AppId != null && !IsRegistering` 4. **RegisterAppAsync implementation:** ``` a. Set IsRegistering = true, ShowFallbackInstructions = false, RegistrationStatus = localized "Checking permissions..." b. Call IsGlobalAdminAsync(SelectedProfile.ClientId, ct) c. If not admin: ShowFallbackInstructions = true, RegistrationStatus = localized "Insufficient permissions", IsRegistering = false, return d. RegistrationStatus = localized "Registering application..." e. Call RegisterAsync(SelectedProfile.ClientId, SelectedProfile.Name, ct) f. If result.IsSuccess: SelectedProfile.AppId = result.AppId, save profile via _profileService.UpdateProfileAsync, RegistrationStatus = localized "Registration successful", OnPropertyChanged(nameof(HasRegisteredApp)) g. If result.IsFallback or !IsSuccess: RegistrationStatus = result.ErrorMessage ?? localized "Registration failed" h. Finally: IsRegistering = false, notify command CanExecute ``` 5. **RemoveAppAsync implementation:** ``` a. Set IsRegistering = true, RegistrationStatus = localized "Removing application..." b. Call RemoveAsync(SelectedProfile.ClientId, SelectedProfile.AppId!, ct) c. Call ClearMsalSessionAsync(SelectedProfile.ClientId, SelectedProfile.TenantUrl) d. SelectedProfile.AppId = null, save profile, RegistrationStatus = localized "Application removed", OnPropertyChanged(nameof(HasRegisteredApp)) e. Finally: IsRegistering = false, notify command CanExecute f. Wrap in try/catch, log errors, show error in RegistrationStatus ``` 6. **Partial method for IsRegistering changes:** Add `partial void OnIsRegisteringChanged(bool value)` that calls `RegisterAppCommand.NotifyCanExecuteChanged()` and `RemoveAppCommand.NotifyCanExecuteChanged()`. 7. **Update App.xaml.cs DI registration:** - Register `services.AddSingleton();` - Update `ProfileManagementViewModel` transient registration (it already resolves from DI, the new constructor param will be injected automatically) 8. **Add localization strings to Strings.resx (EN):** - `profile.register` = "Register App" - `profile.remove` = "Remove App" - `profile.register.checking` = "Checking permissions..." - `profile.register.registering` = "Registering application..." - `profile.register.success` = "Application registered successfully" - `profile.register.failed` = "Registration failed" - `profile.register.noperm` = "Insufficient permissions for automatic registration" - `profile.remove.removing` = "Removing application..." - `profile.remove.success` = "Application removed successfully" - `profile.fallback.title` = "Manual Registration Required" - `profile.fallback.step1` = "1. Go to Azure Portal > App registrations > New registration" - `profile.fallback.step2` = "2. Name: 'SharePoint Toolbox - {0}', Supported account types: Single tenant" - `profile.fallback.step3` = "3. Redirect URI: Public client, https://login.microsoftonline.com/common/oauth2/nativeclient" - `profile.fallback.step4` = "4. Under API permissions, add: Microsoft Graph (User.Read, User.Read.All, Group.Read.All, Directory.Read.All) and SharePoint (AllSites.FullControl)" - `profile.fallback.step5` = "5. Grant admin consent for all permissions" - `profile.fallback.step6` = "6. Copy the Application (client) ID and paste it in the Client ID field above" 9. **Add localization strings to Strings.fr.resx (FR):** - `profile.register` = "Enregistrer l'app" - `profile.remove` = "Supprimer l'app" - `profile.register.checking` = "Verification des permissions..." - `profile.register.registering` = "Enregistrement de l'application..." - `profile.register.success` = "Application enregistree avec succes" - `profile.register.failed` = "L'enregistrement a echoue" - `profile.register.noperm` = "Permissions insuffisantes pour l'enregistrement automatique" - `profile.remove.removing` = "Suppression de l'application..." - `profile.remove.success` = "Application supprimee avec succes" - `profile.fallback.title` = "Enregistrement manuel requis" - `profile.fallback.step1` = "1. Allez dans le portail Azure > Inscriptions d'applications > Nouvelle inscription" - `profile.fallback.step2` = "2. Nom: 'SharePoint Toolbox - {0}', Types de comptes: Locataire unique" - `profile.fallback.step3` = "3. URI de redirection: Client public, https://login.microsoftonline.com/common/oauth2/nativeclient" - `profile.fallback.step4` = "4. Sous Permissions API, ajouter: Microsoft Graph (User.Read, User.Read.All, Group.Read.All, Directory.Read.All) et SharePoint (AllSites.FullControl)" - `profile.fallback.step5` = "5. Accorder le consentement administrateur pour toutes les permissions" - `profile.fallback.step6` = "6. Copier l'ID d'application (client) et le coller dans le champ ID Client ci-dessus" Use proper accented characters in FR strings (e with accents etc.). dotnet build SharepointToolbox/SharepointToolbox.csproj --no-restore 2>&1 | tail -5 ProfileManagementViewModel has RegisterAppCommand and RemoveAppCommand, DI wired, all localization strings in EN and FR, solution builds clean Task 2: Profile dialog XAML + ViewModel tests SharepointToolbox/Views/Dialogs/ProfileManagementDialog.xaml, SharepointToolbox.Tests/ViewModels/ProfileManagementViewModelRegistrationTests.cs 1. **Update ProfileManagementDialog.xaml:** - Increase window Height from 620 to 750 to accommodate new section - Add a new row (insert as Row 4, shift existing buttons to Row 5) with an "App Registration" section: ```xml