diff --git a/android/app/src/main/kotlin/com/payfrit/app/MainActivity.kt b/android/app/src/main/kotlin/com/payfrit/app/MainActivity.kt
index 7708194..9436d49 100644
--- a/android/app/src/main/kotlin/com/payfrit/app/MainActivity.kt
+++ b/android/app/src/main/kotlin/com/payfrit/app/MainActivity.kt
@@ -1,5 +1,5 @@
package com.payfrit.app
-import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.android.FlutterFragmentActivity
-class MainActivity : FlutterActivity()
+class MainActivity : FlutterFragmentActivity()
diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml
index 06952be..1cd289c 100644
--- a/android/app/src/main/res/values-night/styles.xml
+++ b/android/app/src/main/res/values-night/styles.xml
@@ -1,7 +1,7 @@
-
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
index 6d2e15d..9c002ad 100644
--- a/android/app/src/main/res/values/styles.xml
+++ b/android/app/src/main/res/values/styles.xml
@@ -1,7 +1,7 @@
-
diff --git a/lib/main.dart b/lib/main.dart
index 2752b8f..99c0382 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,4 +1,5 @@
import "package:flutter/material.dart";
+import "package:flutter_stripe/flutter_stripe.dart";
import "package:provider/provider.dart";
import "app/app_router.dart" show AppRoutes;
@@ -10,6 +11,11 @@ final GlobalKey rootScaffoldMessengerKey =
void main() {
WidgetsFlutterBinding.ensureInitialized();
+
+ // Initialize Stripe with test publishable key
+ // This will be updated dynamically when processing payments if needed
+ Stripe.publishableKey = 'pk_test_sPBNzSyJ9HcEPJGC7dSo8NqN';
+
runApp(const PayfritApp());
}
diff --git a/lib/screens/cart_view_screen.dart b/lib/screens/cart_view_screen.dart
index d5caf0f..cc2b51d 100644
--- a/lib/screens/cart_view_screen.dart
+++ b/lib/screens/cart_view_screen.dart
@@ -7,6 +7,7 @@ import '../models/cart.dart';
import '../models/menu_item.dart';
import '../services/api.dart';
import '../services/order_polling_service.dart';
+import '../services/stripe_service.dart';
/// Helper class to store modifier breadcrumb paths
class ModifierPath {
@@ -29,9 +30,37 @@ class CartViewScreen extends StatefulWidget {
class _CartViewScreenState extends State {
Cart? _cart;
bool _isLoading = true;
+ bool _isProcessingPayment = false;
String? _error;
Map _menuItemsById = {};
+ // Tip options as percentages
+ static const List _tipPercentages = [0, 15, 18, 20, 25];
+ int _selectedTipIndex = 1; // Default to 15%
+
+ double get _tipAmount {
+ if (_cart == null) return 0.0;
+ return _cart!.subtotal * (_tipPercentages[_selectedTipIndex] / 100);
+ }
+
+ FeeBreakdown get _feeBreakdown {
+ if (_cart == null) {
+ return const FeeBreakdown(
+ subtotal: 0,
+ tax: 0,
+ tip: 0,
+ payfritFee: 0,
+ cardFee: 0,
+ total: 0,
+ );
+ }
+ return StripeService.calculateFees(
+ subtotal: _cart!.subtotal,
+ tax: _cart!.tax,
+ tip: _tipAmount,
+ );
+ }
+
@override
void initState() {
super.initState();
@@ -146,14 +175,44 @@ class _CartViewScreenState extends State {
}
}
- Future _submitOrder() async {
+ Future _processPaymentAndSubmit() async {
+ if (_cart == null) return;
+
+ final appState = context.read();
+ final cartOrderId = appState.cartOrderId;
+ final businessId = appState.selectedBusinessId;
+
+ if (cartOrderId == null || businessId == null) return;
+
+ setState(() => _isProcessingPayment = true);
+
try {
- final appState = context.read();
- final cartOrderId = appState.cartOrderId;
- if (cartOrderId == null) return;
+ // 1. Process payment with Stripe
+ final paymentResult = await StripeService.processPayment(
+ context: context,
+ businessId: businessId,
+ orderId: cartOrderId,
+ subtotal: _cart!.subtotal,
+ tax: _cart!.tax,
+ tip: _tipAmount,
+ );
- setState(() => _isLoading = true);
+ if (!paymentResult.success) {
+ if (!mounted) return;
+ setState(() => _isProcessingPayment = false);
+ if (paymentResult.error != 'Payment cancelled') {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(paymentResult.error ?? 'Payment failed'),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+ return;
+ }
+
+ // 2. Payment successful, now submit the order
await Api.submitOrder(orderId: cartOrderId);
// Set active order for polling (status 1 = submitted)
@@ -187,20 +246,27 @@ class _CartViewScreenState extends State {
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(
- content: Text("Order submitted successfully! You'll receive notifications as your order is prepared."),
+ SnackBar(
+ content: Text(
+ "Payment successful! Order placed. You'll receive notifications as your order is prepared.",
+ ),
backgroundColor: Colors.green,
- duration: Duration(seconds: 5),
+ duration: const Duration(seconds: 5),
),
);
// Navigate back
Navigator.of(context).pop();
} catch (e) {
- setState(() {
- _error = e.toString();
- _isLoading = false;
- });
+ if (!mounted) return;
+ setState(() => _isProcessingPayment = false);
+
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Error: ${e.toString()}'),
+ backgroundColor: Colors.red,
+ ),
+ );
}
}
@@ -492,6 +558,8 @@ class _CartViewScreenState extends State {
Widget _buildCartSummary() {
if (_cart == null) return const SizedBox.shrink();
+ final fees = _feeBreakdown;
+
return Container(
decoration: BoxDecoration(
color: Colors.grey[100],
@@ -506,53 +574,79 @@ class _CartViewScreenState extends State {
padding: const EdgeInsets.all(16),
child: SafeArea(
child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- const Text(
- "Subtotal",
- style: TextStyle(fontSize: 16),
- ),
- Text(
- "\$${_cart!.subtotal.toStringAsFixed(2)}",
- style: const TextStyle(fontSize: 16),
- ),
- ],
+ // Tip Selection
+ const Text(
+ "Add a tip",
+ style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
- // Sales tax
const SizedBox(height: 8),
Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- const Text(
- "Tax (8.25%)",
- style: TextStyle(fontSize: 16),
- ),
- Text(
- "\$${_cart!.tax.toStringAsFixed(2)}",
- style: const TextStyle(fontSize: 16),
- ),
- ],
+ children: List.generate(_tipPercentages.length, (index) {
+ final isSelected = _selectedTipIndex == index;
+ final percent = _tipPercentages[index];
+ return Expanded(
+ child: Padding(
+ padding: EdgeInsets.only(
+ left: index == 0 ? 0 : 4,
+ right: index == _tipPercentages.length - 1 ? 0 : 4,
+ ),
+ child: GestureDetector(
+ onTap: () => setState(() => _selectedTipIndex = index),
+ child: Container(
+ padding: const EdgeInsets.symmetric(vertical: 10),
+ decoration: BoxDecoration(
+ color: isSelected ? Colors.black : Colors.white,
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(
+ color: isSelected ? Colors.black : Colors.grey.shade300,
+ ),
+ ),
+ child: Center(
+ child: Text(
+ percent == 0 ? "No tip" : "$percent%",
+ style: TextStyle(
+ fontSize: 14,
+ fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
+ color: isSelected ? Colors.white : Colors.black,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }),
),
+ const SizedBox(height: 16),
+ const Divider(height: 1),
+ const SizedBox(height: 12),
+ // Subtotal
+ _buildSummaryRow("Subtotal", fees.subtotal),
+ const SizedBox(height: 6),
+ // Tax
+ _buildSummaryRow("Tax (8.25%)", fees.tax),
// Only show delivery fee for delivery orders (OrderTypeID = 3)
if (_cart!.deliveryFee > 0 && _cart!.orderTypeId == 3) ...[
- const SizedBox(height: 8),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- const Text(
- "Delivery Fee",
- style: TextStyle(fontSize: 16),
- ),
- Text(
- "\$${_cart!.deliveryFee.toStringAsFixed(2)}",
- style: const TextStyle(fontSize: 16),
- ),
- ],
- ),
+ const SizedBox(height: 6),
+ _buildSummaryRow("Delivery Fee", _cart!.deliveryFee),
],
- const Divider(height: 24),
+ // Tip
+ if (_tipAmount > 0) ...[
+ const SizedBox(height: 6),
+ _buildSummaryRow("Tip", _tipAmount),
+ ],
+ const SizedBox(height: 6),
+ // Payfrit fee
+ _buildSummaryRow("Service Fee", fees.payfritFee, isGrey: true),
+ const SizedBox(height: 6),
+ // Card processing fee
+ _buildSummaryRow("Card Processing", fees.cardFee, isGrey: true),
+ const SizedBox(height: 12),
+ const Divider(height: 1),
+ const SizedBox(height: 12),
+ // Total
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -564,7 +658,7 @@ class _CartViewScreenState extends State {
),
),
Text(
- "\$${_cart!.total.toStringAsFixed(2)}",
+ "\$${fees.total.toStringAsFixed(2)}",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
@@ -576,16 +670,28 @@ class _CartViewScreenState extends State {
SizedBox(
width: double.infinity,
child: ElevatedButton(
- onPressed: _cart!.itemCount > 0 ? _submitOrder : null,
+ onPressed: (_cart!.itemCount > 0 && !_isProcessingPayment)
+ ? _processPaymentAndSubmit
+ : null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
+ disabledBackgroundColor: Colors.grey,
),
- child: const Text(
- "Submit Order",
- style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
- ),
+ child: _isProcessingPayment
+ ? const SizedBox(
+ height: 20,
+ width: 20,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ valueColor: AlwaysStoppedAnimation(Colors.white),
+ ),
+ )
+ : Text(
+ "Pay \$${fees.total.toStringAsFixed(2)}",
+ style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
+ ),
),
),
],
@@ -594,6 +700,28 @@ class _CartViewScreenState extends State {
);
}
+ Widget _buildSummaryRow(String label, double amount, {bool isGrey = false}) {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ label,
+ style: TextStyle(
+ fontSize: 15,
+ color: isGrey ? Colors.grey.shade600 : Colors.black,
+ ),
+ ),
+ Text(
+ "\$${amount.toStringAsFixed(2)}",
+ style: TextStyle(
+ fontSize: 15,
+ color: isGrey ? Colors.grey.shade600 : Colors.black,
+ ),
+ ),
+ ],
+ );
+ }
+
void _confirmRemoveItem(OrderLineItem item, String itemName) {
showDialog(
context: context,
diff --git a/lib/screens/menu_browse_screen.dart b/lib/screens/menu_browse_screen.dart
index 8648fb2..500908d 100644
--- a/lib/screens/menu_browse_screen.dart
+++ b/lib/screens/menu_browse_screen.dart
@@ -23,6 +23,8 @@ class _MenuBrowseScreenState extends State {
List