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 00000000000..3c962b5c612 Binary files /dev/null and b/mobile/ios/Runner/Assets.xcassets/CommonsAppIcon.appiconset/Commons-1024.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/CommonsAppIcon.appiconset/Contents.json b/mobile/ios/Runner/Assets.xcassets/CommonsAppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..d44ff641388 --- /dev/null +++ b/mobile/ios/Runner/Assets.xcassets/CommonsAppIcon.appiconset/Contents.json @@ -0,0 +1,11 @@ +{ + "images": [ + { + "filename": "Commons-1024.png", + "idiom": "universal", + "platform": "ios", + "size": "1024x1024" + } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/mobile/ios/Runner/Assets.xcassets/CommonsLaunchImage.imageset/Commons@1x.png b/mobile/ios/Runner/Assets.xcassets/CommonsLaunchImage.imageset/Commons@1x.png new file mode 100644 index 00000000000..3a3c9618c1c Binary files /dev/null and b/mobile/ios/Runner/Assets.xcassets/CommonsLaunchImage.imageset/Commons@1x.png differ 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 00000000000..f29337ce59c Binary files /dev/null and b/mobile/ios/Runner/Assets.xcassets/CommonsLaunchImage.imageset/Commons@2x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/CommonsLaunchImage.imageset/Commons@3x.png b/mobile/ios/Runner/Assets.xcassets/CommonsLaunchImage.imageset/Commons@3x.png new file mode 100644 index 00000000000..4703e225590 Binary files /dev/null and b/mobile/ios/Runner/Assets.xcassets/CommonsLaunchImage.imageset/Commons@3x.png differ diff --git a/mobile/ios/Runner/Assets.xcassets/CommonsLaunchImage.imageset/Contents.json b/mobile/ios/Runner/Assets.xcassets/CommonsLaunchImage.imageset/Contents.json new file mode 100644 index 00000000000..283082654e9 --- /dev/null +++ b/mobile/ios/Runner/Assets.xcassets/CommonsLaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images": [ + { + "idiom": "universal", + "filename": "Commons@1x.png", + "scale": "1x" + }, + { + "idiom": "universal", + "filename": "Commons@2x.png", + "scale": "2x" + }, + { + "idiom": "universal", + "filename": "Commons@3x.png", + "scale": "3x" + } + ], + "info": { + "version": 1, + "author": "xcode" + } +} diff --git a/mobile/ios/Runner/CommonsLaunchScreen.storyboard b/mobile/ios/Runner/CommonsLaunchScreen.storyboard new file mode 100644 index 00000000000..a3d56eb2857 --- /dev/null +++ b/mobile/ios/Runner/CommonsLaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + +