Руководство по iOS SDK (Checkout)

Требования к подключению

Поддерживаемая версия iOS SDK: 13.0 и выше.

Подключение

Шаг 1. Настройка авторизации

1.1 Получите Client Id

  1. Зарегистрируйте приложение в сервисе Яндекс OAuth.

  2. В разделе Платформы выберите iOS-приложение и укажите параметры вашего сервиса:

    • iOS Appid — точный идентификатор iOS-приложения, например A1B2C3D4E5.com.domain.application. Состоит из Prefix и Bundle ID. Подробнее про идентификаторы iOS-приложений читайте в документации Apple.
    • iOS AppStore URL — ссылка на приложение в AppStore.
  3. Убедитесь, что на Яндекс.OAuth у вашего приложения добавлен доступ к Яндекс Пэй. Для этого перейдите к редактированию приложения и в блоке Какие данные вам нужны? выберите Яндекс ПэйОплата через Яндекс Пэй.

1.2 Настройте Info.plist

Добавьте в файл Info.plist строки:

<key>LSApplicationQueriesSchemes</key>
<array>
    <string>yandexauth</string>
    <string>yandexauth2</string>
    <string>yandexauth4</string>
</array>
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLName</key>
        <string>YandexLoginSDK</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>yx<Вставьте ваш Client Id></string>
        </array>
    </dict>
</array>

1.3 Настройте Entitlements

Сервис авторизации общается с приложениями Яндекса через Universal Links. Для их работы добавьте в Capability: Associated Domains строку:

applinks:yx<Вставьте ваш Client id>.oauth.yandex.ru

Шаг 2. Установка Яндекс Пэй SDK

Текущая версия YandexPaySDK - 1.6.0

Добавьте зависимость в Podfile:

...
pod 'YandexPaySDK/Static'
...

В окне Xcode навигатора проектов (Project Navigator) выберите свой проект (если у вас используется Workspace). Затем в верхнем меню нажмите File и выберите Add Package Dependencies...

Добавьте пакет по ссылке:

https://github.com/yandexmobile/yandex-pay-ios

На текущий момент есть поддержка только динамической библиотеки, которая включает все необходимые зависимости. Распространение исходным кодом недоступно из-за ограничений, связанных с PCI DSS.

Добавьте package зависимость в Package.swift:

let package = Package(
    ...
    dependencies: [
        .package(
            name: "YandexPaySDK",
            url: " https://github.com/yandexmobile/yandex-pay-ios"
        ),
    ],
    ...
)

На текущий момент есть поддержка только динамической библиотеки, которая включает все необходимые зависимости. Распространение исходным кодом недоступно из-за ограничений, связанных с PCI DSS.

Шаг 3. Инициализация Яндекс Пэй SDK

Инициализируйте SDK в AppDelegate.swift вашего проекта в методе application(_:didFinishLaunchingWithOptions:):

import YandexPaySDK

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
      do {
          // Укажите конфигурацию
          let merchant = YandexPaySDKMerchant(
              // ID продавца в системе Яндекс Пэй
              id: "MERCHANT_ID",
              // Имя продавца
              name: "MERCHANT_NAME",
              // URL продавца
              url: "https://example.org/"
          )
          let configuration = YandexPaySDKConfiguration(
              // Необходимое окружение
              environment: .sandbox,
              // Информация о мерчанте
              merchant: merchant,
              // Локализация
              locale: .ru
          )
          // Инициализируйте SDK
          try YandexPaySDKApi.initialize(configuration: configuration)
      } catch {
          // Отреагируйте на ошибку должным образом
          assertionFailure("Unable to initialize YandexPaySDKApi.")
      }

      // Инициализируйте UIWindow и ViewController
      let controller = PaymentURLViewController()
      let window = UIWindow(frame: UIScreen.main.bounds)
      window.rootViewController = controller
      window.makeKeyAndVisible()
      self.window = window
      return true
  }

Также в AppDelegate.swift вашего проекта добавьте нотификацию YandexPaySDK о событиях жизненного цикла приложения:

import YandexPaySDK

  func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
      // Проверьте, что SDK проинициализирован
      guard YandexPaySDKApi.isInitialized else {
        assertionFailure("YandexPaySDK is not initialized.")
        return false
      }

      return YandexPaySDKApi.instance.applicationDidReceiveUserActivity(userActivity)
  }

  func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
      // Проверьте, что SDK проинициализирован
      guard YandexPaySDKApi.isInitialized else {
        assertionFailure("YandexPaySDK is not initialized.")
        return false
      }

      return YandexPaySDKApi.instance.applicationDidReceiveOpen(url, options: options)
  }

Примеры использования

Для ознакомления доступен демо-проект.

Добавление кнопки чекаута Яндекс Пэй на экран в Swift проекте

