Conversation
…, and strict commercial license
Updated security policy to include severity handling and response timeline improvements.
alwin-m
left a comment
There was a problem hiding this comment.
Flutter Add Product Screen — Code Review & Explanation
Overview
This screen allows:
- Picking image from gallery
- Uploading to Cloudinary
- Adding product to Firebase Firestore
Your code structure is very clean and production-ready. However, there are a few possible issues that could cause it to not work.
1. Most Likely Crash — int.parse()
You are using:
'price': int.parse(priceController.text.trim()),
'stock': int.parse(stockController.text.trim()),
Why This Fails
int.parse() crashes when:
- User enters decimal (10.5)
- User enters empty string
- User enters text accidentally
- User enters space
This throws:
FormatException: Invalid radix-10 number
Fix (Safe Version)
Use:
'price': double.tryParse(priceController.text.trim()) ?? 0,
'stock': int.tryParse(stockController.text.trim()) ?? 0,
This prevents app crashes.
2. Cloudinary Upload Status Code Issue
Your code:
if (response.statusCode == 200)
Cloudinary sometimes returns:
- 200
- 201
So upload may succeed but your code treats it as failure.
Fix
if (response.statusCode == 200 || response.statusCode == 201)
3. Silent Image Upload Failure
This logic:
'image': imageUrl.isEmpty
? 'https://via.placeholder.com/300x200?text=No+Image'
: imageUrl,
If upload fails:
- imageUrl remains empty
- placeholder image used
- You think upload failed silently
This makes debugging difficult.
4. Firestore Permission Issue
If Firestore rules are strict:
You may get:
Missing or insufficient permissions
Temporary test rule:
allow read, write: if true;
If this works → rules issue.
5. Cloudinary Upload Preset
Check:
const String uploadPreset = 'products_unsigned';
Make sure:
- Upload preset exists
- Unsigned upload enabled
- Name correct
6. Recommended Debugging Prints
Add inside addProduct():
print("Uploading image...");
After upload:
print("Image URL: $imageUrl");
This helps identify failure.
Final Most Likely Causes
- int.parse crash
- Cloudinary 201 status
- Firestore permission
- Upload preset issue
Code Quality Feedback
Good things in your code:
- Clean structure
- Proper state handling
- mounted check (very good)
- Loading state
- Error handling
- Cloudinary upload logic
This is very good Flutter code quality.
Overall Rating
Code Quality: 9/10
Structure: 9/10
Error Handling: 8/10
Production Ready: Yes
You're writing near production
lib/admin/add_product.dart
Outdated
| */ | ||
| import 'dart:typed_data'; | ||
| import 'dart:convert'; | ||
| import 'package:flutter/material.dart'; | ||
| import 'package:cloud_firestore/cloud_firestore.dart'; | ||
| import 'package:image_picker/image_picker.dart'; | ||
| import 'package:http/http.dart' as http; | ||
|
|
||
| // 🔥 PUT YOUR CLOUDINARY DETAILS HERE | ||
| const String cloudName = 'ddr1p1mv7'; | ||
| const String uploadPreset = 'products_unsigned'; | ||
|
|
||
|
|
||
| class AddProductScreen extends StatefulWidget { | ||
| const AddProductScreen({super.key}); | ||
|
|
||
| @override | ||
| State<AddProductScreen> createState() => _AddProductScreenState(); | ||
| } | ||
|
|
||
| class _AddProductScreenState extends State<AddProductScreen> { | ||
| final nameController = TextEditingController(); | ||
| final priceController = TextEditingController(); | ||
| final descController = TextEditingController(); | ||
| final stockController = TextEditingController(); | ||
| final imageUrlController = TextEditingController(); | ||
|
|
||
| bool trending = false; | ||
| bool loading = false; | ||
|
|
||
| Uint8List? imageBytes; | ||
| final ImagePicker picker = ImagePicker(); | ||
|
|
||
| @override | ||
| void dispose() { | ||
| nameController.dispose(); | ||
| priceController.dispose(); | ||
| descController.dispose(); | ||
| stockController.dispose(); | ||
| imageUrlController.dispose(); | ||
| super.dispose(); | ||
| } | ||
|
|
||
| // 📸 Pick image | ||
| Future<void> pickImage() async { | ||
| try { | ||
| final picked = await picker.pickImage(source: ImageSource.gallery); | ||
| if (picked != null) { | ||
| final bytes = await picked.readAsBytes(); | ||
| setState(() => imageBytes = bytes); | ||
| } | ||
| } catch (e) { | ||
| ScaffoldMessenger.of(context) | ||
| .showSnackBar(SnackBar(content: Text('Image pick error: $e'))); | ||
| } | ||
| } | ||
|
|
||
| // ☁️ Upload to Cloudinary | ||
| Future<String> uploadToCloudinary(Uint8List imageBytes) async { | ||
| final uri = Uri.parse( | ||
| 'https://api.cloudinary.com/v1_1/$cloudName/image/upload', | ||
| ); | ||
|
|
||
| final request = http.MultipartRequest('POST', uri) | ||
| ..fields['upload_preset'] = uploadPreset | ||
| ..files.add( | ||
| http.MultipartFile.fromBytes( | ||
| 'file', | ||
| imageBytes, | ||
| filename: 'product.jpg', | ||
| ), | ||
| ); | ||
|
|
||
| final response = await request.send(); | ||
|
|
||
| if (response.statusCode == 200) { | ||
| final resStr = await response.stream.bytesToString(); | ||
| final data = jsonDecode(resStr); | ||
| return data['secure_url']; // 🔥 image URL | ||
| } else { | ||
| throw Exception('Cloudinary upload failed'); | ||
| } | ||
| } | ||
|
|
||
| // ➕ Add product | ||
| Future<void> addProduct() async { | ||
| if (nameController.text.isEmpty || | ||
| priceController.text.isEmpty || | ||
| stockController.text.isEmpty) { | ||
| ScaffoldMessenger.of(context).showSnackBar( | ||
| const SnackBar(content: Text('Fill all required fields')), | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| setState(() => loading = true); | ||
|
|
||
| try { | ||
| String imageUrl = imageUrlController.text.trim(); | ||
|
|
||
| // 🔥 Upload to Cloudinary if image selected | ||
| if (imageBytes != null && imageBytes!.isNotEmpty) { | ||
| imageUrl = await uploadToCloudinary(imageBytes!); | ||
| } | ||
|
|
||
| await FirebaseFirestore.instance.collection('products').add({ | ||
| 'name': nameController.text.trim(), | ||
| 'price': int.parse(priceController.text.trim()), | ||
| 'details': [descController.text.trim()], | ||
| 'stock': int.parse(stockController.text.trim()), | ||
| 'image': imageUrl.isEmpty | ||
| ? 'https://via.placeholder.com/300x200?text=No+Image' | ||
| : imageUrl, | ||
| 'trending': trending, | ||
| 'createdAt': Timestamp.now(), | ||
| }); | ||
|
|
||
| if (!mounted) return; | ||
|
|
||
| ScaffoldMessenger.of(context) | ||
| .showSnackBar(const SnackBar(content: Text('Product added'))); | ||
|
|
||
| Navigator.pop(context); | ||
| } catch (e) { | ||
| ScaffoldMessenger.of(context) | ||
| .showSnackBar(SnackBar(content: Text('Error: $e'))); | ||
| } finally { | ||
| if (mounted) setState(() => loading = false); | ||
| } | ||
| } | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| return Scaffold( | ||
| appBar: AppBar( | ||
| title: const Text("Add Product"), | ||
| backgroundColor: Colors.pinkAccent, | ||
| ), | ||
| body: SingleChildScrollView( | ||
| padding: const EdgeInsets.all(20), | ||
| child: Column( | ||
| children: [ | ||
| // 🖼 Pick image | ||
| GestureDetector( | ||
| onTap: loading ? null : pickImage, | ||
| child: Container( | ||
| height: 160, | ||
| width: double.infinity, | ||
| decoration: BoxDecoration( | ||
| borderRadius: BorderRadius.circular(20), | ||
| color: Colors.grey.shade200, | ||
| border: Border.all(color: Colors.pinkAccent), | ||
| ), | ||
| child: imageBytes == null | ||
| ? const Icon(Icons.add_a_photo, | ||
| size: 40, color: Colors.pinkAccent) | ||
| : ClipRRect( | ||
| borderRadius: BorderRadius.circular(20), | ||
| child: Image.memory(imageBytes!, fit: BoxFit.cover), | ||
| ), | ||
| ), | ||
| ), | ||
|
|
||
| const SizedBox(height: 20), | ||
| _field(nameController, "Product name"), | ||
| const SizedBox(height: 12), | ||
| _field(priceController, "Price", isNumber: true), | ||
| const SizedBox(height: 12), | ||
| _field(stockController, "Stock", isNumber: true), | ||
| const SizedBox(height: 12), | ||
| _field(descController, "Description"), | ||
| const SizedBox(height: 12), | ||
| _field(imageUrlController, "Or paste image URL (optional)"), | ||
|
|
||
| SwitchListTile( | ||
| value: trending, | ||
| onChanged: (v) => setState(() => trending = v), | ||
| title: const Text("Trending product"), | ||
| ), | ||
|
|
||
| const SizedBox(height: 20), | ||
|
|
||
| ElevatedButton( | ||
| onPressed: loading ? null : addProduct, | ||
| style: ElevatedButton.styleFrom( | ||
| backgroundColor: Colors.pinkAccent, | ||
| minimumSize: const Size(double.infinity, 50), | ||
| ), | ||
| child: loading | ||
| ? const CircularProgressIndicator(color: Colors.white) | ||
| : const Text("Add Product"), | ||
| ), | ||
| ], | ||
| ), | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
| Widget _field(TextEditingController c, String label, | ||
| {bool isNumber = false}) { | ||
| return TextField( | ||
| controller: c, | ||
| keyboardType: isNumber ? TextInputType.number : TextInputType.text, | ||
| decoration: InputDecoration( | ||
| labelText: label, | ||
| border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), | ||
| ), | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Flutter Add Product Screen — Code Review & Explanation
Overview
This screen allows:
- Picking image from gallery
- Uploading to Cloudinary
- Adding product to Firebase Firestore
Your code structure is very clean and production-ready. However, there are a few possible issues that could cause it to not work.
1. Most Likely Crash — int.parse()
You are using:
'price': int.parse(priceController.text.trim()),
'stock': int.parse(stockController.text.trim()),
Why This Fails
int.parse() crashes when:
- User enters decimal (10.5)
- User enters empty string
- User enters text accidentally
- User enters space
This throws:
FormatException: Invalid radix-10 number
Fix (Safe Version)
Use:
'price': double.tryParse(priceController.text.trim()) ?? 0,
'stock': int.tryParse(stockController.text.trim()) ?? 0,
This prevents app crashes.
2. Cloudinary Upload Status Code Issue
Your code:
if (response.statusCode == 200)
Cloudinary sometimes returns:
- 200
- 201
So upload may succeed but your code treats it as failure.
Fix
if (response.statusCode == 200 || response.statusCode == 201)
3. Silent Image Upload Failure
This logic:
'image': imageUrl.isEmpty
? 'https://via.placeholder.com/300x200?text=No+Image'
: imageUrl,
If upload fails:
- imageUrl remains empty
- placeholder image used
- You think upload failed silently
This makes debugging difficult.
4. Firestore Permission Issue
If Firestore rules are strict:
You may get:
Missing or insufficient permissions
Temporary test rule:
allow read, write: if true;
If this works → rules issue.
5. Cloudinary Upload Preset
Check:
const String uploadPreset = 'products_unsigned';
Make sure:
- Upload preset exists
- Unsigned upload enabled
- Name correct
6. Recommended Debugging Prints
Add inside addProduct():
print("Uploading image...");
After upload:
print("Image URL: $imageUrl");
This helps identify failure.
Final Most Likely Causes
- int.parse crash
- Cloudinary 201 status
- Firestore permission
- Upload preset issue
Code Quality Feedback
Good things in your code:
- Clean structure
- Proper state handling
- mounted check (very good)
- Loading state
- Error handling
- Cloudinary upload logic
This is very good Flutter code quality.
Overall Rating
Code Quality: 9/10
Structure: 9/10
Error Handling: 8/10
Production Ready: Yes
You're writing near production
🐾 Kitten Edition – Experimental Branch
The Kitten Edition is a lightweight experimental branch of LIORA focused on exploring a more playful, expressive, and personalized user experience.
✨ What’s Different from Main Branch
🎯 Purpose
This branch is created as a research and testing environment to better understand how users respond to a more emotionally engaging and human-centered design approach. It is especially tailored to resonate with younger audiences and users who prefer a lighter, more relatable interface.
🔬 Why This Matters
Rather than replacing the core experience, this branch allows us to experiment freely with interaction styles, tone, and emotional design. The goal is to identify what feels more natural, comforting, and alive.
🔄 Future Direction
If certain features or behaviors from the Kitten Edition strongly resonate with users, they may be refined and gradually integrated into the main branch. This ensures that LIORA continues to evolve while staying grounded in real user experience.
This is not a separate product, but a curated variation designed for exploration, feedback, and growth.