From c9e0c405df6a00b67b55e40d913c86f79178132e Mon Sep 17 00:00:00 2001 From: PianoNic <79938743+Pianonic@users.noreply.github.com> Date: Fri, 6 Nov 2026 15:21:08 +0100 Subject: [PATCH] Use the official Sign in with Microsoft button and add a trademark notice - Draw the Microsoft provider as Microsoft's branded button: the official logo SVG, 41 high, 12 padding, Segoe UI 15 semibold, light and dark colours from the branding guidelines. - Add a short trademark notice for Microsoft and Schulnetz under About in Settings, in English and German. --- assets/sso/microsoft.svg | 1 + lib/l10n/app_de.arb | 4 +- lib/l10n/app_en.arb | 10 +++- lib/ui/auth/microsoft_sign_in_button.dart | 64 +++++++++++++++++++++++ lib/ui/auth/sign_in_screen.dart | 4 ++ lib/ui/settings/settings_screen.dart | 8 +++ pubspec.yaml | 1 + 7 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 assets/sso/microsoft.svg create mode 100644 lib/ui/auth/microsoft_sign_in_button.dart diff --git a/assets/sso/microsoft.svg b/assets/sso/microsoft.svg new file mode 100644 index 00000000..1f739764 --- /dev/null +++ b/assets/sso/microsoft.svg @@ -0,0 +1 @@ +MS-SymbolLockup \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index fd9fadf6..f7e8ec10 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -102,5 +102,7 @@ "privacyNoticeTitle": "Bevor es weitergeht", "privacyNoticeContinue": "Weiter", "privacyNoticeSignOut": "Abmelden", - "privacyNoticeFailed": "Das hat nicht geklappt. Versuche es nochmals." + "privacyNoticeFailed": "Das hat nicht geklappt. Versuche es nochmals.", + "signInWithMicrosoft": "Mit Microsoft anmelden", + "trademarkNotice": "Microsoft 365, Microsoft Outlook, Microsoft Teams, Microsoft OneDrive und Microsoft OneNote sind Marken der Microsoft Corporation. Schulnetz ist eine Marke der Centerboard AG. Schuly ist ein unabhängiges Projekt und steht in keiner Verbindung zu Microsoft oder Centerboard." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1c6da376..2fdeb284 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -358,5 +358,13 @@ "privacyNoticeTitle": "Before you continue", "privacyNoticeContinue": "Continue", "privacyNoticeSignOut": "Sign out", - "privacyNoticeFailed": "That did not work. Try again." + "privacyNoticeFailed": "That did not work. Try again.", + "signInWithMicrosoft": "Sign in with Microsoft", + "@signInWithMicrosoft": { + "description": "Label of the Sign in with Microsoft button, in Microsoft's own wording" + }, + "trademarkNotice": "Microsoft 365, Microsoft Outlook, Microsoft Teams, Microsoft OneDrive and Microsoft OneNote are trademarks of Microsoft Corporation. Schulnetz is a trademark of Centerboard AG. Schuly is an independent project and is not affiliated with Microsoft or Centerboard.", + "@trademarkNotice": { + "description": "Trademark notice under About in Settings" + } } diff --git a/lib/ui/auth/microsoft_sign_in_button.dart b/lib/ui/auth/microsoft_sign_in_button.dart new file mode 100644 index 00000000..4210f803 --- /dev/null +++ b/lib/ui/auth/microsoft_sign_in_button.dart @@ -0,0 +1,64 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:forui/forui.dart'; + +import '../../l10n/app_localizations.dart'; +import '../../services/auth_service.dart'; + +/// Microsoft providers get the branded button. The server names their icon `/sso-icons/microsoft.svg` +/// (older servers `.png`). +bool isMicrosoftProvider(SignInProvider provider) { + final path = Uri.tryParse(provider.iconUrl ?? '')?.path.toLowerCase() ?? ''; + return provider.key.toLowerCase() == 'microsoft' || path.endsWith('/sso-icons/microsoft.svg') || path.endsWith('/sso-icons/microsoft.png'); +} + +/// The "Sign in with Microsoft" button as Microsoft's branding guidelines specify it +/// (learn.microsoft.com/entra/identity-platform/howto-add-branding-in-apps): the official logo SVG at 21, +/// 41 high, 12 padding and 12 between logo and text, Segoe UI 15 weight 600, square corners. +/// Light: #FFFFFF with a 1 #8C8C8C border and #5E5E5E text. Dark: #2F2F2F with #FFFFFF text. +class MicrosoftSignInButton extends StatelessWidget { + const MicrosoftSignInButton({super.key, required this.onPress}); + + final VoidCallback? onPress; + + @override + Widget build(BuildContext context) { + final dark = context.theme.colors.brightness == Brightness.dark; + final background = dark ? const Color(0xFF2F2F2F) : const Color(0xFFFFFFFF); + final border = dark ? const Color(0xFF2F2F2F) : const Color(0xFF8C8C8C); + final text = dark ? const Color(0xFFFFFFFF) : const Color(0xFF5E5E5E); + final label = AppLocalizations.of(context)!.signInWithMicrosoft; + + return Center( + child: Semantics( + button: true, + enabled: onPress != null, + label: label, + excludeSemantics: true, + child: Opacity( + opacity: onPress == null ? 0.6 : 1, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onPress, + child: Container( + height: 41, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration(color: background, border: Border.all(color: border)), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset('assets/sso/microsoft.svg', width: 21, height: 21), + const SizedBox(width: 12), + Text( + label, + style: TextStyle(fontFamily: 'Segoe UI', fontFamilyFallback: const ['Roboto', '.SF UI Text'], fontSize: 15, fontWeight: FontWeight.w600, color: text, height: 1), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/ui/auth/sign_in_screen.dart b/lib/ui/auth/sign_in_screen.dart index acfd19eb..905077ec 100644 --- a/lib/ui/auth/sign_in_screen.dart +++ b/lib/ui/auth/sign_in_screen.dart @@ -8,6 +8,7 @@ import '../../l10n/app_localizations.dart'; import '../../services/auth_service.dart'; import '../update/check_for_updates.dart'; import '../widgets/remote_icon.dart'; +import 'microsoft_sign_in_button.dart'; import '../privacy/privacy_notice_screen.dart'; enum _Step { signIn, register, code, verifyEmail, forgot } @@ -377,6 +378,9 @@ class _SignInScreenState extends State { ), if (_step == _Step.signIn) ...[ for (final provider in _providers) + if (isMicrosoftProvider(provider)) + MicrosoftSignInButton(onPress: _busy ? null : () => _signInWith(provider)) + else FButton( style: FButtonStyle.outline(), onPress: _busy ? null : () => _signInWith(provider), diff --git a/lib/ui/settings/settings_screen.dart b/lib/ui/settings/settings_screen.dart index 886fce5b..6447dea3 100644 --- a/lib/ui/settings/settings_screen.dart +++ b/lib/ui/settings/settings_screen.dart @@ -4,6 +4,7 @@ import 'package:forui/forui.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../config/backend_config.dart'; +import '../../l10n/app_localizations.dart'; import '../../services/active_account_service.dart'; import '../../services/app_mode_service.dart'; import '../../services/auth_service.dart'; @@ -128,6 +129,13 @@ class _SettingsScreenState extends State { applicationName: 'Schuly', ), ), + Padding( + padding: const EdgeInsets.fromLTRB(4, 12, 4, 0), + child: Text( + AppLocalizations.of(context)!.trademarkNotice, + style: context.theme.typography.xs.copyWith(color: context.theme.colors.mutedForeground), + ), + ), ], ), ); diff --git a/pubspec.yaml b/pubspec.yaml index 5c9c5ff8..72abd8c5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -105,6 +105,7 @@ flutter: - assets/schuly_icon.png - assets/cropped_schuly_icon.png - assets/schoolsystems/ + - assets/sso/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images