diff --git a/.github/workflows/sideload-apk.yml b/.github/workflows/sideload-apk.yml deleted file mode 100644 index d6b290e1bfb..00000000000 --- a/.github/workflows/sideload-apk.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Sideload APK -run-name: Build sideloadable debug APK - -on: - workflow_dispatch: - push: - branches: [dive-3578-rebrand] - -concurrency: - group: sideload-apk-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: write - -jobs: - apk: - name: Debug APK - runs-on: ubuntu-latest - timeout-minutes: 45 - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 - - name: Prime Flutter SDK - run: flutter --version - - name: Install dependencies - run: cd mobile && flutter pub get - - name: Build Android debug APK - run: just mobile-build-android - - name: Show APK output - run: ls -la mobile/build/app/outputs/flutter-apk/ - - name: Name the APK for the phone - run: | - set -euo pipefail - cp mobile/build/app/outputs/flutter-apk/app-debug.apk 5dive-${{ github.run_number }}.apk - ls -la 5dive-*.apk - - name: Upload APK (CI artifact, needs a GitHub login to fetch) - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: fivedive-debug-apk - path: 5dive-*.apk - if-no-files-found: error - # A CI artifact link is useless on a phone (it 302s to a login). Publish the - # same bytes as a prerelease asset so the download is anonymous. - - name: Publish as a prerelease asset - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - tag="mobile-sideload-5dive-${{ github.run_number }}" - gh release create "$tag" "5dive-${{ github.run_number }}.apk" \ - --repo "${{ github.repository }}" \ - --prerelease \ - --title "5dive mobile (sideload debug) - $tag" \ - --notes "Debug APK of the 5dive-branded mobile app. Android only. applicationId com.fivedive.app - installs alongside any existing Buzz app." - gh release view "$tag" --repo "${{ github.repository }}" --json assets --jq '.assets[].url' diff --git a/mobile/README.md b/mobile/README.md index e52a127beef..6a7f38bdf27 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -23,7 +23,7 @@ cd mobile && flutter run Debug builds produced from a git worktree get a unique app identifier keyed to the **worktree directory name** (`com.buzz.buzzMobile.` on iOS, -`com.fivedive.app.` on Android) plus a display-only branch label +`xyz.block.buzz.mobile.` on Android) plus a display-only branch label in the app name (`Buzz (my-branch)`, or a short SHA when the worktree is detached). Because the identifier follows the directory rather than the branch, one worktree keeps exactly one installed app — and its login state — diff --git a/mobile/android/app/build.gradle.kts b/mobile/android/app/build.gradle.kts index 8842aea042d..ca40ee16c69 100644 --- a/mobile/android/app/build.gradle.kts +++ b/mobile/android/app/build.gradle.kts @@ -83,7 +83,7 @@ android { } defaultConfig { - applicationId = "com.fivedive.app" + applicationId = "xyz.block.buzz.mobile" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion @@ -91,7 +91,7 @@ android { versionCode = flutter.versionCode versionName = flutter.versionName testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - resValue("string", "app_name", "5dive") + resValue("string", "app_name", "Buzz") } signingConfigs { @@ -113,7 +113,7 @@ android { applicationIdSuffix = worktreeIdSuffix } if (worktreeLabel != null) { - resValue("string", "app_name", "5dive ($worktreeLabel)") + resValue("string", "app_name", "Buzz ($worktreeLabel)") } } release { diff --git a/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index 4b58271462e..d208067bfc8 100644 Binary files a/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png index ca8f80f489f..4d6094a2e5f 100644 Binary files a/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and b/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png index da5e3ee070a..d208067bfc8 100644 Binary files a/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/mobile/android/app/src/main/res/mipmap-hdpi/launch_image.png b/mobile/android/app/src/main/res/mipmap-hdpi/launch_image.png index eb62971f738..a455bf3a5d0 100644 Binary files a/mobile/android/app/src/main/res/mipmap-hdpi/launch_image.png and b/mobile/android/app/src/main/res/mipmap-hdpi/launch_image.png differ diff --git a/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 7c298575007..5689f03941e 100644 Binary files a/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png index d53a1590c6f..1f13b223a68 100644 Binary files a/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and b/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index 5f3d1541386..5689f03941e 100644 Binary files a/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/mobile/android/app/src/main/res/mipmap-mdpi/launch_image.png b/mobile/android/app/src/main/res/mipmap-mdpi/launch_image.png index 40b85016409..693a977f0d5 100644 Binary files a/mobile/android/app/src/main/res/mipmap-mdpi/launch_image.png and b/mobile/android/app/src/main/res/mipmap-mdpi/launch_image.png differ diff --git a/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 8bcb29d0b5b..d3f0d129f0a 100644 Binary files a/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png index 3b877b44b13..c99da7935d7 100644 Binary files a/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and b/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png index 021def915b2..d3f0d129f0a 100644 Binary files a/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/mobile/android/app/src/main/res/mipmap-xhdpi/launch_image.png b/mobile/android/app/src/main/res/mipmap-xhdpi/launch_image.png index c43ed19c3b7..bb2e93b3854 100644 Binary files a/mobile/android/app/src/main/res/mipmap-xhdpi/launch_image.png and b/mobile/android/app/src/main/res/mipmap-xhdpi/launch_image.png differ diff --git a/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index daa4628a93b..50252891ab9 100644 Binary files a/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png index fb45af6b892..a6f183b68ea 100644 Binary files a/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and b/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index 101ca3fc383..50252891ab9 100644 Binary files a/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/mobile/android/app/src/main/res/mipmap-xxhdpi/launch_image.png b/mobile/android/app/src/main/res/mipmap-xxhdpi/launch_image.png index 835728272f8..384d0dc9ded 100644 Binary files a/mobile/android/app/src/main/res/mipmap-xxhdpi/launch_image.png and b/mobile/android/app/src/main/res/mipmap-xxhdpi/launch_image.png differ diff --git a/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 95ca4e00550..19603f8913f 100644 Binary files a/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png index 60c031afecd..2e58de23cc0 100644 Binary files a/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and b/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index f83be66f129..19603f8913f 100644 Binary files a/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/mobile/android/app/src/main/res/mipmap-xxxhdpi/launch_image.png b/mobile/android/app/src/main/res/mipmap-xxxhdpi/launch_image.png index 444101dec6c..935e5ba93a8 100644 Binary files a/mobile/android/app/src/main/res/mipmap-xxxhdpi/launch_image.png and b/mobile/android/app/src/main/res/mipmap-xxxhdpi/launch_image.png differ diff --git a/mobile/android/app/src/main/res/values/ic_launcher_background.xml b/mobile/android/app/src/main/res/values/ic_launcher_background.xml index d0910421b8e..4efb408f52c 100644 --- a/mobile/android/app/src/main/res/values/ic_launcher_background.xml +++ b/mobile/android/app/src/main/res/values/ic_launcher_background.xml @@ -1,4 +1,4 @@ - #1A1A1F + #000 \ No newline at end of file diff --git a/mobile/assets/images/5dive-mark.png b/mobile/assets/images/5dive-mark.png deleted file mode 100644 index d5b92aa59d9..00000000000 Binary files a/mobile/assets/images/5dive-mark.png and /dev/null differ diff --git a/mobile/assets/images/buzz-icon.png b/mobile/assets/images/buzz-icon.png new file mode 100644 index 00000000000..d5ca6fe7fb4 Binary files /dev/null and b/mobile/assets/images/buzz-icon.png differ diff --git a/mobile/ios/Flutter/Debug.xcconfig b/mobile/ios/Flutter/Debug.xcconfig index 87b832e0ab6..1f35905051e 100644 --- a/mobile/ios/Flutter/Debug.xcconfig +++ b/mobile/ios/Flutter/Debug.xcconfig @@ -6,7 +6,7 @@ // `mobile/ios/Flutter/AppOverrides.xcconfig` containing // `BUNDLE_IDENTIFIER = your.app.id` (gitignored). BUNDLE_IDENTIFIER = com.buzz.buzzMobile -APP_DISPLAY_NAME = 5dive +APP_DISPLAY_NAME = Buzz // Worktree-aware debug identity (gitignored, written by // scripts/mobile-worktree-overrides.sh): a per-worktree bundle identifier diff --git a/mobile/ios/Flutter/Release.xcconfig b/mobile/ios/Flutter/Release.xcconfig index ad7d3d4d7d7..d287c5fb432 100644 --- a/mobile/ios/Flutter/Release.xcconfig +++ b/mobile/ios/Flutter/Release.xcconfig @@ -5,7 +5,7 @@ // enterprise-signed distribution) override these without patching tracked // files by writing `mobile/ios/Flutter/AppOverrides.xcconfig` (gitignored). BUNDLE_IDENTIFIER = com.buzz.buzzMobile -APP_DISPLAY_NAME = 5dive +APP_DISPLAY_NAME = Buzz CODE_SIGN_STYLE = Automatic CODE_SIGN_IDENTITY = iPhone Developer #include? "AppOverrides.xcconfig" diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 9951493a73d..0a64b894216 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 4befcea001a..e16e7f07838 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index c4c60c7d856..b6dfec93d71 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index 008b860eae6..6c5318ba7aa 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index cfbdd7990a3..2819823b9e2 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index ad2424824d8..c3acd8a6ca3 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index 66fdd962cef..c4bf37bd8da 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index c4c60c7d856..b6dfec93d71 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index d9f54ecc07c..92a18bbbe2e 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index 2925cfb1da5..ee562852202 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index 2925cfb1da5..ee562852202 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index a64f5e865ca..1411e93d3bb 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index b8641800844..895ec807875 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 81ae68e842a..4ee6677d5d2 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index 4327ca30e0e..ab8c7f8d916 100644 Binary files a/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 864129c9760..80292ff8bb7 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -15,7 +15,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - 5dive + Buzz CFBundlePackageType APPL CFBundleShortVersionString diff --git a/mobile/lib/app.dart b/mobile/lib/app.dart index d64c456952f..f2726b9f13a 100644 --- a/mobile/lib/app.dart +++ b/mobile/lib/app.dart @@ -113,7 +113,7 @@ class App extends HookConsumerWidget { }); return MaterialApp( - title: '5dive', + title: 'Buzz', theme: AppTheme.light( colorScheme: lightScheme, topSectionGradient: buzzLightGradient, @@ -162,7 +162,7 @@ class _SplashScreen extends StatelessWidget { Widget build(BuildContext context) { return const Scaffold( body: Center( - child: BuzzLoadingIndicator(size: 56, semanticLabel: 'Starting 5dive'), + child: BuzzLoadingIndicator(size: 56, semanticLabel: 'Starting Buzz'), ), ); } diff --git a/mobile/lib/features/activity/activity_page.dart b/mobile/lib/features/activity/activity_page.dart index ebfd5f453be..dcae998ed0f 100644 --- a/mobile/lib/features/activity/activity_page.dart +++ b/mobile/lib/features/activity/activity_page.dart @@ -14,7 +14,7 @@ import '../../shared/theme/theme.dart'; import '../../shared/utils/string_utils.dart'; import '../../shared/widgets/avatar_image.dart'; import '../../shared/widgets/anchored_popover_menu.dart'; -import '../../shared/widgets/brand_refresh_indicator.dart'; +import '../../shared/widgets/bee_refresh_indicator.dart'; import '../../shared/widgets/buzz_loading_indicator.dart'; import '../../shared/widgets/frosted_app_bar.dart'; import '../../shared/widgets/frosted_scaffold.dart'; @@ -318,7 +318,7 @@ class ActivityPage extends HookConsumerWidget { : -1; bodyRidesOverTopSection = true; - body = BrandRefreshIndicator( + body = BeeRefreshIndicator( edgeOffset: topSectionHeight, onRefresh: refresh, child: CustomScrollView( diff --git a/mobile/lib/features/activity/activity_page/lists.dart b/mobile/lib/features/activity/activity_page/lists.dart index 7ca360440cd..ad970492723 100644 --- a/mobile/lib/features/activity/activity_page/lists.dart +++ b/mobile/lib/features/activity/activity_page/lists.dart @@ -38,7 +38,7 @@ class _RemindersList extends ConsumerWidget { } final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; - return BrandRefreshIndicator( + return BeeRefreshIndicator( onRefresh: onRefresh, child: ListView.builder( controller: scrollController, diff --git a/mobile/lib/features/channels/channels_page.dart b/mobile/lib/features/channels/channels_page.dart index ef2e4e0b02f..c77ef278ec6 100644 --- a/mobile/lib/features/channels/channels_page.dart +++ b/mobile/lib/features/channels/channels_page.dart @@ -17,7 +17,7 @@ import '../../shared/relay/relay.dart'; import '../../shared/theme/theme.dart'; import '../../shared/widgets/avatar_image.dart'; import '../../shared/widgets/anchored_popover_menu.dart'; -import '../../shared/widgets/brand_refresh_indicator.dart'; +import '../../shared/widgets/bee_refresh_indicator.dart'; import '../../shared/widgets/buzz_loading_indicator.dart'; import '../../shared/widgets/buzz_titled_sheet_layout.dart'; import '../../shared/widgets/frosted_app_bar.dart'; diff --git a/mobile/lib/features/channels/channels_page/body.dart b/mobile/lib/features/channels/channels_page/body.dart index 3a5550e69f6..34faa068eba 100644 --- a/mobile/lib/features/channels/channels_page/body.dart +++ b/mobile/lib/features/channels/channels_page/body.dart @@ -40,7 +40,7 @@ class _ChannelsBody extends StatelessWidget { ) : loadedChannels == null ? const SizedBox.shrink() - : BrandRefreshIndicator( + : BeeRefreshIndicator( edgeOffset: barHeight, onRefresh: onRefresh, child: CustomScrollView( diff --git a/mobile/lib/features/channels/compose_bar/camera_preview.dart b/mobile/lib/features/channels/compose_bar/camera_preview.dart index 69653011aa4..1a06534d6e3 100644 --- a/mobile/lib/features/channels/compose_bar/camera_preview.dart +++ b/mobile/lib/features/channels/compose_bar/camera_preview.dart @@ -313,7 +313,7 @@ String _cameraErrorMessage(Object error) { return switch (error.code) { 'CameraAccessDenied' || 'CameraAccessDeniedWithoutPrompt' || - 'CameraAccessRestricted' => 'Camera access is turned off for 5dive.', + 'CameraAccessRestricted' => 'Camera access is turned off for Buzz.', 'no-cameras' => 'Camera isn’t available on this device.', _ => 'Camera couldn’t start. Try again.', }; diff --git a/mobile/lib/features/channels/photo_library.dart b/mobile/lib/features/channels/photo_library.dart index 1b09925cebb..8a01de01e0f 100644 --- a/mobile/lib/features/channels/photo_library.dart +++ b/mobile/lib/features/channels/photo_library.dart @@ -39,7 +39,7 @@ class PhotoLibraryAccessException implements Exception { const PhotoLibraryAccessException(); @override - String toString() => 'Photo access is turned off for 5dive.'; + String toString() => 'Photo access is turned off for Buzz.'; } /// Provides the device photo library. Tests can override this with fixtures. diff --git a/mobile/lib/features/forum/forum_posts_view.dart b/mobile/lib/features/forum/forum_posts_view.dart index 4623142528b..13880427cfc 100644 --- a/mobile/lib/features/forum/forum_posts_view.dart +++ b/mobile/lib/features/forum/forum_posts_view.dart @@ -8,7 +8,7 @@ import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../shared/theme/theme.dart'; import '../../shared/widgets/buzz_loading_indicator.dart'; import '../../shared/widgets/frosted_app_bar.dart'; -import '../../shared/widgets/brand_refresh_indicator.dart'; +import '../../shared/widgets/bee_refresh_indicator.dart'; import '../channels/channel.dart'; import '../channels/compose_bar.dart'; import 'forum_models.dart'; @@ -93,7 +93,7 @@ class ForumPostsView extends HookConsumerWidget { isArchived: channel.isArchived, ); } - return BrandRefreshIndicator( + return BeeRefreshIndicator( onRefresh: () async { ref.invalidate(forumPostsProvider(channel.id)); await ref.read(forumPostsProvider(channel.id).future); diff --git a/mobile/lib/features/invites/invite_join_sheet.dart b/mobile/lib/features/invites/invite_join_sheet.dart index 9e399e850cb..b0e0cb6f7b4 100644 --- a/mobile/lib/features/invites/invite_join_sheet.dart +++ b/mobile/lib/features/invites/invite_join_sheet.dart @@ -41,7 +41,7 @@ class InviteJoinSheet extends ConsumerWidget { Icon(LucideIcons.userPlus, size: 40, color: context.colors.primary), const SizedBox(height: Grid.sm), Text( - 'Join this 5dive community?', + 'Join this Buzz community?', style: context.textTheme.titleLarge, ), const SizedBox(height: Grid.xxs), diff --git a/mobile/lib/features/pairing/pairing_page.dart b/mobile/lib/features/pairing/pairing_page.dart index e941fd3ea9b..7aabe8aed74 100644 --- a/mobile/lib/features/pairing/pairing_page.dart +++ b/mobile/lib/features/pairing/pairing_page.dart @@ -10,7 +10,7 @@ import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../shared/security/sensitive_action_authorizer.dart'; import '../../shared/theme/theme.dart'; import '../../shared/widgets/buzz_loading_indicator.dart'; -import '../../shared/widgets/fivedive_mark.dart'; +import '../../shared/widgets/tappable_flapping_bee.dart'; import 'pairing_provider.dart'; import 'pairing_qr_scanner.dart'; @@ -279,8 +279,8 @@ class _SasVerificationView extends StatelessWidget { Text( sendsIdentityToDesktop - ? 'This sends your full 5dive identity to the desktop\nand grants it permanent access. Only confirm a\ndesktop you trust and a recovery you started.' - : 'You are about to transfer your 5dive identity\nto this device. Only confirm if you initiated\nthis pairing from your desktop.', + ? 'This sends your full Buzz identity to the desktop\nand grants it permanent access. Only confirm a\ndesktop you trust and a recovery you started.' + : 'You are about to transfer your Buzz identity\nto this device. Only confirm if you initiated\nthis pairing from your desktop.', textAlign: TextAlign.center, style: context.textTheme.bodySmall?.copyWith( color: context.colors.onSurfaceVariant, diff --git a/mobile/lib/features/pairing/pairing_page/pairing_welcome_view.dart b/mobile/lib/features/pairing/pairing_page/pairing_welcome_view.dart index 433a0aff9cd..49fc185133f 100644 --- a/mobile/lib/features/pairing/pairing_page/pairing_welcome_view.dart +++ b/mobile/lib/features/pairing/pairing_page/pairing_welcome_view.dart @@ -50,11 +50,14 @@ class _PairingWelcomeView extends StatelessWidget { shape: BoxShape.circle, color: Color(0x4DFFFFFF), ), - child: const FiveDiveMark(height: 72, color: _onboardingInk), + child: const TappableFlappingBee( + width: 76, + color: _onboardingInk, + ), ), const SizedBox(height: Grid.sm), Text( - 'Welcome to 5dive', + 'Welcome to Buzz', textAlign: TextAlign.center, style: context.textTheme.headlineSmall?.copyWith( color: _onboardingInk, diff --git a/mobile/lib/features/pulse/pulse_page.dart b/mobile/lib/features/pulse/pulse_page.dart index 22291892c84..08568fc8dc6 100644 --- a/mobile/lib/features/pulse/pulse_page.dart +++ b/mobile/lib/features/pulse/pulse_page.dart @@ -6,7 +6,7 @@ import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../shared/relay/relay.dart'; import '../../shared/theme/theme.dart'; import '../../shared/widgets/filter_chip_bar.dart'; -import '../../shared/widgets/brand_refresh_indicator.dart'; +import '../../shared/widgets/bee_refresh_indicator.dart'; import '../../shared/widgets/frosted_app_bar.dart'; import '../../shared/widgets/frosted_scaffold.dart'; import 'agent_activity_card.dart'; @@ -100,7 +100,7 @@ class PulsePage extends HookConsumerWidget { ], ), Expanded( - child: BrandRefreshIndicator( + child: BeeRefreshIndicator( onRefresh: () async => _refresh(ref, active.value, currentPubkey), child: _PulseBody( tab: active.value, diff --git a/mobile/lib/features/settings/settings_page/connection_section.dart b/mobile/lib/features/settings/settings_page/connection_section.dart index 31531db1d32..15cecdd4c67 100644 --- a/mobile/lib/features/settings/settings_page/connection_section.dart +++ b/mobile/lib/features/settings/settings_page/connection_section.dart @@ -51,7 +51,7 @@ class _ConnectionSection extends ConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( - '5dive did not return to the foreground. Try again.', + 'Buzz did not return to the foreground. Try again.', ), ), ); diff --git a/mobile/lib/shared/security/sensitive_action_authorizer.dart b/mobile/lib/shared/security/sensitive_action_authorizer.dart index abfaeaedc79..b229f633302 100644 --- a/mobile/lib/shared/security/sensitive_action_authorizer.dart +++ b/mobile/lib/shared/security/sensitive_action_authorizer.dart @@ -25,7 +25,7 @@ class LocalSensitiveActionAuthorizer implements SensitiveActionAuthorizer { Future authorizeIdentityAction({ required bool biometricOnly, }) => _authorize( - localizedReason: 'Confirm sending your 5dive identity to desktop', + localizedReason: 'Confirm sending your Buzz identity to desktop', biometricOnly: biometricOnly, ); diff --git a/mobile/lib/shared/theme/theme_catalog.dart b/mobile/lib/shared/theme/theme_catalog.dart index b7d15d8dfd7..9e00cfc8119 100644 --- a/mobile/lib/shared/theme/theme_catalog.dart +++ b/mobile/lib/shared/theme/theme_catalog.dart @@ -24,33 +24,13 @@ class ThemeColors { bool get isDark => bg.computeLuminance() < 0.5; - /// Human-readable display name: 'catppuccin-mocha' → 'Catppuccin Mocha', - /// unless [themeDisplayNameOverrides] renames it. - String get displayName => - themeDisplayNameOverrides[name] ?? - name - .split('-') - .map( - (w) => w.isNotEmpty ? '${w[0].toUpperCase()}${w.substring(1)}' : w, - ) - .join(' '); + /// Human-readable display name: 'catppuccin-mocha' → 'Catppuccin Mocha'. + String get displayName => name + .split('-') + .map((w) => w.isNotEmpty ? '${w[0].toUpperCase()}${w.substring(1)}' : w) + .join(' '); } -/// Labels for themes whose display name does not follow from their key. -/// -/// The first-party pair is persisted AND published to the community relay, -/// where Buzz Desktop reads it back (`community_theme_preference.dart`; the -/// desktop catalog in `desktop/src/shared/theme/theme-loader.ts` holds the same -/// 'buzz' key and rejects a preference naming a theme it does not know). The -/// wire key therefore cannot be renamed from the mobile client alone. The -/// label is overridden here instead — the one place both -/// [ThemeColors.displayName] and `pairedThemeLabel` read, so the picker never -/// shows two names for one palette. -const themeDisplayNameOverrides = { - 'buzz': '5dive', - 'buzz-dark': '5dive Dark', -}; - /// Known light theme names — used to show sun/moon icons before loading. const lightThemeNames = { 'buzz', diff --git a/mobile/lib/shared/theme/theme_pairs.dart b/mobile/lib/shared/theme/theme_pairs.dart index 8eed1e3be81..cfbf6a53a89 100644 --- a/mobile/lib/shared/theme/theme_pairs.dart +++ b/mobile/lib/shared/theme/theme_pairs.dart @@ -102,9 +102,6 @@ const _modeTokens = { /// mode-specific tokens so `github-light` reads as "Github" and stands for both /// halves. Mirrors desktop's `pairedThemeLabel`. String pairedThemeLabel(String lightName) { - final override = themeDisplayNameOverrides[lightName]; - if (override != null) return override; - final stripped = lightName .split('-') .where((token) => !_modeTokens.contains(token)) diff --git a/mobile/lib/shared/theme/theme_provider.dart b/mobile/lib/shared/theme/theme_provider.dart index 57ccf2d7cbc..908134ebf7b 100644 --- a/mobile/lib/shared/theme/theme_provider.dart +++ b/mobile/lib/shared/theme/theme_provider.dart @@ -16,7 +16,7 @@ const _schemeKey = 'buzz_color_scheme'; /// Buzz ships as the default: the first-party pair, so a fresh install gets the /// branded top-section gradient without picking a theme first. const defaultSchemeName = buzzThemeName; -const defaultSchemeDisplayName = '5dive'; +const defaultSchemeDisplayName = 'Buzz'; /// Pre-loaded SharedPreferences instance, overridden in main(). final savedPrefsProvider = Provider( diff --git a/mobile/lib/shared/widgets/bee_refresh_indicator.dart b/mobile/lib/shared/widgets/bee_refresh_indicator.dart new file mode 100644 index 00000000000..4a9802f0dae --- /dev/null +++ b/mobile/lib/shared/widgets/bee_refresh_indicator.dart @@ -0,0 +1,440 @@ +import 'dart:async' show Timer, unawaited; +import 'dart:math' show cos, min, pi, sin; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../theme/theme.dart'; +import 'flapping_bee.dart'; + +/// Replaces the standard pull-to-refresh spinner with Buzz's loading bee. +/// +/// Flutter continues to own the gesture, refresh lifecycle, and accessibility +/// semantics. This widget maps those states into the elastic pull, retained +/// loading gap, and bee animation. +class BeeRefreshIndicator extends HookConsumerWidget { + /// Called when the user completes a pull, to load fresh data. + /// + /// The bee keeps flapping until this future settles, so it should complete + /// only once the refresh is done. + final Future Function() onRefresh; + + /// The scrollable this indicator wraps. + /// + /// It must scroll vertically; the indicator reads its scroll notifications + /// to couple the bee to the user's finger. + final Widget child; + + /// The vertical offset of the scrollable's top edge, such as a pinned header. + final double edgeOffset; + + const BeeRefreshIndicator({ + required this.onRefresh, + required this.child, + this.edgeOffset = 0, + super.key, + }); + + static const _beeWidth = 60.0; + static const _beeHeight = _beeWidth * 309 / 466; + static const _triggerDistance = 100.0; + static const _loadingGap = 72.0; + static const _beeVerticalAlignment = 0.75; + static const _beeInitialScale = 0.6; + static const _beeRevealStartProgress = 0.18; + static const _pupilStartBeyondArmDistance = 96.0; + static const _pupilFullBeforeEmojiDistance = 8.0; + static const _eyeEmojiSwapViewportFraction = 0.26; + static const _eyeEmojiMinSwapBeyondArmDistance = 220.0; + static const _eyeEmojiMaxSwapBeyondArmDistance = 280.0; + static const _pupilMinBeyondArmDuration = Duration(milliseconds: 300); + static const _eyeEmojiMinBeyondArmDuration = Duration(milliseconds: 700); + static const _eyeShakePeriod = Duration(milliseconds: 140); + static const _settleDuration = Duration(milliseconds: 180); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final status = useState(null); + final pullProgress = useState(0.0); + final pullDistance = useState(0.0); + final pullBeyondArmDistance = useState(0.0); + final pullBeyondArmDuration = useState(Duration.zero); + final activePointers = useRef({}); + final lastPointerTime = useRef(Duration.zero); + final armedAt = useRef(null); + final didTriggerArmHaptic = useRef(false); + final didTriggerEmojiHaptic = useRef(false); + final eyeShakeHapticTimer = useRef(null); + final completionController = useAnimationController( + duration: _settleDuration, + ); + final gapController = useAnimationController( + duration: _settleDuration, + reverseDuration: _settleDuration, + ); + final flapController = useAnimationController( + duration: const Duration(milliseconds: 480), + ); + final eyeShakeController = useAnimationController( + duration: _eyeShakePeriod, + ); + final completionProgress = useAnimation(completionController); + final gapProgress = useAnimation(gapController); + final flapProgress = useAnimation(flapController); + final eyeShakeProgress = useAnimation(eyeShakeController); + final reducedMotion = MediaQuery.disableAnimationsOf(context); + useEffect( + () => + () => eyeShakeHapticTimer.value?.cancel(), + const [], + ); + final eyeEmojiSwapDistance = + (MediaQuery.sizeOf(context).height * _eyeEmojiSwapViewportFraction) + .clamp( + _eyeEmojiMinSwapBeyondArmDistance, + _eyeEmojiMaxSwapBeyondArmDistance, + ) + .toDouble(); + + void stopEyeShake() { + eyeShakeController + ..stop() + ..reset(); + eyeShakeHapticTimer.value?.cancel(); + eyeShakeHapticTimer.value = null; + } + + void startEyeShake() { + stopEyeShake(); + if (reducedMotion) return; + + eyeShakeController.repeat(); + eyeShakeHapticTimer.value = Timer.periodic( + _eyeShakePeriod, + (_) => unawaited(HapticFeedback.selectionClick()), + ); + } + + void beginExpressionTracking() { + if (activePointers.value.isEmpty || armedAt.value != null) return; + pullBeyondArmDistance.value = 0; + pullBeyondArmDuration.value = Duration.zero; + armedAt.value = lastPointerTime.value; + if (!didTriggerArmHaptic.value) { + didTriggerArmHaptic.value = true; + unawaited(HapticFeedback.mediumImpact()); + } + } + + void updateStatus(RefreshIndicatorStatus? nextStatus) { + status.value = nextStatus; + + if (nextStatus == RefreshIndicatorStatus.drag) { + completionController.reset(); + gapController.reset(); + flapController.stop(); + if (!didTriggerArmHaptic.value) { + pullBeyondArmDistance.value = 0; + pullBeyondArmDuration.value = Duration.zero; + armedAt.value = null; + } + } else if (nextStatus == RefreshIndicatorStatus.armed) { + pullProgress.value = 1; + beginExpressionTracking(); + } else if (nextStatus == RefreshIndicatorStatus.snap || + nextStatus == RefreshIndicatorStatus.refresh) { + stopEyeShake(); + pullProgress.value = 1; + pullBeyondArmDistance.value = 0; + pullBeyondArmDuration.value = Duration.zero; + armedAt.value = null; + if (reducedMotion) { + gapController.value = 1; + } else { + gapController.animateTo(1, curve: Curves.easeOutCubic); + } + if (!reducedMotion) flapController.repeat(); + } else if (nextStatus == RefreshIndicatorStatus.done) { + stopEyeShake(); + if (reducedMotion) { + gapController.value = 0; + pullProgress.value = 0; + pullDistance.value = 0; + pullBeyondArmDistance.value = 0; + pullBeyondArmDuration.value = Duration.zero; + status.value = null; + } else { + flapController.repeat(); + gapController + .animateTo(1, curve: Curves.easeOutCubic) + .whenCompleteOrCancel(() { + if (!gapController.isCompleted || status.value == null) return; + gapController.animateBack(0, curve: Curves.easeInOutCubic); + completionController.forward(from: 0).whenCompleteOrCancel(() { + if (!completionController.isCompleted) return; + flapController.stop(); + pullProgress.value = 0; + pullDistance.value = 0; + pullBeyondArmDistance.value = 0; + pullBeyondArmDuration.value = Duration.zero; + status.value = null; + }); + }); + } + } else if (nextStatus == RefreshIndicatorStatus.canceled || + nextStatus == null) { + stopEyeShake(); + pullProgress.value = 0; + pullDistance.value = 0; + pullBeyondArmDistance.value = 0; + pullBeyondArmDuration.value = Duration.zero; + armedAt.value = null; + if (!gapController.isAnimating) gapController.reset(); + flapController.stop(); + } + } + + bool trackPull(ScrollNotification notification) { + if (notification.metrics.axis != Axis.vertical) return false; + + if (notification is! ScrollStartNotification && + notification.metrics.extentBefore == 0) { + // BouncingScrollPhysics reports a live negative scroll position while + // the user is pulling. Reading it keeps the bee coupled to the finger. + final elasticPull = + (notification.metrics.minScrollExtent - notification.metrics.pixels) + .clamp(0.0, double.infinity) + .toDouble(); + if (elasticPull > 0 || pullDistance.value > 0) { + pullDistance.value = elasticPull; + final nextProgress = (elasticPull / _triggerDistance).clamp(0.0, 1.0); + pullProgress.value = nextProgress; + if (nextProgress >= 1) beginExpressionTracking(); + } else if (notification case OverscrollNotification()) { + // Clamping physics does not expose a negative position, so build the + // same progress from its overscroll deltas. + final nextDistance = + pullDistance.value + notification.overscroll.abs(); + pullDistance.value = nextDistance; + pullProgress.value = (nextDistance / _triggerDistance).clamp( + 0.0, + 1.0, + ); + if (pullProgress.value >= 1) beginExpressionTracking(); + } + } + return false; + } + + void startPointer(PointerDownEvent event) { + final isNewGesture = activePointers.value.isEmpty; + activePointers.value.add(event.pointer); + if (!isNewGesture) return; + + lastPointerTime.value = event.timeStamp; + armedAt.value = null; + didTriggerArmHaptic.value = false; + didTriggerEmojiHaptic.value = false; + stopEyeShake(); + pullProgress.value = 0; + pullDistance.value = 0; + pullBeyondArmDistance.value = 0; + pullBeyondArmDuration.value = Duration.zero; + } + + void trackPointer(PointerMoveEvent event) { + if (!activePointers.value.contains(event.pointer)) return; + lastPointerTime.value = event.timeStamp; + if (armedAt.value == null) return; + + pullBeyondArmDistance.value = + (pullBeyondArmDistance.value + event.delta.dy) + .clamp(0.0, double.infinity) + .toDouble(); + if (armedAt.value case final armedTime?) { + pullBeyondArmDuration.value = event.timeStamp - armedTime; + } + if (!didTriggerEmojiHaptic.value && + pullBeyondArmDuration.value >= _eyeEmojiMinBeyondArmDuration && + pullBeyondArmDistance.value >= eyeEmojiSwapDistance) { + didTriggerEmojiHaptic.value = true; + unawaited(HapticFeedback.heavyImpact()); + startEyeShake(); + } + } + + void finishPointer(PointerEvent event) { + if (!activePointers.value.remove(event.pointer)) return; + if (activePointers.value.isNotEmpty) return; + + stopEyeShake(); + armedAt.value = null; + pullBeyondArmDistance.value = 0; + pullBeyondArmDuration.value = Duration.zero; + } + + final isLoading = switch (status.value) { + RefreshIndicatorStatus.snap || + RefreshIndicatorStatus.refresh || + RefreshIndicatorStatus.done => true, + _ => false, + }; + final dragRevealProgress = + ((pullProgress.value - _beeRevealStartProgress) / + (1 - _beeRevealStartProgress)) + .clamp(0.0, 1.0); + final isVisible = + status.value != null && + (isLoading || dragRevealProgress > 0 || completionProgress > 0); + final retainedGap = _loadingGap * gapProgress; + final visibleGap = pullDistance.value + retainedGap; + final top = edgeOffset + (visibleGap - _beeHeight) * _beeVerticalAlignment; + final opacity = isLoading ? 1 - completionProgress : dragRevealProgress; + final beeScale = isLoading + ? 1.0 + : _beeInitialScale + (1 - _beeInitialScale) * dragRevealProgress; + final flapAmount = reducedMotion + ? 0.0 + : isLoading + ? 0.5 - (0.5 * cos(flapProgress * 4 * pi)) + : pullProgress.value * 0.18; + final pupilFullDistance = + eyeEmojiSwapDistance - _pupilFullBeforeEmojiDistance; + final pupilDistanceProgress = + ((pullBeyondArmDistance.value - _pupilStartBeyondArmDistance) / + (pupilFullDistance - _pupilStartBeyondArmDistance)) + .clamp(0.0, 1.0); + final pupilGrowthDuration = + _eyeEmojiMinBeyondArmDuration - _pupilMinBeyondArmDuration; + final pupilProgress = + pullBeyondArmDuration.value >= _pupilMinBeyondArmDuration + ? min( + pupilDistanceProgress, + ((pullBeyondArmDuration.value - _pupilMinBeyondArmDuration) + .inMicroseconds / + pupilGrowthDuration.inMicroseconds) + .clamp(0.0, 1.0), + ).toDouble() + : 0.0; + final showEyeEmoji = !isLoading && didTriggerEmojiHaptic.value; + final eyeShakeOffset = showEyeEmoji && !reducedMotion + ? sin(eyeShakeProgress * 2 * pi) * 0.75 + : 0.0; + final scrollBehavior = ScrollConfiguration.of(context).copyWith( + overscroll: false, + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + ); + + return Stack( + clipBehavior: Clip.none, + children: [ + RefreshIndicator.noSpinner( + onRefresh: onRefresh, + onStatusChange: updateStatus, + semanticsLabel: 'Pull to refresh', + child: NotificationListener( + onNotification: trackPull, + child: Listener( + behavior: HitTestBehavior.translucent, + onPointerDown: startPointer, + onPointerMove: trackPointer, + onPointerUp: finishPointer, + onPointerCancel: finishPointer, + child: ScrollConfiguration( + behavior: scrollBehavior, + child: Transform.translate( + key: const ValueKey('bee-refresh-retained-gap'), + offset: Offset(0, retainedGap), + child: child, + ), + ), + ), + ), + ), + if (isVisible) + Positioned( + top: edgeOffset, + left: 0, + right: 0, + bottom: 0, + child: ClipRect( + child: Align( + alignment: Alignment.topCenter, + child: Transform.translate( + offset: Offset(0, top - edgeOffset), + child: IgnorePointer( + child: Opacity( + key: const ValueKey('bee-refresh-opacity'), + opacity: opacity.clamp(0.0, 1.0), + child: Transform.scale( + key: const ValueKey('bee-refresh-scale'), + scale: beeScale, + child: SizedBox( + width: _beeWidth, + height: _beeHeight, + child: Stack( + clipBehavior: Clip.none, + alignment: Alignment.topCenter, + children: [ + Positioned.fill( + child: FlappingBee( + width: _beeWidth, + color: context.colors.primary, + flapAmount: flapAmount, + eyeProgress: + !isLoading && + !showEyeEmoji && + pupilProgress > 0 + ? pupilProgress + : null, + ), + ), + if (showEyeEmoji) + Positioned( + top: 2, + left: 0, + right: 0, + child: ExcludeSemantics( + child: Transform.translate( + key: const ValueKey( + 'bee-refresh-eyes-emoji-offset', + ), + offset: const Offset(2, 0), + child: Transform.translate( + key: const ValueKey( + 'bee-refresh-eyes-emoji-shake', + ), + offset: Offset(eyeShakeOffset, 0), + child: const Text( + '👀', + key: ValueKey( + 'bee-refresh-eyes-emoji', + ), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + height: 1, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ), + ), + ], + ); + } +} diff --git a/mobile/lib/shared/widgets/brand_refresh_indicator.dart b/mobile/lib/shared/widgets/brand_refresh_indicator.dart deleted file mode 100644 index c6ab5e64038..00000000000 --- a/mobile/lib/shared/widgets/brand_refresh_indicator.dart +++ /dev/null @@ -1,269 +0,0 @@ -import 'dart:async' show unawaited; -import 'dart:math' show cos, pi; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -import '../theme/theme.dart'; -import 'fivedive_mark.dart'; - -/// Replaces the standard pull-to-refresh spinner with the 5dive mark. -/// -/// Flutter continues to own the gesture, refresh lifecycle, and accessibility -/// semantics. This widget maps those states into the elastic pull, the retained -/// loading gap, and the mark's reveal and loading pulse. -class BrandRefreshIndicator extends HookConsumerWidget { - /// Called when the user completes a pull, to load fresh data. - /// - /// The mark keeps pulsing until this future settles, so it should complete - /// only once the refresh is done. - final Future Function() onRefresh; - - /// The scrollable this indicator wraps. - /// - /// It must scroll vertically; the indicator reads its scroll notifications - /// to couple the mark to the user's finger. - final Widget child; - - /// The vertical offset of the scrollable's top edge, such as a pinned header. - final double edgeOffset; - - const BrandRefreshIndicator({ - required this.onRefresh, - required this.child, - this.edgeOffset = 0, - super.key, - }); - - static const _markHeight = 44.0; - static const _markWidth = _markHeight * 342 / 512; - static const _triggerDistance = 100.0; - static const _loadingGap = 72.0; - static const _markVerticalAlignment = 0.75; - static const _markInitialScale = 0.6; - static const _markRevealStartProgress = 0.18; - static const _settleDuration = Duration(milliseconds: 180); - static const _pulseDuration = Duration(milliseconds: 900); - - /// Peak shrink of the loading pulse, as a fraction of the resting size. - static const _pulseDepth = 0.12; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final status = useState(null); - final pullProgress = useState(0.0); - final pullDistance = useState(0.0); - final activePointers = useRef({}); - final didTriggerArmHaptic = useRef(false); - final completionController = useAnimationController( - duration: _settleDuration, - ); - final gapController = useAnimationController( - duration: _settleDuration, - reverseDuration: _settleDuration, - ); - final pulseController = useAnimationController(duration: _pulseDuration); - final completionProgress = useAnimation(completionController); - final gapProgress = useAnimation(gapController); - final pulseProgress = useAnimation(pulseController); - final reducedMotion = MediaQuery.disableAnimationsOf(context); - - void arm() { - if (activePointers.value.isEmpty) return; - if (didTriggerArmHaptic.value) return; - didTriggerArmHaptic.value = true; - unawaited(HapticFeedback.mediumImpact()); - } - - void updateStatus(RefreshIndicatorStatus? nextStatus) { - status.value = nextStatus; - - if (nextStatus == RefreshIndicatorStatus.drag) { - completionController.reset(); - gapController.reset(); - pulseController.stop(); - } else if (nextStatus == RefreshIndicatorStatus.armed) { - pullProgress.value = 1; - arm(); - } else if (nextStatus == RefreshIndicatorStatus.snap || - nextStatus == RefreshIndicatorStatus.refresh) { - pullProgress.value = 1; - if (reducedMotion) { - gapController.value = 1; - } else { - gapController.animateTo(1, curve: Curves.easeOutCubic); - pulseController.repeat(); - } - } else if (nextStatus == RefreshIndicatorStatus.done) { - if (reducedMotion) { - gapController.value = 0; - pullProgress.value = 0; - pullDistance.value = 0; - status.value = null; - } else { - pulseController.repeat(); - gapController - .animateTo(1, curve: Curves.easeOutCubic) - .whenCompleteOrCancel(() { - if (!gapController.isCompleted || status.value == null) return; - gapController.animateBack(0, curve: Curves.easeInOutCubic); - completionController.forward(from: 0).whenCompleteOrCancel(() { - if (!completionController.isCompleted) return; - pulseController.stop(); - pullProgress.value = 0; - pullDistance.value = 0; - status.value = null; - }); - }); - } - } else if (nextStatus == RefreshIndicatorStatus.canceled || - nextStatus == null) { - pullProgress.value = 0; - pullDistance.value = 0; - if (!gapController.isAnimating) gapController.reset(); - pulseController.stop(); - } - } - - bool trackPull(ScrollNotification notification) { - if (notification.metrics.axis != Axis.vertical) return false; - - if (notification is! ScrollStartNotification && - notification.metrics.extentBefore == 0) { - // BouncingScrollPhysics reports a live negative scroll position while - // the user is pulling. Reading it keeps the mark coupled to the finger. - final elasticPull = - (notification.metrics.minScrollExtent - notification.metrics.pixels) - .clamp(0.0, double.infinity) - .toDouble(); - if (elasticPull > 0 || pullDistance.value > 0) { - pullDistance.value = elasticPull; - final nextProgress = (elasticPull / _triggerDistance).clamp(0.0, 1.0); - pullProgress.value = nextProgress; - if (nextProgress >= 1) arm(); - } else if (notification case OverscrollNotification()) { - // Clamping physics does not expose a negative position, so build the - // same progress from its overscroll deltas. - final nextDistance = - pullDistance.value + notification.overscroll.abs(); - pullDistance.value = nextDistance; - pullProgress.value = (nextDistance / _triggerDistance).clamp( - 0.0, - 1.0, - ); - if (pullProgress.value >= 1) arm(); - } - } - return false; - } - - void startPointer(PointerDownEvent event) { - final isNewGesture = activePointers.value.isEmpty; - activePointers.value.add(event.pointer); - if (!isNewGesture) return; - - didTriggerArmHaptic.value = false; - pullProgress.value = 0; - pullDistance.value = 0; - } - - void finishPointer(PointerEvent event) { - activePointers.value.remove(event.pointer); - } - - final isLoading = switch (status.value) { - RefreshIndicatorStatus.snap || - RefreshIndicatorStatus.refresh || - RefreshIndicatorStatus.done => true, - _ => false, - }; - final dragRevealProgress = - ((pullProgress.value - _markRevealStartProgress) / - (1 - _markRevealStartProgress)) - .clamp(0.0, 1.0); - final isVisible = - status.value != null && - (isLoading || dragRevealProgress > 0 || completionProgress > 0); - final retainedGap = _loadingGap * gapProgress; - final visibleGap = pullDistance.value + retainedGap; - final top = - edgeOffset + (visibleGap - _markHeight) * _markVerticalAlignment; - final opacity = isLoading ? 1 - completionProgress : dragRevealProgress; - // While loading the mark breathes; while dragging it grows into place. - final pulseAmount = isLoading && !reducedMotion - ? _pulseDepth * 0.5 * (1 - cos(pulseProgress * 2 * pi)) - : 0.0; - final markScale = isLoading - ? 1.0 - pulseAmount - : _markInitialScale + (1 - _markInitialScale) * dragRevealProgress; - final scrollBehavior = ScrollConfiguration.of(context).copyWith( - overscroll: false, - physics: const BouncingScrollPhysics( - parent: AlwaysScrollableScrollPhysics(), - ), - ); - - return Stack( - clipBehavior: Clip.none, - children: [ - RefreshIndicator.noSpinner( - onRefresh: onRefresh, - onStatusChange: updateStatus, - semanticsLabel: 'Pull to refresh', - child: NotificationListener( - onNotification: trackPull, - child: Listener( - behavior: HitTestBehavior.translucent, - onPointerDown: startPointer, - onPointerUp: finishPointer, - onPointerCancel: finishPointer, - child: ScrollConfiguration( - behavior: scrollBehavior, - child: Transform.translate( - key: const ValueKey('brand-refresh-retained-gap'), - offset: Offset(0, retainedGap), - child: child, - ), - ), - ), - ), - ), - if (isVisible) - Positioned( - top: edgeOffset, - left: 0, - right: 0, - bottom: 0, - child: ClipRect( - child: Align( - alignment: Alignment.topCenter, - child: Transform.translate( - offset: Offset(0, top - edgeOffset), - child: IgnorePointer( - child: Opacity( - key: const ValueKey('brand-refresh-opacity'), - opacity: opacity.clamp(0.0, 1.0), - child: Transform.scale( - key: const ValueKey('brand-refresh-scale'), - scale: markScale, - child: SizedBox( - width: _markWidth, - height: _markHeight, - child: FiveDiveMark( - height: _markHeight, - color: context.colors.primary, - ), - ), - ), - ), - ), - ), - ), - ), - ), - ], - ); - } -} diff --git a/mobile/lib/shared/widgets/fivedive_mark.dart b/mobile/lib/shared/widgets/fivedive_mark.dart deleted file mode 100644 index 9b943996c3d..00000000000 --- a/mobile/lib/shared/widgets/fivedive_mark.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter/material.dart'; - -/// The 5dive mark, painted as a solid silhouette in [color]. -/// -/// The asset is an alpha mask cut from the same source art the launcher icons -/// are generated from, so the home screen and the onboarding hero show one -/// mark. It is the app's one brand mark: the onboarding hero and the -/// pull-to-refresh indicator both render it. -class FiveDiveMark extends StatelessWidget { - /// The rendered height of the mark. - /// - /// Width follows from the mark's 342:512 aspect ratio. - final double height; - - /// The color used for the mark's silhouette. - final Color color; - - const FiveDiveMark({required this.height, required this.color, super.key}); - - @override - Widget build(BuildContext context) { - return Image.asset( - 'assets/images/5dive-mark.png', - height: height, - color: color, - semanticLabel: '5dive', - ); - } -} diff --git a/mobile/lib/shared/widgets/flapping_bee.dart b/mobile/lib/shared/widgets/flapping_bee.dart new file mode 100644 index 00000000000..9a99bd87966 --- /dev/null +++ b/mobile/lib/shared/widgets/flapping_bee.dart @@ -0,0 +1,155 @@ +import 'dart:math' show min; + +import 'package:flutter/material.dart'; + +/// The Buzz mark at a caller-controlled wing position. +/// +/// The geometry matches the desktop loading bee. Callers drive the wings +/// themselves, which lets one painter serve both the tap-to-flutter mark +/// ([TappableFlappingBee]) and the pull-to-refresh indicator +/// ([BeeRefreshIndicator]). +class FlappingBee extends StatelessWidget { + /// The rendered width of the complete bee mark. + /// + /// Height follows from the mark's 466:309 aspect ratio. + final double width; + + /// The color used for the bee silhouette, wings, and pupils. + final Color color; + + /// How far the wings are tucked toward the body, from 0 to 1. + /// + /// 0 renders the wings fully spread; 1 renders them at their innermost + /// tuck. Callers animate this to flap the wings. + final double flapAmount; + + /// How far the pupils have grown, from 0 to 1, or null for cutout eyes. + /// + /// Only the pull-to-refresh treatment sets this. Leaving it null preserves + /// the mark's ordinary cutout eyes; a non-null value fills them in, with 1 + /// drawing the pupils at full size. + final double? eyeProgress; + + const FlappingBee({ + required this.width, + required this.color, + required this.flapAmount, + this.eyeProgress, + super.key, + }); + + @override + Widget build(BuildContext context) { + return RepaintBoundary( + child: CustomPaint( + size: Size(width, width * 309 / 466), + painter: _FlappingBeePainter( + color: color, + flapAmount: flapAmount, + eyeProgress: eyeProgress, + ), + ), + ); + } +} + +class _FlappingBeePainter extends CustomPainter { + final Color color; + final double flapAmount; + final double? eyeProgress; + + const _FlappingBeePainter({ + required this.color, + required this.flapAmount, + this.eyeProgress, + }); + + @override + void paint(Canvas canvas, Size size) { + final scale = min(size.width / 466, size.height / 309); + final renderedWidth = 466 * scale; + final renderedHeight = 309 * scale; + + canvas + ..save() + ..translate( + (size.width - renderedWidth) / 2, + (size.height - renderedHeight) / 2, + ) + ..scale(scale); + + final wingRadiusX = 91.7 * (1 - (0.38 * flapAmount)); + final wingTranslation = 30 * flapAmount; + final leftWing = Path() + ..addOval( + Rect.fromCenter( + center: Offset(91.7 + wingTranslation, 154.5), + width: wingRadiusX * 2, + height: 183.4, + ), + ); + final rightWing = Path() + ..addOval( + Rect.fromCenter( + center: Offset(374.3 - wingTranslation, 154.5), + width: wingRadiusX * 2, + height: 183.4, + ), + ); + final body = Path() + ..addRRect( + RRect.fromRectAndRadius( + const Rect.fromLTWH(128, 0, 210, 309), + const Radius.circular(34), + ), + ); + final cutouts = Path() + ..addOval( + Rect.fromCenter( + center: const Offset(193.3, 84.4), + width: 54, + height: 54, + ), + ) + ..addOval( + Rect.fromCenter(center: const Offset(276, 84.4), width: 54, height: 54), + ) + ..addRRect( + RRect.fromRectAndRadius( + const Rect.fromLTWH(166.3, 157.2, 136.9, 38.3), + const Radius.circular(5), + ), + ) + ..addRRect( + RRect.fromRectAndRadius( + const Rect.fromLTWH(166.9, 235.1, 136.2, 37.6), + const Radius.circular(5), + ), + ); + final wings = Path.combine(PathOperation.union, leftWing, rightWing); + final silhouette = Path.combine(PathOperation.union, wings, body); + final finishedMark = Path.combine( + PathOperation.difference, + silhouette, + cutouts, + ); + + canvas.drawPath(finishedMark, Paint()..color = color); + + if (eyeProgress case final progress?) { + final pupilRadius = 20 * progress.clamp(0.0, 1.0); + final pupilPaint = Paint()..color = color; + canvas + ..drawCircle(const Offset(193.3, 84.4), pupilRadius, pupilPaint) + ..drawCircle(const Offset(276, 84.4), pupilRadius, pupilPaint); + } + + canvas.restore(); + } + + @override + bool shouldRepaint(_FlappingBeePainter oldDelegate) => + color != oldDelegate.color || + flapAmount != oldDelegate.flapAmount || + eyeProgress != oldDelegate.eyeProgress; +} diff --git a/mobile/lib/shared/widgets/tappable_flapping_bee.dart b/mobile/lib/shared/widgets/tappable_flapping_bee.dart new file mode 100644 index 00000000000..2313de50f97 --- /dev/null +++ b/mobile/lib/shared/widgets/tappable_flapping_bee.dart @@ -0,0 +1,63 @@ +import 'dart:math' show cos, pi; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import 'flapping_bee.dart'; + +/// The Buzz mark with wings that flutter twice when the user taps it. +/// +/// The geometry and wing tuck match the desktop loading bee. When reduced +/// motion is enabled, the mark stays static. +class TappableFlappingBee extends HookConsumerWidget { + /// The rendered width of the complete bee mark. + final double width; + + /// The color used for the bee silhouette. + final Color color; + + const TappableFlappingBee({ + required this.width, + required this.color, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final animation = useAnimationController( + duration: const Duration(milliseconds: 480), + ); + final reducedMotion = MediaQuery.disableAnimationsOf(context); + + void flutterWings() { + if (reducedMotion) return; + animation.forward(from: 0); + } + + return Semantics( + button: true, + label: 'Buzz bee', + hint: 'Tap to make its wings flutter', + onTap: flutterWings, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + excludeFromSemantics: true, + onTap: flutterWings, + child: RepaintBoundary( + child: AnimatedBuilder( + animation: animation, + builder: (context, _) { + final flapAmount = 0.5 - (0.5 * cos(animation.value * 4 * pi)); + return FlappingBee( + width: width, + color: color, + flapAmount: flapAmount, + ); + }, + ), + ), + ), + ); + } +} diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 6d7d7bab802..2cc049c5fea 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -56,7 +56,7 @@ dev_dependencies: flutter: uses-material-design: true assets: - - assets/images/5dive-mark.png + - assets/images/buzz-icon.png # Generated by `just mobile-emoji-data` from the same emoji-mart dataset # desktop uses. Do not hand-edit. - assets/emoji/emoji-data.json diff --git a/mobile/test/features/channels/deep_link_dispatcher_test.dart b/mobile/test/features/channels/deep_link_dispatcher_test.dart index 78960218c34..ea6bc226c21 100644 --- a/mobile/test/features/channels/deep_link_dispatcher_test.dart +++ b/mobile/test/features/channels/deep_link_dispatcher_test.dart @@ -225,7 +225,7 @@ void main() { expect(storage.loadCalls, 1); expect(pending.consumeCalls, 1); - expect(find.text('Join this 5dive community?'), findsOneWidget); + expect(find.text('Join this Buzz community?'), findsOneWidget); }); testWidgets('waits for an invite modal before preparing the next invite', ( @@ -263,7 +263,7 @@ void main() { expect(pending.consumeCalls, 1); expect(pending.current, same(second)); expect(container.read(inviteJoinProvider).invite, same(first)); - expect(find.text('Join this 5dive community?'), findsOneWidget); + expect(find.text('Join this Buzz community?'), findsOneWidget); await tester.tap(find.widgetWithText(OutlinedButton, 'Cancel')); await tester.pumpAndSettle(); @@ -271,7 +271,7 @@ void main() { expect(pending.consumeCalls, 2); expect(pending.current, isNull); expect(container.read(inviteJoinProvider).invite, same(second)); - expect(find.text('Join this 5dive community?'), findsOneWidget); + expect(find.text('Join this Buzz community?'), findsOneWidget); }); testWidgets('dispatches a queued channel after preparing an invite', ( @@ -308,7 +308,7 @@ void main() { expect(pending.consumeCalls, 1); expect(pending.current, same(channelLink)); - expect(find.text('Join this 5dive community?'), findsOneWidget); + expect(find.text('Join this Buzz community?'), findsOneWidget); expect(find.byType(_CapturedDestination), findsNothing); await tester.tap(find.widgetWithText(OutlinedButton, 'Cancel')); @@ -354,7 +354,7 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.text('Join this 5dive community?'), findsOneWidget); + expect(find.text('Join this Buzz community?'), findsOneWidget); expect(inviteContainer.read(pendingDeepLinkProvider), isNull); final messageContainer = ProviderContainer( diff --git a/mobile/test/features/pairing/pairing_page_test.dart b/mobile/test/features/pairing/pairing_page_test.dart index 19a5ec5a9a9..34b9f4cfb1c 100644 --- a/mobile/test/features/pairing/pairing_page_test.dart +++ b/mobile/test/features/pairing/pairing_page_test.dart @@ -10,7 +10,7 @@ import 'package:buzz/shared/community/community.dart'; import 'package:buzz/shared/security/sensitive_action_authorizer.dart'; import 'package:buzz/shared/theme/theme.dart'; import 'package:buzz/shared/widgets/buzz_loading_indicator.dart'; -import 'package:buzz/shared/widgets/fivedive_mark.dart'; +import 'package:buzz/shared/widgets/tappable_flapping_bee.dart'; import '../../helpers/widget_helpers.dart'; @@ -23,8 +23,8 @@ void main() { WidgetHelpers.testable(child: const PairingPage()), ); - expect(find.byType(FiveDiveMark), findsOneWidget); - expect(find.text('Welcome to 5dive'), findsOneWidget); + expect(find.byType(TappableFlappingBee), findsOneWidget); + expect(find.text('Welcome to Buzz'), findsOneWidget); expect(find.text('Scan a QR code'), findsOneWidget); expect(find.text('Use pairing code'), findsOneWidget); expect(find.text('Connect'), findsNothing); @@ -340,7 +340,7 @@ void main() { ), ); - expect(find.textContaining('full 5dive identity'), findsOneWidget); + expect(find.textContaining('full Buzz identity'), findsOneWidget); expect(find.textContaining('permanent access'), findsOneWidget); expect(find.text('Codes Match'), findsOneWidget); }); diff --git a/mobile/test/features/settings/connection_section_test.dart b/mobile/test/features/settings/connection_section_test.dart index 2978474b1fb..2a197222ab5 100644 --- a/mobile/test/features/settings/connection_section_test.dart +++ b/mobile/test/features/settings/connection_section_test.dart @@ -86,7 +86,7 @@ void main() { expect(pairing.resetCalls, 1); expect(find.text('Identity recovery'), findsNothing); expect( - find.text('5dive did not return to the foreground. Try again.'), + find.text('Buzz did not return to the foreground. Try again.'), findsOneWidget, ); }); diff --git a/mobile/test/shared/security/sensitive_action_authorizer_test.dart b/mobile/test/shared/security/sensitive_action_authorizer_test.dart index 966399f9b03..940b9f12eaf 100644 --- a/mobile/test/shared/security/sensitive_action_authorizer_test.dart +++ b/mobile/test/shared/security/sensitive_action_authorizer_test.dart @@ -35,7 +35,7 @@ void main() { expect(result, DeviceAuthResult.success); verify( () => authentication.authenticate( - localizedReason: 'Confirm sending your 5dive identity to desktop', + localizedReason: 'Confirm sending your Buzz identity to desktop', authMessages: any(named: 'authMessages'), biometricOnly: false, sensitiveTransaction: true, diff --git a/mobile/test/shared/theme/buzz_theme_test.dart b/mobile/test/shared/theme/buzz_theme_test.dart index ad23a44f4f9..51b5ad1f3bf 100644 --- a/mobile/test/shared/theme/buzz_theme_test.dart +++ b/mobile/test/shared/theme/buzz_theme_test.dart @@ -31,13 +31,13 @@ void main() { expect(themePairFor(buzzDarkThemeName), buzzThemeName); }); - test('appear as a single System-mode option labelled "5dive"', () { + test('appear as a single System-mode option labelled "Buzz"', () { final paired = themeGroups().paired.map((t) => t.name); expect(paired, contains(buzzThemeName)); expect(paired, isNot(contains(buzzDarkThemeName))); - expect(pairedThemeLabel(buzzThemeName), '5dive'); - expect(themeSelectionLabel(buzzThemeName, ThemeMode.system), '5dive'); - expect(themeSelectionLabel(buzzDarkThemeName, ThemeMode.system), '5dive'); + expect(pairedThemeLabel(buzzThemeName), 'Buzz'); + expect(themeSelectionLabel(buzzThemeName, ThemeMode.system), 'Buzz'); + expect(themeSelectionLabel(buzzDarkThemeName, ThemeMode.system), 'Buzz'); }); test('forces neutral rendering without changing the stored accent', () { diff --git a/mobile/test/shared/widgets/bee_refresh_indicator_test.dart b/mobile/test/shared/widgets/bee_refresh_indicator_test.dart new file mode 100644 index 00000000000..de440ebb624 --- /dev/null +++ b/mobile/test/shared/widgets/bee_refresh_indicator_test.dart @@ -0,0 +1,379 @@ +import 'dart:async'; + +import 'package:buzz/shared/widgets/bee_refresh_indicator.dart'; +import 'package:buzz/shared/widgets/flapping_bee.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../helpers/widget_helpers.dart'; + +void main() { + testWidgets('shows the bee while pulling to refresh', (tester) async { + const contentKey = ValueKey('loading-content'); + var refreshes = 0; + final refreshCompleter = Completer(); + + await tester.pumpWidget( + WidgetHelpers.testable( + child: BeeRefreshIndicator( + onRefresh: () { + refreshes++; + return refreshCompleter.future; + }, + child: ListView( + children: const [SizedBox(key: contentKey, height: 800)], + ), + ), + ), + ); + + final listFinder = find.byType(ListView); + final restingTop = tester.getTopLeft(listFinder).dy; + final restingContentTop = tester.getTopLeft(find.byKey(contentKey)).dy; + await tester.timedDrag( + listFinder, + const Offset(0, 320), + const Duration(milliseconds: 500), + ); + await tester.pump(const Duration(milliseconds: 16)); + await tester.pump(const Duration(milliseconds: 300)); + + final beeFinder = find.byType(FlappingBee); + final loadingTop = tester.getTopLeft(listFinder).dy; + final loadingContentTop = tester.getTopLeft(find.byKey(contentKey)).dy; + final gapTransform = tester.widget( + find.byKey(const ValueKey('bee-refresh-retained-gap')), + ); + expect(beeFinder, findsOneWidget); + expect(refreshes, 1); + expect(gapTransform.transform.getTranslation().y, closeTo(72, 1)); + expect(loadingTop - restingTop, closeTo(72, 1)); + final loadingBeeRect = tester.getRect(beeFinder); + final loadingGap = loadingContentTop - restingContentTop; + expect( + loadingBeeRect.center.dy, + closeTo( + restingContentTop + + (loadingGap - loadingBeeRect.height) * 0.75 + + loadingBeeRect.height / 2, + 1, + ), + ); + + refreshCompleter.complete(); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 90)); + + final closingTop = tester.getTopLeft(listFinder).dy; + expect(closingTop, greaterThan(restingTop)); + expect(closingTop, lessThan(loadingTop)); + + await tester.pumpAndSettle(); + expect(tester.getTopLeft(listFinder).dy, closeTo(restingTop, 1)); + expect(beeFinder, findsNothing); + }); + + testWidgets('tracks each stage of an active pull', (tester) async { + tester.view.physicalSize = const Size(420, 912); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + final hapticCalls = []; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, (call) async { + if (call.method == 'HapticFeedback.vibrate') hapticCalls.add(call); + return null; + }); + addTearDown( + () => TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, null), + ); + + const contentKey = ValueKey('pull-content'); + final refreshCompleter = Completer(); + await tester.pumpWidget( + WidgetHelpers.testable( + child: BeeRefreshIndicator( + onRefresh: () => refreshCompleter.future, + child: ListView( + children: const [SizedBox(key: contentKey, height: 800)], + ), + ), + ), + ); + + final restingContentTop = tester.getTopLeft(find.byKey(contentKey)).dy; + final gesture = await tester.startGesture( + tester.getCenter(find.byType(ListView)), + pointer: 1, + ); + await gesture.moveBy(const Offset(0, 12)); + await tester.pump(); + + final beeFinder = find.byType(FlappingBee); + expect(beeFinder, findsNothing); + expect( + tester.getTopLeft(find.byKey(contentKey)).dy, + greaterThan(restingContentTop), + ); + + await gesture.moveBy(const Offset(0, 44)); + await tester.pump(); + + final earlyTop = tester.getTopLeft(beeFinder).dy; + final partialOpacity = tester.widget( + find.byKey(const ValueKey('bee-refresh-opacity')), + ); + expect(partialOpacity.opacity, greaterThan(0)); + expect(partialOpacity.opacity, lessThan(1)); + final partialScale = tester + .widget(find.byKey(const ValueKey('bee-refresh-scale'))) + .transform + .storage[0]; + expect(partialScale, greaterThan(0.6)); + expect(partialScale, lessThan(1)); + expect(tester.widget(beeFinder).eyeProgress, isNull); + expect(hapticCalls, isEmpty); + + await gesture.moveBy(const Offset(0, 120)); + await tester.pump(); + + final pulledContentTop = tester.getTopLeft(find.byKey(contentKey)).dy; + expect(tester.getTopLeft(beeFinder).dy, greaterThan(earlyTop)); + expect(tester.widget(beeFinder).eyeProgress, isNull); + expect(find.byKey(const ValueKey('bee-refresh-eyes-emoji')), findsNothing); + final pulledBeeRect = tester.getRect(beeFinder); + final pulledGap = pulledContentTop - restingContentTop; + expect( + pulledBeeRect.center.dy, + closeTo( + restingContentTop + + (pulledGap - pulledBeeRect.height) * 0.75 + + pulledBeeRect.height / 2, + 1, + ), + ); + + await gesture.moveBy(const Offset(0, 120)); + await tester.pump(); + + expect( + tester + .widget(find.byKey(const ValueKey('bee-refresh-scale'))) + .transform + .storage[0], + closeTo(1, 0.001), + ); + expect(hapticCalls, hasLength(1)); + expect(hapticCalls.single.arguments, 'HapticFeedbackType.mediumImpact'); + expect(tester.widget(beeFinder).eyeProgress, isNull); + expect(find.byKey(const ValueKey('bee-refresh-eyes-emoji')), findsNothing); + + await tester.pump(const Duration(milliseconds: 350)); + await gesture.moveBy( + const Offset(0, 120), + timeStamp: const Duration(milliseconds: 350), + ); + await tester.pump(); + + final pupilProgress = tester.widget(beeFinder).eyeProgress; + expect(pupilProgress, greaterThan(0)); + expect(pupilProgress, lessThan(0.5)); + expect(find.byKey(const ValueKey('bee-refresh-eyes-emoji')), findsNothing); + expect(hapticCalls, hasLength(1)); + + await tester.pump(const Duration(milliseconds: 400)); + await gesture.moveBy( + const Offset(0, 240), + timeStamp: const Duration(milliseconds: 750), + ); + await tester.pump(); + + expect(tester.widget(beeFinder).eyeProgress, isNull); + expect( + find.byKey(const ValueKey('bee-refresh-eyes-emoji')), + findsOneWidget, + ); + expect(hapticCalls, hasLength(2)); + expect(hapticCalls.last.arguments, 'HapticFeedbackType.heavyImpact'); + expect( + tester + .widget( + find.byKey(const ValueKey('bee-refresh-eyes-emoji-offset')), + ) + .transform + .storage[12], + 2, + ); + final initialShake = tester + .widget( + find.byKey(const ValueKey('bee-refresh-eyes-emoji-shake')), + ) + .transform + .storage[12]; + await tester.pump(const Duration(milliseconds: 35)); + final movedShake = tester + .widget( + find.byKey(const ValueKey('bee-refresh-eyes-emoji-shake')), + ) + .transform + .storage[12]; + expect(movedShake, isNot(closeTo(initialShake, 0.01))); + expect(movedShake.abs(), lessThanOrEqualTo(0.75)); + await tester.pump(const Duration(milliseconds: 105)); + expect(hapticCalls, hasLength(3)); + expect(hapticCalls.last.arguments, 'HapticFeedbackType.selectionClick'); + + final secondGesture = await tester.startGesture( + tester.getCenter(find.byType(ListView)), + pointer: 2, + ); + await secondGesture.moveBy( + const Offset(0, 40), + timeStamp: const Duration(milliseconds: 800), + ); + await gesture.up(); + await tester.pump(); + expect( + find.byKey(const ValueKey('bee-refresh-eyes-emoji')), + findsOneWidget, + ); + expect(hapticCalls, hasLength(3)); + + await secondGesture.moveBy( + const Offset(0, 80), + timeStamp: const Duration(milliseconds: 900), + ); + await tester.pump(); + expect( + find.byKey(const ValueKey('bee-refresh-eyes-emoji')), + findsOneWidget, + ); + expect(hapticCalls, hasLength(3)); + + await secondGesture.up(); + await tester.pump(); + final hapticsAfterRelease = hapticCalls.length; + await tester.pump(const Duration(milliseconds: 300)); + + expect(tester.widget(beeFinder).eyeProgress, isNull); + expect(find.byKey(const ValueKey('bee-refresh-eyes-emoji')), findsNothing); + expect(hapticCalls, hasLength(hapticsAfterRelease)); + + refreshCompleter.complete(); + await tester.pumpAndSettle(); + }); + + testWidgets('keeps expressive eyes hidden for a quick refresh flick', ( + tester, + ) async { + final hapticCalls = []; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, (call) async { + if (call.method == 'HapticFeedback.vibrate') hapticCalls.add(call); + return null; + }); + addTearDown( + () => TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, null), + ); + final refreshCompleter = Completer(); + var refreshes = 0; + await tester.pumpWidget( + WidgetHelpers.testable( + child: BeeRefreshIndicator( + onRefresh: () { + refreshes++; + return refreshCompleter.future; + }, + child: ListView(children: const [SizedBox(height: 800)]), + ), + ), + ); + + final gesture = await tester.startGesture( + tester.getCenter(find.byType(ListView)), + ); + final beeFinder = find.byType(FlappingBee); + for (var step = 0; step < 10; step++) { + await gesture.moveBy( + const Offset(0, 50), + timeStamp: Duration(milliseconds: (step + 1) * 10), + ); + await tester.pump(const Duration(milliseconds: 10)); + if (beeFinder.evaluate().isNotEmpty) { + expect(tester.widget(beeFinder).eyeProgress, isNull); + expect( + find.byKey(const ValueKey('bee-refresh-eyes-emoji')), + findsNothing, + ); + } + } + + await gesture.up(); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 300)); + + expect(refreshes, 1); + expect(hapticCalls, hasLength(1)); + expect(hapticCalls.single.arguments, 'HapticFeedbackType.mediumImpact'); + expect(tester.widget(beeFinder).eyeProgress, isNull); + expect(find.byKey(const ValueKey('bee-refresh-eyes-emoji')), findsNothing); + + refreshCompleter.complete(); + await tester.pumpAndSettle(); + }); + + testWidgets('keeps the bee static when motion is disabled', (tester) async { + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(disableAnimations: true), + child: WidgetHelpers.testable( + child: Builder( + builder: (context) => MediaQuery( + data: MediaQuery.of(context).copyWith(disableAnimations: true), + child: BeeRefreshIndicator( + onRefresh: () async {}, + child: ListView(children: const [SizedBox(height: 800)]), + ), + ), + ), + ), + ), + ); + + await tester.timedDrag( + find.byType(ListView), + const Offset(0, 160), + const Duration(milliseconds: 400), + ); + await tester.pump(); + + final bee = tester.widget(find.byType(FlappingBee)); + expect(bee.flapAmount, 0); + }); + + testWidgets('provides elastic always-scrollable physics', (tester) async { + late ScrollPhysics physics; + + await tester.pumpWidget( + WidgetHelpers.testable( + child: BeeRefreshIndicator( + onRefresh: () async {}, + child: Builder( + builder: (context) { + physics = ScrollConfiguration.of( + context, + ).getScrollPhysics(context); + return ListView(children: const [SizedBox(height: 20)]); + }, + ), + ), + ), + ); + + expect(physics, isA()); + expect(physics.parent, isA()); + }); +} diff --git a/mobile/test/shared/widgets/brand_refresh_indicator_test.dart b/mobile/test/shared/widgets/brand_refresh_indicator_test.dart deleted file mode 100644 index 088ea2ff0dc..00000000000 --- a/mobile/test/shared/widgets/brand_refresh_indicator_test.dart +++ /dev/null @@ -1,280 +0,0 @@ -import 'dart:async'; - -import 'package:buzz/shared/widgets/brand_refresh_indicator.dart'; -import 'package:buzz/shared/widgets/fivedive_mark.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../../helpers/widget_helpers.dart'; - -void main() { - testWidgets('shows the 5dive mark while pulling to refresh', (tester) async { - const contentKey = ValueKey('loading-content'); - var refreshes = 0; - final refreshCompleter = Completer(); - - await tester.pumpWidget( - WidgetHelpers.testable( - child: BrandRefreshIndicator( - onRefresh: () { - refreshes++; - return refreshCompleter.future; - }, - child: ListView( - children: const [SizedBox(key: contentKey, height: 800)], - ), - ), - ), - ); - - final listFinder = find.byType(ListView); - final restingTop = tester.getTopLeft(listFinder).dy; - final restingContentTop = tester.getTopLeft(find.byKey(contentKey)).dy; - await tester.timedDrag( - listFinder, - const Offset(0, 320), - const Duration(milliseconds: 500), - ); - await tester.pump(const Duration(milliseconds: 16)); - await tester.pump(const Duration(milliseconds: 300)); - - final markFinder = find.byType(FiveDiveMark); - final loadingTop = tester.getTopLeft(listFinder).dy; - final loadingContentTop = tester.getTopLeft(find.byKey(contentKey)).dy; - final gapTransform = tester.widget( - find.byKey(const ValueKey('brand-refresh-retained-gap')), - ); - expect(markFinder, findsOneWidget); - expect(refreshes, 1); - expect(gapTransform.transform.getTranslation().y, closeTo(72, 1)); - expect(loadingTop - restingTop, closeTo(72, 1)); - final loadingMarkRect = tester.getRect(markFinder); - final loadingGap = loadingContentTop - restingContentTop; - expect( - loadingMarkRect.center.dy, - closeTo( - restingContentTop + - (loadingGap - loadingMarkRect.height) * 0.75 + - loadingMarkRect.height / 2, - 1, - ), - ); - - refreshCompleter.complete(); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 90)); - - final closingTop = tester.getTopLeft(listFinder).dy; - expect(closingTop, greaterThan(restingTop)); - expect(closingTop, lessThan(loadingTop)); - - await tester.pumpAndSettle(); - expect(tester.getTopLeft(listFinder).dy, closeTo(restingTop, 1)); - expect(markFinder, findsNothing); - }); - - testWidgets('reveals and arms the mark across an active pull', ( - tester, - ) async { - tester.view.physicalSize = const Size(420, 912); - tester.view.devicePixelRatio = 1; - addTearDown(tester.view.resetPhysicalSize); - addTearDown(tester.view.resetDevicePixelRatio); - final hapticCalls = []; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(SystemChannels.platform, (call) async { - if (call.method == 'HapticFeedback.vibrate') hapticCalls.add(call); - return null; - }); - addTearDown( - () => TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(SystemChannels.platform, null), - ); - - const contentKey = ValueKey('pull-content'); - final refreshCompleter = Completer(); - await tester.pumpWidget( - WidgetHelpers.testable( - child: BrandRefreshIndicator( - onRefresh: () => refreshCompleter.future, - child: ListView( - children: const [SizedBox(key: contentKey, height: 800)], - ), - ), - ), - ); - - final restingContentTop = tester.getTopLeft(find.byKey(contentKey)).dy; - final gesture = await tester.startGesture( - tester.getCenter(find.byType(ListView)), - pointer: 1, - ); - await gesture.moveBy(const Offset(0, 12)); - await tester.pump(); - - final markFinder = find.byType(FiveDiveMark); - expect(markFinder, findsNothing); - expect( - tester.getTopLeft(find.byKey(contentKey)).dy, - greaterThan(restingContentTop), - ); - - await gesture.moveBy(const Offset(0, 44)); - await tester.pump(); - - final earlyTop = tester.getTopLeft(markFinder).dy; - final partialOpacity = tester.widget( - find.byKey(const ValueKey('brand-refresh-opacity')), - ); - expect(partialOpacity.opacity, greaterThan(0)); - expect(partialOpacity.opacity, lessThan(1)); - final partialScale = tester - .widget(find.byKey(const ValueKey('brand-refresh-scale'))) - .transform - .storage[0]; - expect(partialScale, greaterThan(0.6)); - expect(partialScale, lessThan(1)); - expect(hapticCalls, isEmpty); - - await gesture.moveBy(const Offset(0, 120)); - await tester.pump(); - - final pulledContentTop = tester.getTopLeft(find.byKey(contentKey)).dy; - expect(tester.getTopLeft(markFinder).dy, greaterThan(earlyTop)); - final pulledMarkRect = tester.getRect(markFinder); - final pulledGap = pulledContentTop - restingContentTop; - expect( - pulledMarkRect.center.dy, - closeTo( - restingContentTop + - (pulledGap - pulledMarkRect.height) * 0.75 + - pulledMarkRect.height / 2, - 1, - ), - ); - - await gesture.moveBy(const Offset(0, 120)); - await tester.pump(); - - expect( - tester - .widget(find.byKey(const ValueKey('brand-refresh-scale'))) - .transform - .storage[0], - closeTo(1, 0.001), - ); - expect(hapticCalls, hasLength(1)); - expect(hapticCalls.single.arguments, 'HapticFeedbackType.mediumImpact'); - - // Pulling further does not add a second arm haptic. - await gesture.moveBy(const Offset(0, 240)); - await tester.pump(); - expect(hapticCalls, hasLength(1)); - - await gesture.up(); - await tester.pump(); - refreshCompleter.complete(); - await tester.pumpAndSettle(); - }); - - testWidgets('pulses the mark while the refresh is in flight', (tester) async { - final refreshCompleter = Completer(); - await tester.pumpWidget( - WidgetHelpers.testable( - child: BrandRefreshIndicator( - onRefresh: () => refreshCompleter.future, - child: ListView(children: const [SizedBox(height: 800)]), - ), - ), - ); - - await tester.timedDrag( - find.byType(ListView), - const Offset(0, 320), - const Duration(milliseconds: 500), - ); - await tester.pump(const Duration(milliseconds: 16)); - await tester.pump(const Duration(milliseconds: 300)); - - double scale() => tester - .widget(find.byKey(const ValueKey('brand-refresh-scale'))) - .transform - .storage[0]; - - final first = scale(); - await tester.pump(const Duration(milliseconds: 220)); - final second = scale(); - expect(second, isNot(closeTo(first, 0.005))); - expect(second, lessThanOrEqualTo(1.0)); - expect(second, greaterThanOrEqualTo(0.88 - 0.001)); - - refreshCompleter.complete(); - await tester.pumpAndSettle(); - }); - - testWidgets('keeps the mark static when motion is disabled', (tester) async { - final refreshCompleter = Completer(); - await tester.pumpWidget( - MediaQuery( - data: const MediaQueryData(disableAnimations: true), - child: WidgetHelpers.testable( - child: Builder( - builder: (context) => MediaQuery( - data: MediaQuery.of(context).copyWith(disableAnimations: true), - child: BrandRefreshIndicator( - onRefresh: () => refreshCompleter.future, - child: ListView(children: const [SizedBox(height: 800)]), - ), - ), - ), - ), - ), - ); - - await tester.timedDrag( - find.byType(ListView), - const Offset(0, 320), - const Duration(milliseconds: 400), - ); - await tester.pump(const Duration(milliseconds: 16)); - await tester.pump(const Duration(milliseconds: 300)); - - double scale() => tester - .widget(find.byKey(const ValueKey('brand-refresh-scale'))) - .transform - .storage[0]; - - expect(find.byType(FiveDiveMark), findsOneWidget); - final resting = scale(); - expect(resting, closeTo(1, 0.001)); - await tester.pump(const Duration(milliseconds: 220)); - expect(scale(), closeTo(resting, 0.001)); - - refreshCompleter.complete(); - await tester.pumpAndSettle(); - }); - - testWidgets('provides elastic always-scrollable physics', (tester) async { - late ScrollPhysics physics; - - await tester.pumpWidget( - WidgetHelpers.testable( - child: BrandRefreshIndicator( - onRefresh: () async {}, - child: Builder( - builder: (context) { - physics = ScrollConfiguration.of( - context, - ).getScrollPhysics(context); - return ListView(children: const [SizedBox(height: 20)]); - }, - ), - ), - ), - ); - - expect(physics, isA()); - expect(physics.parent, isA()); - }); -} diff --git a/mobile/test/widget_test.dart b/mobile/test/widget_test.dart index 775f3f9e5ac..a72b34852a6 100644 --- a/mobile/test/widget_test.dart +++ b/mobile/test/widget_test.dart @@ -22,7 +22,7 @@ void main() { ), ); await tester.pump(); - expect(find.text('Welcome to 5dive'), findsOneWidget); + expect(find.text('Welcome to Buzz'), findsOneWidget); }); } diff --git a/scripts/mobile-worktree-clean.sh b/scripts/mobile-worktree-clean.sh index 66f1c26b67a..a64f52a0d16 100755 --- a/scripts/mobile-worktree-clean.sh +++ b/scripts/mobile-worktree-clean.sh @@ -1,14 +1,14 @@ #!/usr/bin/env bash # Uninstalls stale worktree-suffixed Buzz debug builds from booted iOS # simulators and connected Android devices/emulators. Production installs -# (com.buzz.buzzMobile / com.fivedive.app, no suffix) are never touched: +# (com.buzz.buzzMobile / xyz.block.buzz.mobile, no suffix) are never touched: # only identifiers with a worktree suffix appended after the production id # are matched. Run `just mobile-clean` (or this script directly); pass # --dry-run to list what would be removed without uninstalling. set -euo pipefail ios_prefix="com.buzz.buzzMobile." -android_prefix="com.fivedive.app." +android_prefix="xyz.block.buzz.mobile." dry_run=0 if [[ "${1:-}" == "--dry-run" ]]; then diff --git a/scripts/mobile-worktree-overrides.sh b/scripts/mobile-worktree-overrides.sh index 33126eb490e..176180db1e9 100755 --- a/scripts/mobile-worktree-overrides.sh +++ b/scripts/mobile-worktree-overrides.sh @@ -78,7 +78,7 @@ cat > "$ios_overrides" < "$android_props" <$(APP_DISPLAY_NAME)' "$plist" \ && pass "Info.plist display name resolves from build settings" \ || fail "Info.plist CFBundleDisplayName must be \$(APP_DISPLAY_NAME)" grep -q 'android:label="@string/app_name"' "$manifest" \ && pass "Android manifest label resolves from resources" \ || fail "Android manifest label must be @string/app_name" -grep -q 'resValue("string", "app_name", "5dive")' "$gradle" \ - && pass "Gradle default app_name stays 5dive" \ +grep -q 'resValue("string", "app_name", "Buzz")' "$gradle" \ + && pass "Gradle default app_name stays Buzz" \ || fail "Gradle must declare the default app_name resValue" grep -q 'worktreeLabel.matches' "$gradle" \ && pass "Gradle validates the worktree label before use" \ @@ -208,9 +208,9 @@ case "$1 $2" in esac if [[ "$1" == "devices" ]]; then exit 0; fi if [[ "$3 $4 $5" == "shell pm list" ]]; then - printf 'package:com.fivedive.app\n' - printf 'package:com.fivedive.app.feature_work_1\n' - printf 'package:com.fivedive.app.w_2fast\n' + printf 'package:xyz.block.buzz.mobile\n' + printf 'package:xyz.block.buzz.mobile.feature_work_1\n' + printf 'package:xyz.block.buzz.mobile.w_2fast\n' printf 'package:com.android.settings\n' exit 0 fi @@ -221,10 +221,10 @@ chmod +x "$stub_bin/adb" # No xcrun stub: the iOS pass is skipped when xcrun is absent, which also # keeps this test honest on Linux CI. clean_out="$(PATH="$stub_bin:/usr/bin:/bin" bash "$clean_script" --dry-run)" -printf '%s\n' "$clean_out" | grep -q 'com\.fivedive\.app\.feature_work_1' \ +printf '%s\n' "$clean_out" | grep -q 'xyz\.block\.buzz\.mobile\.feature_work_1' \ && pass "cleanup targets worktree-suffixed Android installs" \ || fail "cleanup must list suffixed installs, got: $clean_out" -printf '%s\n' "$clean_out" | grep -q 'com\.fivedive\.app\.w_2fast' \ +printf '%s\n' "$clean_out" | grep -q 'xyz\.block\.buzz\.mobile\.w_2fast' \ && pass "cleanup targets letter-prefixed suffixed installs" \ || fail "cleanup must list w_-prefixed installs, got: $clean_out" printf '%s\n' "$clean_out" | grep -q 'mobile\.feature_work_1' || true