Интеграция Yandex Pay iOS SDK

Yandex Pay iOS SDK — это библиотека, которая позволяет устанавливать кнопку оплаты Yandex Pay в ваше приложение и начинать принимать платежи от пользователей.

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

Требуемая версия iOS SDK: 12.0.
Yandex Pay SDK использует YandexLoginSDK. Настройте его по инструкции.

Установка Yandex Pay SDK с помощью CocoaPods

Выполните следующие действия:

  1. В Podfile для вашего таргета добавьте зависимость вида pod 'YandexPaySDK/Static' или pod 'YandexPaySDK/Dynamic'.
  2. Если в вашем проекте нет Podfile, то проинициализируйте его в соответствии с инструкциями CocoaPods и повторите предыдущий шаг.
  3. Установите зависимости, выполнив pod install --repo-update.
  4. Откройте сгенерированный .xcworkspace и соберите проект.

Инициализация Yandex Pay SDK

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

import YandexPaySDK

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    do {
        // Укажите конфигурацию
        let merchant = YandexPaySDKMerchant(
            // ID продавца в системе Yandex Pay
            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 = ViewController()
    let window = UIWindow(frame: UIScreen.main.bounds)
    window.rootViewController = controller
    window.makeKeyAndVisible()
    self.window = window

    return true
}
#import <YandexPaySDK/YandexPaySDK-Swift.h>

    // Укажите конфигурацию
    YandexPaySDKMerchant* merchant = [[YandexPaySDKMerchant alloc] initWithMerchantId: @"MERCHANT_ID" // ID мерчанта в системе YandexPay 
                                                                                 name: @"MERCHANT_NAME" // Имя мерчанта
                                                                                  url: @"https://example.org/"]; // URL мерчанта
    // Необходимое окружение
    YandexPaySDKConfiguration* configuration = [[YandexPaySDKConfiguration alloc] initWithEnvironment: YandexPaySDKEnvironmentSandbox
                                                                                             merchant: merchant // Информация о мерчанте
                                                                                               locale: YandexPaySDKLocaleRU]; // Локализация
    // Инициализируйте SDK
    NSError *initializationError;
    [YandexPaySDKApi initializeWithConfiguration: configuration error:&initializationError];

    // Отреагируйте на ошибку должным образом
    if (initializationError != nil) {
        [NSException raise:@"FailedToInitializePay" format:@"Subclasses must implement a valid init method"];
    }

    // Инициализируйте UIWindow и ViewController
    UIViewController *controller = [[ViewController alloc] init];
    UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    [window setRootViewController:controller];
    [window makeKeyAndVisible];
    _window = window;

    return YES;

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

import YandexPaySDK

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    YandexPaySDKApi.instance.applicationDidReceiveUserActivity(userActivity)
    return true
}

func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    YandexPaySDKApi.instance.applicationDidReceiveOpen(url, sourceApplication: options[.sourceApplication] as? String)
    return true
}

func applicationWillEnterForeground(_ application: UIApplication) {
    YandexPaySDKApi.instance.applicationWillEnterForeground()
}

func applicationDidBecomeActive(_ application: UIApplication) {
    YandexPaySDKApi.instance.applicationDidBecomeActive()
}
#import <YandexPaySDK/YandexPaySDK-Swift.h>

- (void)application:(UIApplication *)application didUpdateUserActivity:(NSUserActivity *)userActivity {
    [[YandexPaySDKApi instance] applicationDidReceiveUserActivity:userActivity];
}

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
    [[YandexPaySDKApi instance] applicationDidReceiveOpen:url sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey]];
    return YES;
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
    [[YandexPaySDKApi instance] applicationWillEnterForeground];
}

- (void)applicationDidBecomeActive:(UIApplication *)application {
    [[YandexPaySDKApi instance] applicationDidBecomeActive];
}

Добавление кнопки Yandex Pay на экран в Swift проекте

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

import YandexPaySDK

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

        // Проверьте, что SDK проинициализирован
        guard YandexPaySDKApi.isInitialized else {
          assertionFailure("YandexPaySDK is not initialized.")
          return
        }

        // Укажите тему для кнопки
        let theme: YandexPayButtonTheme
        if #available(iOS 13.0, *) {
            // Параметр `dynamic` позволяет указать, нужно ли кнопке
            // менять свою цветовую палитру вместе со сменой системной темы
            theme = YandexPayButtonTheme(appearance: .dark, dynamic: true)
        } else {
            theme = YandexPayButtonTheme(appearance: .dark)
        }

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

        // Создайте кнопку
        let button = YandexPaySDKApi.instance.createButton(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)
        ])
    }
}

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

import YandexPaySDK

extension ViewController: YandexPayButtonDelegate {
    // Обработайте результат оплаты
    func yandexPayButton(_ button: YandexPayButton, didCompletePaymentWithResult result: YPPaymentResult) {
        switch result {
        case .succeeded(let paymentInfo):
            // Оплата была совершена успешно
        case .failed(let paymentError):
            // В процессе оплаты произошла ошибка
        case .cancelled:
            // Пользователь закрыл/смахнул форму YandexPay
        }
    }

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

