feat(mobile): show build number with version in settings (#7697)

Mobile Settings now identifies the installed build as e.g. `v0.16.0
(432)` rather than just `v0.16.0`. If the platform supplies no build
number, the footer keeps the existing version-only display.

| Before | After |
| --- | --- |
| Marketing version only | Version and native build number |
| ![Before:
v0.16.0](https://raw.githubusercontent.com/block/buzz/e8449f63d1fabe80ff86d395ac1177912c5d1bc5/pr-7697--before.png)
| ![After: v0.16.0
(432)](https://raw.githubusercontent.com/block/buzz/e8449f63d1fabe80ff86d395ac1177912c5d1bc5/pr-7697--after.png)
|

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
Tom Brow
2026-09-16 20:29:42 -07:00
committed by GitHub
parent 6dfd145cde
commit b36600fc44
4 changed files with 140 additions and 5 deletions

View File

@@ -1523,7 +1523,7 @@ mod tests {
conn_id: Uuid::new_v4(),
tenant: tenant.clone(),
remote_addr: "127.0.0.1:1234".parse().unwrap(),
auth_state: RwLock::new(crate::connection::AuthState::Authenticated(
auth_state: std::sync::Mutex::new(crate::connection::AuthState::Authenticated(
buzz_auth::AuthContext {
pubkey: keys.public_key(),
scopes: vec![],
@@ -1668,7 +1668,7 @@ mod tests {
conn_id: Uuid::new_v4(),
tenant: tenant.clone(),
remote_addr: "127.0.0.1:1234".parse().unwrap(),
auth_state: RwLock::new(crate::connection::AuthState::Authenticated(
auth_state: std::sync::Mutex::new(crate::connection::AuthState::Authenticated(
buzz_auth::AuthContext {
pubkey: keys.public_key(),
scopes: vec![],

View File

@@ -225,7 +225,10 @@ class SettingsPage extends HookConsumerWidget {
),
),
if (packageInfo.hasData)
_VersionFooter(version: packageInfo.data!.version),
_VersionFooter(
version: packageInfo.data!.version,
buildNumber: packageInfo.data!.buildNumber,
),
],
),
);
@@ -233,9 +236,10 @@ class SettingsPage extends HookConsumerWidget {
}
class _VersionFooter extends StatelessWidget {
const _VersionFooter({required this.version});
const _VersionFooter({required this.version, required this.buildNumber});
final String version;
final String buildNumber;
@override
Widget build(BuildContext context) {
@@ -245,7 +249,7 @@ class _VersionFooter extends StatelessWidget {
padding: const EdgeInsets.only(bottom: Grid.xs, top: Grid.xxs),
child: Center(
child: Text(
'v$version',
buildNumber.isEmpty ? 'v$version' : 'v$version ($buildNumber)',
style: context.textTheme.bodySmall?.copyWith(
color: context.colors.onSurfaceVariant.withValues(alpha: 0.6),
),

View File

@@ -0,0 +1,69 @@
import 'dart:async';
import 'package:buzz/features/settings/settings_page.dart';
import 'package:buzz/shared/community/community_membership_provider.dart';
import 'package:buzz/shared/theme/theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
testWidgets('settings remains usable while package metadata loads', (
tester,
) async {
final metadata = Completer<Map<String, String>>();
const channel = MethodChannel('dev.fluttercommunity.plus/package_info');
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
channel,
(_) => metadata.future,
);
addTearDown(
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
channel,
null,
),
);
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
await tester.pumpWidget(
ProviderScope(
overrides: [
savedPrefsProvider.overrideWithValue(prefs),
currentCommunityRoleProvider.overrideWithValue(
const AsyncData<CommunityMemberRole?>(CommunityMemberRole.admin),
),
],
child: MaterialApp(
theme: AppTheme.light(),
builder: (context, child) => MediaQuery(
data: MediaQuery.of(
context,
).copyWith(textScaler: TextScaler.linear(2)),
child: child!,
),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
invitePageBuilder: (_) => const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
),
);
expect(find.text('Invite to community'), findsOneWidget);
expect(find.text('v0.16.0 (432)'), findsNothing);
expect(find.byTooltip('Close settings'), findsOneWidget);
metadata.complete({
'appName': 'Buzz',
'packageName': 'xyz.block.buzz',
'version': '0.16.0',
'buildNumber': '432',
'buildSignature': '',
});
await tester.pumpAndSettle();
expect(find.text('v0.16.0 (432)'), findsOneWidget);
expect(tester.takeException(), isNull);
});
}

View File

@@ -11,9 +11,71 @@ import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
setUp(() {
PackageInfo.setMockInitialValues(
appName: 'Buzz',
packageName: 'xyz.block.buzz',
version: '0.16.0',
buildNumber: '432',
buildSignature: '',
);
});
for (final buildNumber in ['432', '', '2147483647']) {
testWidgets('shows version with build number "$buildNumber"', (
tester,
) async {
tester.view.physicalSize = const Size(320, 700);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
PackageInfo.setMockInitialValues(
appName: 'Buzz',
packageName: 'xyz.block.buzz',
version: '0.16.0',
buildNumber: buildNumber,
buildSignature: '',
);
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
await tester.pumpWidget(
ProviderScope(
overrides: [
savedPrefsProvider.overrideWithValue(prefs),
currentCommunityRoleProvider.overrideWithValue(
const AsyncData<CommunityMemberRole?>(CommunityMemberRole.admin),
),
],
child: MaterialApp(
theme: AppTheme.light(),
builder: (context, child) => MediaQuery(
data: MediaQuery.of(
context,
).copyWith(textScaler: TextScaler.linear(2)),
child: child!,
),
home: SettingsPage(
profileHeader: const SizedBox.shrink(),
invitePageBuilder: (_) => const SizedBox.shrink(),
identityRecoveryPageBuilder: (_) => const SizedBox.shrink(),
),
),
),
);
expect(find.text('Invite to community'), findsOneWidget);
await tester.pumpAndSettle();
expect(
find.text(buildNumber.isEmpty ? 'v0.16.0' : 'v0.16.0 ($buildNumber)'),
findsOneWidget,
);
expect(tester.takeException(), isNull);
});
}
testWidgets('shows the persisted per-community push opt-in on iOS', (
tester,
) async {