Настройка интеграции Stripe Payments с Flutter

Настройка интеграции Stripe Payments с Flutter

13 января 2024 г.

Привет! В этой статье я хочу поделиться своим опытом настройки платежей Stripe с помощью Flutter.

Я работаю над мобильным приложением, которое должно позволить пользователям создавать задачи (например, уборка, выгул собаки и другие), а другим пользователям — отвечать на задачи и получать оплату, когда задача будет завершена. Короче говоря, между пользователями должен осуществляться денежный перевод.

Самый популярный и хорошо документированный способ совершать платежи — использовать Stripe. Но статей, в которых показано, как сохранить банковскую карту в Stripe, использовать ее в дальнейшем или перевести деньги со своей банковской карты на карту другого человека, я не нашел. Вся информация, которую я нашел, была связана с библиотеками со скидкой или просто небезопасна.

В этой статье я хочу показать, как я решил эту проблему и совершил платежи с помощью Stripe. В конце на GitHub не будет проекта-примера, но все методы работают, и вы можете просто скопировать их и использовать, если вам нужно. Конечно, вы можете лучше реализовать мои идеи, и если да, то напишите, пожалуйста, свой код в комментариях. Итак, начнем!

Аккаунт Stripe

Чтобы иметь возможность работать с Stripe, первое, что вам нужно сделать, это, очевидно, создать учетную запись. Платформа проведет вас через процесс заполнения различной информации о том, кто вы, но вы можете пока пропустить это и сделать это позже.

На панели управления выберите раздел Разработчик и найдите Ключи API.

The API keys

Надежно храните в своем приложении как Publishable, так и Secret ключи (я использую dotenv), чтобы позже связаться с Stripe API. Вы также можете заметить, что пока работаете в тестовом режиме, поэтому можете безопасно работать с транзакциями.

Интеграция Flutter

Следующий шаг — добавить Stripe в ваше приложение. Вы можете сделать это, добавив пакет flutter_stripe в свои зависимости.

Затем нам нужно подготовить Stripe для использования в нашем приложении. В main.dart добавьте следующий код:

// The main function that kicks off the execution of the Flutter application.
Future<void> main() async {
  // Ensure that the Flutter app's widgets are initialized properly before running the app.
  WidgetsFlutterBinding.ensureInitialized();

  // Load environment variables from a .env file.
  await dotenv.load();

  // Set the Stripe publishable key from the loaded environment variables.
  Stripe.publishableKey = dotenv.env[Keys.stripePublicKey]!;

  // Set the Stripe merchant identifier for the app.
  Stripe.merchantIdentifier = 'merchant.flutter.stripe.appTitle';

  // Set the URL scheme for handling deep links related to Stripe payments.
  Stripe.urlScheme = 'flutterstripe';

  // Apply settings for the Stripe SDK.
  await Stripe.instance.applySettings();

  // Run the Flutter app with the provided App widget.
  runApp(App());
}

Теперь мы готовы использовать Stripe. Но в некоторых случаях мы не будем использовать методы SDK, а напишем свои.

Понимание процесса оплаты Stripe

Дело в том, что нельзя просто получить данные банковской карты и передать их на биллинговый сервер без шифрования. Это небезопасно, и вы можете потерять деньги. Так как же Stripe нам в этом поможет…

Короче говоря, ваша учетная запись Stripe — это точка, через которую ваши пользователи будут переводить деньги и платить некоторую комиссию за предоставляемую вами услугу. Пользователей вашего приложения в Stripe называют Клиенты. Подробнее об этом можно узнать здесь.

Чтобы создать объект Клиент, мы напишем собственный метод, использующий http-пакет:

// A function to create a customer in the Stripe payment system using the provided email.
Future<void> createCustomer({
    required String email,
  }) async {
    try {
      // Prepare the request body with the customer’s email.
      final body = <String, dynamic>{
        email: email,
      };

      // Make a POST request to the Stripe API to create a customer.
      final response = await http.post(
        Uri.parse(https://api.stripe.com/v1/customers’), // Stripe API endpoint for creating customers.
        headers: {
          Authorization: Bearer ${dotenv.env[Keys.stripeSecretKey]}, // Set the API key for authentication.
          Content-Type: application/x-www-form-urlencoded // Set the request body content type.
        },
        body: body, // Include the request body in the POST request.
      );

      // Parse the response JSON and store the created customer data in ‘stripeCustomer’ variable.
      // 'stripeCustomer' is declared earlier.
      stripeCustomer = StripeCustomer.fromJson(
        json: json.decode(response.body),
      );
    } catch (err) {
      // If an error occurs during the process, throw an exception with the error message.
      throw Exception(err.toString());
    }
  }

Ответ официального API инкапсулирован в нашем собственном классе StripeCustomer. Код самого класса следующий:

/// Represents a customer entity in the Stripe payment system.
class StripeCustomer {
  /// Unique identifier for the customer.
  final String id;

  /// Customer's balance, represented in cents.
  final int balance;

  /// Date and time when the customer was created.
  final DateTime created;

  /// Email address associated with the customer.
  final String email;

  /// Constructor to create a [StripeCustomer] object.
  StripeCustomer({
    required this.id,
    required this.balance,
    required this.created,
    required this.email,
  });

  /// Factory method to create a [StripeCustomer] object from JSON data.
  ///
  /// [json] - A Map containing the customer information in JSON format.
  /// Throws an exception if the required fields are missing or invalid in the [json].
  factory StripeCustomer.fromJson({
    required Map<String, dynamic> json,
  }) =>
      StripeCustomer(
        id: json['id'].toString(),                                             
        balance: json['balance'].toString().toInt ~/ 100,                     
        created: DateTime.fromMicrosecondsSinceEpoch(int.parse(json['created'].toString())),
        email: json['email'].toString(),
      );
}

На этом этапе вы можете проверить своего Клиента на панели инструментов Stripe.

The Customer’s dashboard

На данный момент мы подготовили Stripe в нашем приложении и метод для создания объекта Customer. Теперь определимся со сценариями платежей в приложении.

1. Пополните баланс клиента.

В нашем приложении платеж пользователя может быть произведен только в тот момент, когда пользователю необходимо пополнить свой баланс Stripe. У Клиента в Stripe есть свойство баланса, в котором хранится сумма средств. И для этого мы воспользуемся методами пакета flutter_stripe.

Чтобы пополнить баланс, нам необходимо создать объект Payment Intent. Это можно сделать с помощью следующего кода:

/// Creates a payment intent.
  Future<Map<String, dynamic>> createPaymentIntent({
    required String amount,
  }) async {
    try {
      // Request body
      final body = <String, dynamic>{
        // The amount should be in cents
        'amount': (int.parse(amount) * 100).toString(),
        'currency': 'THB',

        // The Customer ID, which balance will be toped-up
        'customer': stripeCustomer!.id,
        'receipt_email': stripeCustomer!.email,
      };

      // Make post request to Stripe
      final response = await http.post(
        Uri.parse('https://api.stripe.com/v1/payment_intents'),
        headers: {
          'Authorization': 'Bearer ${dotenv.env[Keys.stripeSecretKey]}',
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: body,
      );

      // Return the Map object
      return json.decode(response.body);
    } catch (err) {
      throw Exception(err.toString());
    }
  }

Далее нам нужно инициализировать платежный лист Stripe, чтобы завершить созданное Платежное намерение. Сделайте это с помощью следующего кода:

Future<void> initStripePayment({
  required int amount,
}) async {
  // Create a payment intent, the method described above
  final paymentIntent = await createPaymentIntent(
    amount: (amount).toString(),
  );

  try {
    // Initialize Stripe payment sheet
    await Stripe.instance.initPaymentSheet(
      paymentSheetParameters: SetupPaymentSheetParameters(
        customerId: stripeCustomer!.id,
        paymentIntentClientSecret: paymentIntent['client_secret'], // Gotten from payment intent
        merchantDisplayName: 'MyCoolApp',
      ),
    );

  } on Exception catch (e) {
    log('initStripePayment() exception in PaymentStore: $e');
    topUpError = e.toString();
    paymentSheetInitialized = false;
  }
}

И теперь нам нужно показать пользователю форму Stripe для ввода данных карты и продолжения оплаты. Это можно сделать с помощью следующего кода:

await Stripe.instance.presentPaymentSheet();

Появится форма для оплаты.

Stripe’s card credential form (TEST mode)

После завершения платежа баланс Клиента останется неизменным. Это связано с тем, что Клиент добавил средства на счет Stripe нашего приложения, и теперь нам нужно создать Транзакцию баланса клиента, чтобы добавить средства конкретному Клиенту. . Это будет сделано с помощью следующего кода:

Future<void> topUpBalance({
    required int amount,
  }) async {
    try {
      //Request body
      final body = <String, dynamic>{
        'amount': (amount * 100).toInt().toString(),
        'currency': 'thb',
      };

      //Make post request to Stripe
      final response = await http.post(
        Uri.parse(
          'https://api.stripe.com/v1/customers/${stripeCustomer?.id}/balance_transactions',
        ),
        headers: {
          'Authorization': 'Bearer ${dotenv.env[Keys.stripeSecretKey]}',
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: body,
      );

    } catch (err) {
      log('topUpBalance() exception in PaymentStore: $err');
      throw Exception(err.toString());
    }
  }

После выполнения этого действия баланс Клиента будет изменен.

Customer’s dashboard with transaction history

2. Вывод средств за обслуживание приложения.

Теперь на балансе нашего пользователя достаточно средств для оплаты в нашем приложении ответов на задачи.

Исполнитель выбирает в приложении задачу, на которую хочет ответить. И для этого ему необходимо иметь достаточно средств на балансе Stripe. Если средств достаточно, после того, как он ответит на задание, с его баланса будет снята определенная сумма денег. Если средств недостаточно, пользователь будет переведен на страницу пополнения (и снова будет показана форма Stripe).

Для вывода средств с баланса Клиента мы будем использовать следующий метод:

Future<void> withdrawCustomerBalance({
    required int amount,
  }) async {
    try {
      // Request body
      final body = <String, dynamic>{
        // Before this operation, we hae to be sure
        // that the `amount` is less or equal to `stripeCustomer!.balance`
        'balance': ((stripeCustomer!.balance - amount) * 100).toInt().toString(),
      };

      // Make post request to Stripe
      final response = await http.post(
        Uri.parse(
          'https://api.stripe.com/v1/customers/${stripeCustomer?.id}',
        ),
        headers: {
          'Authorization': 'Bearer ${dotenv.env[Keys.stripeSecretKey]}',
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: body,
      );

      if (response.statusCode == 402) {
        withdrawError = json.decode(response.body)['error']['decline_code'].toString();
      }

      // Refresh the Stripe customer data
      await getStripeCustomer(
        email: stripeCustomer!.email,
      );
    } catch (err) {
      log('withdrawCustomerBalance() exception in PaymentStore: $err');
      throw Exception(err.toString());
    }
  }

Другими словами, мы просто вручную меняем баланс Клиента в Stripe.

Сводка

В итоге у нас есть метод пополнения баланса пользователя на платформе Stripe и метод вывода средств за сервис приложения.

Спасибо за внимание, надеюсь, эта статья кому-нибудь поможет. Любые рекомендации будут оценены. Если вы обнаружили, что я сделал что-то неправильно, сообщите мне об этом.

:::информация Также опубликовано здесь.

:::


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE