CheckoutController
controllers/checkout_controller.dart · Notifier<CheckoutState>
Overview
Drives the entire checkout pipeline. Holds a CartService + ProvisionService directly (no DI). Each step (init, address, shipping, payment) mutates CheckoutState via copyWith and exposes a 'shallow-cart safeguard' that refetches the full cart if Medusa returns a stripped response.
User Flow
- initCheckout: createPhysicalCart → addItemToCart × N → getCart → savePendingCheckout → fetchPaymentProviders + fetchShippingOptions → ProvisionService.getProvisionQuestions/Policy.
- updateShippingAddress: updateCart with shipping_address + email, then fetchShippingOptions for that address.
- setShippingOption / setPaymentProvider: store choice in state (paymentProvider does NOT call backend).
- applyPromoCode / removePromoCode: deferred to CartService; re-fetch full cart on shallow response.
- createPaymentSession: createPaymentCollection → updatePaymentCollectionAmount if the cart total drifted → createPaymentSession(providerId) → pull clientSecret out of payment_sessions list.
- confirmStripePayment: validate required questions → updateCart with booking_answers → init+present PaymentSheet → completeCart → addOrderToHistory → clear local cart entries → clearPendingCheckout.
Cases & Edge Cases
- Shallow-cart safeguard repeats after every mutating call (updateCart, addShippingMethod, applyPromoCode, removePromoCode) because Medusa sometimes returns the cart without items inlined; we detect by 'previous had items, response has none' and refetch.
- Payment-collection amount drift is reconciled by comparing the existing collection amount with (cart.total * 100) cents and PATCH-ing if needed.
- Booking-question validation runs IN confirmStripePayment, not earlier — required because answers can change up until the moment the user taps Pay.
- StripeException.FailureCode.Canceled is treated as a soft outcome (snack 'Payment Cancelled') rather than an error toast.
- PaymentSheet flow is wrapped in try/catch with a Stripe-specific branch — non-Stripe errors fall through to a generic 'Error: $e' snack.
- TODO in updateShippingAddress: email is hard-coded to 'user@example.com'; this currently breaks email receipts for production users.
Code References
- lib/src/features/shared/checkout/controllers/checkout_controller.dart:16 // CheckoutController
- lib/src/features/shared/checkout/controllers/checkout_controller.dart:27 // initCheckout
- lib/src/features/shared/checkout/controllers/checkout_controller.dart:243 // createPaymentSession (amount sync)
- lib/src/features/shared/checkout/controllers/checkout_controller.dart:308 // confirmStripePayment
- lib/src/features/shared/checkout/controllers/checkout_controller.dart:437 // checkoutControllerProvider
API Calls
Empty — fill this in later. Open the manifest and add content here.
Notes
Side-effect-heavy method (confirmStripePayment) takes BuildContext and a callback — keep it untestable in isolation; tests are integration-only for now.