From 416283c6b9782a9cd0a66ae67badfcf0e760f013 Mon Sep 17 00:00:00 2001 From: Tee8z Date: Fri, 30 Oct 2026 16:51:32 -0400 Subject: [PATCH] feat(mobile): add an independent Voltage Commons distribution Signed-off-by: Tee8z --- .github/workflows/voltage-commons-mobile.yml | 39 ++++ mobile/README.md | 2 + mobile/assets/images/commons-mark.svg | 4 + mobile/assets/licenses/buzz-LICENSE.txt | 201 ++++++++++++++++++ mobile/ios/Flutter/Debug.xcconfig | 4 + mobile/ios/Flutter/Release.xcconfig | 4 + mobile/ios/Runner.xcodeproj/project.pbxproj | 13 +- .../Commons-1024.png | Bin 0 -> 30918 bytes .../CommonsAppIcon.appiconset/Contents.json | 11 + .../Commons@1x.png | Bin 0 -> 6253 bytes .../Commons@2x.png | Bin 0 -> 12759 bytes .../Commons@3x.png | Bin 0 -> 19431 bytes .../CommonsLaunchImage.imageset/Contents.json | 23 ++ .../ios/Runner/CommonsLaunchScreen.storyboard | 37 ++++ mobile/ios/Runner/Info.plist | 19 +- mobile/lib/app.dart | 9 +- .../age_gate/age_restriction_page.dart | 8 +- mobile/lib/features/channels/compose_bar.dart | 2 + .../channels/compose_bar/camera_preview.dart | 3 +- .../voice_note_composer_recorder.dart | 6 +- mobile/lib/features/pairing/pairing_page.dart | 2 + .../pairing_page/onboarding_background.dart | 3 +- .../pairing_page/onboarding_wordmark.dart | 33 ++- .../pairing_page/sas_verification_view.dart | 2 +- .../lib/features/settings/settings_page.dart | 19 ++ .../settings_page/connection_section.dart | 3 +- mobile/lib/main.dart | 3 + mobile/lib/shared/branding/app_brand.dart | 27 +++ mobile/lib/shared/branding/commons_mark.dart | 23 ++ mobile/lib/shared/deeplink/deep_link.dart | 15 +- .../security/sensitive_action_authorizer.dart | 4 +- .../shared/widgets/bee_refresh_indicator.dart | 4 +- mobile/lib/shared/widgets/flapping_bee.dart | 10 + .../shared/widgets/tappable_flapping_bee.dart | 10 + mobile/pubspec.yaml | 3 + mobile/scripts/require-app-brand.sh | 46 ++++ .../features/settings/settings_page_test.dart | 40 +++- .../test/shared/branding/app_brand_test.dart | 94 ++++++++ scripts/prepare-commons-ios.sh | 25 +++ scripts/test-commons-brand.py | 86 ++++++++ voltage/mobile/Commons.xcconfig | 10 + voltage/mobile/README.md | 77 +++++++ voltage/mobile/commons-icon.svg | 5 + 43 files changed, 898 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/voltage-commons-mobile.yml create mode 100644 mobile/assets/images/commons-mark.svg create mode 100644 mobile/assets/licenses/buzz-LICENSE.txt create mode 100644 mobile/ios/Runner/Assets.xcassets/CommonsAppIcon.appiconset/Commons-1024.png create mode 100644 mobile/ios/Runner/Assets.xcassets/CommonsAppIcon.appiconset/Contents.json create mode 100644 mobile/ios/Runner/Assets.xcassets/CommonsLaunchImage.imageset/Commons@1x.png create mode 100644 mobile/ios/Runner/Assets.xcassets/CommonsLaunchImage.imageset/Commons@2x.png create mode 100644 mobile/ios/Runner/Assets.xcassets/CommonsLaunchImage.imageset/Commons@3x.png create mode 100644 mobile/ios/Runner/Assets.xcassets/CommonsLaunchImage.imageset/Contents.json create mode 100644 mobile/ios/Runner/CommonsLaunchScreen.storyboard create mode 100644 mobile/lib/shared/branding/app_brand.dart create mode 100644 mobile/lib/shared/branding/commons_mark.dart create mode 100644 mobile/scripts/require-app-brand.sh create mode 100644 mobile/test/shared/branding/app_brand_test.dart create mode 100755 scripts/prepare-commons-ios.sh create mode 100644 scripts/test-commons-brand.py create mode 100644 voltage/mobile/Commons.xcconfig create mode 100644 voltage/mobile/README.md create mode 100644 voltage/mobile/commons-icon.svg diff --git a/.github/workflows/voltage-commons-mobile.yml b/.github/workflows/voltage-commons-mobile.yml new file mode 100644 index 00000000000..b5cac44ae55 --- /dev/null +++ b/.github/workflows/voltage-commons-mobile.yml @@ -0,0 +1,39 @@ +name: Voltage Commons mobile + +on: + workflow_dispatch: + pull_request: + paths: + - mobile/** + - voltage/mobile/** + - scripts/prepare-commons-ios.sh + - scripts/test-commons-brand.py + - .github/workflows/voltage-commons-mobile.yml + +permissions: + contents: read + +concurrency: + group: voltage-commons-mobile-${{ github.ref }} + cancel-in-progress: true + +jobs: + ios: + name: Commons unsigned iOS build + runs-on: macos-latest + timeout-minutes: 40 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + - uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1 + - name: Check distribution configuration + run: python3 scripts/test-commons-brand.py -v + - name: Configure Commons + run: scripts/prepare-commons-ios.sh + - name: Install dependencies + run: cd mobile && flutter pub get + - name: Test Commons presentation + run: cd mobile && flutter test --no-pub --dart-define=BUZZ_APP_VARIANT=voltage-commons test/shared/branding/app_brand_test.dart + - name: Build complete unsigned Commons app + run: cd mobile && flutter build ios --release --no-codesign --no-pub diff --git a/mobile/README.md b/mobile/README.md index eaf82f3a28e..a76bc56d88e 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -5,6 +5,8 @@ Flutter mobile client for Buzz. See [VISION_MOBILE.md](../VISION_MOBILE.md) for intended behavior and architecture. +For Voltage's independent iPhone build, see [Voltage Commons](../voltage/mobile/README.md). + ## Setup Use the Flutter SDK pinned by the repository. Activate Hermit from the repo diff --git a/mobile/assets/images/commons-mark.svg b/mobile/assets/images/commons-mark.svg new file mode 100644 index 00000000000..12736b9593b --- /dev/null +++ b/mobile/assets/images/commons-mark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/mobile/assets/licenses/buzz-LICENSE.txt b/mobile/assets/licenses/buzz-LICENSE.txt new file mode 100644 index 00000000000..862ee3c2864 --- /dev/null +++ b/mobile/assets/licenses/buzz-LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Copyright 2026 Block, Inc. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/mobile/ios/Flutter/Debug.xcconfig b/mobile/ios/Flutter/Debug.xcconfig index 8660e4e3ed0..f180a5c50eb 100644 --- a/mobile/ios/Flutter/Debug.xcconfig +++ b/mobile/ios/Flutter/Debug.xcconfig @@ -1,3 +1,4 @@ +// Modified by Voltage: optional independent Commons distribution. #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) DEBUG @@ -8,6 +9,9 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) DEBUG // `BUNDLE_IDENTIFIER = your.app.id` (gitignored). BUNDLE_IDENTIFIER = xyz.block.buzz.dogfood.mobile APP_DISPLAY_NAME = Buzz +BUZZ_APP_ICON_NAME = AppIcon +BUZZ_LAUNCH_STORYBOARD = LaunchScreen +BUZZ_URL_SCHEME = buzz BUZZ_DEVELOPMENT_TEAM = JMTDPW9CG3 // Push support is always present in the iOS artifact. The current relay's diff --git a/mobile/ios/Flutter/Release.xcconfig b/mobile/ios/Flutter/Release.xcconfig index cdacb9e89bc..1d11d4ad757 100644 --- a/mobile/ios/Flutter/Release.xcconfig +++ b/mobile/ios/Flutter/Release.xcconfig @@ -1,3 +1,4 @@ +// Modified by Voltage: optional independent Commons distribution. #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" @@ -6,6 +7,9 @@ // files by writing `mobile/ios/Flutter/AppOverrides.xcconfig` (gitignored). BUNDLE_IDENTIFIER = xyz.block.buzz.mobile APP_DISPLAY_NAME = Buzz +BUZZ_APP_ICON_NAME = AppIcon +BUZZ_LAUNCH_STORYBOARD = LaunchScreen +BUZZ_URL_SCHEME = buzz CODE_SIGN_STYLE = Automatic CODE_SIGN_IDENTITY = iPhone Developer BUZZ_DEVELOPMENT_TEAM = EYF346PHUG diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 0d9974d1f49..c95e131daa8 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -1,4 +1,5 @@ // !$*UTF8*$! +// Modified by Voltage: select the distribution icon and validate Flutter branding. { archiveVersion = 1; classes = { @@ -7,6 +8,7 @@ objects = { /* Begin PBXBuildFile section */ + C01100000000000000000001 /* CommonsLaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C01100000000000000000002 /* CommonsLaunchScreen.storyboard */; }; C0AF00000000000000000001 /* NativeConfirmationDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0AF00000000000000000002 /* NativeConfirmationDialog.swift */; }; C0AF00000000000000000021 /* NativePresenceMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0AF00000000000000000022 /* NativePresenceMenu.swift */; }; C0AF00000000000000000011 /* NativeAddMembersSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0AF00000000000000000012 /* NativeAddMembersSheet.swift */; }; @@ -92,6 +94,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + C01100000000000000000002 /* CommonsLaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = CommonsLaunchScreen.storyboard; sourceTree = ""; }; C0AF00000000000000000002 /* NativeConfirmationDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeConfirmationDialog.swift; sourceTree = ""; }; C0AF00000000000000000022 /* NativePresenceMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePresenceMenu.swift; sourceTree = ""; }; C0AF00000000000000000012 /* NativeAddMembersSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeAddMembersSheet.swift; sourceTree = ""; }; @@ -260,6 +263,7 @@ 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + C01100000000000000000002 /* CommonsLaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, BZZ0000000000000000000A /* Runner.entitlements */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, @@ -457,6 +461,7 @@ buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + C01100000000000000000001 /* CommonsLaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, @@ -539,7 +544,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -e\n. \"$SRCROOT/../scripts/require-push-gateway-origin.sh\"\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + shellScript = "set -e\n. \"$SRCROOT/../scripts/require-app-brand.sh\"\n. \"$SRCROOT/../scripts/require-push-gateway-origin.sh\"\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; E0B5862D106D142B580309AF /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; @@ -737,7 +742,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_APPICON_NAME = "$(BUZZ_APP_ICON_NAME)"; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = "$(BUZZ_DEVELOPMENT_TEAM)"; @@ -926,7 +931,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_APPICON_NAME = "$(BUZZ_APP_ICON_NAME)"; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = "$(BUZZ_DEVELOPMENT_TEAM)"; @@ -950,7 +955,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_APPICON_NAME = "$(BUZZ_APP_ICON_NAME)"; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = "$(BUZZ_DEVELOPMENT_TEAM)"; diff --git a/mobile/ios/Runner/Assets.xcassets/CommonsAppIcon.appiconset/Commons-1024.png b/mobile/ios/Runner/Assets.xcassets/CommonsAppIcon.appiconset/Commons-1024.png new file mode 100644 index 0000000000000000000000000000000000000000..3c962b5c612e649cbd32000cdfb9f7d0944e5a69 GIT binary patch literal 30918 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

i;)ritAs*6nv=b887~<^XfatK%rN!0+Z-4}{YG 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 Yz1-SN*6Tk;`tE?DtSY!%+9dQp07@kwHvj+t literal 0 HcmV?d00001 diff --git a/mobile/ios/Runner/Assets.xcassets/CommonsLaunchImage.imageset/Commons@2x.png b/mobile/ios/Runner/Assets.xcassets/CommonsLaunchImage.imageset/Commons@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f29337ce59ca5cd1c3ae5823770d1b6ac4502fde GIT binary patch literal 12759 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 a&##rlIH59^?KSY+C + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 5dfde907785..e9d7e21100c 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -1,5 +1,6 @@ + BuzzAppGroupIdentifier @@ -19,7 +20,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - Buzz + $(APP_DISPLAY_NAME) CFBundlePackageType APPL CFBundleShortVersionString @@ -32,10 +33,10 @@ CFBundleTypeRole Editor CFBundleURLName - com.buzz.deeplink + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleURLSchemes - buzz + $(BUZZ_URL_SCHEME) @@ -48,15 +49,15 @@ LSRequiresIPhoneOS NSFaceIDUsageDescription - Buzz uses Face ID to confirm sensitive identity transfers. + $(APP_DISPLAY_NAME) uses Face ID to confirm sensitive identity transfers. NSCameraUsageDescription - Buzz uses the camera to take profile photos and animated avatars, attach photos to messages, and scan QR codes for device pairing. + $(APP_DISPLAY_NAME) uses the camera to take profile photos and animated avatars, attach photos to messages, and scan QR codes for device pairing. NSMicrophoneUsageDescription - Buzz needs microphone access so you can speak in Huddles and record voice notes. + $(APP_DISPLAY_NAME) needs microphone access so you can speak in Huddles and record voice notes. NSPhotoLibraryUsageDescription - Buzz uses your photo library to select profile photos and images to attach to messages. + $(APP_DISPLAY_NAME) uses your photo library to select profile photos and images to attach to messages. NSPhotoLibraryAddUsageDescription - Buzz needs permission to save images to your photo library. + $(APP_DISPLAY_NAME) needs permission to save images to your photo library. NSUserActivityTypes INSendMessageIntent @@ -87,7 +88,7 @@ UIApplicationSupportsIndirectInputEvents UILaunchStoryboardName - LaunchScreen + $(BUZZ_LAUNCH_STORYBOARD) UIMainStoryboardFile Main UISupportedInterfaceOrientations diff --git a/mobile/lib/app.dart b/mobile/lib/app.dart index d5e5bac9b13..e42f0679ac4 100644 --- a/mobile/lib/app.dart +++ b/mobile/lib/app.dart @@ -1,4 +1,6 @@ +// Modified by Voltage: optional independent distribution branding. import 'dart:async'; +import 'shared/branding/app_brand.dart'; import 'shared/widgets/ios_navigation_metrics.dart'; import 'shared/community/paired_community_landing.dart'; import 'shared/community/community_loading_surface.dart'; @@ -395,7 +397,7 @@ class App extends HookConsumerWidget { return MaterialApp( navigatorKey: _mobileRootNavigatorKey, navigatorObservers: [voiceNoteRouteObserver], - title: 'Buzz', + title: appDisplayName, theme: AppTheme.light( colorScheme: lightScheme, topSectionGradient: buzzLightGradient, @@ -490,7 +492,10 @@ class _SplashScreen extends StatelessWidget { Widget build(BuildContext context) { return const Scaffold( body: Center( - child: BuzzLoadingIndicator(size: 56, semanticLabel: 'Starting Buzz'), + child: BuzzLoadingIndicator( + size: 56, + semanticLabel: 'Starting $appDisplayName', + ), ), ); } diff --git a/mobile/lib/features/age_gate/age_restriction_page.dart b/mobile/lib/features/age_gate/age_restriction_page.dart index f5be3e683c6..26214e7e006 100644 --- a/mobile/lib/features/age_gate/age_restriction_page.dart +++ b/mobile/lib/features/age_gate/age_restriction_page.dart @@ -1,3 +1,5 @@ +// Modified by Voltage: attribute the age restriction to the selected app. +import '../../shared/branding/app_brand.dart'; import 'package:flutter/material.dart'; import '../../shared/theme/theme.dart'; @@ -26,7 +28,7 @@ class AgeRestrictionPage extends StatelessWidget { ), const SizedBox(height: Grid.lg), Text( - 'Buzz is for people 18 and older', + '$appDisplayName is for people 18 and older', textAlign: TextAlign.center, style: context.textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.w600, @@ -34,7 +36,9 @@ class AgeRestrictionPage extends StatelessWidget { ), const SizedBox(height: Grid.sm), Text( - "You must be 18 or older to use Buzz under Buzz's Terms.", + isVoltageCommons + ? 'You must be 18 or older to use this app.' + : "You must be 18 or older to use Buzz under Buzz's Terms.", textAlign: TextAlign.center, style: context.textTheme.bodyLarge?.copyWith( color: context.colors.onSurfaceVariant, diff --git a/mobile/lib/features/channels/compose_bar.dart b/mobile/lib/features/channels/compose_bar.dart index c43d4057fef..f76ac05fed2 100644 --- a/mobile/lib/features/channels/compose_bar.dart +++ b/mobile/lib/features/channels/compose_bar.dart @@ -1,3 +1,5 @@ +// Modified by Voltage: permission errors identify the selected app. +import '../../shared/branding/app_brand.dart'; import 'dart:async'; import 'dart:collection'; import 'dart:io'; diff --git a/mobile/lib/features/channels/compose_bar/camera_preview.dart b/mobile/lib/features/channels/compose_bar/camera_preview.dart index 12e80e53eef..b2c8cf6ca3a 100644 --- a/mobile/lib/features/channels/compose_bar/camera_preview.dart +++ b/mobile/lib/features/channels/compose_bar/camera_preview.dart @@ -323,7 +323,8 @@ String _cameraErrorMessage(Object error) { return switch (error.code) { 'CameraAccessDenied' || 'CameraAccessDeniedWithoutPrompt' || - 'CameraAccessRestricted' => 'Camera access is turned off for Buzz.', + 'CameraAccessRestricted' => + 'Camera access is turned off for $appDisplayName.', 'no-cameras' => 'Camera isn’t available on this device.', _ => 'Camera couldn’t start. Try again.', }; diff --git a/mobile/lib/features/channels/voice_note_composer_recorder.dart b/mobile/lib/features/channels/voice_note_composer_recorder.dart index deb60e11f30..3412a7a2fa9 100644 --- a/mobile/lib/features/channels/voice_note_composer_recorder.dart +++ b/mobile/lib/features/channels/voice_note_composer_recorder.dart @@ -1,3 +1,5 @@ +// Modified by Voltage: use the selected distribution name. +import '../../shared/branding/app_brand.dart'; import 'dart:async'; import 'dart:io'; @@ -83,7 +85,7 @@ class VoiceNoteComposerRecorder extends HookConsumerWidget { } } catch (_) { if (context.mounted) { - error.value = 'Buzz could not finish the voice note.'; + error.value = '$appDisplayName could not finish the voice note.'; isStopping.value = false; } } @@ -117,7 +119,7 @@ class VoiceNoteComposerRecorder extends HookConsumerWidget { } catch (_) { if (active) { error.value = - 'Buzz could not start recording. Check microphone access.'; + '$appDisplayName could not start recording. Check microphone access.'; } } }()); diff --git a/mobile/lib/features/pairing/pairing_page.dart b/mobile/lib/features/pairing/pairing_page.dart index 88e7157ec97..c5ba80393f2 100644 --- a/mobile/lib/features/pairing/pairing_page.dart +++ b/mobile/lib/features/pairing/pairing_page.dart @@ -1,4 +1,6 @@ +// Modified by Voltage: independent welcome branding. import 'dart:async'; +import '../../shared/branding/app_brand.dart'; import 'dart:math' show sqrt2; import 'dart:ui' show ImageFilter; diff --git a/mobile/lib/features/pairing/pairing_page/onboarding_background.dart b/mobile/lib/features/pairing/pairing_page/onboarding_background.dart index 9f4acbd19bc..862d1c2852c 100644 --- a/mobile/lib/features/pairing/pairing_page/onboarding_background.dart +++ b/mobile/lib/features/pairing/pairing_page/onboarding_background.dart @@ -1,3 +1,4 @@ +// Modified by Voltage: optional independent Commons distribution. part of '../pairing_page.dart'; class _OnboardingBackground extends StatelessWidget { @@ -12,7 +13,7 @@ class _OnboardingBackground extends StatelessWidget { key: const Key('pairing-onboarding-background'), decoration: BoxDecoration( color: isDark ? const Color(0xFF11181D) : const Color(0xFFE7F0EF), - image: isDark + image: isDark || isVoltageCommons ? null : const DecorationImage( // Same supplied artwork and full-image stretch as Buzz 2.0. diff --git a/mobile/lib/features/pairing/pairing_page/onboarding_wordmark.dart b/mobile/lib/features/pairing/pairing_page/onboarding_wordmark.dart index f3783bde976..f104556dada 100644 --- a/mobile/lib/features/pairing/pairing_page/onboarding_wordmark.dart +++ b/mobile/lib/features/pairing/pairing_page/onboarding_wordmark.dart @@ -1,4 +1,7 @@ +// Modified by Voltage: optional Commons welcome identity. import 'dart:ui' as ui; +import '../../../shared/branding/app_brand.dart'; +import '../../../shared/branding/commons_mark.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -38,7 +41,7 @@ class _OnboardingWordmarkState extends State _motionEnabled = !MediaQuery.disableAnimationsOf(context) && TickerMode.valuesOf(context).enabled; - if (_motionEnabled && !_loading) { + if (!isVoltageCommons && _motionEnabled && !_loading) { _loading = true; _loadArtwork(); } @@ -104,6 +107,34 @@ class _OnboardingWordmarkState extends State @override Widget build(BuildContext context) { + if (isVoltageCommons) { + final ink = widget.color ?? const Color(0xFF163B46); + return Semantics( + label: appDisplayName, + header: true, + child: ExcludeSemantics( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CommonsMark(size: 64, color: ink), + const SizedBox(height: 8), + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + appDisplayName, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: ink, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ); + } return Semantics( label: 'Buzz', image: true, diff --git a/mobile/lib/features/pairing/pairing_page/sas_verification_view.dart b/mobile/lib/features/pairing/pairing_page/sas_verification_view.dart index 91ae8655b96..795b4297b9c 100644 --- a/mobile/lib/features/pairing/pairing_page/sas_verification_view.dart +++ b/mobile/lib/features/pairing/pairing_page/sas_verification_view.dart @@ -134,7 +134,7 @@ class _SasVerificationView extends HookConsumerWidget { ), const SizedBox(height: Grid.xxs), Text( - '$biometricLabel to confirm it’s you before sending your Buzz identity to another device.', + '$biometricLabel to confirm it’s you before sending your $appDisplayName identity to another device.', textAlign: TextAlign.center, style: context.textTheme.bodyMedium?.copyWith( color: context._onboardingMutedInk, diff --git a/mobile/lib/features/settings/settings_page.dart b/mobile/lib/features/settings/settings_page.dart index 4d9199be745..0fe338a6b32 100644 --- a/mobile/lib/features/settings/settings_page.dart +++ b/mobile/lib/features/settings/settings_page.dart @@ -1,4 +1,6 @@ +// Modified by Voltage: expose upstream credit and distribution licenses. import 'dart:async'; +import '../../shared/branding/app_brand.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -125,6 +127,23 @@ class SettingsPage extends HookConsumerWidget { _ConnectionSection( identityRecoveryPageBuilder: identityRecoveryPageBuilder, ), + if (isVoltageCommons) + AppListCard( + verticalPadding: Grid.twelve, + children: [ + AppListRow( + title: 'About $appDisplayName', + subtitle: 'Built on Buzz by Block', + trailing: const _RowChevron(), + onTap: () => showAboutDialog( + context: context, + applicationName: appDisplayName, + applicationVersion: packageInfo.data?.version, + applicationLegalese: upstreamAttribution, + ), + ), + ], + ), if (packageInfo.hasData) _VersionFooter( version: packageInfo.data!.version, diff --git a/mobile/lib/features/settings/settings_page/connection_section.dart b/mobile/lib/features/settings/settings_page/connection_section.dart index b45860012d5..835d3c69847 100644 --- a/mobile/lib/features/settings/settings_page/connection_section.dart +++ b/mobile/lib/features/settings/settings_page/connection_section.dart @@ -1,3 +1,4 @@ +// Modified by Voltage: identify the selected app in foreground errors. part of '../settings_page.dart'; class _ConnectionSection extends ConsumerWidget { @@ -47,7 +48,7 @@ class _ConnectionSection extends ConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( - 'Buzz did not return to the foreground. Try again.', + '$appDisplayName did not return to the foreground. Try again.', ), ), ); diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 6c1aa734df7..6a21117762e 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -2,7 +2,9 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; +// Modified by Voltage: include upstream distribution license notices. import 'app.dart'; +import 'shared/branding/app_brand.dart'; import 'features/age_gate/age_signal_push_bootstrap.dart'; import 'features/invites/invite_join_provider.dart'; import 'shared/push/push_bridge.dart'; @@ -13,6 +15,7 @@ void main() => runBuzzApp(const App()); Future runBuzzApp(Widget app) async { WidgetsFlutterBinding.ensureInitialized(); + registerDistributionLicenses(); installBuzzPushMethodHandler(); await syncPendingBuzzPushNotificationResponse(); diff --git a/mobile/lib/shared/branding/app_brand.dart b/mobile/lib/shared/branding/app_brand.dart new file mode 100644 index 00000000000..997774b4db3 --- /dev/null +++ b/mobile/lib/shared/branding/app_brand.dart @@ -0,0 +1,27 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +/// The distribution selected at build time. Protocol names remain unchanged. +const isVoltageCommons = + String.fromEnvironment('BUZZ_APP_VARIANT') == 'voltage-commons'; + +/// The user-facing application name, independent of its permanent bundle ID. +const appDisplayName = isVoltageCommons ? 'Voltage Commons' : 'Buzz'; + +/// Attribution shown by the independent Voltage distribution. +const upstreamAttribution = + 'Built on Buzz, an open-source project by Block, Inc. ' + 'This modified client is maintained by Voltage and is not an official ' + 'Block release.\n\nSource: https://github.com/block/buzz\n' + 'Voltage modifications: https://github.com/voltagecloud/buzz'; + +/// Includes the upstream license in Flutter's in-app license viewer. +void registerDistributionLicenses() { + if (!isVoltageCommons) return; + LicenseRegistry.addLicense(() async* { + final license = await rootBundle.loadString( + 'assets/licenses/buzz-LICENSE.txt', + ); + yield LicenseEntryWithLineBreaks(['Buzz — Block, Inc.'], license); + }); +} diff --git a/mobile/lib/shared/branding/commons_mark.dart b/mobile/lib/shared/branding/commons_mark.dart new file mode 100644 index 00000000000..2a2c0ca3e4b --- /dev/null +++ b/mobile/lib/shared/branding/commons_mark.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +/// The independent Commons mark. Callers own any accessibility label. +class CommonsMark extends StatelessWidget { + /// Creates a mark that follows the surrounding foreground color. + const CommonsMark({required this.size, required this.color, super.key}); + + /// The available width and height. + final double size; + + /// The foreground color. + final Color color; + + @override + Widget build(BuildContext context) => SvgPicture.asset( + 'assets/images/commons-mark.svg', + width: size, + height: size, + colorFilter: ColorFilter.mode(color, BlendMode.srcIn), + excludeFromSemantics: true, + ); +} diff --git a/mobile/lib/shared/deeplink/deep_link.dart b/mobile/lib/shared/deeplink/deep_link.dart index c58acefd218..5482f85857a 100644 --- a/mobile/lib/shared/deeplink/deep_link.dart +++ b/mobile/lib/shared/deeplink/deep_link.dart @@ -7,6 +7,8 @@ /// half-formed target. library; +// Modified by Voltage: accept the independent distribution's OS handoffs. +import '../branding/app_brand.dart'; import '../relay/relay_validation.dart'; /// A parsed deep link supported by the app. @@ -148,7 +150,7 @@ String buildMessageLink({ /// parameters and fragments are rejected so malformed or ambiguous links never /// become navigation targets. ChannelDeepLink? parseChannelDeepLink(Uri uri) { - if (uri.scheme != 'buzz' || uri.host != 'channel') return null; + if (!_isAppScheme(uri.scheme) || uri.host != 'channel') return null; if (uri.hasQuery || uri.hasFragment || uri.userInfo.isNotEmpty || @@ -174,7 +176,7 @@ ChannelDeepLink? parseChannelDeepLink(Uri uri) { /// shape: no path, fragment, credentials, duplicate or unknown parameters; a /// UUID channel; and 64-character hexadecimal message/thread event IDs. MessageDeepLink? parseMessageDeepLink(Uri uri) { - if (uri.scheme != 'buzz' || uri.host != 'message') return null; + if (!_isAppScheme(uri.scheme) || uri.host != 'message') return null; if (uri.path.isNotEmpty || uri.hasFragment || uri.userInfo.isNotEmpty || @@ -225,7 +227,7 @@ MessageDeepLink? parseMessageDeepLink(Uri uri) { InviteDeepLink? parseInviteDeepLink(Uri uri) { if (uri.hasFragment || uri.userInfo.isNotEmpty) return null; - if (uri.scheme == 'buzz') { + if (_isAppScheme(uri.scheme)) { if (uri.host != 'join') return null; final relay = uri.queryParameters['relay']; final code = uri.queryParameters['code']; @@ -313,7 +315,8 @@ class EntityDeepLink extends BuzzDeepLink { /// Parse canonical `buzz://repo|pr|issue` permalinks for inline presentation. EntityDeepLink? parseEntityDeepLink(Uri uri) { - if (uri.scheme != 'buzz' || !{'repo', 'pr', 'issue'}.contains(uri.host)) { + if (!_isAppScheme(uri.scheme) || + !{'repo', 'pr', 'issue'}.contains(uri.host)) { return null; } if (uri.path.isNotEmpty || @@ -352,3 +355,7 @@ EntityDeepLink? parseEntityDeepLink(Uri uri) { eventId: eventId?.toLowerCase(), ); } + +// Canonical shared links remain buzz:// for interoperation with other clients. +bool _isAppScheme(String scheme) => + scheme == 'buzz' || (isVoltageCommons && scheme == 'voltage-workspace'); diff --git a/mobile/lib/shared/security/sensitive_action_authorizer.dart b/mobile/lib/shared/security/sensitive_action_authorizer.dart index b229f633302..d92b83c181f 100644 --- a/mobile/lib/shared/security/sensitive_action_authorizer.dart +++ b/mobile/lib/shared/security/sensitive_action_authorizer.dart @@ -1,3 +1,5 @@ +// Modified by Voltage: use the selected distribution name. +import '../branding/app_brand.dart'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:local_auth/local_auth.dart'; @@ -25,7 +27,7 @@ class LocalSensitiveActionAuthorizer implements SensitiveActionAuthorizer { Future authorizeIdentityAction({ required bool biometricOnly, }) => _authorize( - localizedReason: 'Confirm sending your Buzz identity to desktop', + localizedReason: 'Confirm sending your $appDisplayName identity to desktop', biometricOnly: biometricOnly, ); diff --git a/mobile/lib/shared/widgets/bee_refresh_indicator.dart b/mobile/lib/shared/widgets/bee_refresh_indicator.dart index f5c71adc17e..19cecb3f52f 100644 --- a/mobile/lib/shared/widgets/bee_refresh_indicator.dart +++ b/mobile/lib/shared/widgets/bee_refresh_indicator.dart @@ -7,6 +7,8 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../theme/theme.dart'; +// Modified by Voltage: the Commons mark has no bee-eye animation. +import '../branding/app_brand.dart'; import 'flapping_bee.dart'; /// Replaces the standard pull-to-refresh spinner with Buzz's loading bee. @@ -392,7 +394,7 @@ class BeeRefreshIndicator extends HookConsumerWidget { : null, ), ), - if (showEyeEmoji) + if (showEyeEmoji && !isVoltageCommons) Positioned( top: 2, left: 0, diff --git a/mobile/lib/shared/widgets/flapping_bee.dart b/mobile/lib/shared/widgets/flapping_bee.dart index a9057b60f2a..ea0a3a49e6e 100644 --- a/mobile/lib/shared/widgets/flapping_bee.dart +++ b/mobile/lib/shared/widgets/flapping_bee.dart @@ -1,4 +1,7 @@ +// Modified by Voltage: use the Commons mark in the Voltage distribution. import 'dart:math' show min; +import '../branding/app_brand.dart'; +import '../branding/commons_mark.dart'; import 'package:flutter/material.dart'; @@ -40,6 +43,13 @@ class FlappingBee extends StatelessWidget { @override Widget build(BuildContext context) { + if (isVoltageCommons) { + return SizedBox( + width: width, + height: width * 309 / 466, + child: CommonsMark(size: width * 309 / 466, color: color), + ); + } return RepaintBoundary( child: CustomPaint( size: Size(width, width * 309 / 466), diff --git a/mobile/lib/shared/widgets/tappable_flapping_bee.dart b/mobile/lib/shared/widgets/tappable_flapping_bee.dart index 2313de50f97..893ca2d0111 100644 --- a/mobile/lib/shared/widgets/tappable_flapping_bee.dart +++ b/mobile/lib/shared/widgets/tappable_flapping_bee.dart @@ -1,4 +1,7 @@ +// Modified by Voltage: label the independent Commons mark accurately. import 'dart:math' show cos, pi; +import '../branding/app_brand.dart'; +import '../branding/commons_mark.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -25,6 +28,13 @@ class TappableFlappingBee extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + if (isVoltageCommons) { + return Semantics( + image: true, + label: appDisplayName, + child: CommonsMark(size: width, color: color), + ); + } final animation = useAnimationController( duration: const Duration(milliseconds: 480), ); diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 2e09552ae8e..9bf63146db7 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -1,3 +1,4 @@ +# Modified by Voltage: package the upstream distribution license. name: buzz description: Buzz mobile client publish_to: 'none' @@ -68,9 +69,11 @@ flutter: shaders: - shaders/onboarding_wordmark.frag assets: + - assets/licenses/buzz-LICENSE.txt - assets/images/buzz-icon.png - assets/images/shell-gradient.png - assets/images/buzz-wordmark.png + - assets/images/commons-mark.svg # 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/scripts/require-app-brand.sh b/mobile/scripts/require-app-brand.sh new file mode 100644 index 00000000000..6bb01617f42 --- /dev/null +++ b/mobile/scripts/require-app-brand.sh @@ -0,0 +1,46 @@ +#!/bin/sh +# Modified by Voltage: bind native app identity to the Flutter distribution. +# Sourced by Xcode before the Flutter build phase. +set -eu + +brand=${BUZZ_APP_VARIANT:-} +seen=false +old_ifs=$IFS +IFS=',' +for encoded in ${DART_DEFINES:-}; do + decoded=$(printf '%s' "$encoded" | base64 --decode 2>/dev/null || printf '%s' "$encoded" | base64 -D 2>/dev/null || true) + case "$decoded" in + BUZZ_APP_VARIANT=*) + if [ "$seen" = true ]; then + echo 'error: BUZZ_APP_VARIANT must be supplied at most once.' >&2 + exit 1 + fi + seen=true + if [ "${decoded#BUZZ_APP_VARIANT=}" != "$brand" ]; then + echo 'error: Native and Flutter app branding must match.' >&2 + exit 1 + fi + ;; + esac +done +IFS=$old_ifs + +case "$brand" in + '') ;; + voltage-commons) + if [ "${PRODUCT_BUNDLE_IDENTIFIER:-}" != cloud.voltage.workspace ] || + [ "${APP_DISPLAY_NAME:-}" != 'Voltage Commons' ] || + [ "${BUZZ_APP_ICON_NAME:-}" != CommonsAppIcon ] || + [ "${BUZZ_URL_SCHEME:-}" != voltage-workspace ] || + [ "${BUZZ_LAUNCH_STORYBOARD:-}" != CommonsLaunchScreen ]; then + echo 'error: Commons requires its own bundle identifier, display name, icon, launch screen, and URL scheme.' >&2 + exit 1 + fi + if [ "$seen" = false ]; then + encoded=$(printf '%s' "BUZZ_APP_VARIANT=$brand" | base64 | tr -d '\n') + DART_DEFINES=${DART_DEFINES:+$DART_DEFINES,}$encoded + export DART_DEFINES + fi + ;; + *) echo 'error: Unknown BUZZ_APP_VARIANT.' >&2; exit 1 ;; +esac diff --git a/mobile/test/features/settings/settings_page_test.dart b/mobile/test/features/settings/settings_page_test.dart index 5cc2f78b385..5f01ee0db88 100644 --- a/mobile/test/features/settings/settings_page_test.dart +++ b/mobile/test/features/settings/settings_page_test.dart @@ -1,3 +1,5 @@ +// Modified by Voltage: check both distribution identities and scrollable credits. +import 'package:buzz/shared/branding/app_brand.dart'; import 'dart:async'; import 'dart:io'; import 'dart:ui' as ui; @@ -86,13 +88,49 @@ void main() { findsOneWidget, ); final beforeScroll = tester.getTopLeft(version).dy; - await tester.drag(find.byType(ListView), const Offset(0, 100)); + await tester.timedDrag( + find.byType(ListView), + const Offset(0, 20), + const Duration(milliseconds: 500), + ); await tester.pumpAndSettle(); expect(tester.getTopLeft(version).dy, greaterThan(beforeScroll)); expect(tester.takeException(), isNull); }); } + testWidgets('Commons exposes independent publisher credit in About', ( + tester, + ) async { + SharedPreferences.setMockInitialValues({}); + final prefs = await SharedPreferences.getInstance(); + await tester.pumpWidget( + ProviderScope( + overrides: [savedPrefsProvider.overrideWithValue(prefs)], + child: MaterialApp( + theme: AppTheme.light(), + home: SettingsPage( + profileHeader: const SizedBox.shrink(), + identityRecoveryPageBuilder: (_) => const SizedBox.shrink(), + ), + ), + ), + ); + await tester.pumpAndSettle(); + final about = find.text('About Voltage Commons'); + if (!isVoltageCommons) { + expect(about, findsNothing); + return; + } + await tester.scrollUntilVisible(about, 200); + await tester.tap(about); + await tester.pumpAndSettle(); + expect(find.text(appDisplayName), findsOneWidget); + expect(find.text(upstreamAttribution), findsOneWidget); + expect(find.text('View licenses'), findsOneWidget); + expect(tester.takeException(), isNull); + }); + for (final outcome in ['absent', 'error']) { testWidgets('hides notifications while capability loads and is $outcome', ( tester, diff --git a/mobile/test/shared/branding/app_brand_test.dart b/mobile/test/shared/branding/app_brand_test.dart new file mode 100644 index 00000000000..99cfa1d3783 --- /dev/null +++ b/mobile/test/shared/branding/app_brand_test.dart @@ -0,0 +1,94 @@ +import 'package:buzz/features/pairing/pairing_page/onboarding_wordmark.dart'; +import 'package:buzz/shared/branding/app_brand.dart'; +import 'package:buzz/shared/branding/commons_mark.dart'; +import 'package:buzz/shared/deeplink/deep_link.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'; + +void main() { + testWidgets( + 'welcome uses the selected distribution and one accessible label', + (tester) async { + final semantics = tester.ensureSemantics(); + await tester.pumpWidget( + MaterialApp( + home: MediaQuery( + data: const MediaQueryData( + disableAnimations: true, + textScaler: TextScaler.linear(2), + ), + child: const Center( + child: SizedBox( + width: 280, + height: 140, + child: OnboardingWordmark(), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + expect(find.bySemanticsLabel(appDisplayName), findsOneWidget); + expect( + find.byType(CommonsMark), + isVoltageCommons ? findsOneWidget : findsNothing, + ); + expect( + find.text('Voltage Commons'), + isVoltageCommons ? findsOneWidget : findsNothing, + ); + expect(tester.takeException(), isNull); + semantics.dispose(); + }, + ); + + testWidgets('shared loading mark uses the distribution artwork', ( + tester, + ) async { + await tester.pumpWidget( + const MaterialApp( + home: Center( + child: FlappingBee(width: 60, color: Colors.teal, flapAmount: 0), + ), + ), + ); + await tester.pumpAndSettle(); + expect( + find.byType(CommonsMark), + isVoltageCommons ? findsOneWidget : findsNothing, + ); + expect(tester.takeException(), isNull); + }); + + test('independent OS handoffs are accepted only in Commons builds', () { + const channel = 'fe3d5502-7481-4220-9573-82fa58f70017'; + final standard = Uri.parse('buzz://channel/$channel'); + final commons = Uri.parse('voltage-workspace://channel/$channel'); + expect(parseChannelDeepLink(standard), isNotNull); + expect( + parseChannelDeepLink(commons), + isVoltageCommons ? isNotNull : isNull, + ); + final message = Uri.parse( + 'voltage-workspace://message?channel=$channel&id=${'a' * 64}', + ); + expect( + parseMessageDeepLink(message), + isVoltageCommons ? isNotNull : isNull, + ); + final invite = Uri.parse( + 'voltage-workspace://join?relay=wss%3A%2F%2Frelay.example&code=hello', + ); + expect(parseInviteDeepLink(invite), isVoltageCommons ? isNotNull : isNull); + }); + + test('distribution packages the complete upstream license', () async { + final license = await rootBundle.loadString( + 'assets/licenses/buzz-LICENSE.txt', + ); + expect(license, contains('Copyright 2026 Block, Inc.')); + expect(license, contains('6. Trademarks.')); + }); +} diff --git a/scripts/prepare-commons-ios.sh b/scripts/prepare-commons-ios.sh new file mode 100755 index 00000000000..712c000f013 --- /dev/null +++ b/scripts/prepare-commons-ios.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Configure the independent Voltage client without changing upstream defaults. +set -euo pipefail +repo_root=$(cd "$(dirname "$0")/.." && pwd) +destination="$repo_root/mobile/ios/Flutter/AppOverrides.xcconfig" +if [[ -e "$destination" ]]; then + echo "AppOverrides.xcconfig already exists. Review it before replacing your local signing configuration." >&2 + exit 1 +fi +team=${VOLTAGE_APPLE_TEAM_ID:-} +if [[ -n "$team" && ! "$team" =~ ^[A-Z0-9]{10}$ ]]; then + echo 'VOLTAGE_APPLE_TEAM_ID must be a ten-character Apple team ID.' >&2 + exit 1 +fi +temporary=$(mktemp "$destination.XXXXXX") +trap 'rm -f "$temporary"' EXIT +cat "$repo_root/voltage/mobile/Commons.xcconfig" > "$temporary" +if [[ -n "$team" ]]; then + printf '\nBUZZ_DEVELOPMENT_TEAM = %s\n' "$team" >> "$temporary" +fi +mv "$temporary" "$destination" +echo 'Configured Voltage Commons (cloud.voltage.workspace).' +if [[ -z "$team" ]]; then + echo 'Signing team is unset. Unsigned builds are available; device distribution requires a Voltage Apple team.' +fi diff --git a/scripts/test-commons-brand.py b/scripts/test-commons-brand.py new file mode 100644 index 00000000000..c5134f6d2fe --- /dev/null +++ b/scripts/test-commons-brand.py @@ -0,0 +1,86 @@ +"""Exercise the production native branding gate and override preparation.""" +import base64 +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +GATE = ROOT / 'mobile/scripts/require-app-brand.sh' + + +def encoded(value): + return base64.b64encode(value.encode()).decode() + + +class BrandingTests(unittest.TestCase): + def gate(self, **changes): + env = {key: value for key, value in os.environ.items() + if key not in ('DART_DEFINES', 'BUZZ_APP_VARIANT')} + env.update(changes) + return subprocess.run( + ['sh', '-c', '. "$1"; printf "%s" "${DART_DEFINES:-}"', 'brand', str(GATE)], + env=env, text=True, capture_output=True, timeout=5, + ) + + def commons(self, **changes): + env = dict(BUZZ_APP_VARIANT='voltage-commons', + PRODUCT_BUNDLE_IDENTIFIER='cloud.voltage.workspace', + APP_DISPLAY_NAME='Voltage Commons', BUZZ_APP_ICON_NAME='CommonsAppIcon', + BUZZ_URL_SCHEME='voltage-workspace', BUZZ_LAUNCH_STORYBOARD='CommonsLaunchScreen') + env.update(changes) + return self.gate(**env) + + def test_upstream_build_keeps_existing_defines(self): + original = encoded('OTHER=kept') + result = self.gate(DART_DEFINES=original) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, original) + + def test_native_build_forwards_brand_without_losing_other_defines(self): + original = encoded('OTHER=kept') + result = self.commons(DART_DEFINES=original) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.split(','), [original, encoded('BUZZ_APP_VARIANT=voltage-commons')]) + + def test_conflicts_duplicates_unknown_variants_and_block_identity_fail(self): + for changes in [ + {'DART_DEFINES': encoded('BUZZ_APP_VARIANT=other')}, + {'DART_DEFINES': ','.join([encoded('BUZZ_APP_VARIANT=voltage-commons')] * 2)}, + {'PRODUCT_BUNDLE_IDENTIFIER': 'xyz.block.buzz.mobile'}, + {'BUZZ_APP_ICON_NAME': 'AppIcon'}, + {'BUZZ_URL_SCHEME': 'buzz'}, + {'BUZZ_LAUNCH_STORYBOARD': 'LaunchScreen'}, + {'BUZZ_APP_VARIANT': 'unknown'}, + ]: + with self.subTest(changes=changes): + self.assertNotEqual(self.commons(**changes).returncode, 0) + self.assertNotEqual(self.gate(DART_DEFINES=encoded('BUZZ_APP_VARIANT=voltage-commons')).returncode, 0) + + def test_xcode_executes_gate_before_flutter_build(self): + project = (ROOT / 'mobile/ios/Runner.xcodeproj/project.pbxproj').read_text() + self.assertLess(project.index('require-app-brand.sh'), project.index('xcode_backend.sh\\\" build')) + + def test_preparation_never_overwrites_existing_signing_settings(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for relative in ['scripts', 'voltage/mobile', 'mobile/ios/Flutter']: + (root / relative).mkdir(parents=True, exist_ok=True) + for relative in ['scripts/prepare-commons-ios.sh', 'voltage/mobile/Commons.xcconfig']: + shutil.copyfile(ROOT / relative, root / relative) + env = {**os.environ, 'VOLTAGE_APPLE_TEAM_ID': 'ABCDEFGHIJ'} + command = ['bash', str(root / 'scripts/prepare-commons-ios.sh')] + first = subprocess.run(command, env=env, capture_output=True, timeout=5) + self.assertEqual(first.returncode, 0, first.stderr) + config = root / 'mobile/ios/Flutter/AppOverrides.xcconfig' + saved = config.read_bytes() + self.assertIn(b'BUZZ_DEVELOPMENT_TEAM = ABCDEFGHIJ', saved) + second = subprocess.run(command, env=env, capture_output=True, timeout=5) + self.assertNotEqual(second.returncode, 0) + self.assertEqual(config.read_bytes(), saved) + + +if __name__ == '__main__': + unittest.main() diff --git a/voltage/mobile/Commons.xcconfig b/voltage/mobile/Commons.xcconfig new file mode 100644 index 00000000000..38667d2ad65 --- /dev/null +++ b/voltage/mobile/Commons.xcconfig @@ -0,0 +1,10 @@ +// Voltage's independent mobile distribution. Signing is configured separately. +// Keep the bundle ID stable when changing the visible product name. +BUNDLE_IDENTIFIER = cloud.voltage.workspace +APP_DISPLAY_NAME = Voltage Commons +BUZZ_APP_VARIANT = voltage-commons +BUZZ_APP_ICON_NAME = CommonsAppIcon +BUZZ_LAUNCH_STORYBOARD = CommonsLaunchScreen +BUZZ_URL_SCHEME = voltage-workspace +// An unsigned build must never inherit Block's signing team. +BUZZ_DEVELOPMENT_TEAM = diff --git a/voltage/mobile/README.md b/voltage/mobile/README.md new file mode 100644 index 00000000000..e995233c58f --- /dev/null +++ b/voltage/mobile/README.md @@ -0,0 +1,77 @@ +# Voltage Commons for iPhone + +Voltage Commons is the working name of Voltage's modified Buzz client. +The stable proposed bundle identifier is `cloud.voltage.workspace`. +The name can change without replacing that identifier. + +Ordinary builds retain the upstream Buzz identity and artwork. +Commons builds use a separate icon, welcome screen, permission prompts, and About entry. +The About entry credits Block and links to both source repositories. +The license viewer includes the upstream Apache 2.0 license. +The iOS build registers `voltage-workspace://` instead of claiming the official `buzz://` OS scheme. +Pasted and scanned Buzz links remain supported, and shared message links retain the canonical Buzz format. +Pairing codes and agent identities retain their existing formats. + +## Configure an iOS build + +Activate the repository's Hermit environment and install mobile dependencies. +Then run: + +```sh +VOLTAGE_APPLE_TEAM_ID=YOURTEAMID ./scripts/prepare-commons-ios.sh +``` + +The script creates the gitignored `mobile/ios/Flutter/AppOverrides.xcconfig`. +It refuses to replace an existing file. +Without a team ID, the configuration supports unsigned builds only. +It never inherits Block's signing team. + +On a macOS builder, check the complete unsigned application: + +```sh +cd mobile +flutter build ios --release --no-codesign --no-pub +``` + +The Xcode build phase forwards the distribution to Flutter. +It rejects conflicting Dart defines and native branding. + +To check the Flutter presentation from Linux: + +```sh +cd mobile +flutter test --no-pub --dart-define=BUZZ_APP_VARIANT=voltage-commons \ + test/shared/branding/app_brand_test.dart +``` + +## Device distribution prerequisites + +No signed build or TestFlight upload is configured by this change. +Voltage must supply an active Apple Developer membership and distribution credentials. +Register the app identifier, its `.NotificationService` extension, and `group.cloud.voltage.workspace` under that account. +Configure the required capabilities and matching provisioning profiles for both targets. +Use Voltage's own support and privacy information in App Store Connect. + +A GitHub macOS runner can build and upload from this repository. +The developer's computer can remain on NixOS. +Keep signing credentials in GitHub secrets; never commit them. + +For an initial test without push, omit `BUZZ_PUSH_GATEWAY_URL`. +Push requires a gateway configured for the new app identifier and signing team. +See [mobile setup](../../mobile/README.md#ios-push-capability). + +Use TestFlight for genuine testing and evaluation. +For ongoing employee distribution, evaluate Apple's private Custom App distribution. +Both distribution and naming remain subject to Apple's review and applicable rights. +No permission to reuse Buzz's name or icon is assumed. + +## Upstream coordination + +Before wider distribution, the human maintainer can send this note: + +> We use Buzz for Voltage's workspace and are testing a modified client for our team. +> We plan to publish it under a separate name, icon, and Apple account. +> We will preserve upstream notices, credit Block prominently, and contribute general fixes upstream. +> Do you have additional fork-branding or distribution guidance we should follow? + +This draft has not been sent. diff --git a/voltage/mobile/commons-icon.svg b/voltage/mobile/commons-icon.svg new file mode 100644 index 00000000000..bca72958e93 --- /dev/null +++ b/voltage/mobile/commons-icon.svg @@ -0,0 +1,5 @@ + + + + +