mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 20:48:24 +00:00
Improve toast notifications/actions
This commit is contained in:
@@ -224,7 +224,7 @@ class _AuthenticationSection extends ConsumerWidget {
|
||||
await ref.read(amberSignerProvider).signOut();
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
context.showError('Sign out failed: $e');
|
||||
context.showError('Sign out failed', description: '$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -760,7 +760,7 @@ class _DebugMessagesSection extends HookConsumerWidget {
|
||||
if (log.exception != null) 'Error: ${log.exception}',
|
||||
].where((e) => e.isNotEmpty).join(' | ');
|
||||
Clipboard.setData(ClipboardData(text: text));
|
||||
context.showInfo('Copied');
|
||||
context.showInfo('Debug info copied');
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -839,7 +839,10 @@ class _DebugMessagesSection extends HookConsumerWidget {
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: req));
|
||||
context.showInfo('REQ copied');
|
||||
context.showInfo(
|
||||
'REQ filter copied',
|
||||
description: 'Paste into a Nostr client to debug this query.',
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -1093,7 +1096,7 @@ class _DataManagementSection extends ConsumerWidget {
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
context.showError('Failed to restart: ${e.toString()}');
|
||||
context.showError('Restart failed', description: '${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,100 +1,424 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toastification/toastification.dart';
|
||||
import 'package:zapstore/theme.dart';
|
||||
|
||||
extension ContextX on BuildContext {
|
||||
ThemeData get theme => Theme.of(this);
|
||||
void showInfo(String message, {String? description, Icon? icon}) {
|
||||
const actionColor = AppColors.darkActionPrimary;
|
||||
|
||||
toastification.show(
|
||||
void showInfo(String title, {String? description, IconData? icon}) {
|
||||
_showCustomToast(
|
||||
context: this,
|
||||
type: ToastificationType.info,
|
||||
icon: icon ?? const Icon(Icons.info, color: Colors.white),
|
||||
style: ToastificationStyle.fillColored,
|
||||
alignment: Alignment.topCenter,
|
||||
title: _ToastTitleWidget(message),
|
||||
description: description != null
|
||||
? _ToastDescriptionWidget(description: description)
|
||||
: null,
|
||||
autoCloseDuration: const Duration(seconds: 4),
|
||||
showProgressBar: false,
|
||||
closeOnClick: true,
|
||||
primaryColor: actionColor,
|
||||
backgroundColor: actionColor,
|
||||
foregroundColor: Colors.white,
|
||||
title: title,
|
||||
description: description,
|
||||
icon: icon ?? Icons.info_outline_rounded,
|
||||
type: _ToastType.info,
|
||||
);
|
||||
}
|
||||
|
||||
void showError(
|
||||
String title, {
|
||||
String? description,
|
||||
Icon? icon,
|
||||
IconData? icon,
|
||||
List<(String, Future<void> Function())> actions = const [],
|
||||
}) {
|
||||
toastification.show(
|
||||
_showCustomToast(
|
||||
context: this,
|
||||
type: ToastificationType.error,
|
||||
style: ToastificationStyle.fillColored,
|
||||
alignment: Alignment.topCenter,
|
||||
icon: icon ?? const Icon(Icons.error, color: Colors.white),
|
||||
title: _ToastTitleWidget(title),
|
||||
autoCloseDuration: const Duration(seconds: 5),
|
||||
showProgressBar: false,
|
||||
closeOnClick: true,
|
||||
foregroundColor: Colors.white,
|
||||
description: description != null
|
||||
? _ToastDescriptionWidget(description: description, actions: actions)
|
||||
: null,
|
||||
title: title,
|
||||
description: description,
|
||||
icon: icon ?? Icons.error_outline_rounded,
|
||||
type: _ToastType.error,
|
||||
actions: actions,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ToastTitleWidget extends StatelessWidget {
|
||||
final String text;
|
||||
const _ToastTitleWidget(this.text);
|
||||
enum _ToastType { info, error }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 4,
|
||||
);
|
||||
}
|
||||
OverlayEntry? _currentToast;
|
||||
|
||||
void _showCustomToast({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
String? description,
|
||||
required IconData icon,
|
||||
required _ToastType type,
|
||||
List<(String, Future<void> Function())> actions = const [],
|
||||
}) {
|
||||
// Dismiss existing toast
|
||||
_currentToast?.remove();
|
||||
_currentToast = null;
|
||||
|
||||
final overlay = Overlay.of(context);
|
||||
|
||||
late OverlayEntry entry;
|
||||
entry = OverlayEntry(
|
||||
builder: (context) => _ToastOverlay(
|
||||
title: title,
|
||||
description: description,
|
||||
icon: icon,
|
||||
type: type,
|
||||
actions: actions,
|
||||
onDismiss: () {
|
||||
entry.remove();
|
||||
if (_currentToast == entry) _currentToast = null;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
_currentToast = entry;
|
||||
overlay.insert(entry);
|
||||
}
|
||||
|
||||
class _ToastDescriptionWidget extends StatelessWidget {
|
||||
class _ToastOverlay extends StatefulWidget {
|
||||
final String title;
|
||||
final String? description;
|
||||
final IconData icon;
|
||||
final _ToastType type;
|
||||
final List<(String, Future<void> Function())> actions;
|
||||
final VoidCallback onDismiss;
|
||||
|
||||
const _ToastDescriptionWidget({this.description, this.actions = const []});
|
||||
const _ToastOverlay({
|
||||
required this.title,
|
||||
this.description,
|
||||
required this.icon,
|
||||
required this.type,
|
||||
required this.actions,
|
||||
required this.onDismiss,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_ToastOverlay> createState() => _ToastOverlayState();
|
||||
}
|
||||
|
||||
class _ToastOverlayState extends State<_ToastOverlay>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _fadeAnimation;
|
||||
late Animation<Offset> _slideAnimation;
|
||||
bool _isHovered = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_fadeAnimation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
|
||||
_slideAnimation = Tween<Offset>(
|
||||
begin: const Offset(0, -1),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOutCubic,
|
||||
));
|
||||
|
||||
_controller.forward();
|
||||
_startAutoClose();
|
||||
}
|
||||
|
||||
void _startAutoClose() {
|
||||
final duration = widget.type == _ToastType.error
|
||||
? const Duration(seconds: 6)
|
||||
: const Duration(seconds: 4);
|
||||
|
||||
Future.delayed(duration, () {
|
||||
if (mounted && !_isHovered) {
|
||||
_dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _dismiss() async {
|
||||
await _controller.reverse();
|
||||
widget.onDismiss();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: description,
|
||||
style: const TextStyle(fontSize: 16, color: Colors.white),
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
final maxHeight = screenHeight * 0.7;
|
||||
final topPadding = MediaQuery.of(context).padding.top;
|
||||
|
||||
final isError = widget.type == _ToastType.error;
|
||||
|
||||
// Colors
|
||||
final backgroundColor = isError
|
||||
? const Color(0xFF1A1215) // Dark reddish-black
|
||||
: const Color(0xFF111827); // Dark blue-black
|
||||
|
||||
final borderColor = isError
|
||||
? const Color(0xFF7F1D1D).withValues(alpha: 0.6) // Dark red border
|
||||
: AppColors.darkPrimary.withValues(alpha: 0.4);
|
||||
|
||||
final accentColor = isError
|
||||
? const Color(0xFFEF4444) // Red
|
||||
: AppColors.darkPrimary;
|
||||
|
||||
final iconBgColor = isError
|
||||
? const Color(0xFF7F1D1D).withValues(alpha: 0.3)
|
||||
: AppColors.darkPrimary.withValues(alpha: 0.15);
|
||||
|
||||
return Positioned(
|
||||
top: topPadding + 8,
|
||||
left: 12,
|
||||
right: 12,
|
||||
child: SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHovered = true),
|
||||
onExit: (_) {
|
||||
setState(() => _isHovered = false);
|
||||
_startAutoClose();
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTap: _dismiss,
|
||||
onVerticalDragEnd: (details) {
|
||||
if (details.primaryVelocity != null &&
|
||||
details.primaryVelocity! < -100) {
|
||||
_dismiss();
|
||||
}
|
||||
},
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: maxHeight),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: borderColor, width: 1),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.4),
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 8),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
BoxShadow(
|
||||
color: accentColor.withValues(alpha: 0.08),
|
||||
blurRadius: 40,
|
||||
offset: const Offset(0, 4),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Accent line at top
|
||||
Container(
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
accentColor.withValues(alpha: 0.8),
|
||||
accentColor.withValues(alpha: 0.4),
|
||||
accentColor.withValues(alpha: 0.1),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Content
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: _ToastContent(
|
||||
icon: widget.icon,
|
||||
iconBgColor: iconBgColor,
|
||||
accentColor: accentColor,
|
||||
title: widget.title,
|
||||
description: widget.description,
|
||||
actions: widget.actions,
|
||||
onDismiss: _dismiss,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
for (final (text, fn) in actions)
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await fn.call();
|
||||
toastification.dismissAll(delayForAnimation: false);
|
||||
},
|
||||
child: Text(text),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ToastContent extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color iconBgColor;
|
||||
final Color accentColor;
|
||||
final String title;
|
||||
final String? description;
|
||||
final List<(String, Future<void> Function())> actions;
|
||||
final VoidCallback onDismiss;
|
||||
|
||||
const _ToastContent({
|
||||
required this.icon,
|
||||
required this.iconBgColor,
|
||||
required this.accentColor,
|
||||
required this.title,
|
||||
this.description,
|
||||
required this.actions,
|
||||
required this.onDismiss,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Icon
|
||||
Container(
|
||||
width: 38,
|
||||
height: 38,
|
||||
decoration: BoxDecoration(
|
||||
color: iconBgColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: accentColor,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
// Text content
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Title
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontFamily: kFontFamily,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
height: 1.35,
|
||||
),
|
||||
),
|
||||
// Description
|
||||
if (description != null) ...[
|
||||
const SizedBox(height: 6),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
description!,
|
||||
style: TextStyle(
|
||||
fontFamily: kFontFamily,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.white.withValues(alpha: 0.7),
|
||||
height: 1.45,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
// Actions
|
||||
if (actions.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: actions
|
||||
.map((action) => _ToastActionButton(
|
||||
label: action.$1,
|
||||
onPressed: () async {
|
||||
await action.$2();
|
||||
onDismiss();
|
||||
},
|
||||
accentColor: accentColor,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
// Close button
|
||||
const SizedBox(width: 8),
|
||||
GestureDetector(
|
||||
onTap: onDismiss,
|
||||
child: Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.06),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.close_rounded,
|
||||
size: 16,
|
||||
color: Colors.white.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ToastActionButton extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback onPressed;
|
||||
final Color accentColor;
|
||||
|
||||
const _ToastActionButton({
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
required this.accentColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: accentColor.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: accentColor.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontFamily: kFontFamily,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: accentColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,11 @@ class SignInPromptDialog extends HookConsumerWidget {
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
context.showError('Sign-in failed: $e');
|
||||
context.showError(
|
||||
'Sign-in failed',
|
||||
description:
|
||||
'Amber could not complete the sign-in. Make sure Amber is installed and try again.\n\n$e',
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -101,7 +101,10 @@ class BookmarkDialog extends HookConsumerWidget {
|
||||
|
||||
if (signer == null || signedInPubkey == null) {
|
||||
if (context.mounted) {
|
||||
context.showError('You must be signed in to save apps');
|
||||
context.showError(
|
||||
'Sign in required',
|
||||
description: 'You need to sign in to save apps to your bookmarks.',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -165,7 +168,7 @@ class BookmarkDialog extends HookConsumerWidget {
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
context.showError('Failed to update bookmark: $e');
|
||||
context.showError('Failed to update bookmark', description: '$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -448,7 +451,10 @@ class AddToPackDialog extends HookConsumerWidget {
|
||||
final signer = ref.read(Signer.activeSignerProvider);
|
||||
if (signer == null) {
|
||||
if (context.mounted) {
|
||||
context.showError('You must be signed in');
|
||||
context.showError(
|
||||
'Sign in required',
|
||||
description: 'You need to sign in to manage app packs.',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -548,7 +554,7 @@ class AddToPackDialog extends HookConsumerWidget {
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
context.showError('Failed to save: $e');
|
||||
context.showError('Failed to save', description: '$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,7 +397,10 @@ class _CommentComposer extends HookConsumerWidget {
|
||||
final signer = ref.read(Signer.activeSignerProvider);
|
||||
if (signer == null) {
|
||||
if (context.mounted) {
|
||||
context.showError('Please sign in to comment');
|
||||
context.showError(
|
||||
'Sign in required',
|
||||
description: 'You need to sign in with Amber to post comments.',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -405,7 +408,10 @@ class _CommentComposer extends HookConsumerWidget {
|
||||
final app = fileMetadata.release.value?.app.value;
|
||||
if (app == null) {
|
||||
if (context.mounted) {
|
||||
context.showError('App information not available');
|
||||
context.showError(
|
||||
'Unable to comment',
|
||||
description: 'App information is not available. Try again later.',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -434,7 +440,7 @@ class _CommentComposer extends HookConsumerWidget {
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
context.showError('Failed to post comment: $e');
|
||||
context.showError('Failed to post comment', description: '$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,10 +43,18 @@ class InstallButton extends ConsumerWidget {
|
||||
next.errorDetails != null &&
|
||||
previous?.errorDetails != next.errorDetails) {
|
||||
if (context.mounted) {
|
||||
final errorMessage = next.errorDetails == 'CERTIFICATE_MISMATCH'
|
||||
? 'Installation failed: Certificate mismatch detected'
|
||||
: 'Installation failed: ${next.errorDetails}';
|
||||
context.showError(errorMessage);
|
||||
if (next.errorDetails == 'CERTIFICATE_MISMATCH') {
|
||||
context.showError(
|
||||
'Installation failed',
|
||||
description:
|
||||
'Certificate mismatch detected. The app signature does not match the expected developer.',
|
||||
);
|
||||
} else {
|
||||
context.showError(
|
||||
'Installation failed',
|
||||
description: next.errorDetails,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -211,7 +219,11 @@ class InstallButton extends ConsumerWidget {
|
||||
),
|
||||
onError: () {
|
||||
if (context.mounted) {
|
||||
context.showError('Installation failed. Please try again.');
|
||||
context.showError(
|
||||
'Installation failed',
|
||||
description:
|
||||
'The package could not be installed. Check storage space and try again.',
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -579,7 +591,11 @@ class InstallButton extends ConsumerWidget {
|
||||
await packageManager.launchApp(app.identifier);
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
context.showError('Failed to launch ${app.name ?? app.identifier}: $e');
|
||||
context.showError(
|
||||
'Failed to launch ${app.name ?? app.identifier}',
|
||||
description:
|
||||
'The app may have been uninstalled or moved. Try reinstalling.\n\n$e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,7 +669,7 @@ class InstallButton extends ConsumerWidget {
|
||||
// Don't show error for user cancellation
|
||||
final message = e.toString();
|
||||
if (!message.contains('cancelled')) {
|
||||
context.showError('Uninstall failed: $e');
|
||||
context.showError('Uninstall failed', description: '$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -767,7 +783,7 @@ class InstallButton extends ConsumerWidget {
|
||||
} catch (e) {
|
||||
final message = e.toString();
|
||||
if (context.mounted && !message.contains('cancelled')) {
|
||||
context.showError('Force update failed: $e');
|
||||
context.showError('Force update failed', description: '$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,11 +222,17 @@ class NWCConnectionCard extends HookConsumerWidget {
|
||||
await secureStorage.clearNWCString();
|
||||
nwcStatus.value = NWCStatus.disconnected;
|
||||
if (context.mounted) {
|
||||
context.showInfo('Wallet connection removed');
|
||||
context.showInfo(
|
||||
'Wallet disconnected',
|
||||
description: 'You can reconnect anytime from Settings.',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
context.showError('Failed to remove connection: $e');
|
||||
context.showError(
|
||||
'Failed to disconnect wallet',
|
||||
description: '$e',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -320,13 +326,19 @@ class NWCConnectionDialog extends HookWidget {
|
||||
ValueNotifier<bool> isLoading,
|
||||
) async {
|
||||
if (nwcString.trim().isEmpty) {
|
||||
context.showError('Please enter a valid NWC connection string');
|
||||
context.showError(
|
||||
'Missing connection string',
|
||||
description:
|
||||
'Get a NWC connection string from your Lightning wallet (e.g., Alby, Zeus, Coinos).',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!nwcString.trim().startsWith('nostr+walletconnect://')) {
|
||||
context.showError(
|
||||
'Invalid NWC format. Should start with nostr+walletconnect://',
|
||||
'Invalid NWC format',
|
||||
description:
|
||||
'Connection string should start with nostr+walletconnect://',
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -343,7 +355,11 @@ class NWCConnectionDialog extends HookWidget {
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
context.showError('Connection failed: $e');
|
||||
context.showError(
|
||||
'Wallet connection failed',
|
||||
description:
|
||||
'Could not connect to the wallet. Verify the connection string and try again.\n\n$e',
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
|
||||
@@ -323,7 +323,9 @@ class ZapAmountDialog extends HookConsumerWidget {
|
||||
// Navigate to Amber app page
|
||||
if (context.mounted) {
|
||||
context.showInfo(
|
||||
'Install Amber to sign in and unlock zapping features',
|
||||
'Amber required',
|
||||
description:
|
||||
'Install Amber to sign in and unlock zapping features.',
|
||||
);
|
||||
context.push(
|
||||
'/profile/apps/naddr1qqdkxmmd9enhyet9deshyaphvvejumn0wd68yumfvahx2uszyp6hjpmdntls5n8aa7n7ypzlyjrv0ewch33ml3452wtjx0smhl93jqcyqqq8uzcgpp6ky',
|
||||
@@ -336,7 +338,11 @@ class ZapAmountDialog extends HookConsumerWidget {
|
||||
// because pubkey will no longer be null
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
context.showError('Sign-in failed: $e');
|
||||
context.showError(
|
||||
'Sign-in failed',
|
||||
description:
|
||||
'Amber could not complete the sign-in. Make sure Amber is installed and try again.\n\n$e',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -550,7 +556,7 @@ class ZapAmountDialog extends HookConsumerWidget {
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
context.showError('Zap failed: $e');
|
||||
context.showError('Zap failed', description: '$e');
|
||||
Navigator.of(context).pop(false);
|
||||
}
|
||||
}
|
||||
@@ -705,13 +711,17 @@ class NWCConnectionDialogInline extends HookConsumerWidget {
|
||||
final nwcString = controller.text;
|
||||
if (nwcString.trim().isEmpty) {
|
||||
context.showError(
|
||||
'Please enter a valid NWC connection string',
|
||||
'Missing connection string',
|
||||
description:
|
||||
'Get a NWC connection string from your Lightning wallet (e.g., Alby, Zeus, Coinos).',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!nwcString.trim().startsWith('nostr+walletconnect://')) {
|
||||
context.showError(
|
||||
'Invalid NWC format. Should start with nostr+walletconnect://',
|
||||
'Invalid NWC format',
|
||||
description:
|
||||
'Connection string should start with nostr+walletconnect://',
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -726,7 +736,11 @@ class NWCConnectionDialogInline extends HookConsumerWidget {
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
context.showError('Connection failed: $e');
|
||||
context.showError(
|
||||
'Wallet connection failed',
|
||||
description:
|
||||
'Could not connect to the wallet. Verify the connection string and try again.\n\n$e',
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
|
||||
@@ -553,14 +553,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
iconsax_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: iconsax_flutter
|
||||
sha256: d14b4cec8586025ac15276bdd40f6eea308cb85748135965bb6255f14beb2564
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -776,14 +768,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
pausable_timer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pausable_timer
|
||||
sha256: "6ef1a95441ec3439de6fb63f39a011b67e693198e7dae14e20675c3c00e86074"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0+3"
|
||||
percent_indicator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1189,14 +1173,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.1"
|
||||
toastification:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: toastification
|
||||
sha256: "69db2bff425b484007409650d8bcd5ed1ce2e9666293ece74dcd917dacf23112"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -47,7 +47,6 @@ dependencies:
|
||||
intl: ^0.20.2
|
||||
collection: ^1.19.1
|
||||
qr_flutter: ^4.1.0
|
||||
toastification: ^3.0.3
|
||||
share_plus: ^12.0.0
|
||||
background_downloader: ^9.2.6
|
||||
connectivity_plus: ^7.0.0
|
||||
|
||||
Reference in New Issue
Block a user