Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env.stencil
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#API KEYS AND SECRETS

API_KEY=
API_SECRET=
PINATA_API_KEY=
PINATA_API_SECRET=
ALCHEMY_API_KEY=

#APPLICATION CONFIGURATION
Expand Down
7 changes: 4 additions & 3 deletions lib/components/bottom_navigation_widget.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:tree_planting_protocol/utils/constants/bottom_nav_constants.dart';
import 'package:tree_planting_protocol/utils/constants/ui/color_constants.dart';

class BottomNavigationWidget extends StatelessWidget {
final String currentRoute;
Expand All @@ -24,9 +25,9 @@ class BottomNavigationWidget extends StatelessWidget {
return BottomNavigationBar(
currentIndex: _getCurrentIndex(),
type: BottomNavigationBarType.fixed,
selectedItemColor: Theme.of(context).colorScheme.primary,
unselectedItemColor: Theme.of(context).colorScheme.onSurfaceVariant,
backgroundColor: const Color.fromARGB(255, 37, 236, 147),
selectedItemColor: getThemeColors(context)['secondary'],
unselectedItemColor: getThemeColors(context)['textSecondary'],
backgroundColor: getThemeColors(context)['primary'],
elevation: 8,
onTap: (index) {
final route = BottomNavConstants.items[index].route;
Expand Down
256 changes: 256 additions & 0 deletions lib/components/transaction_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tree_planting_protocol/utils/constants/ui/color_constants.dart';
import 'package:tree_planting_protocol/utils/constants/ui/dimensions.dart';

class TransactionDialog extends StatelessWidget {
final String title;
final String message;
final String? transactionHash;
final bool isSuccess;
final VoidCallback? onClose;

const TransactionDialog({
super.key,
required this.title,
required this.message,
this.transactionHash,
this.isSuccess = true,
this.onClose,
});

@override
Widget build(BuildContext context) {
final colors = getThemeColors(context);

return Dialog(
backgroundColor: colors['background'],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(buttonCircularRadius),
side: BorderSide(
color: colors['border']!,
width: buttonborderWidth,
),
),
child: Container(
constraints: const BoxConstraints(maxWidth: 400),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: colors['background'],
borderRadius: BorderRadius.circular(buttonCircularRadius),
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header with icon and title
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isSuccess ? colors['primary'] : colors['error'],
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: colors['border']!,
width: 2,
),
),
child: Icon(
isSuccess ? Icons.check_circle : Icons.error,
color: colors['textPrimary'],
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: colors['textPrimary'],
),
),
),
],
),
const SizedBox(height: 20),

// Message
Text(
message,
style: TextStyle(
fontSize: 14,
color: colors['textPrimary'],
height: 1.5,
),
),

if (transactionHash != null && transactionHash!.isNotEmpty) ...[
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: colors['secondary'],
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: colors['border']!,
width: 2,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.receipt_long,
size: 16,
color: colors['textPrimary'],
),
const SizedBox(width: 8),
Text(
'Transaction Hash',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: colors['textPrimary'],
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: Text(
'${transactionHash!.substring(0, 10)}...${transactionHash!.substring(transactionHash!.length - 8)}',
style: TextStyle(
fontSize: 12,
fontFamily: 'monospace',
color: colors['textPrimary'],
),
),
Comment on lines +128 to +135
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard short hashes to prevent RangeError.

Substring indexes assume length ≥ 18. Build the preview defensively.

-                          Expanded(
-                            child: Text(
-                              '${transactionHash!.substring(0, 10)}...${transactionHash!.substring(transactionHash!.length - 8)}',
+                          Expanded(
+                            child: Text(
+                              () {
+                                final h = transactionHash!;
+                                if (h.length <= 10) return h;
+                                final left = h.substring(0, 10);
+                                final right = h.length > 18 ? h.substring(h.length - 8) : '';
+                                return right.isEmpty ? left : '$left...$right';
+                              }(),
                               style: TextStyle(
                                 fontSize: 12,
                                 fontFamily: 'monospace',
                                 color: colors['textPrimary'],
                               ),
                             ),

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In lib/components/transaction_dialog.dart around lines 128 to 135, the substring
calls assume the transactionHash length is at least 18 and will throw a
RangeError for shorter hashes; update the UI rendering to defensively handle
short or null hashes by computing safe start/end indices (or simply display the
full hash when length is below the truncation threshold) and avoid
force-unwrapping nulls—use length checks (or fallback empty string) and only
perform substring/truncation when indexes are valid so the preview never tries
to access out-of-range positions.

),
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
Clipboard.setData(
ClipboardData(text: transactionHash!));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Hash copied!'),
duration: const Duration(seconds: 1),
backgroundColor: colors['primary'],
),
);
},
icon: Icon(
Icons.copy,
size: 16,
color: colors['textPrimary'],
),
),
],
),
],
),
),
],

const SizedBox(height: 24),

// Close Button
SizedBox(
width: double.infinity,
height: 50,
child: Material(
elevation: 4,
borderRadius: BorderRadius.circular(buttonCircularRadius),
child: InkWell(
onTap: () {
Navigator.of(context).pop();
if (onClose != null) onClose!();
},
borderRadius: BorderRadius.circular(buttonCircularRadius),
child: Container(
decoration: BoxDecoration(
color:
isSuccess ? colors['primary'] : colors['secondary'],
border: Border.all(
color: colors['border']!,
width: buttonborderWidth,
),
borderRadius:
BorderRadius.circular(buttonCircularRadius),
),
child: Center(
child: Text(
'Close',
style: TextStyle(
color: colors['textPrimary'],
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
),
],
),
),
),
);
}

static void showSuccess(
BuildContext context, {
required String title,
required String message,
String? transactionHash,
VoidCallback? onClose,
}) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (context.mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => TransactionDialog(
title: title,
message: message,
transactionHash: transactionHash,
isSuccess: true,
onClose: onClose,
),
);
}
});
}

static void showError(
BuildContext context, {
required String title,
required String message,
VoidCallback? onClose,
}) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (context.mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => TransactionDialog(
title: title,
message: message,
isSuccess: false,
onClose: onClose,
),
);
}
});
}
}
Loading
Loading