Comment intégrer le paiement Stripe Connect dans votre app FlutterFlow

24 oct. 2024

10 minutes de lecture

Salut les devs ! 👋 Aujourd'hui, on va voir comment intégrer proprement le Payment Sheet de Stripe Connect dans votre application FlutterFlow. Je sais que beaucoup d'entre vous cherchent à ajouter des paiements à leurs apps, alors j'ai créé une solution simple mais robuste que vous pourrez facilement adapter à vos besoins.

Comprendre Stripe Connect et les Payment Intents

Avant de plonger dans l'implémentation, clarifions quelques concepts clés :

Qu'est-ce que Stripe Connect ?

Stripe Connect est la solution de Stripe pour les marketplaces et les plateformes multi-vendeurs. Elle permet de :

  • Gérer les paiements entre clients et vendeurs

  • Automatiser les reversements aux vendeurs

  • Gérer les commissions de plateforme

  • Respecter les réglementations locales pour les paiements

Les Payment Intents expliqués

Un Payment Intent est comme un "contrat de paiement" qui :

  1. Définit le montant et la devise

  2. Spécifie le compte Connect du vendeur qui recevra le paiement

  3. Configure les commissions de la plateforme

  4. Gère l'authentification 3D Secure si nécessaire

Voici à quoi ressemble une requête Payment Intent pour Stripe Connect en curl :

curl https://api.stripe.com/v1/payment_intents \
  -u "sk_test_VePHdqKTYQjKNInc7u56JBrQ:" \
  -d amount=2000 \
  -d currency=usd \
  -d "automatic_payment_methods[enabled]"=true

La réponse contiendra le client_secret nécessaire pour notre Payment Sheet :

{ "id": "pi_xxxxxxxxxxxxx", 
  "object": "payment_intent", 
  "client_secret": "pi_xxxxxxxxxxxxx_secret_xxxxxxxxxxxxx", 
  "status": "requires_payment_method", 
  "amount": 1000, 
  "currency": "eur", 
  "application_fee_amount": 123, 
  "transfer_data": { "destination": "acct_xxxxxxxxxxxxx" } }

Prérequis

  • Un compte FlutterFlow

  • Un compte Stripe avec Connect activé

  • Les dépendances Flutter suivantes :

    • flutter_stripe

    • google_fonts

Vue d'ensemble de la solution

Notre solution utilise une Custom Action FlutterFlow qui va gérer l'affichage du Payment Sheet Stripe. L'avantage ? Une fois configurée, vous pourrez l'utiliser n'importe où dans votre app avec seulement quelques clics !

Étape 1 : Création de la Custom Action

Dans FlutterFlow, allez dans "Custom Code" > "Custom Actions" et créez une nouvelle action avec ces paramètres :

Nom de l'action : initializeStripeAndShowPaymentSheet

Arguments d'entrée :

  • clientSecret (String) : Le secret client généré par votre API

  • allowGooglePay (bool) : Activer/désactiver Google Pay

  • allowApplePay (bool) : Activer/désactiver Apple Pay

Valeur de retour : Créez un nouveau type de données personnalisé StripeResponse avec :

  • isSuccess (bool)

  • errorMsg (String, optionnel)

Étape 2 : Configuration du Backend

Avant d'utiliser le Payment Sheet, vous devez :

  1. Configurer votre API pour Stripe Connect :

    • Créer le Payment Intent avec le compte Connect du vendeur

    • Définir les commissions de plateforme

    • Gérer les webhooks pour les confirmations de paiement

  2. Sécuriser les transactions :

    • Vérifier que le vendeur est bien connecté à votre plateforme

    • Valider les montants et commissions

    • Gérer les devises supportées

