diff --git a/mobile/lib/features/pairing/pairing_page.dart b/mobile/lib/features/pairing/pairing_page.dart index 6ec7d052b4..efab52ea37 100644 --- a/mobile/lib/features/pairing/pairing_page.dart +++ b/mobile/lib/features/pairing/pairing_page.dart @@ -365,6 +365,7 @@ class PairingPage extends HookConsumerWidget { value: onboardingSystemOverlayStyle, child: _OnboardingBackground( websiteWelcome: websiteWelcome, + scannerProgress: islandScannerProgress, child: Scaffold( resizeToAvoidBottomInset: hostedFlow?.stage != HostedFlowStage.profile, diff --git a/mobile/lib/features/pairing/pairing_page/onboarding_background.dart b/mobile/lib/features/pairing/pairing_page/onboarding_background.dart index c37c3f12c4..968695814e 100644 --- a/mobile/lib/features/pairing/pairing_page/onboarding_background.dart +++ b/mobile/lib/features/pairing/pairing_page/onboarding_background.dart @@ -3,16 +3,21 @@ part of '../pairing_page.dart'; class _OnboardingBackground extends StatelessWidget { final Widget child; final bool websiteWelcome; + final ValueListenable scannerProgress; const _OnboardingBackground({ required this.child, this.websiteWelcome = false, + this.scannerProgress = const AlwaysStoppedAnimation(0), }); @override Widget build(BuildContext context) { if (websiteWelcome) { - return _WebsiteWelcomeBackground(child: child); + return _WebsiteWelcomeBackground( + scannerProgress: scannerProgress, + child: child, + ); } final isDark = context._onboardingIsDark; return DecoratedBox( diff --git a/mobile/lib/features/pairing/pairing_page/website_welcome_background.dart b/mobile/lib/features/pairing/pairing_page/website_welcome_background.dart index 30ee2884a8..670528bbf9 100644 --- a/mobile/lib/features/pairing/pairing_page/website_welcome_background.dart +++ b/mobile/lib/features/pairing/pairing_page/website_welcome_background.dart @@ -4,8 +4,12 @@ part of '../pairing_page.dart'; // The bee layer uses the website’s flight model and remains decorative. class _WebsiteWelcomeBackground extends HookWidget { final Widget child; + final ValueListenable scannerProgress; - const _WebsiteWelcomeBackground({required this.child}); + const _WebsiteWelcomeBackground({ + required this.child, + required this.scannerProgress, + }); @override Widget build(BuildContext context) { @@ -29,11 +33,28 @@ class _WebsiteWelcomeBackground extends HookWidget { Positioned.fill( child: IgnorePointer( child: ExcludeSemantics( - child: LayoutBuilder( - builder: (context, constraints) => RepaintBoundary( - child: _WebsiteBeeSwarm( - size: constraints.biggest, - pointer: pointer, + child: ValueListenableBuilder( + valueListenable: scannerProgress, + builder: (context, progress, child) => Opacity( + key: const Key('scanner-bees-opacity'), + opacity: 1 - progress, + child: Transform.scale( + key: const Key('scanner-bees-scale'), + scale: MediaQuery.disableAnimationsOf(context) + ? 1 + : 1 - 0.04 * progress, + child: TickerMode( + enabled: progress < 1, + child: child!, + ), + ), + ), + child: LayoutBuilder( + builder: (context, constraints) => RepaintBoundary( + child: _WebsiteBeeSwarm( + size: constraints.biggest, + pointer: pointer, + ), ), ), ), diff --git a/mobile/test/features/pairing/pairing_qr_scanner_test.dart b/mobile/test/features/pairing/pairing_qr_scanner_test.dart index 7ee39085b4..35c225a11c 100644 --- a/mobile/test/features/pairing/pairing_qr_scanner_test.dart +++ b/mobile/test/features/pairing/pairing_qr_scanner_test.dart @@ -317,6 +317,23 @@ void main() { await tester.pump(); final opacity = find.byKey(const Key('scanner-welcome-opacity')); final scale = find.byKey(const Key('scanner-welcome-scale')); + void expectBeesMatchWelcome() { + if (adding) return; + expect( + tester + .widget(find.byKey(const Key('scanner-bees-opacity'))) + .opacity, + tester.widget(opacity).opacity, + ); + expect( + tester + .widget(find.byKey(const Key('scanner-bees-scale'))) + .transform + .storage[0], + tester.widget(scale).transform.storage[0], + ); + } + // The welcome swarm animates continuously. Wait for the scanner's // observable transition endpoint rather than global frame idleness. Future waitForOpacity(double target) async { @@ -372,7 +389,9 @@ void main() { closeTo(1 - 0.04 * progress, 0.001), ); } + expectBeesMatchWelcome(); await waitForOpacity(0); + expectBeesMatchWelcome(); expect(tester.widget(opacity).opacity, 0); expect( tester.widget(scale).transform.storage[0], @@ -390,6 +409,7 @@ void main() { await tester.tapAt(const Offset(196.5, 700)); await tester.pump(); await waitForOpacity(1); + expectBeesMatchWelcome(); expect(tester.widget(opacity).opacity, 1); expect(tester.widget(scale).transform.storage[0], 1); expect(