[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-pagamenti-in-app-in-flutter-con-stripe-guida-pratica-a-flutter-stripe":3,"comments-article-pagamenti-in-app-in-flutter-con-stripe-guida-pratica-a-flutter-stripe":26},{"id":4,"title":5,"slug":6,"excerpt":7,"body":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"status":16,"published_at":17,"meta_title":18,"meta_description":19,"category":20,"author":24},105,"Pagamenti in-app in Flutter con Stripe: guida pratica a flutter_stripe","pagamenti-in-app-in-flutter-con-stripe-guida-pratica-a-flutter-stripe","Come integrare pagamenti reali in un'app Flutter con Stripe: setup di flutter_stripe, PaymentSheet, Apple Pay e Google Pay, gestione degli errori, 3D Secure, salvataggio dei metodi di pagamento e verifica lato server tramite webhook.","Quando un'app deve vendere beni fisici, servizi o prenotazioni, gli acquisti in-app degli store non sono l'opzione obbligata (e spesso nemmeno consentita): serve un vero payment gateway. Stripe è oggi la soluzione più diffusa e mantiene un SDK ufficiale per Flutter, `flutter_stripe`, che incapsula gli SDK nativi iOS e Android e offre una UI pronta all'uso: la **PaymentSheet**.\n\nIn questa guida vediamo come costruire un flusso di pagamento completo e sicuro, con particolare attenzione al punto che più spesso viene sbagliato: la divisione delle responsabilità tra client e server.\n\n## Regola numero uno: la secret key non tocca mai il client\n\nUn'app Flutter è codice che finisce sul dispositivo dell'utente: qualunque stringa compilata nel binario è, di fatto, pubblica. Per questo Stripe usa due chiavi:\n\n- **publishable key** (`pk_test_...` \u002F `pk_live_...`): può stare nell'app, serve solo a tokenizzare dati e mostrare la UI;\n- **secret key** (`sk_...`): vive esclusivamente sul tuo backend, dove vengono creati gli oggetti `PaymentIntent`.\n\nIl flusso corretto è quindi:\n\n1. l'app chiede al tuo backend di creare un `PaymentIntent` per un certo ordine;\n2. il backend calcola l'importo (**mai** fidarsi dell'importo inviato dal client) e restituisce il `client_secret`;\n3. l'app presenta la PaymentSheet passando quel `client_secret`;\n4. Stripe notifica il backend via **webhook** che il pagamento è riuscito;\n5. il backend marca l'ordine come pagato ed eroga il servizio.\n\n## Setup del progetto\n\nAggiungi la dipendenza:\n\n```yaml\ndependencies:\n  flutter_stripe: ^11.1.0\n  http: ^1.2.2\n```\n\n### Android\n\n`flutter_stripe` richiede alcune modifiche obbligatorie, altrimenti l'app crasha all'apertura della PaymentSheet:\n\n- `minSdkVersion 21` (o superiore) in `android\u002Fapp\u002Fbuild.gradle`;\n- l'Activity deve estendere `FlutterFragmentActivity`, non `FlutterActivity`;\n- il tema dell'app deve derivare da un tema AppCompat\u002FMaterial.\n\n```kotlin\n\u002F\u002F android\u002Fapp\u002Fsrc\u002Fmain\u002Fkotlin\u002F...\u002FMainActivity.kt\nimport io.flutter.embedding.android.FlutterFragmentActivity\n\nclass MainActivity : FlutterFragmentActivity()\n```\n\n```xml\n\u003C!-- android\u002Fapp\u002Fsrc\u002Fmain\u002Fres\u002Fvalues\u002Fstyles.xml -->\n\u003Cstyle name=\"LaunchTheme\" parent=\"Theme.MaterialComponents.DayNight.NoActionBar\">\n  \u003C!-- ... -->\n\u003C\u002Fstyle>\n```\n\n### iOS\n\nImposta la piattaforma minima a iOS 13 nel `Podfile` (`platform :ios, '13.0'`) e, se vuoi Apple Pay, attiva la capability *Apple Pay* in Xcode e registra il merchant identifier.\n\n## Inizializzazione\n\nLa publishable key va impostata prima di `runApp`. Meglio non hardcodarla: usa `--dart-define` così puoi cambiarla tra flavor.\n\n```dart\nimport 'package:flutter\u002Fmaterial.dart';\nimport 'package:flutter_stripe\u002Fflutter_stripe.dart';\n\nFuture\u003Cvoid> main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n\n  Stripe.publishableKey = const String.fromEnvironment('STRIPE_PK');\n  Stripe.merchantIdentifier = 'merchant.com.acme.app'; \u002F\u002F solo per Apple Pay\n  Stripe.urlScheme = 'acmeapp'; \u002F\u002F redirect per 3D Secure \u002F wallet esterni\n  await Stripe.instance.applySettings();\n\n  runApp(const MyApp());\n}\n```\n\nLancio: `flutter run --dart-define=STRIPE_PK=pk_test_123...`.\n\n## Il backend: creare il PaymentIntent\n\nIl server è la fonte di verità per l'importo. Ecco un endpoint minimale in Node.js con Express, utile come riferimento anche se il tuo backend è in Dart (`dart_frog`, `serverpod`), Python o PHP.\n\n```js\napp.post('\u002Fpayments\u002Fintent', async (req, res) => {\n  const { orderId } = req.body;\n  const order = await db.orders.findById(orderId); \u002F\u002F importo calcolato lato server\n\n  const customer = await stripe.customers.create(); \u002F\u002F o recupera quello esistente\n  const ephemeralKey = await stripe.ephemeralKeys.create(\n    { customer: customer.id },\n    { apiVersion: '2024-06-20' },\n  );\n\n  const intent = await stripe.paymentIntents.create(\n    {\n      amount: order.totalCents, \u002F\u002F in centesimi: 24.90 EUR -> 2490\n      currency: 'eur',\n      customer: customer.id,\n      automatic_payment_methods: { enabled: true },\n      metadata: { orderId: order.id },\n    },\n    { idempotencyKey: `order-${order.id}` }, \u002F\u002F evita doppi addebiti sui retry\n  );\n\n  res.json({\n    clientSecret: intent.client_secret,\n    customerId: customer.id,\n    ephemeralKey: ephemeralKey.secret,\n  });\n});\n```\n\nDue dettagli importanti: gli importi sono sempre **interi in centesimi** (mai `double`, per evitare errori di arrotondamento) e l'`idempotencyKey` protegge da doppie creazioni quando l'app ritenta la richiesta su rete instabile.\n\n## Lato Flutter: PaymentSheet in due passi\n\nLa PaymentSheet gestisce carte, wallet, SEPA, 3D Secure e validazioni: è la strada consigliata rispetto al `CardField` custom, perché riduce drasticamente il codice e i requisiti PCI.\n\n```dart\nclass CheckoutService {\n  CheckoutService(this._api);\n  final PaymentsApi _api;\n\n  Future\u003CPaymentResult> pay({required String orderId}) async {\n    try {\n      final intent = await _api.createIntent(orderId);\n\n      await Stripe.instance.initPaymentSheet(\n        paymentSheetParameters: SetupPaymentSheetParameters(\n          paymentIntentClientSecret: intent.clientSecret,\n          customerId: intent.customerId,\n          customerEphemeralKeySecret: intent.ephemeralKey,\n          merchantDisplayName: 'Acme S.r.l.',\n          style: ThemeMode.system,\n          googlePay: const PaymentSheetGooglePay(\n            merchantCountryCode: 'IT',\n            currencyCode: 'EUR',\n            testEnv: true,\n          ),\n          applePay: const PaymentSheetApplePay(\n            merchantCountryCode: 'IT',\n          ),\n          billingDetailsCollectionConfiguration:\n              const BillingDetailsCollectionConfiguration(\n            email: CollectionMode.always,\n            address: AddressCollectionMode.full,\n          ),\n        ),\n      );\n\n      await Stripe.instance.presentPaymentSheet();\n      return PaymentResult.succeeded;\n    } on StripeException catch (e) {\n      if (e.error.code == FailureCode.Canceled) {\n        return PaymentResult.canceled;\n      }\n      return PaymentResult.failed(e.error.localizedMessage ?? 'Errore');\n    }\n  }\n}\n```\n\nNota che `presentPaymentSheet()` **completa senza errore solo se il pagamento è andato a buon fine**: cancellazione e fallimenti arrivano come `StripeException`. Il campo `localizedMessage` contiene un messaggio già tradotto, adatto a mostrare in UI.\n\n### Personalizzare l'aspetto\n\n```dart\nappearance: PaymentSheetAppearance(\n  colors: PaymentSheetAppearanceColors(\n    primary: Theme.of(context).colorScheme.primary,\n    background: Theme.of(context).colorScheme.surface,\n  ),\n  shapes: const PaymentSheetShape(borderRadius: 16),\n  primaryButton: const PaymentSheetPrimaryButtonAppearance(\n    shapes: PaymentSheetPrimaryButtonShape(blurRadius: 0),\n  ),\n),\n```\n\n## Non fidarti del risultato lato client\n\nUn errore molto comune: sbloccare il contenuto acquistato appena `presentPaymentSheet()` ritorna. L'utente potrebbe chiudere l'app, perdere la rete o (in scenari di abuso) manipolare il client. L'unica conferma affidabile arriva dal **webhook** Stripe:\n\n```js\napp.post('\u002Fstripe\u002Fwebhook', express.raw({ type: 'application\u002Fjson' }), (req, res) => {\n  const event = stripe.webhooks.constructEvent(\n    req.body,\n    req.headers['stripe-signature'],\n    process.env.STRIPE_WEBHOOK_SECRET,\n  );\n\n  if (event.type === 'payment_intent.succeeded') {\n    const intent = event.data.object;\n    fulfillOrder(intent.metadata.orderId); \u002F\u002F idempotente!\n  }\n  res.sendStatus(200);\n});\n```\n\nNell'app, dopo il pagamento, mostra una schermata \"stiamo confermando il tuo ordine\" e fai polling (o ascolta un Firestore\u002FSupabase stream) sullo stato dell'ordine lato server. È una UX un po' più lunga, ma è l'unica corretta.\n\n## Salvare la carta per acquisti futuri\n\nSe vuoi il classico \"paga con un tap la prossima volta\", hai due strade:\n\n- durante un pagamento, creare il `PaymentIntent` con `setup_future_usage: 'off_session'`;\n- senza addebito immediato, creare un **SetupIntent** e presentarlo con `Stripe.instance.initPaymentSheet(... setupIntentClientSecret: ...)`.\n\nIn entrambi i casi il metodo di pagamento viene agganciato al `customer`: passando `customerId` + `customerEphemeralKeySecret` alla PaymentSheet, le carte salvate compaiono automaticamente nell'elenco. Per gli abbonamenti ricorrenti, lato server userai le API `Subscriptions` con i tuoi `price` e passerai all'app il `client_secret` della prima invoice.\n\n## 3D Secure e SCA\n\nIn Europa la PSD2 impone l'autenticazione forte. La buona notizia è che con la PaymentSheet non devi scrivere codice: se la banca richiede il 3DS, l'SDK apre il flusso di challenge e ritorna solo a esito ottenuto. L'unico requisito tecnico è aver configurato `Stripe.urlScheme` e, su Android, il tema corretto (vedi setup) per la WebView di challenge.\n\n## Testare senza spendere\n\nUsa le chiavi `pk_test`\u002F`sk_test` e le carte di test di Stripe:\n\n- `4242 4242 4242 4242` → pagamento riuscito;\n- `4000 0027 6000 3184` → richiede 3D Secure;\n- `4000 0000 0000 9995` → fondi insufficienti (`card_declined`).\n\nPer i webhook in locale, `stripe listen --forward-to localhost:3000\u002Fstripe\u002Fwebhook` della CLI ufficiale è indispensabile. Google Pay in test funziona con `testEnv: true`; Apple Pay richiede un dispositivo fisico con una carta di test nel Wallet sandbox.\n\n## Checklist finale\n\n- Secret key solo sul server, publishable key via `--dart-define` per flavor.\n- Importi in centesimi calcolati lato backend, `idempotencyKey` sulle create.\n- `FlutterFragmentActivity` + tema Material su Android, iOS 13+.\n- Fulfillment guidato dal webhook, mai dal ritorno di `presentPaymentSheet()`.\n- Gestione esplicita di `FailureCode.Canceled` per non mostrare errori inutili.\n- Attenzione alle policy degli store: i contenuti digitali consumati nell'app devono comunque passare dagli in-app purchase; Stripe è per beni e servizi \"reali\".\n\nCon questa struttura ottieni un checkout robusto in poche centinaia di righe, delegando a Stripe la parte più delicata (dati di carta, conformità PCI, SCA) e mantenendo nel tuo backend l'unica cosa che davvero deve rimanere sotto controllo: la verità sull'ordine.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F0eef5e9d-da87-4bb5-84a5-16016db9eee6.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1739989934265-b46240484890?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTAwNDk2ODl8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"SumUp","https:\u002F\u002Funsplash.com\u002F@sumup","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-person-holding-a-credit-card-in-front-of-a-computer-Z-o7nk0joVQ",null,"published","2026-09-22T04:01:29+00:00","Pagamenti in Flutter con Stripe e flutter_stripe","Guida pratica ai pagamenti in Flutter con Stripe: setup di flutter_stripe, PaymentSheet, Apple e Google Pay, 3D Secure, webhook e carte salvate.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1792316909609]