Étape 3 : Configuration dans FlutterFlow

  1. Créez un appel API vers votre backend pour :

    • Générer le Payment Intent avec les bons paramètres Connect

    • Récupérer le client secret

  2. Dans votre page FlutterFlow, ajoutez un bouton pour déclencher le paiement

  3. Dans les actions du bouton :

    • Ajoutez votre appel API

    • Chaînez avec la Custom Action en passant les paramètres :

      clientSecret: [Résultat de votre API] allowGooglePay: true/false (selon vos besoins) allowApplePay: true/false (selon vos besoins)

Gestion des résultats

La Custom Action retourne un StripeResponse que vous pouvez utiliser pour :

  • Vérifier si le paiement a réussi (isSuccess)

  • Gérer les erreurs (errorMsg)

Vous pouvez utiliser ces valeurs dans FlutterFlow pour :

  • Afficher un message de succès

  • Rediriger vers une page de confirmation

  • Gérer les erreurs avec un message utilisateur

Webhooks et Confirmation des paiements

Pour une intégration robuste, n'oubliez pas de :

  1. Configurer les webhooks Stripe Connect :

    • payment_intent.succeeded

    • payment_intent.payment_failed

    • account.updated (pour le statut des comptes vendeurs)

  2. Vérifier les paiements côté serveur :

    • Ne pas faire confiance uniquement au retour client

    • Confirmer via les webhooks

    • Mettre à jour le statut dans votre base de données

Personnalisation

Le Payment Sheet s'adapte automatiquement à votre thème FlutterFlow. Il utilise :

  • Vos couleurs primaires pour les boutons

  • Le mode sombre/clair de l'appareil

  • Les polices Google Fonts

Points importants à noter

  • Le code gère automatiquement les versions Web et Mobile

  • Support intégré pour Apple Pay et Google Pay

  • Gestion des erreurs robuste

  • Adaptation automatique au thème de votre app

Bonnes pratiques pour Stripe Connect

  1. Testez tous les scénarios :

    • Paiements réussis

    • Échecs de paiement

    • Comptes vendeurs non validés

    • Limites de paiement dépassées

  2. Gérez les commissions correctement :

    • Calculez les commissions côté serveur

    • Vérifiez les minimums et maximums

    • Prenez en compte les frais Stripe

  3. Documentation utilisateur :

    • Expliquez le processus aux vendeurs

    • Documentez les délais de paiement

    • Clarifiez les frais et commissions

Code Source Complet

Voici le code complet de la Custom Action que vous pouvez utiliser dans votre projet. Ce code est prêt à l'emploi et inclut toutes les fonctionnalités mentionnées ci-dessus :


import 'dart:math';

import 'package:meerai/flutter_flow/flutter_flow_widgets.dart';

import 'package:google_fonts/google_fonts.dart';

import 'dart:io';

import 'package:flutter/foundation.dart';

import 'dart:convert';

import 'package:flutter_stripe/flutter_stripe.dart';