Во ViewController экрана, на котором необходимо разместить кнопку, создайте ее с помощью метода createCheckoutButton(configuration:delegate:) класса YandexPaySDKApi:

import YandexPaySDK

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        // Проверьте, что SDK проинициализирован
        guard YandexPaySDKApi.isInitialized else {
          assertionFailure("YandexPaySDK is not initialized.")
          return
        }
        
        // Укажите тему для кнопки
        // Параметр `dynamic` позволяет указать, нужно ли кнопке
        // менять свою цветовую палитру вместе со сменой системной темы
        let theme: = YandexPayButtonTheme(appearance: .dark, dynamic: true)

        // Инициализируйте конфигурацию
        let configuration = YandexPayButtonConfiguration(theme: theme)

        // Создайте кнопку
        let button = YandexPaySDKApi.instance.createCheckoutButton(configuration: configuration, delegate: self)

        // Укажите скругления для кнопки (по умолчанию - 8px)
        button.layer.cornerRadius = .zero

        // Добавьте кнопку в иерархию
        view.addSubview(button)

        // Установите layout для кнопки
        button.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            button.widthAnchor.constraint(equalToConstant: 250)
        ])
    }
}

Кнопка для взаимодействия с клиентским приложением использует делегат YandexPayCheckoutButtonDelegate. Реализуйте данный делегат, чтобы передать его кнопке при ее создании:

import YandexPaySDK

extension ViewController: YandexPayCheckoutButtonDelegate {

    // Обработайте результат чекаута
    func yandexPayCheckoutButton(_ button: YandexPayButton, didCompletePaymentWithResult result: YPCheckoutResult) {
        switch result {
        case .succeeded(let checkoutInfo):
            // чекаут был совершен успешно
        case .failed(let checkoutError):
            // В процессе чекаута произошла ошибка
        case .cancelled:
            // Пользователь закрыл/смахнул форму YandexPay
        }
    }

    // Предоставьте UIViewController, с которого необходимо показать форму YandexPay по нажатию на кнопку
    func yandexPayCheckoutButtonDidRequestViewControllerForPresentation(_ button: YandexPayButton) -> UIViewController? {
        return self
    }

    // Предоставьте информацию о заказе
    func yandexPayCheckoutButtonDidRequestPaymentSheet(_ button: YandexPayButton) -> YPCheckoutPaymentSheet? {
        return YPCheckoutPaymentSheet(
            // Код валюты
            currencyCode: .rub,
            // Информация о корзине покупателя
            cart: YPPaymentCart(items: [
                YPProduct(id: "1", total: "434.39", quantity: YPQuantity(count: "1")),
                YPProduct(id: "2", total: "1474.86", quantity: YPQuantity(count: "2")),
            ]),
            orderId: nil,
            metadata: nil
        )
    }
}

Добавление кнопки чекаута YandexPay на экран в Objective-C проекте

Использовать Yandex Pay SDK из Objective-C можно только при инициализации YandexPaySDKApi объекта и сообщении о жизненном цикле приложения SDK. Для создания кнопки, YPCheckoutPaymentSheet объекта и реагирования на события, которые приходят от кнопки, реализуйте прослойку на Swift, доступную для вызова из Objective-C.

Чтобы сделать такую прослойку:

  1. Сделайте ее доступной из Objective-C, унаследовав данный класс от NSObject или предка NSObject.
  2. Пометьте атрибутом @objc свойства и методы данной прослойки, которые должны быть доступны из Objective-C.
  3. Сделайте доступными в Swift классы из Objective-C, добавив их header-файлы в bridging header вашего проекта.
  4. Установите коммуникацию между контейнером и, например, UIViewController объектом, написанным на Objective-C, с помощью объекта-делегата или любым другим способом.

Пример объекта-прослойки:

import YandexPaySDK

// В этом примере мы используем объект-делегат, чтобы сообщать о событиях
// из контейнера в UIViewController, который написан на Objective-C.
// Чтобы данный протокол был доступен в Objective-C, помечаем его @objc
// атрибутом.
@objc
protocol YandexPayCheckoutButtonContainerDelegate: AnyObject {
    // Данный метод будем использовать для сообщения об успешном 
    // получении токена чекаута.
    func yandexPayCheckoutButtonDidSucceedWithOrderId(_ orderId: String)

    // Данный метод будем использовать для сообщения об ошибке,
    // полученной в процессе.
    func yandexPayCheckoutButtonDidFail()

    // Данный метод будем использовать для сообщения об отмене.
    func yandexPayCheckoutButtonDidCancel()

    // Данный метод будем использовать для получения UIViewController
    // объекта, из которого необходимо показать форму Яндекс Пэй.
    func yandexPayCheckoutButtonDidRequestViewController() -> UIViewController?

    // Для примера будем использовать отдельный метод для получения
    // информации о корзине покупателя. Модель CardProduct написана
    // на Objective-C и используется здесь, так как ее header-файл 
    // добавлен в bridging header файл проекта.
    func yandexPayCheckoutButtonDidRequestCardProducts() -> [CardProduct]
}

// Объект-прослойка, который будет содержать в себе кнопку Яндекс Пэй и
// взаимодействовать с ней, а также взаимодействовать с Objective-C кодом.
final class YandexPayCheckoutButtonContainer: UIView {

    // Свойства, которые должны быть доступны из Objective-C помечаем
    // атрибутом @objc.
    @objc
    weak var delegate: YandexPayCheckoutButtonContainerDelegate?

    private var payButton: YandexPayButton!

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupView()
    }

    // Данный контейнер содержит только кнопку, поэтому переопределим
    // intrinsicContentSize. Если же у вас более сложный контейнер,
    // то его разметка может быть любой на ваше усмотрение.
    override var intrinsicContentSize: CGSize {
        return payButton.intrinsicContentSize
    }

    private func setupView() {
        // Проверяем, проинициализирован ли SDK.
        guard YandexPaySDKApi.isInitialized else {
            assertionFailure("YandexPaySDKApi is not initialized.")
            return
        }

        // Указываем тему для кнопки.
        let payButtonTheme = YandexPayButtonTheme(appearance: .dark, dynamic: true)

        // Указываем конфигурацию кнопки.
        let payButtonConfiguration = YandexPayButtonConfiguration(theme: payButtonTheme)

        // Создаем кнопку с помощью YandexPaySDKApi.
        payButton = YandexPaySDKApi.instance.createCheckoutButton(configuration: payButtonConfiguration, delegate: self)

        // Размещаем кнопку в иерархии.
        payButton.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        payButton.frame = bounds
        addSubview(payButton)
    }
}

// Реализуем делегат кнопки, чтобы получать от нее события.
extension YandexPayCheckoutButtonContainer: YandexPayCheckoutButtonDelegate {
    func yandexPayCheckoutButton(_ button: YandexPayButton, didCompletePaymentWithResult result: YPCheckoutResult) {
        // Получаем события об успешном получении токена или об ошибке.
        switch result {
        case .succeeded(let checkoutInfo):
            // Сообщаем через делегат Objective-C коду о получении.
            // платежного токена
            delegate?.yandexPayCheckoutButtonDidSucceedWithOrderId(checkoutInfo.orderId)
        case .failed:
            // Сообщаем через делегат Objective-C коду об ошибке.
            delegate?.yandexPayCheckoutButtonDidFail()
        case .cancelled:
            // Сообщаем через делегат Objective-C коду об отмене.
            delegate?.yandexPayCheckoutButtonDidCancel()
        @unknown default:
            break
        }
    }

    func yandexPayCheckoutButtonDidRequestViewControllerForPresentation(_ button: YandexPayButton) -> UIViewController? {
        // Запрашиваем через делегат у Objective-C кода UIViewController
        // объект, в котором необходимо показывать форму Яндекс Пэй.
        return delegate?.yandexPayCheckoutButtonDidRequestViewController()
    }

    func yandexPayCheckoutButtonDidRequestPaymentSheet(_ button: YandexPayButton) -> YPCheckoutPaymentSheet? {
        guard let delegate = delegate else {
            assertionFailure("Delegate is missing.")
            return nil
        }

        // Здесь конструируем YPCheckoutPaymentSheet объект и возвращаем его
        // кнопке. Для создания YPCheckoutPaymentSheet объекта, очевидно,
        // нужно использовать некоторую бизнес-логику, которая написана
        // на Objective-C. Для того, чтобы с данной бизнес-логикой
        // взаимодействовать, вы можете использовать тот же объект
        // делегата или же можете использовать другие Objective-C
        // классы (например, какие-нибудь сервисы или интеракторы), 
        // которые данную логику инкапсулируют. Для этого
        // сделайте данные классы доступными в Swift, добавив их
        // header-файлы в bridging header файл вашего проекта.
        // Стоит отметить, что бизнес-логика, реализованная в 
        // Objective-C будет использовать модели, реализованные на
        // Objective-C. Поэтому здесь вы можете реализовать их маппинг
        // в модели YandexPaySDK.

        // В текущем примере мы используем объект делегата.
        let products = delegate.yandexPayCheckoutButtonDidRequestCardProducts()

        // Конструируем YPCheckoutPaymentSheet
        return YPCheckoutPaymentSheet(
            currencyCode: .rub,
            cart: YPPaymentCart(items: products.map { 
                YPProduct(id: $0.productID, total: $0.total, quantity: YPQuantity(count: $0.quantity)) 
            }),
            orderId: nil,
            metadata: nil
        )
    }
}

Пример UIViewController объекта, взаимодействующего с прослойкой:

#import <YandexPaySDK/YandexPaySDK-Swift.h>

// Добавляем соответствие протоколу.
@interface ViewController (ViewControllerYandexPayButtonContainer) <YandexPayCheckoutButtonContainerDelegate>

@end

// Объявляем приватные методы.
@interface ViewController ()

- (void)showAlertWithMessage:(NSString *)message;

@end

// Реализация ViewController класса.
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor systemBackgroundColor];

    // Используем объект-прослойку для показа кнопки Яндекс Пэй.
    YandexPayCheckoutButtonContainer *container = [[YandexPayCheckoutButtonContainer alloc] init];

    // Указываем объект делегата для прослойки.
    container.delegate = self;

    // Добавляем объект-прослойку в иерархию.
    container.translatesAutoresizingMaskIntoConstraints = false;
    [self.view addSubview:container];
    [[container.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor] setActive:TRUE];
    [[container.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor] setActive:TRUE];
}

- (void)showAlertWithMessage:(NSString *)message {
    UIAlertController *controller = [UIAlertController alertControllerWithTitle:@"Message" message:message preferredStyle:UIAlertControllerStyleAlert];
    [controller addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
    [self presentViewController:controller animated:TRUE completion:nil];
}

- (void)yandexPayCheckoutButtonDidSucceedWithOrderId:(NSString *)orderId {
    // Получили токен и можем провести чекаут с помощью 
    // этого токена.
    [self showAlertWithMessage:[NSString stringWithFormat:@"Checkout complete: %@", orderId]];

    // Обращение к платежному шлюзу
    // ...
}

- (void)yandexPayCheckoutButtonDidFail {
    // Получили событие об ошибке.
    [self showAlertWithMessage:@"Checkout failed"];
}

- (void)yandexPayCheckoutButtonDidCancel {
    // Получили событие об отмене.
    [self showAlertWithMessage:@"Checkout was cancelled by the user"];
}

- (UIViewController *)yandexPayCheckoutButtonDidRequestViewController {
    // Возвращаем текущий UIViewController для показа формы.
    return self;
}

- (NSArray<CardProduct *> *)yandexPayCheckoutButtonDidRequestCardProducts {
    // Если необходимо, передавайте информацию о заказе и другую
    // информацию через делегат или любым другим удобным способом.
    NSMutableArray *products = [[NSMutableArray alloc] init];
    [items addObject:[[CardProduct alloc] initWithProductID:@"ID-1" total:@"1000.00" quantity: @"1"]];
    [items addObject:[[CardProduct alloc] initWithProductID:@"ID-2" total:@"500.00" quantity: @"2"]];
    return products;
}

@end

Структура CardProduct может быть любой вашей моделью, реализованной на Objective-C, а здесь она используется в качестве иллюстрации.

Файл CardProduct.h:

@interface Product : NSObject

@property (readonly, copy, nonatomic) NSString *productID;
@property (readonly, copy, nonatomic) NSString *total;
@property (readonly, copy, nonatomic) NSString *quantity;

- (instancetype)initWithProductID:(NSString *)productID
                            total:(NSString *)total
                         quantity:(NSString *)quantity;

@end

Вы можете использовать асинхронный делегат, если не можете моментально предоставить YPCheckoutPaymentSheet:

  1. Создайте кнопку, используя специализированный метод класса YandexPaySDKApi:

    import YandexPaySDK
    
    payButtonAsync = YandexPaySDKApi.instance.createCheckoutButton(configuration: configuration, asyncDelegate: self)
    
  2. Реализуйте делегат YandexPayButtonAsyncDelegate:

    import YandexPaySDK
    
    extension ViewController: YandexPayButtonAsyncDelegate
      func yandexPayCheckoutButton(_ button: YandexPayButton, didCompletePaymentWithResult result: YPCheckoutResult) {
         // ...
      }
    
      func yandexPayCheckoutButtonDidRequestViewControllerForPresentation(_ button: YandexPayButton) -> UIViewController? {
        // ...
      }
    
      func yandexPayCheckoutButtonDidRequestPaymentSheet(_ button: YandexPayButton, completion: @escaping (YPCheckoutPaymentSheet?) -> Void) {
        // Покажите лоадер на кнопке, используя предоставленный метод, или же покажите свой лоадер, если необходимо
        payButtonAsync.setLoaderVisible(true, animated: true)
    
        // Сделайте асинхронный вызов
        DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) { [weak self] in
          // Получите YPCheckoutPaymentSheet
          let paymentSheet = YPCheckoutPaymentSheet(...)
    
          DispatchQueue.main.async {
            // Скройте лоадер
            self?.payButtonAsync.setLoaderVisible(false, animated: true)
    
            // Вызовите completion-блок
            completion(paymentSheet)
          }
        }
      }