From 6684f7e2f0dc2add9c6fcb05736af76f03fb71aa Mon Sep 17 00:00:00 2001 From: John Matthew Tennant Date: Mon, 2 Nov 2026 21:46:36 -0500 Subject: [PATCH] fix(mobile): make the Join button readable in dark mode (#8969) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary After scanning or pasting a community invite in dark mode, the native Join button can display white text on a white background. This fix passes the theme’s foreground color to native confirmation and applies it alongside the button’s background color. ### Related issue Related invite handoff: https://github.com/block/buzz/pull/8601. ### Testing A widget regression checks the foreground and background colors sent to native confirmation. The isolated PR build shows readable black Join text on the white native button in dark mode. ### Screenshots Before (consent-only build `3d566de`): ![Join label invisible in dark mode](https://raw.githubusercontent.com/block/buzz/e12a750a31badc9e2a95a18c277b33968c7ec1f5/pr-8969--DARK_JOIN_BEFORE.png) After (this PR): ![Readable Join label in dark mode](https://raw.githubusercontent.com/block/buzz/e12a750a31badc9e2a95a18c277b33968c7ec1f5/pr-8969--DARK_JOIN_AFTER.png) Signed-off-by: John Tennant Signed-off-by: John Tennant Co-authored-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz> Co-authored-by: Aaron --- mobile/ios/Runner/NativeCommunityEntry.swift | 7 +++- .../features/invites/invite_join_sheet.dart | 1 + .../onboarding/self_hosted_entry_sheet.dart | 1 + .../native_community_entry.dart | 2 ++ .../invites/invite_join_sheet_test.dart | 32 +++++++++++++++++++ 5 files changed, 42 insertions(+), 1 deletion(-) diff --git a/mobile/ios/Runner/NativeCommunityEntry.swift b/mobile/ios/Runner/NativeCommunityEntry.swift index a3fc6d2ad9..6dc3299529 100644 --- a/mobile/ios/Runner/NativeCommunityEntry.swift +++ b/mobile/ios/Runner/NativeCommunityEntry.swift @@ -260,7 +260,12 @@ private final class NativeCommunityEntryController: UIViewController, UITextFiel @discardableResult private func addButton(_ title: String, primary: Bool = false, action: @escaping () -> Void) -> UIButton { - let config = onboardingButtonConfiguration(title, primary: primary) + var config = onboardingButtonConfiguration(title, primary: primary) + if primary, let argb = (configuration["onPrimaryArgb"] as? NSNumber)?.uint32Value { + config.baseBackgroundColor = view.tintColor + config.baseForegroundColor = UIColor(red: CGFloat((argb >> 16) & 255) / 255, + green: CGFloat((argb >> 8) & 255) / 255, blue: CGFloat(argb & 255) / 255, alpha: 1) + } let button = UIButton(configuration: config, primaryAction: UIAction { _ in action() }) button.titleLabel?.numberOfLines = 0 button.titleLabel?.adjustsFontForContentSizeCategory = true diff --git a/mobile/lib/features/invites/invite_join_sheet.dart b/mobile/lib/features/invites/invite_join_sheet.dart index e73e91ee7c..d88c91c19f 100644 --- a/mobile/lib/features/invites/invite_join_sheet.dart +++ b/mobile/lib/features/invites/invite_join_sheet.dart @@ -20,6 +20,7 @@ Future showInviteJoinSheet(BuildContext context) async { name: state.host ?? 'Unknown community', dark: Theme.of(context).brightness == Brightness.dark, primaryArgb: Theme.of(context).colorScheme.primary.toARGB32(), + onPrimaryArgb: Theme.of(context).colorScheme.onPrimary.toARGB32(), choices: const [], connect: (_, _, _, _) async {}, confirmInvite: () async { diff --git a/mobile/lib/features/onboarding/self_hosted_entry_sheet.dart b/mobile/lib/features/onboarding/self_hosted_entry_sheet.dart index 5c4b414036..0988c9af84 100644 --- a/mobile/lib/features/onboarding/self_hosted_entry_sheet.dart +++ b/mobile/lib/features/onboarding/self_hosted_entry_sheet.dart @@ -62,6 +62,7 @@ Future showSelfHostedEntry( ), dark: dark, primaryArgb: primary, + onPrimaryArgb: context.colors.onPrimary.toARGB32(), choices: choices, connect: (pubkey, nsec, create, current) async { final community = await service.prepare( diff --git a/mobile/lib/shared/identity_transfer/native_community_entry.dart b/mobile/lib/shared/identity_transfer/native_community_entry.dart index 22fa26daf0..d061957631 100644 --- a/mobile/lib/shared/identity_transfer/native_community_entry.dart +++ b/mobile/lib/shared/identity_transfer/native_community_entry.dart @@ -16,6 +16,7 @@ abstract final class NativeCommunityEntry { required String name, required bool dark, required int primaryArgb, + int? onPrimaryArgb, required List choices, Future> Function()? confirmInvite, required Future Function( @@ -101,6 +102,7 @@ abstract final class NativeCommunityEntry { 'name': name, 'dark': dark, 'primaryArgb': primaryArgb, + 'onPrimaryArgb': ?onPrimaryArgb, 'invite': confirmInvite != null, 'choices': [ for (final choice in choices) diff --git a/mobile/test/features/invites/invite_join_sheet_test.dart b/mobile/test/features/invites/invite_join_sheet_test.dart index a452195199..68fe9b5fb7 100644 --- a/mobile/test/features/invites/invite_join_sheet_test.dart +++ b/mobile/test/features/invites/invite_join_sheet_test.dart @@ -1,3 +1,4 @@ +import 'package:buzz/shared/theme/theme.dart'; import 'package:buzz/features/invites/invite_join_provider.dart'; import 'package:buzz/shared/deeplink/deep_link.dart'; import 'package:buzz/features/invites/invite_join_sheet.dart'; @@ -93,6 +94,37 @@ void main() { expect(find.textContaining('Could not open the browser'), findsOneWidget); expect(tester.widget(button).onPressed, isNotNull); }); + testWidgets('native dark Join uses the theme foreground on its accent fill', ( + tester, + ) async { + const channel = MethodChannel('buzz/community_entry'); + final messenger = + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; + Map? config; + messenger.setMockMethodCallHandler(channel, (call) async { + config = call.arguments as Map; + return false; + }); + addTearDown(() => messenger.setMockMethodCallHandler(channel, null)); + final theme = AppTheme.dark(); + await tester.pumpWidget( + WidgetHelpers.testable( + child: Theme(data: theme, child: const _InviteJoinSheetLauncher()), + overrides: [ + inviteJoinProvider.overrideWith( + () => _StaticInviteJoinNotifier( + const InviteJoinState(status: InviteJoinStatus.confirming), + ), + ), + ], + ), + ); + await tester.tap(find.text('Open invite')); + await tester.pumpAndSettle(); + expect(config?['primaryArgb'], theme.colorScheme.primary.toARGB32()); + expect(config?['onPrimaryArgb'], theme.colorScheme.onPrimary.toARGB32()); + }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); + testWidgets('recovery error is scrollable and exposes retry setup', ( tester, ) async {