Future<StripeResponseStruct> initializeStripeAndShowPaymentSheet(
    BuildContext context,
    String clientSecret,
    bool allowGooglePay,
    bool allowApplePay) async {
  try {
    print(clientSecret);
    // This payment intent would be an parameter later and be removed from here.
    if (kIsWeb) {
      final res = await showWebPaymentSheet(
        context,
        paymentIntentSecret: clientSecret,
        description: "description",
        buttonColor: FlutterFlowTheme.of(context).primary,
        buttonTextColor: FlutterFlowTheme.of(context).primaryText,
      );
      return res;
    }

    await Stripe.instance.initPaymentSheet(
      paymentSheetParameters: SetupPaymentSheetParameters(
        paymentIntentClientSecret: clientSecret,
        // customerEphemeralKeySecret: response['ephemeralKey'],
        merchantDisplayName: 'VOTRENOMDEMARCHANT',
        googlePay: allowGooglePay
            ? PaymentSheetGooglePay(
                merchantCountryCode: 'FR',
                currencyCode: "EUR",
                testEnv: false,
              )
            : null,
        applePay: isiOS && allowApplePay
            ? const PaymentSheetApplePay(
                merchantCountryCode: 'FR',
              )
            : null,
        style: ThemeMode.system,
        appearance: PaymentSheetAppearance(
          primaryButton: PaymentSheetPrimaryButtonAppearance(
            colors: PaymentSheetPrimaryButtonTheme(
              light: PaymentSheetPrimaryButtonThemeColors(
                background: FlutterFlowTheme.of(context).primary,
                text: FlutterFlowTheme.of(context).primaryText,
              ),
              dark: PaymentSheetPrimaryButtonThemeColors(
                background: FlutterFlowTheme.of(context).primary,
                text: FlutterFlowTheme.of(context).primaryText,
              ),
            ),
          ),
        ),
      ),
    );
    // Present the payment sheet
    await Stripe.instance.presentPaymentSheet();
    // If the payment sheet is presented successfully, return a success message
    return StripeResponseStruct(isSuccess: true);
  } catch (e) {
    print(e);
    if (e is StripeException) {
      // return 'Error from Stripe: ${e.error.localizedMessage}' ;
      return StripeResponseStruct(
          isSuccess: false, errorMsg: e.error.localizedMessage);
    } else {
      // return 'Error: $e';
      return StripeResponseStruct(isSuccess: false, errorMsg: 'Error: $e');
    }
  }
}

// Set your action name, define your arguments and return parameter,
// and then add the boilerplate code using the green button on the right!

