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 @@
CFBundleInfoDictionaryVersion6.0CFBundleName
- Buzz
+ $(APP_DISPLAY_NAME)CFBundlePackageTypeAPPLCFBundleShortVersionString
@@ -32,10 +33,10 @@
CFBundleTypeRoleEditorCFBundleURLName
- com.buzz.deeplink
+ $(PRODUCT_BUNDLE_IDENTIFIER)CFBundleURLSchemes
- buzz
+ $(BUZZ_URL_SCHEME)
@@ -48,15 +49,15 @@
LSRequiresIPhoneOSNSFaceIDUsageDescription
- 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.NSUserActivityTypesINSendMessageIntent
@@ -87,7 +88,7 @@
UIApplicationSupportsIndirectInputEventsUILaunchStoryboardName
- LaunchScreen
+ $(BUZZ_LAUNCH_STORYBOARD)UIMainStoryboardFileMainUISupportedInterfaceOrientations
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 @@
+