    // Предоставьте информацию о продавце и о корзине
    func yandexPayButtonDidRequestPaymentSheet(_ button: YandexPayButton) -> YPPaymentSheet? {
        return YPPaymentSheet(
            // Код страны
            countryCode: .ru,
            // Код валюты
            currencyCode: .rub,
            // Информация о заказе
            order: YPOrder(
                // ID заказа
                id: "ORDER-ID",
                // Стоимость заказа
                amount: "10000.00"
            ),
            // Доступные способы оплаты
            paymentMethods: [
                // Пока что доступна только оплата картой
                .card(
                    YPCardPaymentMethod(
                        // ID поставщика платежных услуг
                        gateway: "test-gateway",
                        // ID продавца в системе поставщика платежных услуг
                        gatewayMerchantId: "test-gateway-merchant-id",
                        // Что будет содержаться в платежном токене: зашифрованные данные банковской карты или токенизированная карта
                        allowedAuthMethods: [
                            .panOnly
                        ],
                        // Список поддерживаемых платежных систем
                        allowedCardNetworks: [
                            .mastercard,
                            .visa,
                            .mir
                        ]
                    )
                )
            ]
        )
    }
}

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

Использовать Yandex Pay SDK из Objective-C можно только при инициализации YandexPaySDKApi объекта и сообщении о жизненном цикле приложения SDK. Для создания кнопки, YPPaymentSheet объекта и реагирования на события, которые приходят от кнопки, реализуйте прослойку на 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 YandexPayButtonContainerDelegate: AnyObject {
    // Данный метод будем использовать для сообщения об успешном
    // получении токена оплаты.
    func yandexPayButtonDidSucceedWithPaymentToken(_ paymentToken: String)

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

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

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

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

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

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

    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.createButton(configuration: payButtonConfiguration, delegate: self)

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

// Реализуем делегат кнопки, чтобы получать от нее события.
extension YandexPayButtonContainer: YandexPayButtonDelegate {
    func yandexPayButton(_ button: YandexPayButton, didCompletePaymentWithResult result: YPPaymentResult) {
        // Получаем события об успешном получении токена или об ошибке.
        switch result {
        case .succeeded(let paymentInfo):
            // Сообщаем через делегат Objective-C коду о получении.
            // платежного токена
            delegate?.yandexPayButtonDidSucceedWithPaymentToken(paymentInfo.paymentToken)
        case .failed:
            // Сообщаем через делегат Objective-C коду об ошибке.
            delegate?.yandexPayButtonDidFail()
        case .cancelled:
            // Сообщаем через делегат Objective-C коду об отмене.
            delegate?.yandexPayButtonDidCancel()
        @unknown default:
            break
        }
    }

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

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

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

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

        // Конструируем YPPaymentSheet
        return YPPaymentSheet(
            countryCode: .ru,
            currencyCode: .rub,
            // Делаем маппинг из модели в Objective-C в модель в Swift.
            order: YPOrder(
                id: order.orderId!,
                label: order.label,
                amount: order.amount!,
                items: order.items?.compactMap({ $0 as? OrderItem }).map({ YPOrderItem(label: $0.label, amount: $0.amount) })
            ),
            paymentMethods: [
                .card(
                    YPCardPaymentMethod(
                        gateway: "yandex-trust",
                        gatewayMerchantId: "MerchantGW1",
                        allowedAuthMethods: [
                            .panOnly
                        ],
                        allowedCardNetworks: [
                            .visa,
                            .mastercard,
                            .mir
                        ]
                    )
                )
            ]
        )
    }
}

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

#import <YandexPaySDK/YandexPaySDK-Swift.h>

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

@end

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

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

@end

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

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor systemBackgroundColor];

    // Используем объект-прослойку для показа кнопки Yandex Pay.
    YandexPayButtonContainer *container = [[YandexPayButtonContainer 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)yandexPayButtonDidSucceedWithPaymentToken:(NSString *)paymentToken {
    // Получили платежный токен и можем провести оплату с помощью
    // этого токена.
    [self showAlertWithMessage:[NSString stringWithFormat:@"Payment complete: %@", paymentToken]];

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

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

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

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

- (Order *)yandexPayButtonDidRequestOrder {
    // Если необходимо, передавайте информацию о заказе и другую
    // информацию через делегат или любым другим удобным способом.
    NSMutableArray *items = [[NSMutableArray alloc] init];
    [items addObject:[[OrderItem alloc] initWithLabel:@"Ботики" amount:@"7000"]];
    [items addObject:[[OrderItem alloc] initWithLabel:@"Ремень" amount:@"3000"]];
    return [[Order alloc] initWithOrderId:@"ORDER1" label:@"ORDER1" amount:@"10000" items:[NSArray arrayWithArray:items]];
}

@end

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

Файл Order.h:

@interface Order : NSObject

@property (readonly, copy, nonatomic) NSString *orderId;
@property (readonly, copy, nonatomic) NSString *label;
@property (readonly, copy, nonatomic) NSString *amount;
@property (readonly, copy, nonatomic) NSArray *items;

- (instancetype)initWithOrderId:(NSString *)orderId
                          label:(NSString *)label
                         amount:(NSString *)amount
                          items:(NSArray *)items;

@end

Файл OrderItem.h:

@interface OrderItem : NSObject

@property (readonly, copy, nonatomic) NSString *label;
@property (readonly, copy, nonatomic) NSString *amount;

- (instancetype)initWithLabel:(NSString *)label
                       amount:(NSString *)amount;

@end

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

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

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

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