Future<StripeResponseStruct> showWebPaymentSheet(
  BuildContext context, {
  required String paymentIntentSecret,
  required String description,
  Color? buttonColor,
  Color? buttonTextColor,
  ThemeMode? themeStyle,
}) async {
  final isDarkMode = themeStyle == null
      ? Theme.of(context).brightness == Brightness.dark
      : themeStyle == ThemeMode.dark;
  buttonColor = buttonColor ?? FlutterFlowTheme.of(context).primary;
  final screenWidth = MediaQuery.sizeOf(context).width;

  buildPaymentSheet(BuildContext context, double width) => Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          ClipRRect(
            borderRadius: BorderRadius.circular(8.0),
            child: Material(
              color: Colors.transparent,
              child: Container(
                width: width,
                padding: const EdgeInsets.fromLTRB(24.0, 14.0, 24.0, 24.0),
                color: isDarkMode ? const Color(0xFF101213) : Colors.white,
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Column(
                      mainAxisSize: MainAxisSize.min,
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Row(
                          children: [
                            Expanded(
                              child: Text(
                                'Payment Information',
                                style: GoogleFonts.outfit(
                                  color: isDarkMode
                                      ? Colors.white
                                      : const Color(0xFF101213),
                                  fontSize: 28,
                                  fontWeight: FontWeight.w500,
                                ),
                              ),
                            ),
                            InkWell(
                              onTap: () => Navigator.pop(context),
                              child: Padding(
                                padding: const EdgeInsets.all(4.0),
                                child: Icon(
                                  Icons.close_rounded,
                                  size: 22,
                                  color: isDarkMode
                                      ? const Color(0xFF95A1AC)
                                      : const Color(0xFF57636C),
                                ),
                              ),
                            ),
                          ],
                        ),
                        if (description.isNotEmpty) ...[
                          const SizedBox(height: 8.0),
                          Text(
                            description,
                            style: GoogleFonts.outfit(
                              color: isDarkMode
                                  ? const Color(0xFF95A1AC)
                                  : const Color(0xFF57636C),
                              fontSize: 14,
                            ),
                          ),
                        ],
                      ],
                    ),
                    const SizedBox(height: 16.0),
                    CardField(
                      numberHintText: "0000 00000 00000 000",
                      cvcHintText: "233",
                      expirationHintText: "10/20",
                      decoration: InputDecoration(
                        focusedBorder: OutlineInputBorder(
                          borderSide: BorderSide(
                            color: buttonColor!,
                            width: 2.0,
                          ),
                          borderRadius: BorderRadius.circular(8.0),
                        ),
                        enabledBorder: OutlineInputBorder(
                          borderSide: BorderSide(
                            color: isDarkMode
                                ? const Color(0xFF22282F)
                                : const Color(0xFFE0E3E7),
                            width: 2.0,
                          ),
                          borderRadius: BorderRadius.circular(8.0),
                        ),
                        filled: isDarkMode,
                      ),
                      style: GoogleFonts.outfit(
                        color:
                            isDarkMode ? Colors.white : const Color(0xFF101213),
                        fontSize: 14,
                      ),
                      enablePostalCode: true,
                    ),
                    const SizedBox(height: 20.0),
                    FFButtonWidget(
                      onPressed: () async {
                        try {
                          final response = await Stripe.instance.confirmPayment(
                            paymentIntentClientSecret: paymentIntentSecret,
                            data: const PaymentMethodParams.card(
                              paymentMethodData: PaymentMethodData(),
                            ),
                            options: const PaymentMethodOptions(),
                          );
                          if (response.status ==
                              PaymentIntentsStatus.Succeeded) {
                            Navigator.pop(
                              context,
                              StripeResponseStruct(isSuccess: true),
                            );
                          }
                        } catch (e) {
                          if (e is StripeException &&
                              e.error.code == FailureCode.Canceled) {
                            Navigator.pop(
                                context,
                                StripeResponseStruct(
                                    isSuccess: false, errorMsg: "Cancelled"));
                          }
                          Navigator.pop(
                            context,
                            StripeResponseStruct(
                                isSuccess: false, errorMsg: '$e'),
                          );
                        }
                      },
                      text: 'Payer',
                      options: FFButtonOptions(
                        width: double.infinity,
                        height: 44,
                        color: buttonColor,
                        textStyle: GoogleFonts.outfit(
                          color: buttonTextColor ?? Colors.white,
                          fontSize: 14,
                          fontWeight: FontWeight.w600,
                        ),
                        elevation: 2,
                        borderSide: const BorderSide(
                          color: Colors.transparent,
                          width: 1,
                        ),
                        borderRadius: BorderRadius.circular(6),
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
        ],
      );

  final response = await showDialog<StripeResponseStruct>(
    context: context,
    builder: (context) => AlertDialog(
      backgroundColor: Colors.transparent,
      contentPadding: EdgeInsets.zero,
      content: buildPaymentSheet(context, min(420, screenWidth - 16)),
    ),
  );
  // Return the payment response, or an empty response if the user canceled.
  return response ??
      StripeResponseStruct(isSuccess: false, errorMsg: "Cancelled");
}


Conclusion

Et voilà ! Vous avez maintenant un système de paiement Connect professionnel intégré à votre app FlutterFlow. Cette solution vous permet de gérer facilement les paiements entre vos utilisateurs et vos vendeurs, tout en gardant un contrôle total sur les commissions et les flux financiers.

N'oubliez pas de :

  • Tester en mode développement avant de passer en production

  • Gérer correctement les erreurs côté utilisateur

  • Confirmer les paiements côté serveur

  • Suivre les meilleures pratiques de Stripe Connect

  • Adapter le code à vos besoins spécifiques

Vous avez des questions ou des suggestions d'amélioration ? N'hésitez pas à les partager dans les commentaires !

Happy coding! 🚀


Lancez votre prochain projet

Rejoignez plus de +400 startups qui se sont lancés grâce au no-code

© 2024 Node Agency. All rights reserved.

Twitter

Lancez votre prochain projet

Rejoignez plus de +400 startups qui se sont lancés grâce au no-code

© 2024 Node Agency. All rights reserved.

Twitter

Lancez votre prochain projet

Rejoignez plus de +400 startups qui se sont lancés grâce au no-code

© 2024 Node Agency. All rights reserved.

Twitter