import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_stripe/flutter_stripe.dart'; import 'package:http/http.dart' as http; import 'api.dart'; class PaymentResult { final bool success; final String? paymentIntentId; final String? error; final FeeBreakdown? feeBreakdown; const PaymentResult({ required this.success, this.paymentIntentId, this.error, this.feeBreakdown, }); } class FeeBreakdown { final double subtotal; final double tax; final double tip; final double deliveryFee; final double payfritFee; final double cardFee; final double total; const FeeBreakdown({ required this.subtotal, required this.tax, required this.tip, required this.deliveryFee, required this.payfritFee, required this.cardFee, required this.total, }); factory FeeBreakdown.fromJson(Map json) { return FeeBreakdown( subtotal: (json['SUBTOTAL'] as num?)?.toDouble() ?? 0.0, tax: (json['TAX'] as num?)?.toDouble() ?? 0.0, tip: (json['TIP'] as num?)?.toDouble() ?? 0.0, deliveryFee: (json['DELIVERY_FEE'] as num?)?.toDouble() ?? 0.0, payfritFee: (json['PAYFRIT_FEE'] as num?)?.toDouble() ?? 0.0, cardFee: (json['CARD_FEE'] as num?)?.toDouble() ?? 0.0, total: (json['TOTAL'] as num?)?.toDouble() ?? 0.0, ); } } class StripeService { static bool _isInitialized = false; /// Initialize Stripe with publishable key static Future initialize(String publishableKey) async { if (_isInitialized) return; Stripe.publishableKey = publishableKey; await Stripe.instance.applySettings(); _isInitialized = true; } /// Create payment intent on the server static Future> _createPaymentIntent({ required int businessId, required int orderId, required double subtotal, required double tax, double tip = 0.0, String? customerEmail, }) async { final url = Uri.parse('${Api.baseUrl}/stripe/createPaymentIntent.cfm'); final response = await http.post( url, headers: {'Content-Type': 'application/json; charset=utf-8'}, body: jsonEncode({ 'BusinessID': businessId, 'OrderID': orderId, 'Subtotal': subtotal, 'Tax': tax, 'Tip': tip, if (customerEmail != null) 'CustomerEmail': customerEmail, }), ); final body = response.body; Map? json; try { json = jsonDecode(body) as Map; } catch (_) { // Try to extract JSON from body (handles debug output) final jsonStart = body.indexOf('{'); final jsonEnd = body.lastIndexOf('}'); if (jsonStart >= 0 && jsonEnd > jsonStart) { try { json = jsonDecode(body.substring(jsonStart, jsonEnd + 1)) as Map; } catch (_) {} } } if (json == null) { throw StateError('Failed to parse payment intent response'); } if (json['OK'] != true) { throw StateError(json['ERROR']?.toString() ?? 'Payment intent creation failed'); } return json; } /// Calculate fee breakdown without creating payment intent /// Useful for displaying fees before payment static FeeBreakdown calculateFees({ required double subtotal, required double tax, double tip = 0.0, double deliveryFee = 0.0, }) { const customerFeePercent = 0.05; // 5% Payfrit fee const cardFeePercent = 0.029; // 2.9% Stripe fee const cardFeeFixed = 0.30; // $0.30 Stripe fixed fee final payfritFee = subtotal * customerFeePercent; final totalBeforeCardFee = subtotal + tax + tip + deliveryFee + payfritFee; final cardFee = (totalBeforeCardFee * cardFeePercent) + cardFeeFixed; final total = totalBeforeCardFee + cardFee; return FeeBreakdown( subtotal: subtotal, tax: tax, tip: tip, deliveryFee: deliveryFee, payfritFee: payfritFee, cardFee: cardFee, total: total, ); } /// Process payment for an order static Future processPayment({ required BuildContext context, required int businessId, required int orderId, required double subtotal, required double tax, double tip = 0.0, String? customerEmail, }) async { try { print('[Stripe] Starting payment process...'); print('[Stripe] BusinessID=$businessId, OrderID=$orderId, Subtotal=$subtotal, Tax=$tax, Tip=$tip'); // 1. Create payment intent on server final intentData = await _createPaymentIntent( businessId: businessId, orderId: orderId, subtotal: subtotal, tax: tax, tip: tip, customerEmail: customerEmail, ); print('[Stripe] Payment intent response: $intentData'); final clientSecret = intentData['CLIENT_SECRET'] as String?; final publishableKey = intentData['PUBLISHABLE_KEY'] as String?; final paymentIntentId = intentData['PAYMENT_INTENT_ID'] as String?; final feeData = intentData['FEE_BREAKDOWN'] as Map?; if (clientSecret == null || clientSecret.isEmpty) { print('[Stripe] ERROR: No client secret in response'); return const PaymentResult( success: false, error: 'Failed to create payment intent', ); } print('[Stripe] Client secret received, publishable key: $publishableKey'); // 2. Initialize Stripe if needed if (publishableKey != null && publishableKey.isNotEmpty) { print('[Stripe] Initializing Stripe with key: ${publishableKey.substring(0, 20)}...'); await initialize(publishableKey); } print('[Stripe] Initializing payment sheet...'); // 3. Present payment sheet await Stripe.instance.initPaymentSheet( paymentSheetParameters: SetupPaymentSheetParameters( merchantDisplayName: 'Payfrit', paymentIntentClientSecret: clientSecret, style: ThemeMode.system, returnURL: 'payfrit://stripe-redirect', // Required for Cash App Pay on iOS appearance: const PaymentSheetAppearance( colors: PaymentSheetAppearanceColors( primary: Color(0xFF000000), ), ), ), ); print('[Stripe] Payment sheet initialized, presenting...'); await Stripe.instance.presentPaymentSheet(); print('[Stripe] Payment successful!'); // 4. Payment successful return PaymentResult( success: true, paymentIntentId: paymentIntentId, feeBreakdown: feeData != null ? FeeBreakdown.fromJson(feeData) : null, ); } on StripeException catch (e) { print('[Stripe] StripeException: ${e.error.code} - ${e.error.message} - ${e.error.localizedMessage}'); // User cancelled or payment failed if (e.error.code == FailureCode.Canceled) { return const PaymentResult( success: false, error: 'Payment cancelled', ); } return PaymentResult( success: false, error: e.error.localizedMessage ?? 'Payment failed', ); } catch (e, stackTrace) { print('[Stripe] Exception: $e'); print('[Stripe] Stack trace: $stackTrace'); return PaymentResult( success: false, error: e.toString(), ); } } }