Add Stripe payment integration

- 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 <noreply@anthropic.com>
This commit is contained in:
John Mizerek 2026-01-05 10:46:13 -08:00
parent 4ebfbbc03b
commit 5942deb0c5
9 changed files with 513 additions and 84 deletions

View file

@ -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()

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<style name="LaunchTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
@ -12,7 +12,7 @@
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<style name="NormalTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<style name="LaunchTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
@ -12,7 +12,7 @@
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<style name="NormalTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowBackground">@android:color/black</item>
</style>
</resources>

View file

@ -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<ScaffoldMessengerState> 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());
}

View file

@ -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<CartViewScreen> {
Cart? _cart;
bool _isLoading = true;
bool _isProcessingPayment = false;
String? _error;
Map<int, MenuItem> _menuItemsById = {};
// Tip options as percentages
static const List<int> _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<CartViewScreen> {
}
}
Future<void> _submitOrder() async {
try {
Future<void> _processPaymentAndSubmit() async {
if (_cart == null) return;
final appState = context.read<AppState>();
final cartOrderId = appState.cartOrderId;
if (cartOrderId == null) return;
final businessId = appState.selectedBusinessId;
setState(() => _isLoading = true);
if (cartOrderId == null || businessId == null) return;
setState(() => _isProcessingPayment = true);
try {
// 1. Process payment with Stripe
final paymentResult = await StripeService.processPayment(
context: context,
businessId: businessId,
orderId: cartOrderId,
subtotal: _cart!.subtotal,
tax: _cart!.tax,
tip: _tipAmount,
);
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<CartViewScreen> {
// 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<CartViewScreen> {
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<CartViewScreen> {
padding: const EdgeInsets.all(16),
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Tip Selection
const Text(
"Subtotal",
style: TextStyle(fontSize: 16),
"Add a tip",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
Text(
"\$${_cart!.subtotal.toStringAsFixed(2)}",
style: const TextStyle(fontSize: 16),
),
],
),
// Sales tax
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Tax (8.25%)",
style: 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,
),
Text(
"\$${_cart!.tax.toStringAsFixed(2)}",
style: const TextStyle(fontSize: 16),
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),
],
),
// Tip
if (_tipAmount > 0) ...[
const SizedBox(height: 6),
_buildSummaryRow("Tip", _tipAmount),
],
const Divider(height: 24),
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<CartViewScreen> {
),
),
Text(
"\$${_cart!.total.toStringAsFixed(2)}",
"\$${fees.total.toStringAsFixed(2)}",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
@ -576,15 +670,27 @@ class _CartViewScreenState extends State<CartViewScreen> {
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<Color>(Colors.white),
),
)
: Text(
"Pay \$${fees.total.toStringAsFixed(2)}",
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
),
@ -594,6 +700,28 @@ class _CartViewScreenState extends State<CartViewScreen> {
);
}
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,

View file

@ -23,6 +23,8 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
List<MenuItem> _allItems = [];
final Map<int, List<MenuItem>> _itemsByCategory = {};
final Map<int, List<MenuItem>> _itemsByParent = {};
final Map<int, int> _categorySortOrder = {}; // categoryId -> sortOrder
final Map<int, String> _categoryNames = {}; // categoryId -> categoryName
// Track which category is currently expanded (null = none)
int? _expandedCategoryId;
@ -79,6 +81,8 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
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<MenuBrowseScreen> {
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<MenuBrowseScreen> {
List<int> _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<MenuBrowseScreen> {
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<MenuBrowseScreen> {
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<MenuBrowseScreen> {
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<MenuBrowseScreen> {
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<MenuBrowseScreen> {
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<MenuBrowseScreen> {
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),
],
),
),

View file

@ -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<String, dynamic> 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<void> initialize(String publishableKey) async {
if (_isInitialized) return;
Stripe.publishableKey = publishableKey;
await Stripe.instance.applySettings();
_isInitialized = true;
}
/// Create payment intent on the server
static Future<Map<String, dynamic>> _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<String, dynamic>? json;
try {
json = jsonDecode(body) as Map<String, dynamic>;
} 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<String, dynamic>;
} 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<PaymentResult> 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<String, dynamic>?;
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(),
);
}
}
}

View file

@ -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:

View file

@ -15,6 +15,7 @@ dependencies:
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: