From 5942deb0c54501b6701a1a9cde8bfc1ee8ead6be Mon Sep 17 00:00:00 2001 From: John Mizerek Date: Mon, 5 Jan 2026 10:46:13 -0800 Subject: [PATCH] Add Stripe payment integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Integrate flutter_stripe SDK for payment processing - Create StripeService with payment sheet flow - Add tip selection UI (0%, 15%, 18%, 20%, 25%) - Display fee breakdown (subtotal, tax, tip, service fee, card fee) - Update cart screen with "Pay $X.XX" button - Change MainActivity to FlutterFragmentActivity for Stripe compatibility - Update Android themes to AppCompat for payment sheet styling Fee structure: - 5% Payfrit service fee (customer pays) - 2.9% + $0.30 card processing fee (customer pays) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../kotlin/com/payfrit/app/MainActivity.kt | 4 +- .../app/src/main/res/values-night/styles.xml | 4 +- android/app/src/main/res/values/styles.xml | 4 +- lib/main.dart | 6 + lib/screens/cart_view_screen.dart | 242 +++++++++++++----- lib/screens/menu_browse_screen.dart | 57 +++-- lib/services/stripe_service.dart | 237 +++++++++++++++++ pubspec.lock | 40 +++ pubspec.yaml | 3 +- 9 files changed, 513 insertions(+), 84 deletions(-) create mode 100644 lib/services/stripe_service.dart 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 _allItems = []; final Map> _itemsByCategory = {}; final Map> _itemsByParent = {}; + final Map _categorySortOrder = {}; // categoryId -> sortOrder + final Map _categoryNames = {}; // categoryId -> categoryName // Track which category is currently expanded (null = none) int? _expandedCategoryId; @@ -79,6 +81,8 @@ class _MenuBrowseScreenState extends State { void _organizeItems() { _itemsByCategory.clear(); _itemsByParent.clear(); + _categorySortOrder.clear(); + _categoryNames.clear(); print('[MenuBrowse] _organizeItems: ${_allItems.length} total items'); @@ -90,7 +94,10 @@ class _MenuBrowseScreenState extends State { categoryItemIds.add(item.itemId); // Just register the category key (empty list for now) _itemsByCategory.putIfAbsent(item.itemId, () => []); - print('[MenuBrowse] Category found: ${item.name} (ID=${item.itemId})'); + // Store the sort order and name for this category + _categorySortOrder[item.itemId] = item.sortOrder; + _categoryNames[item.itemId] = item.name; + print('[MenuBrowse] Category found: ${item.name} (ID=${item.itemId}, sortOrder=${item.sortOrder})'); } } @@ -136,7 +143,12 @@ class _MenuBrowseScreenState extends State { List _getUniqueCategoryIds() { final categoryIds = _itemsByCategory.keys.toList(); - categoryIds.sort(); + // Sort by sortOrder (from _categorySortOrder), not by ItemID + categoryIds.sort((a, b) { + final orderA = _categorySortOrder[a] ?? 0; + final orderB = _categorySortOrder[b] ?? 0; + return orderA.compareTo(orderB); + }); return categoryIds; } @@ -267,11 +279,15 @@ class _MenuBrowseScreenState extends State { final categoryIndex = index - 1; final categoryId = categoryIds[categoryIndex]; final items = _itemsByCategory[categoryId] ?? []; - final categoryName = items.isNotEmpty - ? items.first.categoryName - : "Category $categoryId"; + // Use stored category name from the category item itself + final categoryName = _categoryNames[categoryId] ?? "Category $categoryId"; final isExpanded = _expandedCategoryId == categoryId; + // Debug: Print which items are being shown for which category + if (items.isNotEmpty) { + print('[MenuBrowse] DISPLAY: Category "$categoryName" (ID=$categoryId) showing items: ${items.map((i) => i.name).join(", ")}'); + } + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -427,15 +443,16 @@ class _MenuBrowseScreenState extends State { fit: BoxFit.cover, semanticLabel: categoryName, errorBuilder: (context, error, stackTrace) { - // No image - show large styled category name + // No image - show white background with dark forest green text + const darkForestGreen = Color(0xFF1B4D3E); return Container( decoration: BoxDecoration( gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, + begin: Alignment.topCenter, + end: Alignment.bottomCenter, colors: [ - Theme.of(context).colorScheme.primary, - Theme.of(context).colorScheme.tertiary, + Colors.white, + Colors.grey.shade100, ], ), ), @@ -447,13 +464,13 @@ class _MenuBrowseScreenState extends State { style: const TextStyle( fontSize: 28, fontWeight: FontWeight.bold, - color: Colors.white, + color: darkForestGreen, letterSpacing: 1.2, shadows: [ Shadow( - offset: Offset(2, 2), - blurRadius: 4, - color: Colors.black54, + offset: Offset(1, 1), + blurRadius: 2, + color: Colors.black26, ), ], ), @@ -489,7 +506,7 @@ class _MenuBrowseScreenState extends State { children: [ // Category image background or styled text fallback _buildCategoryBackground(categoryId, categoryName), - // Top edge gradient + // Top edge gradient (subtle forest green) Positioned( top: 0, left: 0, @@ -501,14 +518,14 @@ class _MenuBrowseScreenState extends State { begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - Colors.black.withAlpha(180), - Colors.black.withAlpha(0), + const Color(0xFF1B4D3E).withAlpha(120), + const Color(0xFF1B4D3E).withAlpha(0), ], ), ), ), ), - // Bottom edge gradient + // Bottom edge gradient (subtle forest green) Positioned( bottom: 0, left: 0, @@ -520,8 +537,8 @@ class _MenuBrowseScreenState extends State { begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - Colors.black.withAlpha(0), - Colors.black.withAlpha(200), + const Color(0xFF1B4D3E).withAlpha(0), + const Color(0xFF1B4D3E).withAlpha(150), ], ), ), diff --git a/lib/services/stripe_service.dart b/lib/services/stripe_service.dart new file mode 100644 index 0000000..d1d25cc --- /dev/null +++ b/lib/services/stripe_service.dart @@ -0,0 +1,237 @@ +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 payfritFee; + final double cardFee; + final double total; + + const FeeBreakdown({ + required this.subtotal, + required this.tax, + required this.tip, + 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, + 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, + }) { + 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 + payfritFee; + final cardFee = (totalBeforeCardFee * cardFeePercent) + cardFeeFixed; + final total = totalBeforeCardFee + cardFee; + + return FeeBreakdown( + subtotal: subtotal, + tax: tax, + tip: tip, + 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, + 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(), + ); + } + } +} diff --git a/pubspec.lock b/pubspec.lock index ee6eacc..34d257d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -134,6 +134,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + flutter_stripe: + dependency: "direct main" + description: + name: flutter_stripe + sha256: a474b283f4b07e8973687514bf48762e618073b0d6b7acc45cea9a60466d4f8c + url: "https://pub.dev" + source: hosted + version: "11.5.0" flutter_test: dependency: "direct dev" description: flutter @@ -144,6 +152,14 @@ packages: description: flutter source: sdk version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + url: "https://pub.dev" + source: hosted + version: "2.4.4" http: dependency: "direct main" description: @@ -453,6 +469,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + stripe_android: + dependency: transitive + description: + name: stripe_android + sha256: a666352e0c20753ecd8feebb5944882bf597167be4f020641266515a495bd55f + url: "https://pub.dev" + source: hosted + version: "11.5.0" + stripe_ios: + dependency: transitive + description: + name: stripe_ios + sha256: "0f7afed3ac61e544e7525da9b692b23d93e762d56f6c9aa7f77fc6d9a686a65d" + url: "https://pub.dev" + source: hosted + version: "11.5.0" + stripe_platform_interface: + dependency: transitive + description: + name: stripe_platform_interface + sha256: "23c10f3875da07f85a6196fcb676e64c767ad2d04ec73ba4e941ac797a4ee4d3" + url: "https://pub.dev" + source: hosted + version: "11.5.0" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index bc91b28..f184b5b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,10 +11,11 @@ dependencies: sdk: flutter http: ^1.2.2 provider: ^6.1.2 - + permission_handler: ^11.3.1 shared_preferences: ^2.2.3 dchs_flutter_beacon: ^0.6.6 + flutter_stripe: ^11.4.0 dev_dependencies: flutter_test: