mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-11 13:47:43 +01:00
Let a host explain an absent Google button instead of hiding it
lyricanki's Linux desktop cannot use google_sign_in (no Linux implementation) and its loopback replacement is dark until a Desktop OAuth client exists. Hiding the button there makes the screen silently offer less on one platform, which reads as a bug. googleUnavailableReason renders a *disabled* button plus the reason. That is not the "control that can never succeed" trap: the trap is a live-looking button that reports a fake cancellation, and a null onPressed cannot be tapped at all. Null stays the default, so every existing host is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
This commit is contained in:
parent
79130a6dd7
commit
db3d8f827f
@ -163,6 +163,7 @@ class _SyncSettingsScreenState extends State<SyncSettingsScreen> {
|
|||||||
connected: _firebaseConnected,
|
connected: _firebaseConnected,
|
||||||
busy: _busy,
|
busy: _busy,
|
||||||
googleAvailable: _googleAvailable,
|
googleAvailable: _googleAvailable,
|
||||||
|
googleUnavailableReason: widget.googleUnavailableReason,
|
||||||
emailController: _emailController,
|
emailController: _emailController,
|
||||||
passwordController: _passwordController,
|
passwordController: _passwordController,
|
||||||
onConnectPassword: _connectFirebase,
|
onConnectPassword: _connectFirebase,
|
||||||
|
|||||||
@ -10,6 +10,7 @@ class _FirebaseSection extends StatelessWidget {
|
|||||||
required this.connected,
|
required this.connected,
|
||||||
required this.busy,
|
required this.busy,
|
||||||
required this.googleAvailable,
|
required this.googleAvailable,
|
||||||
|
required this.googleUnavailableReason,
|
||||||
required this.emailController,
|
required this.emailController,
|
||||||
required this.passwordController,
|
required this.passwordController,
|
||||||
required this.onConnectPassword,
|
required this.onConnectPassword,
|
||||||
@ -20,6 +21,7 @@ class _FirebaseSection extends StatelessWidget {
|
|||||||
final bool connected;
|
final bool connected;
|
||||||
final bool busy;
|
final bool busy;
|
||||||
final bool googleAvailable;
|
final bool googleAvailable;
|
||||||
|
final String? googleUnavailableReason;
|
||||||
final TextEditingController emailController;
|
final TextEditingController emailController;
|
||||||
final TextEditingController passwordController;
|
final TextEditingController passwordController;
|
||||||
final VoidCallback onConnectPassword;
|
final VoidCallback onConnectPassword;
|
||||||
@ -54,6 +56,25 @@ class _FirebaseSection extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
] else if (googleUnavailableReason != null) ...[
|
||||||
|
// Disabled, not hidden. A null onPressed cannot be tapped, so this
|
||||||
|
// is not the "control that can never succeed" trap -- that one
|
||||||
|
// looks live and reports a fake cancellation. This one says why,
|
||||||
|
// which beats a screen that silently offers less on one platform.
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: FilledButton.icon(
|
||||||
|
onPressed: null,
|
||||||
|
icon: const Icon(Icons.account_circle),
|
||||||
|
label: const Text('Sign in with Google'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
googleUnavailableReason!,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
],
|
],
|
||||||
ExpansionTile(
|
ExpansionTile(
|
||||||
initiallyExpanded: !googleAvailable,
|
initiallyExpanded: !googleAvailable,
|
||||||
|
|||||||
@ -19,6 +19,7 @@ class SyncSettingsScreen extends StatefulWidget {
|
|||||||
required this.firebaseFactory,
|
required this.firebaseFactory,
|
||||||
this.googleFirebaseFactory,
|
this.googleFirebaseFactory,
|
||||||
this.googleAvailable,
|
this.googleAvailable,
|
||||||
|
this.googleUnavailableReason,
|
||||||
this.backup,
|
this.backup,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
@ -59,6 +60,17 @@ class SyncSettingsScreen extends StatefulWidget {
|
|||||||
/// unsupported even though a factory is wired.
|
/// unsupported even though a factory is wired.
|
||||||
final bool? googleAvailable;
|
final bool? googleAvailable;
|
||||||
|
|
||||||
|
/// Why Google sign-in is unavailable here, shown under a *disabled* button.
|
||||||
|
///
|
||||||
|
/// Null hides the button entirely, which stays the default. Supplying a
|
||||||
|
/// reason trades a silently-absent control for one that explains itself --
|
||||||
|
/// worth it where a platform genuinely cannot support the flow, and where
|
||||||
|
/// its absence would otherwise read as a bug.
|
||||||
|
///
|
||||||
|
/// Not the same as a button that can never succeed: this one cannot be
|
||||||
|
/// tapped at all, so it can never report a misleading "cancelled".
|
||||||
|
final String? googleUnavailableReason;
|
||||||
|
|
||||||
/// Optional local export/import section. Null omits the "Backup" section
|
/// Optional local export/import section. Null omits the "Backup" section
|
||||||
/// entirely (diet_guard, wake_alarm have no local backup format).
|
/// entirely (diet_guard, wake_alarm have no local backup format).
|
||||||
final BackupSlot? backup;
|
final BackupSlot? backup;
|
||||||
|
|||||||
@ -5,7 +5,7 @@ description: >-
|
|||||||
injected closures -- no keystore or platform plugin access lives in this
|
injected closures -- no keystore or platform plugin access lives in this
|
||||||
package itself.
|
package itself.
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.2.0
|
version: 0.3.0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.12.2
|
sdk: ^3.12.2
|
||||||
|
|||||||
@ -141,5 +141,55 @@ void main() {
|
|||||||
expect(find.text('Sign in with Google'), findsNothing);
|
expect(find.text('Sign in with Google'), findsNothing);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets('a reason renders a disabled button that explains itself', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_wrap(
|
||||||
|
SyncSettingsScreen(
|
||||||
|
accountLoader: () async => null,
|
||||||
|
accountSaver: (_) async {},
|
||||||
|
accountClearer: () async {},
|
||||||
|
sessionProbe: () async => false,
|
||||||
|
firebaseFactory: () async => null,
|
||||||
|
googleFirebaseFactory: () async => _fakeClient(),
|
||||||
|
googleAvailable: false,
|
||||||
|
googleUnavailableReason: 'Not available on this platform.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Sign in with Google'), findsOneWidget);
|
||||||
|
expect(find.text('Not available on this platform.'), findsOneWidget);
|
||||||
|
|
||||||
|
// Disabled, so it cannot report a misleading "cancelled".
|
||||||
|
final button = tester.widget<FilledButton>(
|
||||||
|
find.widgetWithText(FilledButton, 'Sign in with Google'),
|
||||||
|
);
|
||||||
|
expect(button.onPressed, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('no reason keeps the button hidden, as before', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_wrap(
|
||||||
|
SyncSettingsScreen(
|
||||||
|
accountLoader: () async => null,
|
||||||
|
accountSaver: (_) async {},
|
||||||
|
accountClearer: () async {},
|
||||||
|
sessionProbe: () async => false,
|
||||||
|
firebaseFactory: () async => null,
|
||||||
|
googleFirebaseFactory: () async => _fakeClient(),
|
||||||
|
googleAvailable: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Sign in with Google'), findsNothing);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user