Skip to content

Commit 26a0000

Browse files
committed
feat: allow dismissing divine login banner on profile
1 parent 14850ca commit 26a0000

File tree

2 files changed

+81
-6
lines changed

2 files changed

+81
-6
lines changed

mobile/lib/widgets/profile/profile_header_widget.dart

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:openvine/blocs/email_verification/email_verification_cubit.dart'
1212
import 'package:openvine/blocs/my_profile/my_profile_bloc.dart';
1313
import 'package:openvine/providers/app_providers.dart';
1414
import 'package:openvine/providers/nip05_verification_provider.dart';
15+
import 'package:openvine/providers/shared_preferences_provider.dart';
1516
import 'package:openvine/providers/user_profile_providers.dart';
1617
import 'package:openvine/screens/auth/secure_account_screen.dart';
1718
import 'package:openvine/screens/auth/welcome_screen.dart';
@@ -27,6 +28,9 @@ import 'package:openvine/widgets/user_name.dart';
2728

2829
/// Profile header widget displaying avatar, stats, name, and bio.
2930
class ProfileHeaderWidget extends ConsumerWidget {
31+
static const _dismissedDivineLoginBannerPrefix =
32+
'dismissed_divine_login_banner_';
33+
3034
const ProfileHeaderWidget({
3135
required this.userIdHex,
3236
required this.isOwnProfile,
@@ -89,6 +93,11 @@ class ProfileHeaderWidget extends ConsumerWidget {
8993
ref.watch(currentAuthStateProvider);
9094
final isAnonymous = authService.isAnonymous;
9195
final hasExpiredSession = authService.hasExpiredOAuthSession;
96+
final prefs = ref.watch(sharedPreferencesProvider);
97+
final dismissedDivineLoginBannerKey =
98+
'$_dismissedDivineLoginBannerPrefix$userIdHex';
99+
final isDivineLoginBannerDismissed =
100+
prefs.getBool(dismissedDivineLoginBannerKey) ?? false;
92101

93102
// Use profile color as header background (like original Vine)
94103
// Color covers avatar/stats, then fades to dark for name/bio readability
@@ -128,8 +137,12 @@ class ProfileHeaderWidget extends ConsumerWidget {
128137

129138
// Session expired banner for divineOAuth users (only on own
130139
// profile) — prompts re-login instead of "Secure Your Account"
131-
if (isOwnProfile && hasExpiredSession)
132-
const _SessionExpiredBanner()
140+
if (isOwnProfile &&
141+
hasExpiredSession &&
142+
!isDivineLoginBannerDismissed)
143+
_SessionExpiredBanner(
144+
dismissedPreferenceKey: dismissedDivineLoginBannerKey,
145+
)
133146
// Secure account banner for anonymous users (only on own
134147
// profile)
135148
else if (isOwnProfile && isAnonymous)
@@ -420,7 +433,9 @@ class _IdentityNotRecoverableBanner extends StatelessWidget {
420433
/// Prompts the user to sign in again instead of showing "Secure Your Account".
421434
/// Attempts a silent token refresh first; navigates to login only if that fails.
422435
class _SessionExpiredBanner extends ConsumerStatefulWidget {
423-
const _SessionExpiredBanner();
436+
const _SessionExpiredBanner({required this.dismissedPreferenceKey});
437+
438+
final String dismissedPreferenceKey;
424439

425440
@override
426441
ConsumerState<_SessionExpiredBanner> createState() =>
@@ -429,6 +444,7 @@ class _SessionExpiredBanner extends ConsumerStatefulWidget {
429444

430445
class _SessionExpiredBannerState extends ConsumerState<_SessionExpiredBanner> {
431446
bool _isRefreshing = false;
447+
bool _isDismissed = false;
432448

433449
Future<void> _onSignIn() async {
434450
setState(() => _isRefreshing = true);
@@ -444,8 +460,16 @@ class _SessionExpiredBannerState extends ConsumerState<_SessionExpiredBanner> {
444460
}
445461
}
446462

463+
Future<void> _dismissBanner() async {
464+
setState(() => _isDismissed = true);
465+
final prefs = ref.read(sharedPreferencesProvider);
466+
await prefs.setBool(widget.dismissedPreferenceKey, true);
467+
}
468+
447469
@override
448470
Widget build(BuildContext context) {
471+
if (_isDismissed) return const SizedBox.shrink();
472+
449473
return Container(
450474
margin: const EdgeInsets.only(bottom: 16),
451475
padding: const EdgeInsets.all(16),
@@ -499,6 +523,11 @@ class _SessionExpiredBannerState extends ConsumerState<_SessionExpiredBanner> {
499523
),
500524
),
501525
),
526+
IconButton(
527+
onPressed: _dismissBanner,
528+
icon: const Icon(Icons.close, color: VineTheme.whiteText, size: 20),
529+
tooltip: 'Dismiss',
530+
),
502531
],
503532
),
504533
);

mobile/test/widgets/profile/profile_header_widget_test.dart

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,13 @@ class MockNostrClient extends Mock implements NostrClient {
7878
}
7979

8080
class MockAuthService extends Mock implements AuthService {
81-
MockAuthService({this.isAnonymousValue = false});
81+
MockAuthService({
82+
this.isAnonymousValue = false,
83+
this.hasExpiredOAuthSessionValue = false,
84+
});
8285

8386
final bool isAnonymousValue;
87+
final bool hasExpiredOAuthSessionValue;
8488

8589
@override
8690
bool get isAnonymous => isAnonymousValue;
@@ -96,7 +100,7 @@ class MockAuthService extends Mock implements AuthService {
96100
Stream.value(AuthState.authenticated);
97101

98102
@override
99-
bool get hasExpiredOAuthSession => false;
103+
bool get hasExpiredOAuthSession => hasExpiredOAuthSessionValue;
100104
}
101105

102106
const testUserHex =
@@ -152,8 +156,12 @@ void main() {
152156
bool isAnonymous = false,
153157
String? displayNameHint,
154158
String? avatarUrlHint,
159+
bool hasExpiredSession = false,
155160
}) {
156-
final authService = MockAuthService(isAnonymousValue: isAnonymous);
161+
final authService = MockAuthService(
162+
isAnonymousValue: isAnonymous,
163+
hasExpiredOAuthSessionValue: hasExpiredSession,
164+
);
157165

158166
Widget header = ProfileHeaderWidget(
159167
userIdHex: userIdHex,
@@ -613,6 +621,44 @@ void main() {
613621
final button = tester.widget<ElevatedButton>(registerButton);
614622
expect(button.onPressed, isNotNull);
615623
});
624+
625+
testWidgets('session expired banner can be dismissed and stays hidden', (
626+
tester,
627+
) async {
628+
final testProfile = createTestProfile(displayName: 'Test User');
629+
630+
SharedPreferences.setMockInitialValues({});
631+
632+
await tester.pumpWidget(
633+
buildTestWidget(
634+
userIdHex: testUserHex,
635+
isOwnProfile: true,
636+
profile: testProfile,
637+
hasExpiredSession: true,
638+
),
639+
);
640+
await tester.pumpAndSettle();
641+
642+
expect(find.text('Session Expired'), findsOneWidget);
643+
644+
await tester.tap(find.byIcon(Icons.close));
645+
await tester.pumpAndSettle();
646+
647+
expect(find.text('Session Expired'), findsNothing);
648+
649+
// Rebuild and ensure persisted dismissal hides the banner
650+
await tester.pumpWidget(
651+
buildTestWidget(
652+
userIdHex: testUserHex,
653+
isOwnProfile: true,
654+
profile: testProfile,
655+
hasExpiredSession: true,
656+
),
657+
);
658+
await tester.pumpAndSettle();
659+
660+
expect(find.text('Session Expired'), findsNothing);
661+
});
616662
});
617663
});
618664

0 commit comments

Comments
 (0)