diff --git a/CHANGELOG.md b/CHANGELOG.md index b7071ea3..2d4aa445 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ## Changelog +### 6.0.0 + +1. Добавили поддержку `SberPay`. +2. Добавили авторизацию в `ЮMoney` через мобильное приложение. +3. Обновили иконки платежных систем и банков. + +> Необходимо выполнить [инструкцию](https://github.com/yoomoney/yookassa-payments-swift/blob/master/MIGRATION.md) по миграции с версий ниже. + ## 5.4.1 1. Обновление версии `YandexMobileMetrica`. diff --git a/MIGRATION.md b/MIGRATION.md index e9b77c36..aabd378c 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,16 +1,127 @@ # Migration guide - [Migration guide](#migration-guide) + - [5.\*.\* -> 6.\*.\*](#5---6) + - [Изменить код интеграции](#изменить-код-интеграции) + - [Конфигурация проекта](#конфигурация-проекта) + - [Изменить код подтверждения платежа](#изменить-код-подтверждения-платежа) + - [Добавить поддержку SberPay](#добавить-поддержку-SberPay) + - [Добавить поддержку авторизации в ЮMoney через мобильное приложение](#добавить-поддержку-авторизации-в-ЮMoney-через-мобильное-приложение) + - [5.\*.\* -> 5.3.0](#5---530) - [4.\*.\* -> 5.\*.\*](#4---5) - [Изменить Podfile](#изменить-podfile) - - [Изменить код интеграции](#изменить-код-интеграции) + - [Изменить код интеграции](#изменить-код-интеграции-1) - [\*.\*.\* -> 4.\*.\*](#---4) - [Удалить `YandexLoginSDK`](#удалить-yandexloginsdk) - [Добавить новые зависимости](#добавить-новые-зависимости) - - [Если вы используете метод оплаты "Яндекс.Деньги"](#если-вы-используете-метод-оплаты-яндексденьги) + - [Если вы используете метод оплаты "ЮMoney"](#если-вы-используете-метод-оплаты-юmoney) - [2.\*.\* -> 3.\*.\*](#2---3) - [2.1.0 -> 2.2.0](#210---220) +## 5.\*.\* -> 6.\*.\* + +Для корректной работы сценария `Sberpay` и авторизации в `ЮMoney` через мобильное приложение, необходимо изменить некоторые парамтеры. + +### Изменить код интеграции + +1. В `TokenizationModuleInputData` необходимо передавать `applicationScheme` - схема для возврата в приложение после успешной оплаты с помощью `Sberpay` в приложении СберБанк Онлайн или после успешной авторизации в `ЮMoney` через мобильное приложение. + +Пример `applicationScheme`: + +```swift +let moduleData = TokenizationModuleInputData( + ... + applicationScheme: "examplescheme://" +``` + +2. В `AppDelegate` импортировать зависимость `YooKassaPayments`: + + ```swift + import YooKassaPayments + ``` + +3. Добавить обработку ссылок через `YKSdk` в `AppDelegate`: + +```swift +func application( + _ application: UIApplication, + open url: URL, + sourceApplication: String?, + annotation: Any +) -> Bool { + return YKSdk.shared.handleOpen( + url: url, + sourceApplication: sourceApplication + ) +} + +@available(iOS 9.0, *) +func application( + _ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey: Any] = [:] +) -> Bool { + return YKSdk.shared.handleOpen( + url: url, + sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String + ) +} +``` + +4. Реализовать метод `didSuccessfullyConfirmation(paymentMethodType:)` протокола `TokenizationModuleOutput`, который будет вызван после успешного подтверждения платежа. + +### Конфигурация проекта + +В `Info.plist` добавить следующие строки: + +```plistbase +CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + ${BUNDLE_ID} + CFBundleURLSchemes + + examplescheme + + + +``` + +где `examplescheme` - схема для открытия вашего приложения, которую вы указали в `applicationScheme` при создании `TokenizationModuleInputData`. + +### Изменить код подтверждения платежа + +Для подтверждения платежа необходимо вызвать метод `startConfirmationProcess(confirmationUrl:paymentMethodType:)`. + +После успешного прохождения подтверждения будет вызван метод `didSuccessfullyConfirmation(paymentMethodType:)` протокола `TokenizationModuleOutput`. + +> Обратите внимание, что методы `start3dsProcess(requestUrl:)` и `didSuccessfullyPassedCardSec(on module:)` помечены как `deprecated` - используйте `startConfirmationProcess(confirmationUrl:paymentMethodType:)` и `didSuccessfullyConfirmation(paymentMethodType:)` вместо них. + +### Добавить поддержку `SberPay` + +В `Info.plist` добавить следующие строки: + +```plistbase +LSApplicationQueriesSchemes + + sberpay + +``` + +### Добавить поддержку авторизации в `ЮMoney` через мобильное приложение + +В `Info.plist` добавить следующие строки: + +```plistbase +LSApplicationQueriesSchemes + + yoomoneyauth + +``` + ## 5.\*.\* -> 5.3.0 В версии 5.3.0 зависимости `TMXProfiling` и `TMXProfilingConnections` используются в виде `.xcframework`. diff --git a/Podfile b/Podfile index f37a6f8c..bd628df6 100644 --- a/Podfile +++ b/Podfile @@ -20,7 +20,6 @@ post_install do |installer| target.build_configurations.each do |config| config.build_settings['PROVISIONING_PROFILE_SPECIFIER'] = '' config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'NO' - config.build_settings['SWIFT_SUPPRESS_WARNINGS'] = 'YES' config.build_settings['CLANG_WARN_DOCUMENTATION_COMMENTS'] = 'NO' config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '10.0' end diff --git a/Podfile.lock b/Podfile.lock index 443d3332..ec2961ef 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,27 +1,27 @@ PODS: - CardIO (5.4.1) - FunctionalSwift (1.2.0) - - MoneyAuth (2.21.0): + - MoneyAuth (2.29.0): - FunctionalSwift - ThreatMetrixAdapter - YooMoneyCoreApi - Reveal-SDK (28) - SwiftLint (0.43.1) - - ThreatMetrixAdapter (2.0.0) + - ThreatMetrixAdapter (3.2.0) - YandexMobileMetrica/Dynamic (3.16.0): - YandexMobileMetrica/Dynamic/Core (= 3.16.0) - YandexMobileMetrica/Dynamic/Crashes (= 3.16.0) - YandexMobileMetrica/Dynamic/Core (3.16.0) - YandexMobileMetrica/Dynamic/Crashes (3.16.0): - YandexMobileMetrica/Dynamic/Core - - YooKassaPayments (5.4.1): - - MoneyAuth (~> 2.21.0) - - ThreatMetrixAdapter (~> 2.0.0) + - YooKassaPayments (6.0.0): + - MoneyAuth (~> 2.29.0) + - ThreatMetrixAdapter (~> 3.2.0) - YandexMobileMetrica/Dynamic (~> 3.0) - - YooKassaPaymentsApi (~> 2.3.0) + - YooKassaPaymentsApi (~> 2.5.0) - YooKassaWalletApi (~> 2.3.0) - YooMoneyCoreApi (~> 1.9.0) - - YooKassaPaymentsApi (2.3.0): + - YooKassaPaymentsApi (2.5.0): - FunctionalSwift (~> 1.2.0) - YooMoneyCoreApi (~> 1.9.0) - YooKassaWalletApi (2.3.0): @@ -57,16 +57,16 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: CardIO: 56983b39b62f495fc6dae9ad7cf875143df06443 FunctionalSwift: 1b839fcf50b8db067885938df54f818518356dbb - MoneyAuth: 60e6885924b05d05dbe13c91d034db37cd2d918f + MoneyAuth: daefde9745613c5f9cdd178467d3e3fa48d903c0 Reveal-SDK: 1a2a678648fc4d277bad71c86d15530424324288 SwiftLint: 99f82d07b837b942dd563c668de129a03fc3fb52 - ThreatMetrixAdapter: c5ea4cc20dd82931fabeb596c3e86d300c2d14fb + ThreatMetrixAdapter: 8d2e93fb6ec1e93128116fa584dca52d84dd8736 YandexMobileMetrica: 2ab55883ea552a4c4ea4516a3353ca0577a747cc - YooKassaPayments: 5ef043a777957ba1b173af02f0777875de802df3 - YooKassaPaymentsApi: 66302803d0389fbc27238d8ff9447c10ac1e31bf + YooKassaPayments: d780e5dd8e27be125600e4980933c353dd0da73a + YooKassaPaymentsApi: c32f1f256f0d9f7bc69d89348bb480453872337c YooKassaWalletApi: 9ca2fa3c2827721d646ee8f770509e7eeea98743 YooMoneyCoreApi: 0d4da37a1619a4c92df0962ecc48d1665dbae4f4 -PODFILE CHECKSUM: ecceb3f227ebee8f464a40c7b9ed3b7a0bc72774 +PODFILE CHECKSUM: 06cb858f1cc4e9551da88e13962348b7590eea82 COCOAPODS: 1.10.0 diff --git a/README.md b/README.md index c56e4579..14f4b913 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,9 @@ - [ЮMoney](#юmoney) - [Как получить `client id` центра авторизации системы `ЮMoney`](#как-получить-client-id-центра-авторизации-системы-юmoney) - [Передать `client id` в параметре `moneyAuthClientId`](#передать-client-id-в-параметре-moneyauthclientid) + - [Поддержка авторизации через мобильное приложение](#поддержка-авторизации-через-мобильное-приложение) - [Банковская карта](#банковская-карта) - - [Сбербанк Онлайн](#сбербанк-онлайн) + - [SberPay](#sberpay) - [Apple Pay](#apple-pay) - [Описание публичных параметров](#описание-публичных-параметров) - [TokenizationFlow](#tokenizationflow) @@ -43,7 +44,7 @@ - [CustomizationSettings](#customizationsettings) - [SavePaymentMethod](#savepaymentmethod) - [Сканирование банковских карт](#сканирование-банковских-карт) - - [Настройка 3D Secure](#настройка-3d-secure) + - [Настройка подтверждения платежа](#настройка-подтверждения-платежа) - [Логирование](#логирование) - [Тестовый режим](#тестовый-режим) - [Запуск Example](#запуск-example) @@ -121,7 +122,6 @@ end ```swift import YooKassaPayments -import YooKassaPaymentsApi ``` Пример создания `TokenizationModuleInputData`: @@ -161,9 +161,11 @@ present(viewController, animated: true, completion: nil) ```swift extension ViewController: TokenizationModuleOutput { - func tokenizationModule(_ module: TokenizationModuleInput, - didTokenize token: Tokens, - paymentMethodType: PaymentMethodType) { + func tokenizationModule( + _ module: TokenizationModuleInput, + didTokenize token: Tokens, + paymentMethodType: PaymentMethodType + ) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.dismiss(animated: true) @@ -171,18 +173,22 @@ extension ViewController: TokenizationModuleOutput { // Отправьте токен в вашу систему } - func didFinish(on module: TokenizationModuleInput, - with error: YooKassaPaymentsError?) { + func didFinish( + on module: TokenizationModuleInput, + with error: YooKassaPaymentsError? + ) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.dismiss(animated: true) } } - func didSuccessfullyPassedCardSec(on module: TokenizationModuleInput) { + func didSuccessfullyConfirmation( + paymentMethodType: PaymentMethodType + ) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } - // Создать экран успеха после прохождения 3DS + // Создать экран успеха после прохождения подтверждения (3DS или Sberpay) self.dismiss(animated: true) // Показать экран успеха } @@ -198,7 +204,7 @@ extension ViewController: TokenizationModuleOutput { `.yooMoney` — ЮMoney (платежи из кошелька или привязанной картой)\ `.bankCard` — банковская карта (карты можно сканировать)\ -`.sberbank` — Сбербанк Онлайн (с подтверждением по смс)\ +`.sberbank` — SberPay (с подтверждением через приложение Сбербанк Онлайн, если оно установленно, иначе с подтверждением по смс)\ `.applePay` — Apple Pay ## Настройка способов оплаты @@ -276,20 +282,164 @@ let moduleData = TokenizationModuleInputData( 2. Получите токен. 3. [Создайте платеж](https://yookassa.ru/developers/api#create_payment) с токеном по API ЮKassa. +#### Поддержка авторизации через мобильное приложение + +1. В `TokenizationModuleInputData` необходимо передавать `applicationScheme` – схема для возврата в приложение после успешной авторизации в `ЮMoney` через мобильное приложение. + +Пример `applicationScheme`: + +```swift +let moduleData = TokenizationModuleInputData( + ... + applicationScheme: "examplescheme://" +``` + +2. В `AppDelegate` импортировать зависимость `YooKassaPayments`: + + ```swift + import YooKassaPayments + ``` + +3. Добавить обработку ссылок через `YKSdk` в `AppDelegate`: + +```swift +func application( + _ application: UIApplication, + open url: URL, + sourceApplication: String?, + annotation: Any +) -> Bool { + return YKSdk.shared.handleOpen( + url: url, + sourceApplication: sourceApplication + ) +} + +@available(iOS 9.0, *) +func application( + _ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey: Any] = [:] +) -> Bool { + return YKSdk.shared.handleOpen( + url: url, + sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String + ) +} +``` + +4. В `Info.plist` добавьте следующие строки: + +```plistbase +LSApplicationQueriesSchemes + + yoomoneyauth + +CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + ${BUNDLE_ID} + CFBundleURLSchemes + + examplescheme + + + +``` + +где `examplescheme` - схема для открытия вашего приложения, которую вы указали в `applicationScheme` при создании `TokenizationModuleInputData`. Через эту схему будет открываться ваше приложение после успешной авторизации в `ЮMoney` через мобильное приложение. + ### Банковская карта 1. При создании `TokenizationModuleInputData` передайте значение `.bankcard` в `paymentMethodTypes`. 2. Получите токен. 3. [Создайте платеж](https://yookassa.ru/developers/api#create_payment) с токеном по API ЮKassa. -### Сбербанк Онлайн +### SberPay + +С помощью SDK можно провести платеж через «Мобильный банк» Сбербанка — с подтверждением оплаты через приложение Сбербанк Онлайн, если оно установленно, иначе с подтверждением по смс. + +В `TokenizationModuleInputData` необходимо передавать `applicationScheme` – схема для возврата в приложение после успешной оплаты с помощью `SberPay` в приложении СберБанк Онлайн. + +Пример `applicationScheme`: + +```swift +let moduleData = TokenizationModuleInputData( + ... + applicationScheme: "examplescheme://" +``` -С помощью SDK можно провести платеж через «Мобильный банк» Сбербанка — с подтверждением оплаты по смс: +Чтобы провести платёж: 1. При создании `TokenizationModuleInputData` передайте значение `.sberbank` в `paymentMethodTypes`. 2. Получите токен. 3. [Создайте платеж](https://yookassa.ru/developers/api#create_payment) с токеном по API ЮKassa. +Для подтверждения платежа через приложение СберБанк Онлайн: + +1. В `AppDelegate` импортируйте зависимость `YooKassaPayments`: + + ```swift + import YooKassaPayments + ``` + +2. Добавьте обработку ссылок через `YKSdk` в `AppDelegate`: + +```swift +func application( + _ application: UIApplication, + open url: URL, + sourceApplication: String?, + annotation: Any +) -> Bool { + return YKSdk.shared.handleOpen( + url: url, + sourceApplication: sourceApplication + ) +} + +@available(iOS 9.0, *) +func application( + _ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey: Any] = [:] +) -> Bool { + return YKSdk.shared.handleOpen( + url: url, + sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String + ) +} +``` + +3. В `Info.plist` добавьте следующие строки: + +```plistbase +LSApplicationQueriesSchemes + + sberpay + +CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + ${BUNDLE_ID} + CFBundleURLSchemes + + examplescheme + + + +``` + +где `examplescheme` - схема для открытия вашего приложения, которую вы указали в `applicationScheme` при создании `TokenizationModuleInputData`. Через эту схему будет открываться ваше приложение после успешной оплаты с помощью `SberPay`. + +4. Реализуйте метод `didSuccessfullyConfirmation(paymentMethodType:)` протокола `TokenizationModuleOutput`, который будет вызван после успешного подтверждения платежа (см. [Настройка подтверждения платежа](#настройка-подтверждения-платежа)). + ### Apple Pay 1. Чтобы подключить Apple Pay, нужно передать в ЮKassa сертификат, с помощью которого Apple будет шифровать данные банковских карт. @@ -311,16 +461,8 @@ let moduleData = TokenizationModuleInputData( ```swift let moduleData = TokenizationModuleInputData( ... - applePayMerchantIdentifier: "") -``` -Например, если ваш apple pay identifier — `com.example.identifier`, то код будет следующим: - > -```swift -let moduleData = TokenizationModuleInputData( - ... - applePayMerchantIdentifier: "com.example.identifier") + applePayMerchantIdentifier: "com.example.identifier" ``` - 2. Получите токен. 3. [Создайте платеж](https://yookassa.ru/developers/api#create_payment) с токеном по API ЮKassa. @@ -357,19 +499,19 @@ let moduleData = TokenizationModuleInputData( >Необязательные: -| Параметр | Тип | Описание | -| -------------------------- | --------------------- | -------- | +| Параметр | Тип | Описание | +| -------------------------- | --------------------- | ------------------------------------------------------------ | | gatewayId | String | По умолчанию `nil`. Используется, если у вас несколько платежных шлюзов с разными идентификаторами. | | tokenizationSettings | TokenizationSettings | По умолчанию используется стандартный инициализатор со всеми способами оплаты. Параметр отвечает за настройку токенизации (способы оплаты и логотип ЮKassa). | -| testModeSettings | TestModeSettings | По умолчанию `nil`. Настройки тестового режима. | +| testModeSettings | TestModeSettings | По умолчанию `nil`. Настройки тестового режима. | | cardScanning | CardScanning | По умолчанию `nil`. Возможность сканировать банковские карты. | | applePayMerchantIdentifier | String | По умолчанию `nil`. Apple Pay merchant ID (обязательно для платежей через Apple Pay). | -| returnUrl | String | По умолчанию `nil`. URL страницы (поддерживается только `https`), на которую надо вернуться после прохождения 3-D Secure. Необходим только при кастомной реализации 3-D Secure. Если вы используете `start3dsProcess(requestUrl:)`, не задавайте этот параметр. | +| returnUrl | String | По умолчанию `nil`. URL страницы (поддерживается только `https`), на которую надо вернуться после прохождения 3-D Secure. Необходим только при кастомной реализации 3-D Secure. Если вы используете `startConfirmationProcess(confirmationUrl:paymentMethodType:)`, не задавайте этот параметр. | | isLoggingEnabled | Bool | По умолчанию `false`. Включает логирование сетевых запросов. | -| userPhoneNumber | String | По умолчанию `nil`. Телефонный номер пользователя. | +| userPhoneNumber | String | По умолчанию `nil`. Телефонный номер пользователя. | | customizationSettings | CustomizationSettings | По умолчанию используется цвет blueRibbon. Цвет основных элементов, кнопки, переключатели, поля ввода. | -| moneyAuthClientId | String | По умолчанию `nil`. Идентификатор для центра авторизации в системе YooMoney. - +| moneyAuthClientId | String | По умолчанию `nil`. Идентификатор для центра авторизации в системе YooMoney. | +| applicationScheme | String | По умолчанию `nil`. Схема для возврата в приложение после успешной оплаты с помощью `Sberpay` в приложении СберБанк Онлайн или после успешной авторизации в `YooMoney` через мобильное приложение. | ### BankCardRepeatModuleInputData >Обязательные: @@ -381,15 +523,17 @@ let moduleData = TokenizationModuleInputData( | purchaseDescription | String | Описание заказа в форме оплаты | | paymentMethodId | String | Идентификатор сохраненного способа оплаты | | amount | Amount | Объект, содержащий сумму заказа и валюту | +| savePaymentMethod | SavePaymentMethod | Объект, описывающий логику того, будет ли платеж рекуррентным | >Необязательные: -| Параметр | Тип | Описание | -| -------------------------- | --------------------- | -------- | -| testModeSettings | TestModeSettings | По умолчанию `nil`. Настройки тестового режима. | -| returnUrl | String | По умолчанию `nil`. URL страницы (поддерживается только `https`), на которую надо вернуться после прохождения 3-D Secure. Необходим только при кастомной реализации 3-D Secure. Если вы используете `start3dsProcess(requestUrl:)`, не задавайте этот параметр. | -| isLoggingEnabled | Bool | По умолчанию `false`. Включает логирование сетевых запросов. | -| customizationSettings | CustomizationSettings | По умолчанию используется цвет blueRibbon. Цвет основных элементов, кнопки, переключатели, поля ввода. | +| Параметр | Тип | Описание | +| --------------------- | --------------------- | ------------------------------------------------------------ | +| testModeSettings | TestModeSettings | По умолчанию `nil`. Настройки тестового режима. | +| returnUrl | String | По умолчанию `nil`. URL страницы (поддерживается только `https`), на которую надо вернуться после прохождения 3-D Secure. Необходим только при кастомной реализации 3-D Secure. Если вы используете `startConfirmationProcess(confirmationUrl:paymentMethodType:)`, не задавайте этот параметр. | +| isLoggingEnabled | Bool | По умолчанию `false`. Включает логирование сетевых запросов. | +| customizationSettings | CustomizationSettings | По умолчанию используется цвет blueRibbon. Цвет основных элементов, кнопки, переключатели, поля ввода. | +| gatewayId | String | По умолчанию `nil`. Используется, если у вас несколько платежных шлюзов с разными идентификаторами. | ### TokenizationSettings @@ -486,13 +630,13 @@ let inputData = TokenizationModuleInputData( cardScanning: CardScannerProvider()) ``` -## Настройка 3D Secure +## Настройка подтверждения платежа -Если вы хотите использовать нашу реализацию 3-D Secure, не закрывайте модуль SDK после получения токена.\ +Если вы хотите использовать нашу реализацию подтверждения платежа, не закрывайте модуль SDK после получения токена.\ Отправьте токен на ваш сервер и после успешной оплаты закройте модуль.\ -Если ваш сервер сообщил о необходимости подтверждения платежа (т.е. платёж пришёл со статусом `pending`), вызовите метод `start3dsProcess(requestUrl:)` +Если ваш сервер сообщил о необходимости подтверждения платежа (т.е. платёж пришёл со статусом `pending`), вызовите метод `startConfirmationProcess(confirmationUrl:paymentMethodType:)`. -После успешного прохождения 3-D Secure будет вызван метод `didSuccessfullyPassedCardSec(on module:)` протокола `TokenizationModuleOutput`. +После успешного прохождения подтверждения будет вызван метод `didSuccessfullyConfirmation(paymentMethodType:)` протокола `TokenizationModuleOutput`. Примеры кода: @@ -514,18 +658,19 @@ func tokenizationModule(_ module: TokenizationModuleInput, } ``` -3. Покажите 3-D Secure, если необходимо подтвердить платеж. +3. Вызовите подтверждение платежа, если это необходимо. ```swift -func needsConfirmPayment(requestUrl: String) { - self.tokenizationViewController.start3dsProcess(requestUrl: requestUrl) -} +self.tokenizationViewController.startConfirmationProcess( + confirmationUrl: confirmationUrl, + paymentMethodType: paymentMethodType +) ``` -4. После успешного прохождения 3-D Secure будет вызван метод. +4. После успешного подтверждения платежа будет вызван метод. ```swift -func didSuccessfullyPassedCardSec(on module: TokenizationModuleInput) { +func didSuccessfullyConfirmation(paymentMethodType: PaymentMethodType) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } diff --git a/YooKassaPayments.podspec b/YooKassaPayments.podspec index 08f11b18..dcc2afcc 100644 --- a/YooKassaPayments.podspec +++ b/YooKassaPayments.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'YooKassaPayments' - s.version = '5.4.1' + s.version = '6.0.0' s.homepage = 'https://github.com/yoomoney/yookassa-payments-swift' s.license = { :type => "MIT", @@ -30,11 +30,12 @@ Pod::Spec.new do |s| s.ios.library = 'z' s.ios.dependency 'YooMoneyCoreApi', '~> 1.9.0' - s.ios.dependency 'YooKassaPaymentsApi', '~> 2.3.0' + s.ios.dependency 'YooKassaPaymentsApi', '~> 2.5.0' s.ios.dependency 'YooKassaWalletApi', '~> 2.3.0' - s.ios.dependency 'MoneyAuth', '~> 2.21.0' - s.ios.dependency 'ThreatMetrixAdapter', '~> 2.0.0' - + s.ios.dependency 'MoneyAuth', '~> 2.29.0' + s.ios.dependency 'ThreatMetrixAdapter', '~> 3.2.0' + s.ios.dependency 'YandexMobileMetrica/Dynamic', '~> 3.0' + end diff --git a/YooKassaPayments.xcodeproj/project.pbxproj b/YooKassaPayments.xcodeproj/project.pbxproj index 6a8d2590..88217f35 100644 --- a/YooKassaPayments.xcodeproj/project.pbxproj +++ b/YooKassaPayments.xcodeproj/project.pbxproj @@ -130,7 +130,6 @@ 402EB49D79C96F3D7EC4D00E /* PlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB05B21E70E9A2E81515C /* PlaceholderView.swift */; }; 402EB4A38D9458D726EF2973 /* AnalyticsProviderAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBA44464C238E00928819 /* AnalyticsProviderAssembly.swift */; }; 402EB4AA589D5590C3FCD2AE /* InternalStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB1EEE3DFE557080259B2 /* InternalStyle.swift */; }; - 402EB4D914B8C94E739CAA6C /* Yalta.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB9AC3E2FC9F00509D779 /* Yalta.swift */; }; 402EB4DB7D89DA8CBED9BBBB /* AuthTypeStatesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBFF57151C710CF91D0E6 /* AuthTypeStatesService.swift */; }; 402EB4DC5AAC097E3992B6DE /* SuccessViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB5FD23E637F2CB6C2574 /* SuccessViewController.swift */; }; 402EB4DECF9836134B8DFF2C /* SberbankViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBD38199403D8E554BCA3 /* SberbankViewController.swift */; }; @@ -301,6 +300,7 @@ 402EBC2B75C6DA509D902F48 /* TextControl+ClearMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB17BB3A1CB6CEF3A6554 /* TextControl+ClearMode.swift */; }; 402EBC36449C85722080B20C /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB6F94C13E4F66AD84654 /* ActivityIndicator.swift */; }; 402EBC3A36D0ABDDD0D0C4B4 /* PaymentMethodsAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB4CB069329DCBDCE0E6D /* PaymentMethodsAssembly.swift */; }; + 402EBC3C07CACF8D2020DBC5 /* PriceViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB489BC427881505E2583 /* PriceViewModelFactory.swift */; }; 402EBC41089B09F83AC6D4CA /* ApplePayContractModuleIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB62C69A473FAE82CF019 /* ApplePayContractModuleIO.swift */; }; 402EBC42507159E3CD4859EC /* BankCardDataInputRouterIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB87319D65F5FFA7259D9 /* BankCardDataInputRouterIO.swift */; }; 402EBC44F5138C37BC2B7990 /* WebBrowserInteractorIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBAB6F92FA336D7B10073 /* WebBrowserInteractorIO.swift */; }; @@ -410,6 +410,13 @@ 781CDD7B25D4064100C34912 /* ApplePayContractViewIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781CDD7A25D4064100C34912 /* ApplePayContractViewIO.swift */; }; 781CDD7D25D408F600C34912 /* ApplePayContractInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781CDD7C25D408F600C34912 /* ApplePayContractInteractor.swift */; }; 781CDD7F25D42A1E00C34912 /* ApplePayContractRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781CDD7E25D42A1E00C34912 /* ApplePayContractRouter.swift */; }; + 781FB66B260229CD00567AFA /* BankCardRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781FB66A260229CD00567AFA /* BankCardRegex.swift */; }; + 781FB66E260235AD00567AFA /* BankCardImageFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781FB66D260235AD00567AFA /* BankCardImageFactory.swift */; }; + 781FB671260235EF00567AFA /* BankCardImageFactoryAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781FB670260235EF00567AFA /* BankCardImageFactoryAssembly.swift */; }; + 781FB673260235F700567AFA /* BankCardImageFactoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781FB672260235F700567AFA /* BankCardImageFactoryImpl.swift */; }; + 782E75E1260CCF7500CF2BFD /* YKSdkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 782E75E0260CCF7500CF2BFD /* YKSdkService.swift */; }; + 783CCEFB25FA347C005A298A /* ProcessConfirmation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 783CCEFA25FA347C005A298A /* ProcessConfirmation.swift */; }; + 783CCEFD25FB651C005A298A /* UIScreen+Short.swift in Sources */ = {isa = PBXBuildFile; fileRef = 783CCEFC25FB651C005A298A /* UIScreen+Short.swift */; }; 784A1A8B25B5A44600637CB5 /* ApplePayService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 784A1A8A25B5A44600637CB5 /* ApplePayService.swift */; }; 784A1A8D25B5A45000637CB5 /* ApplePayServiceImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 784A1A8C25B5A45000637CB5 /* ApplePayServiceImpl.swift */; }; 784A1A8F25B5A5A300637CB5 /* ApplePayConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 784A1A8E25B5A5A300637CB5 /* ApplePayConstants.swift */; }; @@ -467,6 +474,8 @@ 7860294D25D1723B00B9E961 /* LinkedCardRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7860294C25D1723B00B9E961 /* LinkedCardRouter.swift */; }; 7860294F25D1726800B9E961 /* LinkedCardRouterIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7860294E25D1726800B9E961 /* LinkedCardRouterIO.swift */; }; 7860295225D194D300B9E961 /* LinkedCardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7860295125D194D300B9E961 /* LinkedCardViewModel.swift */; }; + 7890B24D25FA14BD0054D6B6 /* ProcessViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7890B24B25FA118A0054D6B6 /* ProcessViewController.swift */; }; + 7890B24F25FA196D0054D6B6 /* TextFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7890B24E25FA196D0054D6B6 /* TextFieldView.swift */; }; 78997A2425D562160093CAE2 /* LargeIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78997A2325D562160093CAE2 /* LargeIconView.swift */; }; 789C373D25AF260400BA94D1 /* PaymentMethodHandlerServiceImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 789C373C25AF260400BA94D1 /* PaymentMethodHandlerServiceImpl.swift */; }; 789C373F25AF260F00BA94D1 /* PaymentMethodHandlerServiceAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 789C373E25AF260F00BA94D1 /* PaymentMethodHandlerServiceAssembly.swift */; }; @@ -474,6 +483,18 @@ 789C376F25B06EB600BA94D1 /* AuthorizationProcessingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 789C376E25B06EB600BA94D1 /* AuthorizationProcessingError.swift */; }; 78B6B20E25C1647F00645D8A /* PaymentMethodViewModelFactoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B6B20D25C1647F00645D8A /* PaymentMethodViewModelFactoryImpl.swift */; }; 78B6B21025C1648500645D8A /* PaymentMethodViewModelFactoryAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B6B20F25C1648500645D8A /* PaymentMethodViewModelFactoryAssembly.swift */; }; + 78C1BD1925EFA1010058080F /* SberpayInteractorIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C1BD1825EFA1010058080F /* SberpayInteractorIO.swift */; }; + 78C1BD1B25EFA1090058080F /* SberpayModuleIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C1BD1A25EFA1090058080F /* SberpayModuleIO.swift */; }; + 78C1BD1D25EFA1100058080F /* SberpayRouterIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C1BD1C25EFA1100058080F /* SberpayRouterIO.swift */; }; + 78C1BD1F25EFA11B0058080F /* SberpayViewIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C1BD1E25EFA11B0058080F /* SberpayViewIO.swift */; }; + 78C1BD2125EFA1280058080F /* SberpayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C1BD2025EFA1280058080F /* SberpayViewController.swift */; }; + 78C1BD2325EFA1330058080F /* SberpayRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C1BD2225EFA1330058080F /* SberpayRouter.swift */; }; + 78C1BD2525EFA13C0058080F /* SberpayPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C1BD2425EFA13C0058080F /* SberpayPresenter.swift */; }; + 78C1BD2725EFA1430058080F /* SberpayInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C1BD2625EFA1430058080F /* SberpayInteractor.swift */; }; + 78C1BD2925EFA14C0058080F /* SberpayAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C1BD2825EFA14C0058080F /* SberpayAssembly.swift */; }; + 78C1BD2C25EFA2470058080F /* SberpayViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C1BD2B25EFA2470058080F /* SberpayViewModel.swift */; }; + 78C1BDF525F230550058080F /* DeeplinkFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C1BDF425F230550058080F /* DeeplinkFactory.swift */; }; + 78C1BDF725F2305F0058080F /* Deeplink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C1BDF625F2305F0058080F /* Deeplink.swift */; }; 78CF11DD25D4361300F7154E /* ApplePayContractViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78CF11DC25D4361300F7154E /* ApplePayContractViewController.swift */; }; 78CF11E025D438AC00F7154E /* ApplePayContractViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78CF11DF25D438AC00F7154E /* ApplePayContractViewModel.swift */; }; E06532A71FF13B9B00831588 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0148CD51FE958810087CF5F /* AppDelegate.swift */; }; @@ -611,6 +632,7 @@ 402EB47F1567A31864612F7C /* TokenizationModuleIO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenizationModuleIO.swift; sourceTree = ""; }; 402EB47F288FC29560EE036E /* KeyboardObservable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardObservable.swift; sourceTree = ""; }; 402EB487A00F4E5CBA7498D9 /* PaymentMethodTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentMethodTypes.swift; sourceTree = ""; }; + 402EB489BC427881505E2583 /* PriceViewModelFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PriceViewModelFactory.swift; sourceTree = ""; }; 402EB4CB069329DCBDCE0E6D /* PaymentMethodsAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentMethodsAssembly.swift; sourceTree = ""; }; 402EB4D0C2AEFC68A22F5BA3 /* UILabel+Stylable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UILabel+Stylable.swift"; sourceTree = ""; }; 402EB4F728CFBA828884E459 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.info; path = Info.plist; sourceTree = ""; }; @@ -693,7 +715,6 @@ 402EB98341C0EAD3DE8902C2 /* TableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = ""; }; 402EB9A46DDCE237E4AB1B00 /* SberbankInteractorIO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SberbankInteractorIO.swift; sourceTree = ""; }; 402EB9A63FFF964EED27D4DE /* InputCvcView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputCvcView.swift; sourceTree = ""; }; - 402EB9AC3E2FC9F00509D779 /* Yalta.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Yalta.swift; sourceTree = ""; }; 402EB9AEA4AE184596686453 /* UITableViewCell+Identifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+Identifier.swift"; sourceTree = ""; }; 402EB9CB53DAB7D15EB12891 /* AuthorizationService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationService.swift; sourceTree = ""; }; 402EB9D4435E4C37E367DF6A /* RootViewController+CardScanning+Pods.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "RootViewController+CardScanning+Pods.swift"; sourceTree = ""; }; @@ -852,6 +873,13 @@ 781CDD7A25D4064100C34912 /* ApplePayContractViewIO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplePayContractViewIO.swift; sourceTree = ""; }; 781CDD7C25D408F600C34912 /* ApplePayContractInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplePayContractInteractor.swift; sourceTree = ""; }; 781CDD7E25D42A1E00C34912 /* ApplePayContractRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplePayContractRouter.swift; sourceTree = ""; }; + 781FB66A260229CD00567AFA /* BankCardRegex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BankCardRegex.swift; sourceTree = ""; }; + 781FB66D260235AD00567AFA /* BankCardImageFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BankCardImageFactory.swift; sourceTree = ""; }; + 781FB670260235EF00567AFA /* BankCardImageFactoryAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BankCardImageFactoryAssembly.swift; sourceTree = ""; }; + 781FB672260235F700567AFA /* BankCardImageFactoryImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BankCardImageFactoryImpl.swift; sourceTree = ""; }; + 782E75E0260CCF7500CF2BFD /* YKSdkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YKSdkService.swift; sourceTree = ""; }; + 783CCEFA25FA347C005A298A /* ProcessConfirmation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessConfirmation.swift; sourceTree = ""; }; + 783CCEFC25FB651C005A298A /* UIScreen+Short.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScreen+Short.swift"; sourceTree = ""; }; 784A1A8A25B5A44600637CB5 /* ApplePayService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplePayService.swift; sourceTree = ""; }; 784A1A8C25B5A45000637CB5 /* ApplePayServiceImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplePayServiceImpl.swift; sourceTree = ""; }; 784A1A8E25B5A5A300637CB5 /* ApplePayConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplePayConstants.swift; sourceTree = ""; }; @@ -909,6 +937,8 @@ 7860294C25D1723B00B9E961 /* LinkedCardRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkedCardRouter.swift; sourceTree = ""; }; 7860294E25D1726800B9E961 /* LinkedCardRouterIO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkedCardRouterIO.swift; sourceTree = ""; }; 7860295125D194D300B9E961 /* LinkedCardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkedCardViewModel.swift; sourceTree = ""; }; + 7890B24B25FA118A0054D6B6 /* ProcessViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessViewController.swift; sourceTree = ""; }; + 7890B24E25FA196D0054D6B6 /* TextFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldView.swift; sourceTree = ""; }; 78997A2325D562160093CAE2 /* LargeIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeIconView.swift; sourceTree = ""; }; 789C373C25AF260400BA94D1 /* PaymentMethodHandlerServiceImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentMethodHandlerServiceImpl.swift; sourceTree = ""; }; 789C373E25AF260F00BA94D1 /* PaymentMethodHandlerServiceAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentMethodHandlerServiceAssembly.swift; sourceTree = ""; }; @@ -916,6 +946,18 @@ 789C376E25B06EB600BA94D1 /* AuthorizationProcessingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationProcessingError.swift; sourceTree = ""; }; 78B6B20D25C1647F00645D8A /* PaymentMethodViewModelFactoryImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentMethodViewModelFactoryImpl.swift; sourceTree = ""; }; 78B6B20F25C1648500645D8A /* PaymentMethodViewModelFactoryAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentMethodViewModelFactoryAssembly.swift; sourceTree = ""; }; + 78C1BD1825EFA1010058080F /* SberpayInteractorIO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SberpayInteractorIO.swift; sourceTree = ""; }; + 78C1BD1A25EFA1090058080F /* SberpayModuleIO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SberpayModuleIO.swift; sourceTree = ""; }; + 78C1BD1C25EFA1100058080F /* SberpayRouterIO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SberpayRouterIO.swift; sourceTree = ""; }; + 78C1BD1E25EFA11B0058080F /* SberpayViewIO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SberpayViewIO.swift; sourceTree = ""; }; + 78C1BD2025EFA1280058080F /* SberpayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SberpayViewController.swift; sourceTree = ""; }; + 78C1BD2225EFA1330058080F /* SberpayRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SberpayRouter.swift; sourceTree = ""; }; + 78C1BD2425EFA13C0058080F /* SberpayPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SberpayPresenter.swift; sourceTree = ""; }; + 78C1BD2625EFA1430058080F /* SberpayInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SberpayInteractor.swift; sourceTree = ""; }; + 78C1BD2825EFA14C0058080F /* SberpayAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SberpayAssembly.swift; sourceTree = ""; }; + 78C1BD2B25EFA2470058080F /* SberpayViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SberpayViewModel.swift; sourceTree = ""; }; + 78C1BDF425F230550058080F /* DeeplinkFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeeplinkFactory.swift; sourceTree = ""; }; + 78C1BDF625F2305F0058080F /* Deeplink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deeplink.swift; sourceTree = ""; }; 78CF11DC25D4361300F7154E /* ApplePayContractViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplePayContractViewController.swift; sourceTree = ""; }; 78CF11DF25D438AC00F7154E /* ApplePayContractViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplePayContractViewModel.swift; sourceTree = ""; }; 8BCB318DC03111CB9C1FFD14 /* Pods_YooKassaPaymentsExamplePods.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_YooKassaPaymentsExamplePods.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1219,6 +1261,7 @@ children = ( 402EB36269018072CE3287E2 /* TokenizationAssembly.swift */, 402EB47F1567A31864612F7C /* TokenizationModuleIO.swift */, + 782E75E0260CCF7500CF2BFD /* YKSdkService.swift */, 402EB1E94429DDB4E260133C /* YooKassaPaymentsError.swift */, 402EB731317D09367414E230 /* InputData */, 402EBA421DDACFD7FFA52F93 /* Models */, @@ -1420,15 +1463,16 @@ 402EBFD0D5D351F93484B633 /* 3DS */, 402EB6ADA278BB94CDCE91CD /* ApplePay */, 781CDD6F25D4053400C34912 /* ApplePayContract */, + 402EB8DC7B8A023C9B3961A7 /* BankCard */, 402EB03AF715BD6339B32F52 /* BankCardRepeat */, 7860293525D166E200B9E961 /* LinkedCard */, 402EB84797D7E864A73001A0 /* LogoutConfirmation */, 786028F125C981CF00B9E961 /* PaymentAuthorization */, 402EBA5D1D4920494878050E /* PaymentMethods */, 308E41782385500E00B44490 /* SavePaymentMethodInfo */, - 7811066625C2CD4D004DB71D /* YooMoney */, 402EB8437EC5A9EC41268B10 /* Sberbank */, - 402EB8DC7B8A023C9B3961A7 /* BankCard */, + 78E1BA6325EEA99000E5F275 /* Sberpay */, + 7811066625C2CD4D004DB71D /* YooMoney */, ); path = Modules; sourceTree = ""; @@ -1598,6 +1642,7 @@ children = ( 402EBB549599D3B7DB0DABEC /* Settings.swift */, 402EB7D5657B7828E3545FDC /* TestSettings.swift */, + 783CCEFA25FA347C005A298A /* ProcessConfirmation.swift */, ); path = Models; sourceTree = ""; @@ -1697,11 +1742,12 @@ 402EB6F5F5ECFC24A3415F23 /* UserStories */ = { isa = PBXGroup; children = ( + 402EB3EF1A220B8E654BC3ED /* Cards */, + 7890B24A25FA11720054D6B6 /* Process */, 402EBE7B78B736A4D48B9797 /* Root */, 402EBC01AAF87DAB5FB939E1 /* Settings */, - 402EB3EF1A220B8E654BC3ED /* Cards */, - 402EB22CA276A9F82F3268E5 /* Test Settings */, 402EB56FC0F4030503F712A9 /* Success */, + 402EB22CA276A9F82F3268E5 /* Test Settings */, ); path = UserStories; sourceTree = ""; @@ -1709,9 +1755,10 @@ 402EB72FA2F9530A4A971FF5 /* Extensions */ = { isa = PBXGroup; children = ( + 402EB93F77A21ACAE76D751D /* Bundle+Tools.swift */, 402EB35771FFB12A221F3905 /* String+Tools.swift */, + 783CCEFC25FB651C005A298A /* UIScreen+Short.swift */, 781CDD6D25D2B95300C34912 /* UITextField+InputView.swift */, - 402EB93F77A21ACAE76D751D /* Bundle+Tools.swift */, ); path = Extensions; sourceTree = ""; @@ -1737,7 +1784,6 @@ 402EB76B4E70AD0A3902C605 /* SheetView */ = { isa = PBXGroup; children = ( - 402EB9AC3E2FC9F00509D779 /* Yalta.swift */, 402EB046F2F92BF86F22CE06 /* SheetSize.swift */, 402EB761066455B29DCA05B5 /* SheetOptions.swift */, 402EB8291A43AFF991536FFA /* SheetTransition.swift */, @@ -1762,15 +1808,15 @@ 402EB8437EC5A9EC41268B10 /* Sberbank */ = { isa = PBXGroup; children = ( - 402EB6BB7C6741CD830FA20D /* View */, + 402EB9A46DDCE237E4AB1B00 /* SberbankInteractorIO.swift */, + 402EBB4D71D63BBDA95DB6C4 /* SberbankModuleIO.swift */, + 402EBD5D02739AA84ECC7981 /* SberbankRouterIO.swift */, 402EBCACE6EE65EF9B341007 /* SberbankViewIO.swift */, 402EB675A587DD13842E5702 /* Assembly */, - 402EB492C4DA8738C0F8DBCF /* Presenter */, 402EBADCF9C4CF55E0B06A5E /* Interactor */, - 402EB9A46DDCE237E4AB1B00 /* SberbankInteractorIO.swift */, - 402EBB4D71D63BBDA95DB6C4 /* SberbankModuleIO.swift */, + 402EB492C4DA8738C0F8DBCF /* Presenter */, 402EB46229DE0340B1F89025 /* Router */, - 402EBD5D02739AA84ECC7981 /* SberbankRouterIO.swift */, + 402EB6BB7C6741CD830FA20D /* View */, ); path = Sberbank; sourceTree = ""; @@ -1823,14 +1869,14 @@ isa = PBXGroup; children = ( 402EBC0AAB711E47B4FDF1E4 /* BankCardInteractorIO.swift */, - 402EBCE7F595FBF58EEEF96F /* BankCardViewIO.swift */, - 402EBE3EEE70AA9D7309A7A0 /* Assembly */, 402EB3C75516A9743F28AAB5 /* BankCardModuleIO.swift */, 402EB02843790D7A273BDA2A /* BankCardRouterIO.swift */, - 402EB9BBCDD811A24D83A302 /* View */, + 402EBCE7F595FBF58EEEF96F /* BankCardViewIO.swift */, + 402EBE3EEE70AA9D7309A7A0 /* Assembly */, 402EB2244D468BB6D6BF9AFB /* Interactor */, 402EB8DC46E920C41B8F0991 /* Presenter */, 402EB1ED411EC5BD00482333 /* Router */, + 402EB9BBCDD811A24D83A302 /* View */, ); path = BankCard; sourceTree = ""; @@ -1865,7 +1911,10 @@ 402EB033A1DFDB46B5EBA6D1 /* PhoneNumberFormatterAssembly.swift */, 402EB96DFA8C9D871DE124D0 /* SavePaymentMethodViewModelFactory.swift */, 402EBF7AC88FCC8EA3154C56 /* UserAgentFactory.swift */, + 781FB66F260235D400567AFA /* BankCardImage */, + 78C1BDF325F230460058080F /* Deeplink */, 78B6B20C25C1646D00645D8A /* PaymentMethodViewModel */, + 402EB489BC427881505E2583 /* PriceViewModelFactory.swift */, ); path = Factory; sourceTree = ""; @@ -1964,6 +2013,7 @@ 402EBA8BE37B25AAD457F8D1 /* Models */ = { isa = PBXGroup; children = ( + 781FB66A260229CD00567AFA /* BankCardRegex.swift */, 402EBA3F8618D17FB47AC873 /* BankSettings.swift */, 402EBA04C707C5EA568200DE /* CardData.swift */, 402EBC6E1AC8DE3F0701FE15 /* HostsConfig.swift */, @@ -2084,9 +2134,10 @@ isa = PBXGroup; children = ( 402EBDCA8CE554E9A5B4FFEB /* ContainerTableViewCell.swift */, - 402EB6A48AE583D297E41E9A /* TitledSwitchView.swift */, - 402EB7AF716BE8C008EA6059 /* TextValueView.swift */, + 7890B24E25FA196D0054D6B6 /* TextFieldView.swift */, 402EB7A951380D72FF1C56D6 /* TextHeaderFooterView.swift */, + 402EB7AF716BE8C008EA6059 /* TextValueView.swift */, + 402EB6A48AE583D297E41E9A /* TitledSwitchView.swift */, 402EB0F399573DCE0B6FF15B /* TextControl */, ); path = Views; @@ -2481,6 +2532,16 @@ path = View; sourceTree = ""; }; + 781FB66F260235D400567AFA /* BankCardImage */ = { + isa = PBXGroup; + children = ( + 781FB66D260235AD00567AFA /* BankCardImageFactory.swift */, + 781FB670260235EF00567AFA /* BankCardImageFactoryAssembly.swift */, + 781FB672260235F700567AFA /* BankCardImageFactoryImpl.swift */, + ); + path = BankCardImage; + sourceTree = ""; + }; 784A1A8925B5A42900637CB5 /* ApplePay */ = { isa = PBXGroup; children = ( @@ -2727,6 +2788,14 @@ path = ViewModel; sourceTree = ""; }; + 7890B24A25FA11720054D6B6 /* Process */ = { + isa = PBXGroup; + children = ( + 7890B24B25FA118A0054D6B6 /* ProcessViewController.swift */, + ); + path = Process; + sourceTree = ""; + }; 78997A2225D562080093CAE2 /* LargeIconView */ = { isa = PBXGroup; children = ( @@ -2755,6 +2824,64 @@ path = PaymentMethodViewModel; sourceTree = ""; }; + 78C1BD1325EFA0D40058080F /* Assembly */ = { + isa = PBXGroup; + children = ( + 78C1BD2825EFA14C0058080F /* SberpayAssembly.swift */, + ); + path = Assembly; + sourceTree = ""; + }; + 78C1BD1425EFA0E10058080F /* Interactor */ = { + isa = PBXGroup; + children = ( + 78C1BD2625EFA1430058080F /* SberpayInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + 78C1BD1525EFA0E80058080F /* Presenter */ = { + isa = PBXGroup; + children = ( + 78C1BD2425EFA13C0058080F /* SberpayPresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 78C1BD1625EFA0ED0058080F /* Router */ = { + isa = PBXGroup; + children = ( + 78C1BD2225EFA1330058080F /* SberpayRouter.swift */, + ); + path = Router; + sourceTree = ""; + }; + 78C1BD1725EFA0F10058080F /* View */ = { + isa = PBXGroup; + children = ( + 78C1BD2025EFA1280058080F /* SberpayViewController.swift */, + 78C1BD2A25EFA23C0058080F /* ViewModel */, + ); + path = View; + sourceTree = ""; + }; + 78C1BD2A25EFA23C0058080F /* ViewModel */ = { + isa = PBXGroup; + children = ( + 78C1BD2B25EFA2470058080F /* SberpayViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 78C1BDF325F230460058080F /* Deeplink */ = { + isa = PBXGroup; + children = ( + 78C1BDF625F2305F0058080F /* Deeplink.swift */, + 78C1BDF425F230550058080F /* DeeplinkFactory.swift */, + ); + path = Deeplink; + sourceTree = ""; + }; 78CF11DE25D438A100F7154E /* ViewModel */ = { isa = PBXGroup; children = ( @@ -2763,6 +2890,22 @@ path = ViewModel; sourceTree = ""; }; + 78E1BA6325EEA99000E5F275 /* Sberpay */ = { + isa = PBXGroup; + children = ( + 78C1BD1825EFA1010058080F /* SberpayInteractorIO.swift */, + 78C1BD1A25EFA1090058080F /* SberpayModuleIO.swift */, + 78C1BD1C25EFA1100058080F /* SberpayRouterIO.swift */, + 78C1BD1E25EFA11B0058080F /* SberpayViewIO.swift */, + 78C1BD1325EFA0D40058080F /* Assembly */, + 78C1BD1425EFA0E10058080F /* Interactor */, + 78C1BD1525EFA0E80058080F /* Presenter */, + 78C1BD1625EFA0ED0058080F /* Router */, + 78C1BD1725EFA0F10058080F /* View */, + ); + path = Sberpay; + sourceTree = ""; + }; 9133B1C75F52034A6FB79687 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -3051,6 +3194,7 @@ 402EB87BAE962D317D13D7F4 /* CscInputPresenterStyle.swift in Sources */, 7860293D25D1671000B9E961 /* LinkedCardViewIO.swift in Sources */, 402EBB1D932FB9AA14055422 /* PanInputPresenterStyle.swift in Sources */, + 781FB673260235F700567AFA /* BankCardImageFactoryImpl.swift in Sources */, 402EBCE6F2A52CBB1C0D225A /* ExpiryDateInputPresenterStyle.swift in Sources */, 402EBE1BE753B698DAB812C0 /* PhoneNumberStyleWithAutoCorrection.swift in Sources */, 402EB5B4BFA3AAFB7B8EFAC1 /* PhoneNumberFormatter.swift in Sources */, @@ -3103,6 +3247,7 @@ 780D0D3925DC0DF100CF15D1 /* BankCardRepeatModuleIO.swift in Sources */, 402EBB0834948CFD4278DF96 /* UILabel+Style.swift in Sources */, 402EB52D1B69D9D12BA05F02 /* UIFont+Style.swift in Sources */, + 78C1BD1F25EFA11B0058080F /* SberpayViewIO.swift in Sources */, 784A1AE625B9CE8A00637CB5 /* PaymentMethod.swift in Sources */, 402EBC73F5045B2B33EED401 /* UIImage+Style.swift in Sources */, 402EB2F13EA4EBCE01956D61 /* UIImageView+Style.swift in Sources */, @@ -3110,6 +3255,7 @@ 402EBD2767032917D2486044 /* UINavigationBar+Style.swift in Sources */, 402EBFAAC2EC4686CE4488A8 /* UIBarButtonItem+Style.swift in Sources */, 784A1ADB25B9C57B00637CB5 /* CheckoutTokenIssueExecute.swift in Sources */, + 78C1BD2725EFA1430058080F /* SberpayInteractor.swift in Sources */, 402EB00B9EC9B560D706EC06 /* UIScrollView+Style.swift in Sources */, 402EB43FAD5652F9095B47FF /* ActivityIndicatorView+Style.swift in Sources */, 402EB1BC568F568FD6E10047 /* ActivityIndicatorView.swift in Sources */, @@ -3142,6 +3288,7 @@ 78CF11DD25D4361300F7154E /* ApplePayContractViewController.swift in Sources */, 402EBE3D748B1AD41EBD2771 /* PresentableError.swift in Sources */, 3089EF4923846F6300CB7319 /* SwitcherSavePaymentMethodViewModel.swift in Sources */, + 78C1BD1925EFA1010058080F /* SberpayInteractorIO.swift in Sources */, 7860291025C9913A00B9E961 /* UITextField+Style.swift in Sources */, 402EBD7014CFF4A56F23439F /* PresentableNotification.swift in Sources */, 780D0D3C25DC0E0300CF15D1 /* BankCardRepeatAssembly.swift in Sources */, @@ -3149,6 +3296,7 @@ 402EB9AF1EC6377E4EB997A7 /* NotificationPresenting.swift in Sources */, 402EB3B8F359111B86058AB0 /* ActivityIndicatorFullViewPresenting.swift in Sources */, 402EB469B4798293A88ABAF4 /* ActivityIndicatorPresenting.swift in Sources */, + 78C1BD2925EFA14C0058080F /* SberpayAssembly.swift in Sources */, 402EB45C4D87D353D57D4A2C /* WebBrowserViewController.swift in Sources */, 781CDD7725D4063000C34912 /* ApplePayContractInteractorIO.swift in Sources */, 402EB12A36444BFD7ECCD081 /* WebBrowserRouter.swift in Sources */, @@ -3174,6 +3322,7 @@ 402EBFC348B3BCF7D33FCAB5 /* CustomizationColors.swift in Sources */, 7860292A25CD406500B9E961 /* ImageDownloadServiceImpl.swift in Sources */, 7811066E25C2CD91004DB71D /* YooMoneyInteractor.swift in Sources */, + 781FB66E260235AD00567AFA /* BankCardImageFactory.swift in Sources */, 7860294525D1673800B9E961 /* LinkedCardInteractor.swift in Sources */, 402EB9C4BBC1F6EF4241A9C9 /* CardData.swift in Sources */, 7811066C25C2CD7B004DB71D /* YooMoneyAssembly.swift in Sources */, @@ -3194,6 +3343,7 @@ 402EBBC990C2C0F2A2A0BD69 /* PhoneNumberFormatterAssembly.swift in Sources */, 402EB7C3CF2DBEAF200479D6 /* UserAgentFactory.swift in Sources */, 402EB6BE01411608D225E157 /* Localization.swift in Sources */, + 78C1BD2C25EFA2470058080F /* SberpayViewModel.swift in Sources */, 7860294D25D1723B00B9E961 /* LinkedCardRouter.swift in Sources */, 402EBFA22A30FD6DA60F6FA3 /* PrefferedContentSizeCategory.swift in Sources */, 402EBD79F5F3730B73E8E2B6 /* ResourceLoader.swift in Sources */, @@ -3252,7 +3402,10 @@ 7860290025C982CA00B9E961 /* PaymentAuthorizationAssembly.swift in Sources */, 402EB636FC3E40E0EDE5A096 /* LogoutConfirmationModuleIO.swift in Sources */, 402EB002BD1EC142641CB2D4 /* LogoutConfirmationAssembly.swift in Sources */, + 783CCEFD25FB651C005A298A /* UIScreen+Short.swift in Sources */, 402EB90A3E9FD8733C4EBEA9 /* LoginConfirmationViewController.swift in Sources */, + 78C1BD1D25EFA1100058080F /* SberpayRouterIO.swift in Sources */, + 781FB66B260229CD00567AFA /* BankCardRegex.swift in Sources */, 402EBBE91DE40AA08E3AB8B5 /* ApplePayAssembly.swift in Sources */, 402EB7C74860DFA65827A00C /* ApplePayModuleIO.swift in Sources */, 30B20E2B2535FC1C00941574 /* KeyValueStoringKeys.swift in Sources */, @@ -3260,6 +3413,7 @@ 402EB8D43DDD23DC7A645AA1 /* BankCardRepeatInteractor.swift in Sources */, 402EB1F0EAA3B2E1422FD95D /* BankCardRepeatInteractorIO.swift in Sources */, 3089EF4D23846F7400CB7319 /* SavePaymentMethodViewModel.swift in Sources */, + 781FB671260235EF00567AFA /* BankCardImageFactoryAssembly.swift in Sources */, 402EB38B0B0D9EA3988F9922 /* PaymentService.swift in Sources */, 402EBC0B94C8D1B32962BBA8 /* PaymentProcessingError.swift in Sources */, 402EBD95FD7CAFB6AD2F29FC /* PaymentServiceMock.swift in Sources */, @@ -3267,6 +3421,7 @@ 402EB8203E2273B472E5819C /* HostProvider.swift in Sources */, 402EB70F599439C2EAF90432 /* ApiLogger.swift in Sources */, 402EB96B6F077D9807AD790B /* PaymentMethodHandlerService.swift in Sources */, + 78C1BD2525EFA13C0058080F /* SberpayPresenter.swift in Sources */, 7811067C25C3FCB8004DB71D /* OrderView.swift in Sources */, 402EB8D9C4C6674F176C1E2C /* AuthorizationService.swift in Sources */, 402EBCB6D5E6441798A2DC59 /* AuthorizationServiceImpl.swift in Sources */, @@ -3278,6 +3433,7 @@ 402EB3397345E69677F29347 /* KeychainStorage.swift in Sources */, 402EB52D11504C419695D49F /* UserDefaultsStorage.swift in Sources */, 402EBFE2285B7B73BF2290ED /* WalletLoginServiceImpl.swift in Sources */, + 782E75E1260CCF7500CF2BFD /* YKSdkService.swift in Sources */, 402EBDA16A62F97489BF95AC /* WalletLoginService.swift in Sources */, 402EB5745ABA5C4E1A40294A /* WalletLoginServiceMock.swift in Sources */, 402EBF72D88566C67130C623 /* AuthTypeStatesProvider.swift in Sources */, @@ -3301,6 +3457,7 @@ 402EB10E2B1F8A4E8BD2DEB8 /* BankSettingsService.swift in Sources */, 402EB324EC0352C1CF0A8A59 /* BankSettingsServiceImpl.swift in Sources */, 7860292C25CD406500B9E961 /* ImageCacheImpl.swift in Sources */, + 78C1BD1B25EFA1090058080F /* SberpayModuleIO.swift in Sources */, 402EBF6F0AF3AB32E9C844AC /* TokenizationAssembly.swift in Sources */, 402EB337CF8A33565554E71D /* CardScanning.swift in Sources */, 789C374125AF285C00BA94D1 /* PaymentServiceImpl.swift in Sources */, @@ -3311,6 +3468,7 @@ 786028F825C982B100B9E961 /* PaymentAuthorizationInteractorIO.swift in Sources */, 780D0D4425DC11FB00CF15D1 /* BankCardRepeatRouter.swift in Sources */, 402EB0FBD2A8AFE719A2A013 /* TokenizationSettings.swift in Sources */, + 78C1BDF725F2305F0058080F /* Deeplink.swift in Sources */, 308E417A2385501E00B44490 /* SavePaymentMethodInfoViewController.swift in Sources */, 78CF11E025D438AC00F7154E /* ApplePayContractViewModel.swift in Sources */, 784A1AEA25B9CFA700637CB5 /* PaymentMethodBankCard.swift in Sources */, @@ -3331,7 +3489,6 @@ 781CDD7925D4063A00C34912 /* ApplePayContractRouterIO.swift in Sources */, 402EB34C8185FBEF0E13025B /* UIViewController+Keyboard.swift in Sources */, 402EB952EA7B9CD954D7654E /* KeyboardObservingAccessoryView.swift in Sources */, - 402EB4D914B8C94E739CAA6C /* Yalta.swift in Sources */, 402EBB6E80134E0C43050D07 /* SheetSize.swift in Sources */, 402EB4040D6DB2DC51F53B51 /* SheetOptions.swift in Sources */, 402EBE4E6840A072C11BF813 /* SheetTransition.swift in Sources */, @@ -3348,6 +3505,7 @@ 402EB35A07609716EDED7B93 /* SberbankViewModel.swift in Sources */, 402EB9182B2015B619CAAF81 /* SberbankInteractor.swift in Sources */, 402EB636AA2031BCCDFB4139 /* SberbankInteractorIO.swift in Sources */, + 78C1BDF525F230550058080F /* DeeplinkFactory.swift in Sources */, 402EB3F61C8155EC17F69065 /* SberbankModuleIO.swift in Sources */, 402EB2CB501761740839F22B /* SberbankRouterIO.swift in Sources */, 402EB069F4DDF647DF893AC6 /* SberbankRouter.swift in Sources */, @@ -3366,6 +3524,7 @@ 402EB408F40569680354E993 /* BankCardRouter.swift in Sources */, 402EB571173B519C0F71C65A /* InputCvcView.swift in Sources */, 402EB420CBCEF91141DDBD23 /* InputPanCardView.swift in Sources */, + 78C1BD2125EFA1280058080F /* SberpayViewController.swift in Sources */, 402EBD521E90D0EE32F6611D /* InputExpiryDateView.swift in Sources */, 402EBAAC323B2686A6C43781 /* BankCardDataInputView.swift in Sources */, 402EBC52D30F0A1A37263A97 /* BankCardDataInputAssembly.swift in Sources */, @@ -3379,6 +3538,7 @@ 402EB3AB073F3C17EAE8919C /* BankCardDataInputViewModel.swift in Sources */, 402EBBDF51F6662CEE54298A /* Bundle+Tools.swift in Sources */, 402EBB94AB27CC90E71D3A6D /* TextControl.swift in Sources */, + 78C1BD2325EFA1330058080F /* SberpayRouter.swift in Sources */, 402EBAA71096200333711566 /* TextControl+State.swift in Sources */, 402EB53E4E9278E5E38D3773 /* TextControl+Layout.swift in Sources */, 402EB35B6BF9FBB145DCCA2A /* TextControl+LineMode.swift in Sources */, @@ -3398,6 +3558,7 @@ 402EB4E93D7A00A973BB93FA /* UITableViewHeaderFooterView+Identifier.swift in Sources */, 402EBF5E02044CFB30A2A40A /* UITableViewCell+Identifier.swift in Sources */, 402EBB42DE308BDC3E21CA74 /* NavigationController.swift in Sources */, + 402EBC3C07CACF8D2020DBC5 /* PriceViewModelFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3479,6 +3640,7 @@ 30AB53082383315D0098C7C1 /* UITextView+Styles.swift in Sources */, 402EB8CBDC1DC409DB149528 /* TransitionHandler.swift in Sources */, 402EB76B159E9C503FA2B433 /* ActivityIndicatorPresenting.swift in Sources */, + 7890B24D25FA14BD0054D6B6 /* ProcessViewController.swift in Sources */, 402EB45A688AF741FD5813E3 /* Anchor.swift in Sources */, 402EBB8F4B7C1AF1E2394308 /* UIScreen+SafeAreaInsets.swift in Sources */, 402EBA03EBFAB4CE09BE7DF5 /* UIView+SpecificLayoutGuide.swift in Sources */, @@ -3487,6 +3649,7 @@ 402EB081707DFFC2F5DF3999 /* UITableView+Identifier.swift in Sources */, 402EB38E8A91E6F075E54A78 /* UIViewController+TransitionHandler.swift in Sources */, 402EB059A64C4ADEB677C006 /* UIImage+Tools.swift in Sources */, + 7890B24F25FA196D0054D6B6 /* TextFieldView.swift in Sources */, 402EBAE66EB113D0D9C6F92F /* TextControl.swift in Sources */, 402EB7195FF2185F9F28BF17 /* TextControl+State.swift in Sources */, 402EB1B9B220F60B15DD61C1 /* TextControl+Layout.swift in Sources */, @@ -3502,6 +3665,7 @@ 402EB600C417B9FFA92A9014 /* TextControl+LayoutController.swift in Sources */, 402EBEBBF870303E32A2F04B /* TextControl+Style.swift in Sources */, 402EB49D79C96F3D7EC4D00E /* PlaceholderView.swift in Sources */, + 783CCEFB25FA347C005A298A /* ProcessConfirmation.swift in Sources */, 402EB201DCE41E5997E60960 /* UIView+Tools.swift in Sources */, 402EBD74B5D25C398B38EC0B /* Identifier.swift in Sources */, 402EBC75F208A0541A979ECC /* UITableViewCell+Identifier.swift in Sources */, diff --git a/YooKassaPayments/Info.plist b/YooKassaPayments/Info.plist index c7412356..2462f202 100644 --- a/YooKassaPayments/Info.plist +++ b/YooKassaPayments/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 5.4.1 + 6.0.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass diff --git a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Assembly/BankCardDataInputAssembly.swift b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Assembly/BankCardDataInputAssembly.swift index e98fdfb3..08871d92 100644 --- a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Assembly/BankCardDataInputAssembly.swift +++ b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Assembly/BankCardDataInputAssembly.swift @@ -43,6 +43,7 @@ enum BankCardDataInputAssembly { private static func makePresenter( inputData: BankCardDataInputModuleInputData ) -> BankCardDataInputPresenter { + let bankCardImageFactory = BankCardImageFactoryAssembly.makeFactory() let presenter = BankCardDataInputPresenter( inputPanHint: inputData.inputPanHint, inputPanPlaceholder: inputData.inputPanPlaceholder, @@ -50,7 +51,8 @@ enum BankCardDataInputAssembly { inputExpiryDatePlaceholder: inputData.inputExpiryDatePlaceholder, inputCvcHint: inputData.inputCvcHint, inputCvcPlaceholder: inputData.inputCvcPlaceholder, - cardScanner: inputData.cardScanner + cardScanner: inputData.cardScanner, + bankCardImageFactory: bankCardImageFactory ) return presenter } diff --git a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/BankCardDataInputInteractorIO.swift b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/BankCardDataInputInteractorIO.swift index e1ccc96f..c6342736 100644 --- a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/BankCardDataInputInteractorIO.swift +++ b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/BankCardDataInputInteractorIO.swift @@ -19,5 +19,7 @@ protocol BankCardDataInputInteractorOutput: class { func didFetchBankSettings( _ bankSettings: BankSettings ) - func didFailFetchBankSettings() + func didFailFetchBankSettings( + _ cardMask: String + ) } diff --git a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/BankCardDataInputViewIO.swift b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/BankCardDataInputViewIO.swift index 49242919..70b88a0d 100644 --- a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/BankCardDataInputViewIO.swift +++ b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/BankCardDataInputViewIO.swift @@ -4,6 +4,7 @@ enum BankCardDataInputViewErrorState { case noError case panError case expiryDateError + case invalidCvc } protocol BankCardDataInputViewInput: class { @@ -38,6 +39,8 @@ protocol BankCardDataInputViewOutput: class { func didPressScan() func panDidBeginEditing() func expiryDateDidBeginEditing() + func expiryDateDidEndEditing() + func cvcDidEndEditing() func nextDidPress() func clearDidPress() diff --git a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Interactor/BankCardDataInputInteractor.swift b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Interactor/BankCardDataInputInteractor.swift index ee3c947d..0b3f092e 100644 --- a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Interactor/BankCardDataInputInteractor.swift +++ b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Interactor/BankCardDataInputInteractor.swift @@ -46,7 +46,7 @@ extension BankCardDataInputInteractor: BankCardDataInputInteractorInput { guard let bankSettings = bankSettingsService.bankSettings( cardMask ) else { - output?.didFailFetchBankSettings() + output?.didFailFetchBankSettings(cardMask) return } output?.didFetchBankSettings(bankSettings) diff --git a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Presenter/BankCardDataInputPresenter.swift b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Presenter/BankCardDataInputPresenter.swift index 69b5c17e..8739b451 100644 --- a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Presenter/BankCardDataInputPresenter.swift +++ b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Presenter/BankCardDataInputPresenter.swift @@ -16,6 +16,7 @@ final class BankCardDataInputPresenter { private let inputCvcHint: String private let inputCvcPlaceholder: String private let cardScanner: CardScanning? + private let bankCardImageFactory: BankCardImageFactory init( inputPanHint: String, @@ -24,7 +25,8 @@ final class BankCardDataInputPresenter { inputExpiryDatePlaceholder: String, inputCvcHint: String, inputCvcPlaceholder: String, - cardScanner: CardScanning? + cardScanner: CardScanning?, + bankCardImageFactory: BankCardImageFactory ) { self.inputPanHint = inputPanHint self.inputPanPlaceholder = inputPanPlaceholder @@ -33,6 +35,7 @@ final class BankCardDataInputPresenter { self.inputCvcHint = inputCvcHint self.inputCvcPlaceholder = inputCvcPlaceholder self.cardScanner = cardScanner + self.bankCardImageFactory = bankCardImageFactory } // MARK: - Stored data @@ -109,9 +112,9 @@ extension BankCardDataInputPresenter: BankCardDataInputViewOutput { func didChangeCvc( _ value: String ) { + self.cardData.csc = value DispatchQueue.global(qos: .userInteractive).async { [weak self] in guard let self = self else { return } - self.cardData.csc = value self.interactor.validate( cardData: self.cardData, shouldMoveFocus: true @@ -150,6 +153,26 @@ extension BankCardDataInputPresenter: BankCardDataInputViewOutput { ) } } + + func expiryDateDidEndEditing() { + DispatchQueue.global(qos: .userInteractive).async { [weak self] in + guard let self = self else { return } + self.interactor.validate( + cardData: self.cardData, + shouldMoveFocus: false + ) + } + } + + func cvcDidEndEditing() { + DispatchQueue.global(qos: .userInteractive).async { [weak self] in + guard let self = self else { return } + self.interactor.validate( + cardData: self.cardData, + shouldMoveFocus: false + ) + } + } func nextDidPress() { setViewFocus(.expiryDate) @@ -159,13 +182,6 @@ extension BankCardDataInputPresenter: BankCardDataInputViewOutput { func clearDidPress() { trackCardNumberClearAction() } - - private func setModifiedPan() { - guard let panValue = cardData.pan, - let view = view else { return } - let modifiedPanValue = "••••" + panValue.suffix(4) - view.setPanValue(modifiedPanValue) - } } // MARK: - BankCardDataInputInteractorOutput @@ -181,6 +197,7 @@ extension BankCardDataInputPresenter: BankCardDataInputInteractorOutput { DispatchQueue.main.async { [weak self] in guard let view = self?.view else { return } + view.setErrorState(.noError) if view.focus == .pan { view.setCardViewMode(.next) } @@ -228,10 +245,14 @@ extension BankCardDataInputPresenter: BankCardDataInputInteractorOutput { } } - func didFailFetchBankSettings() { + func didFailFetchBankSettings( + _ cardMask: String + ) { DispatchQueue.main.async { [weak self] in - guard let view = self?.view else { return } - view.setBankLogoImage(nil) + guard let self = self, + let view = self.view else { return } + let image = self.bankCardImageFactory.makeImage(cardMask) + view.setBankLogoImage(image) } } } @@ -254,7 +275,6 @@ extension BankCardDataInputPresenter: BankCardDataInputRouterOutput { } private func setPanAndMoveFocusNext(_ value: String) { - guard let view = view else { return } cardData.pan = value setViewFocus(.expiryDate) } @@ -301,13 +321,18 @@ private extension BankCardDataInputPresenter { view.focus == .pan { view.setErrorState(.panError) trackCardNumberInputError() - } else if expiryDateText.count == Constants.MoveFocusLength.expiryDate, + } else if (view.focus == nil || + view.focus == .expiryDate + && expiryDateText.count == Constants.MoveFocusLength.expiryDate), errors.contains(.expirationDateIsExpired) || errors.contains(.expiryDateEmpty) - || errors.contains(.invalidMonth), - view.focus == .expiryDate { + || errors.contains(.invalidMonth) { view.setErrorState(.expiryDateError) trackCardExpiryInputError() + } else if errors.contains(.cscInvalidLength), + view.focus == nil { + view.setErrorState(.invalidCvc) + trackCardCvcInputError() } else { view.setErrorState(.noError) } @@ -349,7 +374,12 @@ private extension BankCardDataInputPresenter { case .expiryDate?, .cvc?: view.setInputState(.uncollapsed) view.setCardViewMode(.empty) - let modifiedPanValue = "••••" + panValue.suffix(4) + let modifiedPanValue: String + if UIScreen.main.isNarrow { + modifiedPanValue = String(panValue.suffix(4)) + } else { + modifiedPanValue = "••••" + panValue.suffix(4) + } view.setPanValue(modifiedPanValue) default: break @@ -385,6 +415,14 @@ private extension BankCardDataInputPresenter { ) trackEvent(event) } + + func trackCardCvcInputError() { + let event: AnalyticsEvent = .actionBankCardForm( + action: .cardCvcInputError, + sdkVersion: Bundle.frameworkVersion + ) + trackEvent(event) + } func trackCardNumberClearAction() { let event: AnalyticsEvent = .actionBankCardForm( diff --git a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/View/BankCardDataInputView.swift b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/View/BankCardDataInputView.swift index 87ea3c5a..01b5866e 100644 --- a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/View/BankCardDataInputView.swift +++ b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/View/BankCardDataInputView.swift @@ -57,7 +57,9 @@ final class BankCardDataInputView: UIView { let view = UIStackView() view.translatesAutoresizingMaskIntoConstraints = false view.axis = .horizontal - view.spacing = Space.double + view.spacing = UIScreen.main.isNarrow + ? Space.single + : Space.double return view }() @@ -318,6 +320,7 @@ extension BankCardDataInputView: BankCardDataInputViewInput { self.inputsContainerView.setStyles(UIView.Styles.grayBorder) self.inputPanCardView.setStyles(InputPanCardView.Styles.default) self.inputExpiryDateView.setStyles(InputExpiryDateView.Styles.default) + self.inputCvcView.setStyles(InputCvcView.Styles.default) self.bottomHintLabel.text = "" self.bottomHintLabel.alpha = 0 case .panError: @@ -329,8 +332,16 @@ extension BankCardDataInputView: BankCardDataInputViewInput { self.inputsContainerView.setStyles(UIView.Styles.alertBorder) self.inputExpiryDateView.setStyles(InputExpiryDateView.Styles.error) self.inputPanCardView.setStyles(InputPanCardView.Styles.default) + self.inputCvcView.setStyles(InputCvcView.Styles.default) self.bottomHintLabel.text = §Localized.BottomHint.invalidExpiry self.bottomHintLabel.alpha = 1 + case .invalidCvc: + self.inputsContainerView.setStyles(UIView.Styles.alertBorder) + self.inputCvcView.setStyles(InputCvcView.Styles.error) + self.inputExpiryDateView.setStyles(InputExpiryDateView.Styles.default) + self.inputPanCardView.setStyles(InputPanCardView.Styles.default) + self.bottomHintLabel.text = §Localized.BottomHint.invalidCvc + self.bottomHintLabel.alpha = 1 } } } @@ -374,6 +385,10 @@ extension BankCardDataInputView: InputExpiryDateViewDelegate { func expiryDateDidBeginEditing() { output.expiryDateDidBeginEditing() } + + func expiryDateDidEndEditing() { + output.expiryDateDidEndEditing() + } } // MARK: - InputCvcViewDelegate @@ -384,6 +399,10 @@ extension BankCardDataInputView: InputCvcViewDelegate { ) { output.didChangeCvc(value) } + + func cvcDidEndEditing() { + output.cvcDidEndEditing() + } } // MARK: - UITextField from BankCardView.BankCardFocus @@ -408,6 +427,7 @@ private extension BankCardDataInputView { enum BottomHint: String { case invalidPan = "BankCardDataInputView.BottomHint.invalidPan" case invalidExpiry = "BankCardDataInputView.BottomHint.invalidExpiry" + case invalidCvc = "BankCardDataInputView.BottomHint.invalidCvc" } } } diff --git a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/View/Subviews/InputCvcView.swift b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/View/Subviews/InputCvcView.swift index 9459e251..e4fb0786 100644 --- a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/View/Subviews/InputCvcView.swift +++ b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/View/Subviews/InputCvcView.swift @@ -4,6 +4,7 @@ protocol InputCvcViewDelegate: class { func cvcDidChange( _ value: String ) + func cvcDidEndEditing() } final class InputCvcView: UIView { @@ -156,4 +157,31 @@ extension InputCvcView: UITextFieldDelegate { delegate?.cvcDidChange(cachedCvc) return false } + + func textFieldDidEndEditing( + _ textField: UITextField + ) { + delegate?.cvcDidEndEditing() + } +} + +// MARK: Styles + +extension InputCvcView { + enum Styles { + static let `default` = InternalStyle(name: "InputCvcView.Default") { (view: InputCvcView) in + view.cvcHintLabel.setStyles( + UILabel.DynamicStyle.caption1, + UILabel.ColorStyle.ghost, + UILabel.Styles.singleLine + ) + } + static let error = InternalStyle(name: "InputCvcView.Error") { (view: InputCvcView) in + view.cvcHintLabel.setStyles( + UILabel.DynamicStyle.caption1, + UILabel.ColorStyle.alert, + UILabel.Styles.singleLine + ) + } + } } diff --git a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/View/Subviews/InputExpiryDateView.swift b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/View/Subviews/InputExpiryDateView.swift index 06c1fc21..34c52056 100644 --- a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/View/Subviews/InputExpiryDateView.swift +++ b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/View/Subviews/InputExpiryDateView.swift @@ -5,6 +5,7 @@ protocol InputExpiryDateViewDelegate: class { _ value: String ) func expiryDateDidBeginEditing() + func expiryDateDidEndEditing() } final class InputExpiryDateView: UIView { @@ -161,7 +162,12 @@ extension InputExpiryDateView: UITextFieldDelegate { ) { delegate?.expiryDateDidBeginEditing() } - + + func textFieldDidEndEditing( + _ textField: UITextField + ) { + delegate?.expiryDateDidEndEditing() + } } // MARK: Styles diff --git a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/View/Subviews/InputPanCardView.swift b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/View/Subviews/InputPanCardView.swift index 34fd454c..47564d39 100644 --- a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/View/Subviews/InputPanCardView.swift +++ b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/View/Subviews/InputPanCardView.swift @@ -245,6 +245,7 @@ extension InputPanCardView: UITextFieldDelegate { _ textField: UITextField ) { delegate?.panDidBeginEditing() + setCursorToEnd() } } @@ -268,6 +269,17 @@ private extension InputPanCardView { delegate?.nextDidPress() } } + + func setCursorToEnd() { + DispatchQueue.main.async() { [weak self] in + guard let self = self else { return } + let newPosition = self.cardPanTextField.endOfDocument + self.cardPanTextField.selectedTextRange = self.cardPanTextField.textRange( + from: newPosition, + to: newPosition + ) + } + } } // MARK: Styles diff --git a/YooKassaPayments/Private/Atomic Design/Views/NavigationController.swift b/YooKassaPayments/Private/Atomic Design/Views/NavigationController.swift index 223e6d36..9809deef 100644 --- a/YooKassaPayments/Private/Atomic Design/Views/NavigationController.swift +++ b/YooKassaPayments/Private/Atomic Design/Views/NavigationController.swift @@ -10,4 +10,14 @@ extension NavigationController: TokenizationModuleInput { func start3dsProcess(requestUrl: String) { moduleOutput?.start3dsProcess(requestUrl: requestUrl) } + + func startConfirmationProcess( + confirmationUrl: String, + paymentMethodType: PaymentMethodType + ) { + moduleOutput?.startConfirmationProcess( + confirmationUrl: confirmationUrl, + paymentMethodType: paymentMethodType + ) + } } diff --git a/YooKassaPayments/Private/Extensions/UIScreen+Short.swift b/YooKassaPayments/Private/Extensions/UIScreen+Short.swift new file mode 100644 index 00000000..6bf8250d --- /dev/null +++ b/YooKassaPayments/Private/Extensions/UIScreen+Short.swift @@ -0,0 +1,9 @@ +extension UIScreen { + var isShort: Bool { + bounds.height < 600 + } + + var isNarrow: Bool { + bounds.width < 350 + } +} diff --git a/YooKassaPayments/Private/Factory/BankCardImage/BankCardImageFactory.swift b/YooKassaPayments/Private/Factory/BankCardImage/BankCardImageFactory.swift new file mode 100644 index 00000000..16b3e292 --- /dev/null +++ b/YooKassaPayments/Private/Factory/BankCardImage/BankCardImageFactory.swift @@ -0,0 +1,8 @@ +protocol BankCardImageFactory { + + // MARK: - Make bank card image from card mask + + func makeImage( + _ cardMask: String + ) -> UIImage? +} diff --git a/YooKassaPayments/Private/Factory/BankCardImage/BankCardImageFactoryAssembly.swift b/YooKassaPayments/Private/Factory/BankCardImage/BankCardImageFactoryAssembly.swift new file mode 100644 index 00000000..a350c6c2 --- /dev/null +++ b/YooKassaPayments/Private/Factory/BankCardImage/BankCardImageFactoryAssembly.swift @@ -0,0 +1,5 @@ +enum BankCardImageFactoryAssembly { + static func makeFactory() -> BankCardImageFactory { + return BankCardImageFactoryImpl() + } +} diff --git a/YooKassaPayments/Private/Factory/BankCardImage/BankCardImageFactoryImpl.swift b/YooKassaPayments/Private/Factory/BankCardImage/BankCardImageFactoryImpl.swift new file mode 100644 index 00000000..650bd13c --- /dev/null +++ b/YooKassaPayments/Private/Factory/BankCardImage/BankCardImageFactoryImpl.swift @@ -0,0 +1,67 @@ +final class BankCardImageFactoryImpl { + + // MARK: - Stored properties + + private let bankCardRegex: [BankCardRegex] = [ + BankCardRegex( + type: .americanExpress, + regex: "^3[47][0-9]{5,}$" + ), + BankCardRegex( + type: .masterCard, + regex: "^5[1-5][0-9]{5,}$" + ), + BankCardRegex( + type: .visa, + regex: "^4[0-9]{6,}$" + ), + BankCardRegex( + type: .mir, + regex: "^220[0-4]\\d+$" + ), + BankCardRegex( + type: .maestro, + regex: "^(5018|5020|5038|5612|5893|6304|6759|6761|6762|6763|0604|6390)\\d+$" + ), + ] +} + +extension BankCardImageFactoryImpl: BankCardImageFactory { + + // MARK: - Make bank card image from card mask + + func makeImage( + _ cardMask: String + ) -> UIImage? { + guard let cardType = cardTypeFromCardMask(cardMask) else { + return nil + } + + let image: UIImage + switch cardType { + case .americanExpress: + image = PaymentMethodResources.Image.americanExpress + case .masterCard: + image = PaymentMethodResources.Image.mastercard + case .visa: + image = PaymentMethodResources.Image.visa + case .mir: + image = PaymentMethodResources.Image.mir + case .maestro: + image = PaymentMethodResources.Image.maestro + } + return image + } + + private func cardTypeFromCardMask( + _ cardMask: String + ) -> BankCardRegexType? { + for bankCard in bankCardRegex { + let predicate = NSPredicate(format: "SELF MATCHES %@", bankCard.regex) + if predicate.evaluate(with: cardMask) { + return bankCard.type + } + } + return nil + } +} diff --git a/YooKassaPayments/Private/Factory/Deeplink/Deeplink.swift b/YooKassaPayments/Private/Factory/Deeplink/Deeplink.swift new file mode 100644 index 00000000..8796b1c8 --- /dev/null +++ b/YooKassaPayments/Private/Factory/Deeplink/Deeplink.swift @@ -0,0 +1,10 @@ +/// Model for open app with specific deeplink. +enum DeepLink { + /// Открывает экран завершения оплаты через SberPay. + /// - Example: `scheme://invoicing/sberpay` + case invoicingSberpay + + /// Открывает окончание авторизации в приложении YooMoney с криптограмой. + /// - Example: `scheme://yoomoney/exchange?cryptogram=someCryptogram` + case yooMoneyExchange(cryptogram: String) +} diff --git a/YooKassaPayments/Private/Factory/Deeplink/DeeplinkFactory.swift b/YooKassaPayments/Private/Factory/Deeplink/DeeplinkFactory.swift new file mode 100644 index 00000000..9dcecfd5 --- /dev/null +++ b/YooKassaPayments/Private/Factory/Deeplink/DeeplinkFactory.swift @@ -0,0 +1,57 @@ +import Foundation + +enum DeepLinkFactory { + + static let invoicingHost = "invoicing" + static let sberpayPath = "sberpay" + + enum YooMoney { + static let host = "yoomoney" + enum exchange { + static let firstPath = "exchange" + static let cryptogram = "cryptogram" + } + } + + // swiftlint:disable:next cyclomatic_complexity + static func makeDeepLink(url: URL) -> DeepLink? { + guard + let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + let host = components.host + else { return nil } + + let firstPathComponent = components.path + .split(separator: "/") + .filter { !$0.isEmpty } + .map(String.init) + .first + + let query = components + .queryItems? + .map { ($0.name, $0.value) } + .reduce(into: [:]) { $0[$1.0] = $1.1 } + ?? [:] + + let action = components.fragment + + let deepLink: DeepLink? + + switch (host, firstPathComponent, query, action) { + case (invoicingHost, sberpayPath, _, _): + deepLink = .invoicingSberpay + + case (YooMoney.host, YooMoney.exchange.firstPath, query, _): + guard let cryptogram = query["cryptogram"], + !cryptogram.isEmpty else { + deepLink = nil + break + } + deepLink = .yooMoneyExchange(cryptogram: cryptogram) + + default: + deepLink = nil + } + + return deepLink + } +} diff --git a/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactoryImpl.swift b/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactoryImpl.swift index a7433645..f11246cb 100644 --- a/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactoryImpl.swift +++ b/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactoryImpl.swift @@ -162,7 +162,7 @@ extension PaymentMethodViewModelFactoryImpl: PaymentMethodViewModelFactory { case .applePay: name = §PaymentMethodResources.Localized.applePay case .sberbank: - name = §PaymentMethodResources.Localized.sberbank + name = §PaymentMethodResources.Localized.sberpay default: assertionFailure("Unsupported PaymentMethodType") name = "Unsupported" @@ -209,7 +209,7 @@ extension PaymentMethodViewModelFactoryImpl: PaymentMethodViewModelFactory { case .applePay: image = PaymentMethodResources.Image.applePay case .sberbank: - image = PaymentMethodResources.Image.sberbank + image = PaymentMethodResources.Image.sberpay default: assertionFailure("Unsupported PaymentMethodType") image = UIImage() diff --git a/YooKassaPayments/Private/Factory/PriceViewModelFactory.swift b/YooKassaPayments/Private/Factory/PriceViewModelFactory.swift new file mode 100644 index 00000000..46bfcba9 --- /dev/null +++ b/YooKassaPayments/Private/Factory/PriceViewModelFactory.swift @@ -0,0 +1,74 @@ +import YooKassaPaymentsApi + +enum PriceViewModelFactoryAssembly { + static func makeFactory() -> PriceViewModelFactory { + PriceViewModelFactoryImpl() + } +} + +protocol PriceViewModelFactory { + func makeAmountPriceViewModel( + _ paymentOption: PaymentOption + ) -> PriceViewModel + + func makeFeePriceViewModel( + _ paymentOption: PaymentOption + ) -> PriceViewModel? +} + +final class PriceViewModelFactoryImpl {} + +// MARK: - PriceViewModelFactory + +extension PriceViewModelFactoryImpl: PriceViewModelFactory { + func makeAmountPriceViewModel( + _ paymentOption: PaymentOption + ) -> PriceViewModel { + let amountString = paymentOption.charge.value.description + var integerPart = "" + var fractionalPart = "" + + if let separatorIndex = amountString.firstIndex(of: ".") { + integerPart = String(amountString[amountString.startIndex.. PriceViewModel? { + guard let fee = paymentOption.fee, + let service = fee.service else { return nil } + + let amountString = service.charge.value.description + var integerPart = "" + var fractionalPart = "" + + if let separatorIndex = amountString.firstIndex(of: ".") { + integerPart = String(amountString[amountString.startIndex.. UIViewController { - let presenter = CardSecPresenter() + let presenter = CardSecPresenter( + isConfirmation: inputData.isConfirmation + ) let analyticsService = AnalyticsServiceAssembly.makeService( isLoggingEnabled: inputData.isLoggingEnabled diff --git a/YooKassaPayments/Private/Modules/3DS/CardSecModuleIO.swift b/YooKassaPayments/Private/Modules/3DS/CardSecModuleIO.swift index 414d9581..7a849035 100644 --- a/YooKassaPayments/Private/Modules/3DS/CardSecModuleIO.swift +++ b/YooKassaPayments/Private/Modules/3DS/CardSecModuleIO.swift @@ -5,24 +5,30 @@ struct CardSecModuleInputData { let requestUrl: String let redirectUrl: String let isLoggingEnabled: Bool + let isConfirmation: Bool // MARK: - Init init( requestUrl: String, redirectUrl: String, - isLoggingEnabled: Bool + isLoggingEnabled: Bool, + isConfirmation: Bool ) { self.requestUrl = requestUrl self.redirectUrl = redirectUrl self.isLoggingEnabled = isLoggingEnabled + self.isConfirmation = isConfirmation } } protocol CardSecModuleInput: class {} protocol CardSecModuleOutput: class { - func didSuccessfullyPassedCardSec(on module: CardSecModuleInput) + func didSuccessfullyPassedCardSec( + on module: CardSecModuleInput, + isConfirmation: Bool + ) func didPressCloseButton(on module: CardSecModuleInput) func viewWillDisappear() } diff --git a/YooKassaPayments/Private/Modules/3DS/Presenter/CardSecPresenter.swift b/YooKassaPayments/Private/Modules/3DS/Presenter/CardSecPresenter.swift index 1daf3ee6..94b24af8 100644 --- a/YooKassaPayments/Private/Modules/3DS/Presenter/CardSecPresenter.swift +++ b/YooKassaPayments/Private/Modules/3DS/Presenter/CardSecPresenter.swift @@ -8,6 +8,16 @@ final class CardSecPresenter: WebBrowserPresenter { // MARK: - Business logic properties private var shouldCallDidSuccessfullyPassedCardSec = true + + // MARK: - Init data + + private let isConfirmation: Bool + + // MARK: - Init + + init(isConfirmation: Bool) { + self.isConfirmation = isConfirmation + } // MARK: - Overridden funcs @@ -38,7 +48,10 @@ extension CardSecPresenter: CardSecInteractorOutput { func didSuccessfullyPassedCardSec() { guard shouldCallDidSuccessfullyPassedCardSec else { return } shouldCallDidSuccessfullyPassedCardSec = false - cardSecModuleOutput?.didSuccessfullyPassedCardSec(on: self) + cardSecModuleOutput?.didSuccessfullyPassedCardSec( + on: self, + isConfirmation: isConfirmation + ) } } diff --git a/YooKassaPayments/Private/Modules/ApplePayContract/Interactor/ApplePayContractInteractor.swift b/YooKassaPayments/Private/Modules/ApplePayContract/Interactor/ApplePayContractInteractor.swift index cf91d327..847add73 100644 --- a/YooKassaPayments/Private/Modules/ApplePayContract/Interactor/ApplePayContractInteractor.swift +++ b/YooKassaPayments/Private/Modules/ApplePayContract/Interactor/ApplePayContractInteractor.swift @@ -60,7 +60,7 @@ extension ApplePayContractInteractor: ApplePayContractInteractorInput { paymentData: paymentData, savePaymentMethod: savePaymentMethod, amount: amount, - tmxSessionId: tmxSessionId + tmxSessionId: tmxSessionId.value ) case let .failure(error): diff --git a/YooKassaPayments/Private/Modules/ApplePayContract/View/ApplePayContractViewController.swift b/YooKassaPayments/Private/Modules/ApplePayContract/View/ApplePayContractViewController.swift index c51b6903..25df9f87 100644 --- a/YooKassaPayments/Private/Modules/ApplePayContract/View/ApplePayContractViewController.swift +++ b/YooKassaPayments/Private/Modules/ApplePayContract/View/ApplePayContractViewController.swift @@ -365,7 +365,7 @@ extension ApplePayContractViewController: ApplePayContractViewInput { let linkAttributedText = NSMutableAttributedString(string: hyperText, attributes: attributes) let linkRange = NSRange(location: 0, length: hyperText.count) - let fakeLink = URL(string: "https://yookassa.ru") + let fakeLink = URL(string: "https://yookassa.ru")! linkAttributedText.addAttribute(.link, value: fakeLink, range: linkRange) attributedText.append(linkAttributedText) diff --git a/YooKassaPayments/Private/Modules/BankCard/Interactor/BankCardInteractor.swift b/YooKassaPayments/Private/Modules/BankCard/Interactor/BankCardInteractor.swift index 3e19abc6..4dbbfc77 100644 --- a/YooKassaPayments/Private/Modules/BankCard/Interactor/BankCardInteractor.swift +++ b/YooKassaPayments/Private/Modules/BankCard/Interactor/BankCardInteractor.swift @@ -60,8 +60,8 @@ extension BankCardInteractor: BankCardInteractorInput { confirmation: confirmation, savePaymentMethod: savePaymentMethod, amount: self.amount, - tmxSessionId: tmxSessionId - ) { [weak self] result in + tmxSessionId: tmxSessionId.value + ) { result in switch result { case .success(let data): output.didTokenize(data) diff --git a/YooKassaPayments/Private/Modules/BankCard/Presenter/BankCardPresenter.swift b/YooKassaPayments/Private/Modules/BankCard/Presenter/BankCardPresenter.swift index daa94c8e..701a0a01 100644 --- a/YooKassaPayments/Private/Modules/BankCard/Presenter/BankCardPresenter.swift +++ b/YooKassaPayments/Private/Modules/BankCard/Presenter/BankCardPresenter.swift @@ -149,8 +149,7 @@ extension BankCardPresenter: BankCardInteractorOutput { _ data: Tokens ) { DispatchQueue.main.async { [weak self] in - guard let self = self, - let view = self.view else { return } + guard let self = self else { return } self.moduleOutput?.bankCardModule( self, didTokenize: data, diff --git a/YooKassaPayments/Private/Modules/BankCard/View/BankCardViewController.swift b/YooKassaPayments/Private/Modules/BankCard/View/BankCardViewController.swift index b5a3227a..aae7b2d3 100644 --- a/YooKassaPayments/Private/Modules/BankCard/View/BankCardViewController.swift +++ b/YooKassaPayments/Private/Modules/BankCard/View/BankCardViewController.swift @@ -337,7 +337,7 @@ extension BankCardViewController: BankCardViewInput { let linkAttributedText = NSMutableAttributedString(string: hyperText, attributes: attributes) let linkRange = NSRange(location: 0, length: hyperText.count) - let fakeLink = URL(string: "https://yookassa.ru") + let fakeLink = URL(string: "https://yookassa.ru")! linkAttributedText.addAttribute(.link, value: fakeLink, range: linkRange) attributedText.append(linkAttributedText) diff --git a/YooKassaPayments/Private/Modules/BankCardRepeat/Assembly/BankCardRepeatAssembly.swift b/YooKassaPayments/Private/Modules/BankCardRepeat/Assembly/BankCardRepeatAssembly.swift index dbada1af..3fc8d643 100644 --- a/YooKassaPayments/Private/Modules/BankCardRepeat/Assembly/BankCardRepeatAssembly.swift +++ b/YooKassaPayments/Private/Modules/BankCardRepeat/Assembly/BankCardRepeatAssembly.swift @@ -11,8 +11,8 @@ enum BankCardRepeatAssembly { let view = BankCardRepeatViewController() let cardService = CardService() - let paymentMethodViewModelFactory = - PaymentMethodViewModelFactoryAssembly.makeFactory() + let paymentMethodViewModelFactory = PaymentMethodViewModelFactoryAssembly.makeFactory() + let priceViewModelFactory = PriceViewModelFactoryAssembly.makeFactory() let termsOfService = TermsOfServiceFactory.makeTermsOfService() let initialSavePaymentMethod = makeInitialSavePaymentMethod(inputData.savePaymentMethod) let savePaymentMethodViewModel = SavePaymentMethodViewModelFactory.makeSavePaymentMethodViewModel( @@ -22,12 +22,12 @@ enum BankCardRepeatAssembly { let presenter = BankCardRepeatPresenter( cardService: cardService, paymentMethodViewModelFactory: paymentMethodViewModelFactory, + priceViewModelFactory: priceViewModelFactory, isLoggingEnabled: inputData.isLoggingEnabled, returnUrl: inputData.returnUrl, paymentMethodId: inputData.paymentMethodId, shopName: inputData.shopName, purchaseDescription: inputData.purchaseDescription, - amount: inputData.amount, termsOfService: termsOfService, savePaymentMethodViewModel: savePaymentMethodViewModel, initialSavePaymentMethod: initialSavePaymentMethod @@ -45,12 +45,16 @@ enum BankCardRepeatAssembly { testModeSettings: inputData.testModeSettings ) let threatMetrixService = ThreatMetrixServiceFactory.makeService() + let amountNumberFormatter = AmountNumberFormatterAssembly.makeAmountNumberFormatter() let interactor = BankCardRepeatInteractor( analyticsService: analyticsService, analyticsProvider: analyticsProvider, paymentService: paymentService, threatMetrixService: threatMetrixService, - clientApplicationKey: inputData.clientApplicationKey + amountNumberFormatter: amountNumberFormatter, + clientApplicationKey: inputData.clientApplicationKey, + gatewayId: inputData.gatewayId, + amount: inputData.amount ) let router = BankCardRepeatRouter() diff --git a/YooKassaPayments/Private/Modules/BankCardRepeat/BankCardRepeatInteractorIO.swift b/YooKassaPayments/Private/Modules/BankCardRepeat/BankCardRepeatInteractorIO.swift index d1ebcf4a..f91b297e 100644 --- a/YooKassaPayments/Private/Modules/BankCardRepeat/BankCardRepeatInteractorIO.swift +++ b/YooKassaPayments/Private/Modules/BankCardRepeat/BankCardRepeatInteractorIO.swift @@ -1,3 +1,5 @@ +import YooKassaPaymentsApi + protocol BankCardRepeatInteractorInput: AnalyticsTrack, AnalyticsProvider { func fetchPaymentMethod( paymentMethodId: String @@ -9,6 +11,8 @@ protocol BankCardRepeatInteractorInput: AnalyticsTrack, AnalyticsProvider { paymentMethodId: String, csc: String ) + + func fetchPaymentMethods() } protocol BankCardRepeatInteractorOutput: class { @@ -18,11 +22,18 @@ protocol BankCardRepeatInteractorOutput: class { func didFailFetchPaymentMethod( _ error: Error ) - + func didTokenize( _ tokens: Tokens ) func didFailTokenize( _ error: Error ) + + func didFetchPaymentMethods( + _ paymentMethods: [PaymentOption] + ) + func didFetchPaymentMethods( + _ error: Error + ) } diff --git a/YooKassaPayments/Private/Modules/BankCardRepeat/Interactor/BankCardRepeatInteractor.swift b/YooKassaPayments/Private/Modules/BankCardRepeat/Interactor/BankCardRepeatInteractor.swift index 7053c963..1457ef6a 100644 --- a/YooKassaPayments/Private/Modules/BankCardRepeat/Interactor/BankCardRepeatInteractor.swift +++ b/YooKassaPayments/Private/Modules/BankCardRepeat/Interactor/BankCardRepeatInteractor.swift @@ -12,8 +12,11 @@ final class BankCardRepeatInteractor { private let analyticsProvider: AnalyticsProvider private let paymentService: PaymentService private let threatMetrixService: ThreatMetrixService + private let amountNumberFormatter: AmountNumberFormatter private let clientApplicationKey: String + private let gatewayId: String? + private let amount: Amount // MARK: - Init @@ -22,14 +25,20 @@ final class BankCardRepeatInteractor { analyticsProvider: AnalyticsProvider, paymentService: PaymentService, threatMetrixService: ThreatMetrixService, - clientApplicationKey: String + amountNumberFormatter: AmountNumberFormatter, + clientApplicationKey: String, + gatewayId: String?, + amount: Amount ) { self.analyticsService = analyticsService self.analyticsProvider = analyticsProvider self.paymentService = paymentService self.threatMetrixService = threatMetrixService + self.amountNumberFormatter = amountNumberFormatter self.clientApplicationKey = clientApplicationKey + self.gatewayId = gatewayId + self.amount = amount } } @@ -69,7 +78,7 @@ extension BankCardRepeatInteractor: BankCardRepeatInteractorInput { self.paymentService.tokenizeRepeatBankCard( clientApplicationKey: self.clientApplicationKey, amount: amount, - tmxSessionId: tmxSessionId, + tmxSessionId: tmxSessionId.value, confirmation: confirmation, savePaymentMethod: savePaymentMethod, paymentMethodId: paymentMethodId, @@ -91,6 +100,25 @@ extension BankCardRepeatInteractor: BankCardRepeatInteractorInput { } } + func fetchPaymentMethods() { + paymentService.fetchPaymentOptions( + clientApplicationKey: clientApplicationKey, + authorizationToken: nil, + gatewayId: gatewayId, + amount: amountNumberFormatter.string(from: amount.value), + currency: amount.currency.rawValue, + getSavePaymentMethod: false + ) { [weak self] result in + guard let output = self?.output else { return } + switch result { + case let .success(data): + output.didFetchPaymentMethods(data) + case let .failure(error): + output.didFetchPaymentMethods(error) + } + } + } + func trackEvent(_ event: AnalyticsEvent) { analyticsService.trackEvent(event) } diff --git a/YooKassaPayments/Private/Modules/BankCardRepeat/Presenter/BankCardRepeatPresenter.swift b/YooKassaPayments/Private/Modules/BankCardRepeat/Presenter/BankCardRepeatPresenter.swift index 0db914a9..086378ef 100644 --- a/YooKassaPayments/Private/Modules/BankCardRepeat/Presenter/BankCardRepeatPresenter.swift +++ b/YooKassaPayments/Private/Modules/BankCardRepeat/Presenter/BankCardRepeatPresenter.swift @@ -9,20 +9,12 @@ final class BankCardRepeatPresenter { weak var moduleOutput: TokenizationModuleOutput? weak var view: BankCardRepeatViewInput? - - // MARK: - Number formatter - - private lazy var numberFormatter: NumberFormatter = { - let numberFormatter = NumberFormatter() - numberFormatter.maximumFractionDigits = 2 - numberFormatter.decimalSeparator = Constants.decimalSeparator - return numberFormatter - }() // MARK: - Init data private let cardService: CardService private let paymentMethodViewModelFactory: PaymentMethodViewModelFactory + private let priceViewModelFactory: PriceViewModelFactory private let isLoggingEnabled: Bool private let returnUrl: String? @@ -30,7 +22,6 @@ final class BankCardRepeatPresenter { private let paymentMethodId: String private let shopName: String private let purchaseDescription: String - private let amount: Amount private let termsOfService: TermsOfService private let savePaymentMethodViewModel: SavePaymentMethodViewModel? private var initialSavePaymentMethod: Bool @@ -40,18 +31,19 @@ final class BankCardRepeatPresenter { init( cardService: CardService, paymentMethodViewModelFactory: PaymentMethodViewModelFactory, + priceViewModelFactory: PriceViewModelFactory, isLoggingEnabled: Bool, returnUrl: String?, paymentMethodId: String, shopName: String, purchaseDescription: String, - amount: Amount, termsOfService: TermsOfService, savePaymentMethodViewModel: SavePaymentMethodViewModel?, initialSavePaymentMethod: Bool ) { self.cardService = cardService self.paymentMethodViewModelFactory = paymentMethodViewModelFactory + self.priceViewModelFactory = priceViewModelFactory self.isLoggingEnabled = isLoggingEnabled self.returnUrl = returnUrl @@ -59,7 +51,6 @@ final class BankCardRepeatPresenter { self.paymentMethodId = paymentMethodId self.shopName = shopName self.purchaseDescription = purchaseDescription - self.amount = amount self.termsOfService = termsOfService self.savePaymentMethodViewModel = savePaymentMethodViewModel self.initialSavePaymentMethod = initialSavePaymentMethod @@ -68,6 +59,7 @@ final class BankCardRepeatPresenter { // MARK: - Stored Data private var paymentMethod: PaymentMethod? + private var paymentOption: PaymentOption? private var csc: String? } @@ -76,13 +68,12 @@ final class BankCardRepeatPresenter { extension BankCardRepeatPresenter: BankCardRepeatViewOutput { func setupView() { guard let view = view else { return } - + view.showActivity() - + DispatchQueue.global().async { [weak self] in guard let self = self, let interactor = self.interactor else { return } - let (authType, _) = interactor.makeTypeAnalyticsParameters() let event: AnalyticsEvent = .screenPaymentContract( authType: authType, @@ -90,9 +81,7 @@ extension BankCardRepeatPresenter: BankCardRepeatViewOutput { sdkVersion: Bundle.frameworkVersion ) interactor.trackEvent(event) - interactor.fetchPaymentMethod( - paymentMethodId: self.paymentMethodId - ) + interactor.fetchPaymentMethods() } } @@ -134,7 +123,7 @@ extension BankCardRepeatPresenter: BankCardRepeatViewOutput { do { try self.cardService.validate(csc: csc) } catch { - if let error = error as? CardService.ValidationError { + if error is CardService.ValidationError { DispatchQueue.main.async { [weak self] in guard let view = self?.view else { return } view.setConfirmButtonEnabled(false) @@ -160,7 +149,7 @@ extension BankCardRepeatPresenter: BankCardRepeatViewOutput { do { try self.cardService.validate(csc: csc) } catch { - if let error = error as? CardService.ValidationError { + if error is CardService.ValidationError { DispatchQueue.main.async { [weak self] in guard let view = self?.view else { return } view.setCardState(.error) @@ -183,7 +172,7 @@ extension BankCardRepeatPresenter: BankCardRepeatInteractorOutput { _ paymentMethod: PaymentMethod ) { self.paymentMethod = paymentMethod - + guard let card = paymentMethod.card, card.first6.isEmpty == false, card.last4.isEmpty == false else { @@ -196,17 +185,22 @@ extension BankCardRepeatPresenter: BankCardRepeatInteractorOutput { } return } - + + guard let paymentOption = self.paymentOption else { + assertionFailure("PaymentOption should be") + return + } + let cardMask = card.first6 + "••••••" + card.last4 let cardLogo = paymentMethodViewModelFactory.makeBankCardImage(card) - + let priceViewModel = priceViewModelFactory.makeAmountPriceViewModel(paymentOption) + let feeViewModel = priceViewModelFactory.makeFeePriceViewModel(paymentOption) + let viewModel = BankCardRepeatViewModel( shopName: shopName, description: purchaseDescription, - price: makePriceViewModel( - amount, - numberFormatter: numberFormatter - ), + price: priceViewModel, + fee: feeViewModel, cardMask: formattingCardMask(cardMask), cardLogo: cardLogo, terms: termsOfService @@ -215,16 +209,16 @@ extension BankCardRepeatPresenter: BankCardRepeatInteractorOutput { DispatchQueue.main.async { [weak self] in guard let self = self, let view = self.view else { return } - + view.hideActivity() - + view.setupViewModel(viewModel) view.setConfirmButtonEnabled(false) - + if let savePaymentMethodViewModel = self.savePaymentMethodViewModel { view.setSavePaymentMethodViewModel(savePaymentMethodViewModel) } - + DispatchQueue.global().async { [weak self] in let event: AnalyticsEvent = .screenRecurringCardForm( sdkVersion: Bundle.frameworkVersion @@ -235,11 +229,9 @@ extension BankCardRepeatPresenter: BankCardRepeatInteractorOutput { } func didFailFetchPaymentMethod(_ error: Error) { - let authType = AnalyticsEvent.AuthType.withoutAuth - let scheme = AnalyticsEvent.TokenizeScheme.recurringCard let event = AnalyticsEvent.screenError( - authType: authType, - scheme: scheme, + authType: .withoutAuth, + scheme: .recurringCard, sdkVersion: Bundle.frameworkVersion ) interactor.trackEvent(event) @@ -264,7 +256,7 @@ extension BankCardRepeatPresenter: BankCardRepeatInteractorOutput { guard let self = self, let interactor = self.interactor else { return } let type = interactor.makeTypeAnalyticsParameters() let event: AnalyticsEvent = .actionTokenize( - scheme: .bankCard, + scheme: .recurringCard, authType: type.authType, tokenType: type.tokenType, sdkVersion: Bundle.frameworkVersion @@ -283,6 +275,34 @@ extension BankCardRepeatPresenter: BankCardRepeatInteractorOutput { } } + func didFetchPaymentMethods(_ paymentMethods: [PaymentOption]) { + guard let bankCard = paymentMethods.first(where: { + $0.paymentMethodType == .bankCard + }) else { + DispatchQueue.main.async { [weak self] in + guard let self = self, + let view = self.view else { return } + view.hideActivity() + view.showPlaceholder() + } + return + } + self.paymentOption = bankCard + interactor.fetchPaymentMethod( + paymentMethodId: paymentMethodId + ) + } + + func didFetchPaymentMethods(_ error: Error) { + let message = makeMessage(error) + + DispatchQueue.main.async { [weak self] in + guard let view = self?.view else { return } + view.hideActivity() + view.showPlaceholder(with: message) + } + } + private func tokenize() { guard let csc = csc else { return } @@ -290,9 +310,14 @@ extension BankCardRepeatPresenter: BankCardRepeatInteractorOutput { type: .redirect, returnUrl: returnUrl ?? GlobalConstants.returnUrl ) - + + guard let paymentOption = paymentOption else { + assertionFailure("PaymentOption should be") + return + } + interactor.tokenize( - amount: MonetaryAmountFactory.makeMonetaryAmount(amount), + amount: paymentOption.charge.plain, confirmation: confirmation, savePaymentMethod: initialSavePaymentMethod, paymentMethodId: paymentMethodId, @@ -310,10 +335,12 @@ extension BankCardRepeatPresenter: ActionTitleTextDialogDelegate { guard let view = view else { return } view.hidePlaceholder() view.showActivity() - + DispatchQueue.global().async { [weak self] in guard let self = self else { return } - if self.paymentMethod == nil { + if self.paymentOption == nil { + self.interactor.fetchPaymentMethods() + } else if self.paymentMethod == nil { self.interactor.fetchPaymentMethod( paymentMethodId: self.paymentMethodId ) @@ -333,7 +360,27 @@ extension BankCardRepeatPresenter: TokenizationModuleInput { let moduleInputData = CardSecModuleInputData( requestUrl: requestUrl, redirectUrl: returnUrl ?? GlobalConstants.returnUrl, - isLoggingEnabled: isLoggingEnabled + isLoggingEnabled: isLoggingEnabled, + isConfirmation: false + ) + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.router.present3dsModule( + inputData: moduleInputData, + moduleOutput: self + ) + } + } + + func startConfirmationProcess( + confirmationUrl: String, + paymentMethodType: PaymentMethodType + ) { + let moduleInputData = CardSecModuleInputData( + requestUrl: confirmationUrl, + redirectUrl: returnUrl ?? GlobalConstants.returnUrl, + isLoggingEnabled: isLoggingEnabled, + isConfirmation: true ) DispatchQueue.main.async { [weak self] in guard let self = self else { return } @@ -348,8 +395,19 @@ extension BankCardRepeatPresenter: TokenizationModuleInput { // MARK: - CardSecModuleOutput extension BankCardRepeatPresenter: CardSecModuleOutput { - func didSuccessfullyPassedCardSec(on module: CardSecModuleInput) { - moduleOutput?.didSuccessfullyPassedCardSec(on: self) + func didSuccessfullyPassedCardSec( + on module: CardSecModuleInput, + isConfirmation: Bool + ) { + if isConfirmation { + moduleOutput?.didSuccessfullyConfirmation( + paymentMethodType: .bankCard + ) + } else { + moduleOutput?.didSuccessfullyPassedCardSec( + on: self + ) + } } func didPressCloseButton(on module: CardSecModuleInput) { @@ -378,30 +436,6 @@ extension BankCardRepeatPresenter: BankCardRepeatModuleInput { // MARK: - Private global helpers -private func makePriceViewModel( - _ amount: Amount, - numberFormatter: NumberFormatter -) -> PriceViewModel { - let decimalNumber = NSDecimalNumber(decimal: amount.value) - let amountString = numberFormatter.string(from: decimalNumber) ?? amount.value.description - var integerPart = "" - var fractionalPart = "" - - if let separatorIndex = amountString.firstIndex(of: Character(Constants.decimalSeparator)) { - integerPart = String(amountString[amountString.startIndex.. String { let message: String diff --git a/YooKassaPayments/Private/Modules/BankCardRepeat/View/BankCardRepeatViewController.swift b/YooKassaPayments/Private/Modules/BankCardRepeat/View/BankCardRepeatViewController.swift index f989109e..0d8d0f9b 100644 --- a/YooKassaPayments/Private/Modules/BankCardRepeat/View/BankCardRepeatViewController.swift +++ b/YooKassaPayments/Private/Modules/BankCardRepeat/View/BankCardRepeatViewController.swift @@ -418,8 +418,12 @@ extension BankCardRepeatViewController: BankCardRepeatViewInput { orderView.title = viewModel.shopName orderView.subtitle = viewModel.description orderView.value = makePrice(viewModel.price) - orderView.subvalue = nil - + if let fee = viewModel.fee { + orderView.subvalue = "\(§Localized.fee) " + makePrice(fee) + } else { + orderView.subvalue = nil + } + maskedCardView.cardNumber = viewModel.cardMask maskedCardView.cardLogo = viewModel.cardLogo @@ -533,7 +537,7 @@ extension BankCardRepeatViewController: BankCardRepeatViewInput { let linkAttributedText = NSMutableAttributedString(string: hyperText, attributes: attributes) let linkRange = NSRange(location: 0, length: hyperText.count) - let fakeLink = URL(string: "https://yookassa.ru") + let fakeLink = URL(string: "https://yookassa.ru")! linkAttributedText.addAttribute(.link, value: fakeLink, range: linkRange) attributedText.append(linkAttributedText) diff --git a/YooKassaPayments/Private/Modules/BankCardRepeat/View/ViewModel/BankCardRepeatViewModel.swift b/YooKassaPayments/Private/Modules/BankCardRepeat/View/ViewModel/BankCardRepeatViewModel.swift index 156a1c32..a9c5f280 100644 --- a/YooKassaPayments/Private/Modules/BankCardRepeat/View/ViewModel/BankCardRepeatViewModel.swift +++ b/YooKassaPayments/Private/Modules/BankCardRepeat/View/ViewModel/BankCardRepeatViewModel.swift @@ -2,6 +2,7 @@ struct BankCardRepeatViewModel { let shopName: String let description: String? let price: PriceViewModel + let fee: PriceViewModel? let cardMask: String let cardLogo: UIImage let terms: TermsOfService diff --git a/YooKassaPayments/Private/Modules/LinkedCard/Interactor/LinkedCardInteractor.swift b/YooKassaPayments/Private/Modules/LinkedCard/Interactor/LinkedCardInteractor.swift index 5d11f965..3a5e321e 100644 --- a/YooKassaPayments/Private/Modules/LinkedCard/Interactor/LinkedCardInteractor.swift +++ b/YooKassaPayments/Private/Modules/LinkedCard/Interactor/LinkedCardInteractor.swift @@ -94,7 +94,7 @@ extension LinkedCardInteractor: LinkedCardInteractorInput { savePaymentMethod: savePaymentMethod, paymentMethodType: paymentMethodType, amount: amount, - tmxSessionId: tmxSessionId + tmxSessionId: tmxSessionId.value ) case let .failure(error): diff --git a/YooKassaPayments/Private/Modules/LinkedCard/Presenter/LinkedCardPresenter.swift b/YooKassaPayments/Private/Modules/LinkedCard/Presenter/LinkedCardPresenter.swift index 1a04854b..e8d0a11c 100644 --- a/YooKassaPayments/Private/Modules/LinkedCard/Presenter/LinkedCardPresenter.swift +++ b/YooKassaPayments/Private/Modules/LinkedCard/Presenter/LinkedCardPresenter.swift @@ -126,7 +126,7 @@ extension LinkedCardPresenter: LinkedCardViewOutput { do { try self.cardService.validate(csc: csc) } catch { - if let error = error as? CardService.ValidationError { + if error is CardService.ValidationError { DispatchQueue.main.async { [weak self] in guard let view = self?.view else { return } view.setConfirmButtonEnabled(false) @@ -179,7 +179,7 @@ extension LinkedCardPresenter: LinkedCardViewOutput { do { try self.cardService.validate(csc: csc) } catch { - if let error = error as? CardService.ValidationError { + if error is CardService.ValidationError { DispatchQueue.main.async { [weak self] in guard let view = self?.view else { return } view.setCardState(.error) diff --git a/YooKassaPayments/Private/Modules/LinkedCard/View/LinkedCardViewController.swift b/YooKassaPayments/Private/Modules/LinkedCard/View/LinkedCardViewController.swift index ec437f23..d9f328ab 100644 --- a/YooKassaPayments/Private/Modules/LinkedCard/View/LinkedCardViewController.swift +++ b/YooKassaPayments/Private/Modules/LinkedCard/View/LinkedCardViewController.swift @@ -271,8 +271,6 @@ final class LinkedCardViewController: UIViewController, PlaceholderProvider { ) } - scrollViewHeightConstraint.priority = .defaultLow - let constraints = [ scrollViewHeightConstraint, diff --git a/YooKassaPayments/Private/Modules/PaymentAuthorization/View/PaymentAuthorizationViewController.swift b/YooKassaPayments/Private/Modules/PaymentAuthorization/View/PaymentAuthorizationViewController.swift index 64010823..c52bb36b 100644 --- a/YooKassaPayments/Private/Modules/PaymentAuthorization/View/PaymentAuthorizationViewController.swift +++ b/YooKassaPayments/Private/Modules/PaymentAuthorization/View/PaymentAuthorizationViewController.swift @@ -6,6 +6,10 @@ final class PaymentAuthorizationViewController: UIViewController, PlaceholderPro // MARK: - UI properties + private lazy var shouldShowTitleOnNavBar: Bool = { + return UIScreen.main.isShort + }() + private lazy var titleLabel: UILabel = { $0.translatesAutoresizingMaskIntoConstraints = false $0.styledText = §Localized.smsCodePlaceholder @@ -97,8 +101,13 @@ final class PaymentAuthorizationViewController: UIViewController, PlaceholderPro override func loadView() { view = UIView() view.setStyles(UIView.Styles.grayBackground) + setupView() setupConstraints() + + if shouldShowTitleOnNavBar { + navigationItem.title = §Localized.smsCodePlaceholder + } } override func viewDidLoad() { @@ -113,7 +122,7 @@ final class PaymentAuthorizationViewController: UIViewController, PlaceholderPro override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - codeControl.becomeFirstResponder() + _ = codeControl.becomeFirstResponder() } override func viewWillDisappear(_ animated: Bool) { @@ -124,8 +133,11 @@ final class PaymentAuthorizationViewController: UIViewController, PlaceholderPro // MARK: - Setup private func setupView() { + if !shouldShowTitleOnNavBar { + view.addSubview(titleLabel) + } + [ - titleLabel, codeControl, codeErrorLabel, descriptionLabel, @@ -135,12 +147,20 @@ final class PaymentAuthorizationViewController: UIViewController, PlaceholderPro } private func setupConstraints() { - let titleLabelTopConstraint: NSLayoutConstraint + let topConstraint: NSLayoutConstraint if #available(iOS 11.0, *) { - titleLabelTopConstraint = titleLabel.topAnchor.constraint( - equalTo: view.safeAreaLayoutGuide.topAnchor, - constant: Space.single / 4 - ) + if shouldShowTitleOnNavBar { + topConstraint = codeControl.topAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.topAnchor, + constant: 2 * Space.triple + ) + } else { + topConstraint = titleLabel.topAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.topAnchor, + constant: Space.single / 4 + ) + } + resendButtonBottomConstraint = resendCodeButton.bottomAnchor.constraint( equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -Space.double @@ -149,10 +169,18 @@ final class PaymentAuthorizationViewController: UIViewController, PlaceholderPro equalTo: view.safeAreaLayoutGuide.bottomAnchor ) } else { - titleLabelTopConstraint = titleLabel.topAnchor.constraint( - equalTo: topLayoutGuide.bottomAnchor, - constant: Space.single / 4 - ) + if shouldShowTitleOnNavBar { + topConstraint = codeControl.topAnchor.constraint( + equalTo: topLayoutGuide.bottomAnchor, + constant: 2 * Space.triple + ) + } else { + topConstraint = titleLabel.topAnchor.constraint( + equalTo: topLayoutGuide.bottomAnchor, + constant: Space.single / 4 + ) + } + resendButtonBottomConstraint = resendCodeButton.bottomAnchor.constraint( equalTo: bottomLayoutGuide.topAnchor, constant: -Space.double @@ -162,21 +190,8 @@ final class PaymentAuthorizationViewController: UIViewController, PlaceholderPro ) } - let constraints = [ - titleLabelTopConstraint, - titleLabel.leadingAnchor.constraint( - equalTo: view.leadingAnchor, - constant: Space.double - ), - titleLabel.trailingAnchor.constraint( - equalTo: view.trailingAnchor, - constant: -Space.double - ), - - codeControl.topAnchor.constraint( - equalTo: titleLabel.bottomAnchor, - constant: 2 * Space.triple - ), + var constraints = [ + topConstraint, codeControl.centerXAnchor.constraint(equalTo: view.centerXAnchor), codeErrorLabel.topAnchor.constraint( @@ -217,6 +232,25 @@ final class PaymentAuthorizationViewController: UIViewController, PlaceholderPro placeholderView.trailingAnchor.constraint(equalTo: view.trailingAnchor), placeholderViewBottomConstraint, ] + + if !shouldShowTitleOnNavBar { + constraints += [ + titleLabel.leadingAnchor.constraint( + equalTo: view.leadingAnchor, + constant: Space.double + ), + titleLabel.trailingAnchor.constraint( + equalTo: view.trailingAnchor, + constant: -Space.double + ), + + codeControl.topAnchor.constraint( + equalTo: titleLabel.bottomAnchor, + constant: 2 * Space.triple + ), + ] + } + NSLayoutConstraint.activate(constraints) } diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/Assembly/PaymentMethodsAssembly.swift b/YooKassaPayments/Private/Modules/PaymentMethods/Assembly/PaymentMethodsAssembly.swift index d3428c36..d059651b 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/Assembly/PaymentMethodsAssembly.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/Assembly/PaymentMethodsAssembly.swift @@ -1,4 +1,5 @@ import UIKit.UIViewController +import MoneyAuth import YooMoneyCoreApi enum PaymentMethodsAssembly { @@ -19,9 +20,12 @@ enum PaymentMethodsAssembly { let moneyAuthCustomization = MoneyAuthAssembly.makeMoneyAuthCustomization() let paymentMethodViewModelFactory = PaymentMethodViewModelFactoryAssembly.makeFactory() + let priceViewModelFactory = PriceViewModelFactoryAssembly.makeFactory() let presenter = PaymentMethodsPresenter( isLogoVisible: inputData.tokenizationSettings.showYooKassaLogo, paymentMethodViewModelFactory: paymentMethodViewModelFactory, + applicationScheme: inputData.applicationScheme, + priceViewModelFactory: priceViewModelFactory, clientApplicationKey: inputData.clientApplicationKey, applePayMerchantIdentifier: inputData.applePayMerchantIdentifier, testModeSettings: inputData.testModeSettings, @@ -51,18 +55,27 @@ enum PaymentMethodsAssembly { let analyticsService = AnalyticsServiceAssembly.makeService( isLoggingEnabled: inputData.isLoggingEnabled ) + let accountService = AccountServiceFactory.makeService( + config: moneyAuthConfig + ) let analyticsProvider = AnalyticsProviderAssembly.makeProvider( testModeSettings: inputData.testModeSettings ) let threatMetrixService = ThreatMetrixServiceFactory.makeService() let amountNumberFormatter = AmountNumberFormatterAssembly.makeAmountNumberFormatter() + let appDataTransferMediator = AppDataTransferMediatorFactory.makeMediator( + config: moneyAuthConfig + ) + let interactor = PaymentMethodsInteractor( paymentService: paymentService, authorizationService: authorizationService, analyticsService: analyticsService, + accountService: accountService, analyticsProvider: analyticsProvider, threatMetrixService: threatMetrixService, amountNumberFormatter: amountNumberFormatter, + appDataTransferMediator: appDataTransferMediator, clientApplicationKey: inputData.clientApplicationKey, gatewayId: inputData.gatewayId, amount: inputData.amount, diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/Interactor/PaymentMethodsInteractor.swift b/YooKassaPayments/Private/Modules/PaymentMethods/Interactor/PaymentMethodsInteractor.swift index 94677757..6c30f179 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/Interactor/PaymentMethodsInteractor.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/Interactor/PaymentMethodsInteractor.swift @@ -12,9 +12,11 @@ class PaymentMethodsInteractor { private let paymentService: PaymentService private let authorizationService: AuthorizationService private let analyticsService: AnalyticsService + private let accountService: AccountService private let analyticsProvider: AnalyticsProvider private let threatMetrixService: ThreatMetrixService private let amountNumberFormatter: AmountNumberFormatter + private let appDataTransferMediator: AppDataTransferMediator private let clientApplicationKey: String private let gatewayId: String? @@ -27,9 +29,11 @@ class PaymentMethodsInteractor { paymentService: PaymentService, authorizationService: AuthorizationService, analyticsService: AnalyticsService, + accountService: AccountService, analyticsProvider: AnalyticsProvider, threatMetrixService: ThreatMetrixService, amountNumberFormatter: AmountNumberFormatter, + appDataTransferMediator: AppDataTransferMediator, clientApplicationKey: String, gatewayId: String?, amount: Amount, @@ -38,9 +42,11 @@ class PaymentMethodsInteractor { self.paymentService = paymentService self.authorizationService = authorizationService self.analyticsService = analyticsService + self.accountService = accountService self.analyticsProvider = analyticsProvider self.threatMetrixService = threatMetrixService self.amountNumberFormatter = amountNumberFormatter + self.appDataTransferMediator = appDataTransferMediator self.clientApplicationKey = clientApplicationKey self.gatewayId = gatewayId @@ -94,6 +100,34 @@ extension PaymentMethodsInteractor: PaymentMethodsInteractorInput { } } + func fetchAccount( + oauthToken: String + ) { + accountService.fetchAccount( + oauthToken: oauthToken + ) { [weak self] in + guard let output = self?.output else { return } + $0.map { + output.didFetchAccount($0) + }.mapLeft { + output.didFailFetchAccount($0) + } + } + } + + func decryptCryptogram( + _ cryptogram: String + ) { + appDataTransferMediator.decryptData(cryptogram) { [weak self] in + guard let output = self?.output else { return } + $0.map { + output.didDecryptCryptogram($0) + }.mapLeft { + output.didFailDecryptCryptogram($0) + } + } + } + func getWalletDisplayName() -> String? { return authorizationService.getWalletDisplayName() } @@ -140,7 +174,7 @@ extension PaymentMethodsInteractor { paymentData: paymentData, savePaymentMethod: savePaymentMethod, amount: amount, - tmxSessionId: tmxSessionId + tmxSessionId: tmxSessionId.value ) case let .failure(error): diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsInteractorIO.swift b/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsInteractorIO.swift index e01d53a2..3b66dc99 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsInteractorIO.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsInteractorIO.swift @@ -8,6 +8,14 @@ protocol PaymentMethodsInteractorInput: AnalyticsTrack, AnalyticsProvider { moneyCenterAuthToken: String ) + func fetchAccount( + oauthToken: String + ) + + func decryptCryptogram( + _ cryptogram: String + ) + func getWalletDisplayName() -> String? func setAccount(_ account: UserAccount) @@ -40,6 +48,20 @@ protocol PaymentMethodsInteractorOutput: class { _ error: Error ) + func didFetchAccount( + _ account: UserAccount + ) + func didFailFetchAccount( + _ error: Error + ) + + func didDecryptCryptogram( + _ token: String + ) + func didFailDecryptCryptogram( + _ error: Error + ) + func didTokenizeApplePay( _ token: Tokens ) diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsModuleIO.swift b/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsModuleIO.swift index 97ebc5eb..b6c22e56 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsModuleIO.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsModuleIO.swift @@ -1,6 +1,7 @@ import YooKassaPaymentsApi struct PaymentMethodsModuleInputData { + let applicationScheme: String? let clientApplicationKey: String let applePayMerchantIdentifier: String? let gatewayId: String? @@ -18,4 +19,8 @@ struct PaymentMethodsModuleInputData { let cardScanning: CardScanning? } -protocol PaymentMethodsModuleInput: SheetViewModuleOutput {} +protocol PaymentMethodsModuleInput: SheetViewModuleOutput { + func authorizeInYooMoney( + with cryptogram: String + ) +} diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsRouterIO.swift b/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsRouterIO.swift index 476679cd..658918af 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsRouterIO.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsRouterIO.swift @@ -41,6 +41,11 @@ protocol PaymentMethodsRouterInput: class { inputData: SberbankModuleInputData, moduleOutput: SberbankModuleOutput ) + + func openSberpayModule( + inputData: SberpayModuleInputData, + moduleOutput: SberpayModuleOutput + ) func openBankCardModule( inputData: BankCardModuleInputData, diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsViewIO.swift b/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsViewIO.swift index 809c13ca..c3346c2f 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsViewIO.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsViewIO.swift @@ -23,6 +23,7 @@ protocol PaymentMethodsViewInput: ActivityIndicatorFullViewPresenting, Notificat protocol PaymentMethodsViewOutput: ActionTitleTextDialogDelegate { func setupView() func viewDidAppear() + func applicationDidBecomeActive() func numberOfRows() -> Int func viewModelForRow(at indexPath: IndexPath) -> PaymentMethodViewModel? func didSelect(at indexPath: IndexPath) diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/Presenter/PaymentMethodsPresenter.swift b/YooKassaPayments/Private/Modules/PaymentMethods/Presenter/PaymentMethodsPresenter.swift index f275aa5e..5d5b860d 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/Presenter/PaymentMethodsPresenter.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/Presenter/PaymentMethodsPresenter.swift @@ -10,6 +10,11 @@ final class PaymentMethodsPresenter: NSObject { case cancel } + private enum App2AppState { + case idle + case yoomoney + } + // MARK: - VIPER var interactor: PaymentMethodsInteractorInput! @@ -24,7 +29,9 @@ final class PaymentMethodsPresenter: NSObject { private let isLogoVisible: Bool private let paymentMethodViewModelFactory: PaymentMethodViewModelFactory - + private let priceViewModelFactory: PriceViewModelFactory + + private let applicationScheme: String? private let clientApplicationKey: String private let applePayMerchantIdentifier: String? private let testModeSettings: TestModeSettings? @@ -47,6 +54,8 @@ final class PaymentMethodsPresenter: NSObject { init( isLogoVisible: Bool, paymentMethodViewModelFactory: PaymentMethodViewModelFactory, + applicationScheme: String?, + priceViewModelFactory: PriceViewModelFactory, clientApplicationKey: String, applePayMerchantIdentifier: String?, testModeSettings: TestModeSettings?, @@ -64,7 +73,9 @@ final class PaymentMethodsPresenter: NSObject { ) { self.isLogoVisible = isLogoVisible self.paymentMethodViewModelFactory = paymentMethodViewModelFactory - + self.priceViewModelFactory = priceViewModelFactory + + self.applicationScheme = applicationScheme self.clientApplicationKey = clientApplicationKey self.applePayMerchantIdentifier = applePayMerchantIdentifier self.testModeSettings = testModeSettings @@ -96,6 +107,8 @@ final class PaymentMethodsPresenter: NSObject { }() private var shouldReloadOnViewDidAppear = false + private var moneyCenterAuthToken: String? + private var app2AppState: App2AppState = .idle // MARK: - Apple Pay properties @@ -127,6 +140,14 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { } } + func applicationDidBecomeActive() { + if app2AppState == .idle, + paymentMethods?.count == 1, + paymentMethods?.first?.paymentMethodType == .yooMoney { + didFinish(module: self, error: nil) + } + } + func numberOfRows() -> Int { viewModels.count } @@ -170,7 +191,12 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { openYooMoneyAuthorization() case let paymentOption where paymentOption.paymentMethodType == .sberbank: - openSberbankModule(paymentOption: paymentOption, needReplace: needReplace) + if shouldOpenSberpay(paymentOption), + let returnUrl = makeSberpayReturnUrl() { + openSberpayModule(paymentOption: paymentOption, needReplace: needReplace, returnUrl: returnUrl) + } else { + openSberbankModule(paymentOption: paymentOption, needReplace: needReplace) + } case let paymentOption where paymentOption.paymentMethodType == .applePay: openApplePay(paymentOption: paymentOption, needReplace: needReplace) @@ -184,7 +210,7 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { } private func openYooMoneyAuthorization() { - if self.testModeSettings != nil { + if testModeSettings != nil { view?.showActivity() DispatchQueue.global().async { self.interactor.fetchYooMoneyPaymentMethods( @@ -192,30 +218,38 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { ) } } else { - do { - moneyAuthCoordinator = try router.presentYooMoneyAuthorizationModule( - config: moneyAuthConfig, - customization: moneyAuthCustomization, - output: self - ) - let event = AnalyticsEvent.userStartAuthorization( - sdkVersion: Bundle.frameworkVersion - ) - interactor.trackEvent(event) - } catch { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - self.view?.showActivity() - DispatchQueue.global().async { [weak self] in - self?.interactor.fetchPaymentMethods() - } + if shouldOpenYooMoneyApp2App() { + openYooMoneyApp2App() + } else { + openMoneyAuth() + } + } + } + + private func openMoneyAuth() { + do { + moneyAuthCoordinator = try router.presentYooMoneyAuthorizationModule( + config: moneyAuthConfig, + customization: moneyAuthCustomization, + output: self + ) + let event = AnalyticsEvent.userStartAuthorization( + sdkVersion: Bundle.frameworkVersion + ) + interactor.trackEvent(event) + } catch { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.view?.showActivity() + DispatchQueue.global().async { [weak self] in + self?.interactor.fetchPaymentMethods() } - - let event = AnalyticsEvent.userCancelAuthorization( - sdkVersion: Bundle.frameworkVersion - ) - interactor.trackEvent(event) } + + let event = AnalyticsEvent.userCancelAuthorization( + sdkVersion: Bundle.frameworkVersion + ) + interactor.trackEvent(event) } } @@ -234,6 +268,8 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { savePaymentMethod, initialState: initialSavePaymentMethod ) + let priceViewModel = priceViewModelFactory.makeAmountPriceViewModel(paymentOption) + let feeViewModel = priceViewModelFactory.makeFeePriceViewModel(paymentOption) let inputData = YooMoneyModuleInputData( clientApplicationKey: clientApplicationKey, testModeSettings: testModeSettings, @@ -242,8 +278,8 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { tokenizationSettings: tokenizationSettings, shopName: shopName, purchaseDescription: purchaseDescription, - price: makePriceViewModel(paymentOption), - fee: makeFeePriceViewModel(paymentOption), + price: priceViewModel, + fee: feeViewModel, paymentMethod: paymentMethod, paymentOption: paymentOption, termsOfService: termsOfService, @@ -264,6 +300,8 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { needReplace: Bool ) { let initialSavePaymentMethod = makeInitialSavePaymentMethod(savePaymentMethod) + let priceViewModel = priceViewModelFactory.makeAmountPriceViewModel(paymentOption) + let feeViewModel = priceViewModelFactory.makeFeePriceViewModel(paymentOption) let inputData = LinkedCardModuleInputData( clientApplicationKey: clientApplicationKey, testModeSettings: testModeSettings, @@ -272,8 +310,8 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { tokenizationSettings: tokenizationSettings, shopName: shopName, purchaseDescription: purchaseDescription, - price: makePriceViewModel(paymentOption), - fee: makeFeePriceViewModel(paymentOption), + price: priceViewModel, + fee: feeViewModel, paymentOption: paymentOption, termsOfService: termsOfService, returnUrl: returnUrl, @@ -304,6 +342,8 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { savePaymentMethod, initialState: initialSavePaymentMethod ) + let priceViewModel = priceViewModelFactory.makeAmountPriceViewModel(paymentOption) + let feeViewModel = priceViewModelFactory.makeFeePriceViewModel(paymentOption) let inputData = ApplePayContractModuleInputData( clientApplicationKey: clientApplicationKey, testModeSettings: testModeSettings, @@ -311,8 +351,8 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { tokenizationSettings: tokenizationSettings, shopName: shopName, purchaseDescription: purchaseDescription, - price: makePriceViewModel(paymentOption), - fee: makeFeePriceViewModel(paymentOption), + price: priceViewModel, + fee: feeViewModel, paymentOption: paymentOption, termsOfService: termsOfService, merchantIdentifier: applePayMerchantIdentifier, @@ -346,11 +386,8 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { paymentOption: PaymentOption, needReplace: Bool ) { - let paymentMethod = paymentMethodViewModelFactory.makePaymentMethodViewModel( - paymentOption: paymentOption - ) - let priceViewModel = makePriceViewModel(paymentOption) - let feeViewModel = makeFeePriceViewModel(paymentOption) + let priceViewModel = priceViewModelFactory.makeAmountPriceViewModel(paymentOption) + let feeViewModel = priceViewModelFactory.makeFeePriceViewModel(paymentOption) let inputData = SberbankModuleInputData( paymentOption: paymentOption, clientApplicationKey: clientApplicationKey, @@ -371,12 +408,39 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { ) } + private func openSberpayModule( + paymentOption: PaymentOption, + needReplace: Bool, + returnUrl: String + ) { + let priceViewModel = priceViewModelFactory.makeAmountPriceViewModel(paymentOption) + let feeViewModel = priceViewModelFactory.makeFeePriceViewModel(paymentOption) + let inputData = SberpayModuleInputData( + paymentOption: paymentOption, + clientApplicationKey: clientApplicationKey, + tokenizationSettings: tokenizationSettings, + testModeSettings: testModeSettings, + isLoggingEnabled: isLoggingEnabled, + shopName: shopName, + purchaseDescription: purchaseDescription, + priceViewModel: priceViewModel, + feeViewModel: feeViewModel, + termsOfService: termsOfService, + returnUrl: returnUrl, + isBackBarButtonHidden: needReplace + ) + router.openSberpayModule( + inputData: inputData, + moduleOutput: self + ) + } + private func openBankCardModule( paymentOption: PaymentOption, needReplace: Bool ) { - let priceViewModel = makePriceViewModel(paymentOption) - let feeViewModel = makeFeePriceViewModel(paymentOption) + let priceViewModel = priceViewModelFactory.makeAmountPriceViewModel(paymentOption) + let feeViewModel = priceViewModelFactory.makeFeePriceViewModel(paymentOption) let initialSavePaymentMethod = makeInitialSavePaymentMethod(savePaymentMethod) let savePaymentMethodViewModel = SavePaymentMethodViewModelFactory.makeSavePaymentMethodViewModel( paymentOption, @@ -405,6 +469,86 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { moduleOutput: self ) } + + private func shouldOpenSberpay( + _ paymentOption: PaymentOption + ) -> Bool { + guard let confirmationTypes = paymentOption.confirmationTypes, + confirmationTypes.contains(.mobileApplication) else { + return false + } + + return UIApplication.shared.canOpenURL(Constants.sberpayUrlScheme) + } + + private func shouldOpenYooMoneyApp2App() -> Bool { + guard let url = URL(string: Constants.YooMoneyApp2App.scheme) else { + return false + } + + return UIApplication.shared.canOpenURL(url) + } + + private func openYooMoneyApp2App() { + guard let clientId = moneyAuthClientId, + let redirectUri = makeYooMoneyExchangeRedirectUri() else { + return + } + + let scope = makeYooMoneyApp2AppScope() + let fullPathUrl = Constants.YooMoneyApp2App.scheme + + "\(Constants.YooMoneyApp2App.host)/" + + "\(Constants.YooMoneyApp2App.firstPath)?" + + "\(Constants.YooMoneyApp2App.clientId)=\(clientId)&" + + "\(Constants.YooMoneyApp2App.scope)=\(scope)&" + + "\(Constants.YooMoneyApp2App.redirectUri)=\(redirectUri)" + + guard let url = URL(string: fullPathUrl) else { + return + } + + DispatchQueue.main.async { + UIApplication.shared.open( + url, + options: [:], + completionHandler: nil + ) + } + } + + private func makeYooMoneyExchangeRedirectUri() -> String? { + guard let applicationScheme = applicationScheme else { + assertionFailure("Application scheme should be") + return nil + } + + return applicationScheme + + DeepLinkFactory.YooMoney.host + + "/" + + DeepLinkFactory.YooMoney.exchange.firstPath + + "?" + + DeepLinkFactory.YooMoney.exchange.cryptogram + + "=" + } + + private func makeYooMoneyApp2AppScope() -> String { + return [ + Constants.YooMoneyApp2App.Scope.accountInfo, + Constants.YooMoneyApp2App.Scope.balance, + ].joined(separator: ",") + } + + private func makeSberpayReturnUrl() -> String? { + guard let applicationScheme = applicationScheme else { + assertionFailure("Application scheme should be") + return nil + } + + return applicationScheme + + DeepLinkFactory.invoicingHost + + "/" + + DeepLinkFactory.sberpayPath + } } // MARK: - ActionTitleTextDialogDelegate @@ -427,6 +571,26 @@ extension PaymentMethodsPresenter: ActionTitleTextDialogDelegate { // MARK: - PaymentMethodsModuleInput extension PaymentMethodsPresenter: PaymentMethodsModuleInput { + func authorizeInYooMoney( + with cryptogram: String + ) { + guard !cryptogram.isEmpty else { + return + } + app2AppState = .yoomoney + + DispatchQueue.main.async { [weak self] in + guard let self = self, + let view = self.view else { return } + view.showActivity() + + DispatchQueue.global().async { [weak self] in + guard let self = self else { return } + self.interactor.decryptCryptogram(cryptogram) + } + } + } + func didFinish( on module: TokenizationModuleInput, with error: YooKassaPaymentsError? @@ -481,8 +645,7 @@ extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { paymentMethods.count == 1 { let needReplace = self.paymentMethods?.count == 1 DispatchQueue.main.async { [weak self] in - guard let self = self, - let view = self.view else { return } + guard let self = self else { return } self.openYooMoneyWallet( paymentOption: paymentOption, needReplace: needReplace @@ -508,6 +671,61 @@ extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { presentError(error) } + func didFetchAccount( + _ account: UserAccount + ) { + guard let moneyCenterAuthToken = moneyCenterAuthToken else { + return + } + interactor.setAccount(account) + interactor.fetchYooMoneyPaymentMethods( + moneyCenterAuthToken: moneyCenterAuthToken + ) + } + + func didFailFetchAccount( + _ error: Error + ) { + guard let moneyCenterAuthToken = moneyCenterAuthToken else { + return + } + interactor.fetchYooMoneyPaymentMethods( + moneyCenterAuthToken: moneyCenterAuthToken + ) + } + + func didDecryptCryptogram( + _ token: String + ) { + moneyCenterAuthToken = token + DispatchQueue.global().async { [weak self] in + guard let self = self else { return } + let event: AnalyticsEvent = .actionMoneyAuthLogin( + scheme: .yoomoneyApp, + status: .success, + sdkVersion: Bundle.frameworkVersion + ) + self.interactor.trackEvent(event) + self.interactor.fetchAccount(oauthToken: token) + } + } + + func didFailDecryptCryptogram( + _ error: Error + ) { + let event: AnalyticsEvent = .actionMoneyAuthLogin( + scheme: .yoomoneyApp, + status: .fail(error.localizedDescription), + sdkVersion: Bundle.frameworkVersion + ) + interactor.trackEvent(event) + DispatchQueue.main.async { [weak self] in + guard let view = self?.view else { return } + view.hideActivity() + view.presentError(with: §CommonLocalized.Error.unknown) + } + } + func didTokenizeApplePay( _ token: Tokens ) { @@ -648,29 +866,11 @@ extension PaymentMethodsPresenter: AuthorizationCoordinatorDelegate { moneyCenterAuthToken: token ) - let event: AnalyticsEvent - switch authorizationProcess { - case .login: - event = .userSuccessAuthorization( - moneyAuthProcessType: .login, - sdkVersion: Bundle.frameworkVersion - ) - case .enrollment: - event = .userSuccessAuthorization( - moneyAuthProcessType: .enrollment, - sdkVersion: Bundle.frameworkVersion - ) - case .migration: - event = .userSuccessAuthorization( - moneyAuthProcessType: .migration, - sdkVersion: Bundle.frameworkVersion - ) - case .none: - event = .userSuccessAuthorization( - moneyAuthProcessType: .unknown, - sdkVersion: Bundle.frameworkVersion - ) - } + let event: AnalyticsEvent = .actionMoneyAuthLogin( + scheme: .moneyAuthSdk, + status: .success, + sdkVersion: Bundle.frameworkVersion + ) self.interactor.trackEvent(event) } } @@ -703,8 +903,9 @@ extension PaymentMethodsPresenter: AuthorizationCoordinatorDelegate { ) { self.moneyAuthCoordinator = nil - let event = AnalyticsEvent.userFailedAuthorization( - error: error.localizedDescription, + let event: AnalyticsEvent = .actionMoneyAuthLogin( + scheme: .moneyAuthSdk, + status: .fail(error.localizedDescription), sdkVersion: Bundle.frameworkVersion ) interactor.trackEvent(event) @@ -741,10 +942,23 @@ extension PaymentMethodsPresenter: YooMoneyModuleOutput { ) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } - self.router.closeYooMoneyModule() - self.view?.showActivity() - DispatchQueue.global().async { [weak self] in - self?.interactor.fetchPaymentMethods() + self.moneyCenterAuthToken = nil + self.app2AppState = .idle + let condition: (PaymentOption) -> Bool = { + $0 is PaymentInstrumentYooMoneyLinkedBankCard + || $0 is PaymentInstrumentYooMoneyWallet + || $0.paymentMethodType == .yooMoney + } + if let paymentMethods = self.paymentMethods, + paymentMethods.allSatisfy(condition) { + self.didFinish(module: self, error: nil) + } else { + self.shouldReloadOnViewDidAppear = false + self.router.closeYooMoneyModule() + self.view?.showActivity() + DispatchQueue.global().async { [weak self] in + self?.interactor.fetchPaymentMethods() + } } } } @@ -797,10 +1011,10 @@ extension PaymentMethodsPresenter: ApplePayModuleOutput { let message = §Localized.applePayUnavailableTitle if self.paymentMethods?.count == 1 { - self.view?.hideActivity() - self.view?.showPlaceholder(message: message) + view.hideActivity() + view.showPlaceholder(message: message) } else { - self.view?.presentError(with: message) + view.presentError(with: message) } } } @@ -886,6 +1100,22 @@ extension PaymentMethodsPresenter: SberbankModuleOutput { } } +// MARK: - SberpayModuleOutput + +extension PaymentMethodsPresenter: SberpayModuleOutput { + func sberpayModule( + _ module: SberpayModuleInput, + didTokenize token: Tokens, + paymentMethodType: PaymentMethodType + ) { + didTokenize( + tokens: token, + paymentMethodType: paymentMethodType, + scheme: .sberpay + ) + } +} + // MARK: - BankCardModuleOutput extension PaymentMethodsPresenter: BankCardModuleOutput { @@ -912,7 +1142,8 @@ extension PaymentMethodsPresenter: TokenizationModuleInput { let inputData = CardSecModuleInputData( requestUrl: requestUrl, redirectUrl: GlobalConstants.returnUrl, - isLoggingEnabled: isLoggingEnabled + isLoggingEnabled: isLoggingEnabled, + isConfirmation: false ) DispatchQueue.main.async { [weak self] in guard let self = self else { return } @@ -922,18 +1153,60 @@ extension PaymentMethodsPresenter: TokenizationModuleInput { ) } } + + func startConfirmationProcess( + confirmationUrl: String, + paymentMethodType: PaymentMethodType + ) { + switch paymentMethodType { + case .sberbank: + guard let url = URL(string: confirmationUrl) else { + return + } + + DispatchQueue.main.async { + UIApplication.shared.open( + url, + options: [:], + completionHandler: nil + ) + } + + default: + let inputData = CardSecModuleInputData( + requestUrl: confirmationUrl, + redirectUrl: GlobalConstants.returnUrl, + isLoggingEnabled: isLoggingEnabled, + isConfirmation: true + ) + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.router.openCardSecModule( + inputData: inputData, + moduleOutput: self + ) + } + } + } } // MARK: - CardSecModuleOutput extension PaymentMethodsPresenter: CardSecModuleOutput { func didSuccessfullyPassedCardSec( - on module: CardSecModuleInput + on module: CardSecModuleInput, + isConfirmation: Bool ) { interactor.stopAnalyticsService() - tokenizationModuleOutput?.didSuccessfullyPassedCardSec( - on: self - ) + if isConfirmation { + tokenizationModuleOutput?.didSuccessfullyConfirmation( + paymentMethodType: .bankCard + ) + } else { + tokenizationModuleOutput?.didSuccessfullyPassedCardSec( + on: self + ) + } } func didPressCloseButton( @@ -986,6 +1259,26 @@ private extension PaymentMethodsPresenter { private extension PaymentMethodsPresenter { enum Constants { static let dismissApplePayTimeout: TimeInterval = 0.5 + + // swiftlint:disable:next force_unwrapping + static let sberpayUrlScheme = URL(string: "sberpay://")! + + enum YooMoneyApp2App { + // yoomoneyauth://app2app/exchange?clientId={clientId}&scope={scope}&redirect_uri={redirect_uri} + // swiftlint:disable:next force_unwrapping + static let scheme = "yoomoneyauth://" + static let host = "app2app" + static let firstPath = "exchange" + static let clientId = "clientId" + static let scope = "scope" + static let redirectUri = "redirect_uri" + + enum Scope { + static let accountInfo = "user_auth_center:account_info" + static let balance = "wallet:balance" + } + + } } } @@ -1004,57 +1297,6 @@ private extension PaymentMethodsPresenter { // MARK: - Private global helpers -private func makePriceViewModel( - _ paymentOption: PaymentOption -) -> PriceViewModel { - let amountString = paymentOption.charge.value.description - var integerPart = "" - var fractionalPart = "" - - if let separatorIndex = amountString.firstIndex(of: ".") { - integerPart = String(amountString[amountString.startIndex.. PriceViewModel? { - guard let fee = paymentOption.fee, - let service = fee.service else { return nil } - - let amountString = service.charge.value.description - var integerPart = "" - var fractionalPart = "" - - if let separatorIndex = amountString.firstIndex(of: ".") { - integerPart = String(amountString[amountString.startIndex.. Bool { diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/Router/PaymentMethodsRouter.swift b/YooKassaPayments/Private/Modules/PaymentMethods/Router/PaymentMethodsRouter.swift index 4933d8d6..e4d9469a 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/Router/PaymentMethodsRouter.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/Router/PaymentMethodsRouter.swift @@ -126,6 +126,20 @@ extension PaymentMethodsRouter: PaymentMethodsRouterInput { animated: true ) } + + func openSberpayModule( + inputData: SberpayModuleInputData, + moduleOutput: SberpayModuleOutput + ) { + let viewController = SberpayAssembly.makeModule( + inputData: inputData, + moduleOutput: moduleOutput + ) + transitionHandler?.push( + viewController, + animated: true + ) + } func openBankCardModule( inputData: BankCardModuleInputData, diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/View/PaymentMethodsViewController.swift b/YooKassaPayments/Private/Modules/PaymentMethods/View/PaymentMethodsViewController.swift index 13df1eff..e5bfb12a 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/View/PaymentMethodsViewController.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/View/PaymentMethodsViewController.swift @@ -74,6 +74,7 @@ final class PaymentMethodsViewController: UIViewController, PlaceholderProvider view.backgroundColor = .clear setupView() setupConstraints() + setupObserver() } override func viewDidLoad() { @@ -116,6 +117,15 @@ final class PaymentMethodsViewController: UIViewController, PlaceholderProvider leftItem.text = §Localized.paymentMethods navigationItem.leftBarButtonItem = UIBarButtonItem(customView: leftItem) } + + private func setupObserver() { + NotificationCenter.default.addObserver( + self, + selector: #selector(didBecomeActiveNotification(_:)), + name: UIApplication.didBecomeActiveNotification, + object: nil + ) + } // MARK: - Configuring the View’s Layout Behavior @@ -145,6 +155,13 @@ final class PaymentMethodsViewController: UIViewController, PlaceholderProvider ) } } + + // MARK: - Actions + + @objc + private func didBecomeActiveNotification(_ notification: Notification) { + output.applicationDidBecomeActive() + } } // MARK: - UITableViewDataSource diff --git a/YooKassaPayments/Private/Modules/Sberbank/Interactor/SberbankInteractor.swift b/YooKassaPayments/Private/Modules/Sberbank/Interactor/SberbankInteractor.swift index 9002dcb9..4c54be81 100644 --- a/YooKassaPayments/Private/Modules/Sberbank/Interactor/SberbankInteractor.swift +++ b/YooKassaPayments/Private/Modules/Sberbank/Interactor/SberbankInteractor.swift @@ -54,8 +54,8 @@ extension SberbankInteractor: SberbankInteractorInput { confirmation: confirmation, savePaymentMethod: false, amount: self.amount, - tmxSessionId: tmxSessionId - ) { [weak self] result in + tmxSessionId: tmxSessionId.value + ) { result in switch result { case .success(let data): output.didTokenize(data) diff --git a/YooKassaPayments/Private/Modules/Sberbank/View/SberbankViewController.swift b/YooKassaPayments/Private/Modules/Sberbank/View/SberbankViewController.swift index 5a50607a..799199ae 100644 --- a/YooKassaPayments/Private/Modules/Sberbank/View/SberbankViewController.swift +++ b/YooKassaPayments/Private/Modules/Sberbank/View/SberbankViewController.swift @@ -349,7 +349,7 @@ extension SberbankViewController: UIGestureRecognizerDelegate { private extension SberbankViewController { enum Localized: String { - case title = "Sberbank.Contract.Title" + case title = "Sberpay.Contract.Title" case `continue` = "Contract.next" enum PlaceholderView: String { diff --git a/YooKassaPayments/Private/Modules/Sberpay/Assembly/SberpayAssembly.swift b/YooKassaPayments/Private/Modules/Sberpay/Assembly/SberpayAssembly.swift new file mode 100644 index 00000000..5862a5ea --- /dev/null +++ b/YooKassaPayments/Private/Modules/Sberpay/Assembly/SberpayAssembly.swift @@ -0,0 +1,53 @@ +import UIKit + +enum SberpayAssembly { + static func makeModule( + inputData: SberpayModuleInputData, + moduleOutput: SberpayModuleOutput? + ) -> UIViewController { + let view = SberpayViewController() + let presenter = SberpayPresenter( + shopName: inputData.shopName, + purchaseDescription: inputData.purchaseDescription, + priceViewModel: inputData.priceViewModel, + feeViewModel: inputData.feeViewModel, + termsOfService: inputData.termsOfService, + isBackBarButtonHidden: inputData.isBackBarButtonHidden + ) + let paymentService = PaymentServiceAssembly.makeService( + tokenizationSettings: inputData.tokenizationSettings, + testModeSettings: inputData.testModeSettings, + isLoggingEnabled: inputData.isLoggingEnabled + ) + let analyticsProvider = AnalyticsProviderAssembly.makeProvider( + testModeSettings: inputData.testModeSettings + ) + let analyticsService = AnalyticsServiceAssembly.makeService( + isLoggingEnabled: inputData.isLoggingEnabled + ) + let threatMetrixService = ThreatMetrixServiceFactory.makeService() + let interactor = SberpayInteractor( + paymentService: paymentService, + analyticsProvider: analyticsProvider, + analyticsService: analyticsService, + threatMetrixService: threatMetrixService, + clientApplicationKey: inputData.clientApplicationKey, + amount: inputData.paymentOption.charge.plain, + returnUrl: inputData.returnUrl + ) + let router = SberpayRouter() + + view.output = presenter + + presenter.view = view + presenter.interactor = interactor + presenter.router = router + presenter.moduleOutput = moduleOutput + + interactor.output = presenter + + router.transitionHandler = view + + return view + } +} diff --git a/YooKassaPayments/Private/Modules/Sberpay/Interactor/SberpayInteractor.swift b/YooKassaPayments/Private/Modules/Sberpay/Interactor/SberpayInteractor.swift new file mode 100644 index 00000000..8bbffbaa --- /dev/null +++ b/YooKassaPayments/Private/Modules/Sberpay/Interactor/SberpayInteractor.swift @@ -0,0 +1,100 @@ +import ThreatMetrixAdapter + +final class SberpayInteractor { + + // MARK: - VIPER + + weak var output: SberpayInteractorOutput? + + // MARK: - Init + + private let paymentService: PaymentService + private let analyticsProvider: AnalyticsProvider + private let analyticsService: AnalyticsService + private let threatMetrixService: ThreatMetrixService + private let clientApplicationKey: String + private let amount: MonetaryAmount + private let returnUrl: String + + init( + paymentService: PaymentService, + analyticsProvider: AnalyticsProvider, + analyticsService: AnalyticsService, + threatMetrixService: ThreatMetrixService, + clientApplicationKey: String, + amount: MonetaryAmount, + returnUrl: String + ) { + self.paymentService = paymentService + self.analyticsProvider = analyticsProvider + self.analyticsService = analyticsService + self.threatMetrixService = threatMetrixService + self.clientApplicationKey = clientApplicationKey + self.amount = amount + self.returnUrl = returnUrl + } +} + +// MARK: - SberpayInteractorInput + +extension SberpayInteractor: SberpayInteractorInput { + func tokenizeSberpay() { + threatMetrixService.profileApp { [weak self] result in + guard let self = self, + let output = self.output else { return } + + switch result { + case let .success(tmxSessionId): + let confirmation = Confirmation( + type: .mobileApplication, + returnUrl: self.returnUrl + ) + self.paymentService.tokenizeSberpay( + clientApplicationKey: self.clientApplicationKey, + confirmation: confirmation, + savePaymentMethod: false, + amount: self.amount, + tmxSessionId: tmxSessionId.value + ) { [weak self] result in + switch result { + case .success(let data): + output.didTokenize(data) + case .failure(let error): + let mappedError = mapError(error) + output.didFailTokenize(mappedError) + } + } + + case let .failure(error): + let mappedError = mapError(error) + output.didFailTokenize(mappedError) + } + } + } + + func makeTypeAnalyticsParameters() -> ( + authType: AnalyticsEvent.AuthType, + tokenType: AnalyticsEvent.AuthTokenType? + ) { + analyticsProvider.makeTypeAnalyticsParameters() + } + + func trackEvent(_ event: AnalyticsEvent) { + analyticsService.trackEvent(event) + } +} + +// MARK: - Private global helpers + +private func mapError( + _ error: Error +) -> Error { + switch error { + case ProfileError.connectionFail: + return PaymentProcessingError.internetConnection + case let error as NSError where error.domain == NSURLErrorDomain: + return PaymentProcessingError.internetConnection + default: + return error + } +} diff --git a/YooKassaPayments/Private/Modules/Sberpay/Presenter/SberpayPresenter.swift b/YooKassaPayments/Private/Modules/Sberpay/Presenter/SberpayPresenter.swift new file mode 100644 index 00000000..06edd10a --- /dev/null +++ b/YooKassaPayments/Private/Modules/Sberpay/Presenter/SberpayPresenter.swift @@ -0,0 +1,225 @@ +import UIKit + +final class SberpayPresenter { + + // MARK: - VIPER + + weak var moduleOutput: SberpayModuleOutput? + weak var view: SberpayViewInput? + var interactor: SberpayInteractorInput! + var router: SberpayRouterInput! + + // MARK: - Init + + private let shopName: String + private let purchaseDescription: String + private let priceViewModel: PriceViewModel + private let feeViewModel: PriceViewModel? + private let termsOfService: TermsOfService + private let isBackBarButtonHidden: Bool + + init( + shopName: String, + purchaseDescription: String, + priceViewModel: PriceViewModel, + feeViewModel: PriceViewModel?, + termsOfService: TermsOfService, + isBackBarButtonHidden: Bool + ) { + self.shopName = shopName + self.purchaseDescription = purchaseDescription + self.priceViewModel = priceViewModel + self.feeViewModel = feeViewModel + self.termsOfService = termsOfService + self.isBackBarButtonHidden = isBackBarButtonHidden + } +} + +// MARK: - SberpayViewOutput + +extension SberpayPresenter: SberpayViewOutput { + func setupView() { + guard let view = view else { return } + let priceValue = makePrice(priceViewModel) + + var feeValue: String? = nil + if let feeViewModel = feeViewModel { + feeValue = "\(§Localized.fee) " + makePrice(feeViewModel) + } + + let termsOfServiceValue = makeTermsOfService( + termsOfService, + font: UIFont.dynamicCaption2, + foregroundColor: UIColor.AdaptiveColors.secondary + ) + let viewModel = SberpayViewModel( + shopName: shopName, + description: purchaseDescription, + priceValue: priceValue, + feeValue: feeValue, + termsOfService: termsOfServiceValue + ) + view.setupViewModel(viewModel) + + view.setBackBarButtonHidden(isBackBarButtonHidden) + + DispatchQueue.global().async { [weak self] in + guard let self = self, + let interactor = self.interactor else { return } + let (authType, _) = interactor.makeTypeAnalyticsParameters() + let event: AnalyticsEvent = .screenPaymentContract( + authType: authType, + scheme: .sberpay, + sdkVersion: Bundle.frameworkVersion + ) + interactor.trackEvent(event) + } + } + + func didTapActionButton() { + guard let view = view else { return } + view.showActivity() + DispatchQueue.global().async { [weak self] in + guard let self = self, + let interactor = self.interactor else { return } + interactor.tokenizeSberpay() + } + } + + func didTapTermsOfService( + _ url: URL + ) { + router.presentTermsOfServiceModule(url) + } +} + +// MARK: - SberpayInteractorOutput + +extension SberpayPresenter: SberpayInteractorOutput { + func didTokenize( + _ data: Tokens + ) { + let analyticsParameters = interactor.makeTypeAnalyticsParameters() + let event: AnalyticsEvent = .actionTokenize( + scheme: .sberpay, + authType: analyticsParameters.authType, + tokenType: analyticsParameters.tokenType, + sdkVersion: Bundle.frameworkVersion + ) + interactor.trackEvent(event) + moduleOutput?.sberpayModule( + self, + didTokenize: data, + paymentMethodType: .sberbank + ) + + DispatchQueue.main.async { [weak self] in + guard let view = self?.view else { return } + view.hideActivity() + } + } + + func didFailTokenize( + _ error: Error + ) { + let parameters = interactor.makeTypeAnalyticsParameters() + let event: AnalyticsEvent = .screenError( + authType: parameters.authType, + scheme: .smsSbol, + sdkVersion: Bundle.frameworkVersion + ) + interactor.trackEvent(event) + + let message = makeMessage(error) + + DispatchQueue.main.async { [weak self] in + guard let view = self?.view else { return } + view.hideActivity() + view.showPlaceholder(with: message) + } + } +} + +// MARK: - ActionTitleTextDialogDelegate + +extension SberpayPresenter: ActionTitleTextDialogDelegate { + func didPressButton( + in actionTitleTextDialog: ActionTitleTextDialog + ) { + guard let view = view else { return } + view.hidePlaceholder() + view.showActivity() + DispatchQueue.global().async { [weak self] in + guard let self = self, + let interactor = self.interactor else { return } + interactor.tokenizeSberpay() + } + } +} + +// MARK: - SberpayModuleInput + +extension SberpayPresenter: SberpayModuleInput {} + +// MARK: - Private helpers + +private extension SberpayPresenter { + func makePrice( + _ priceViewModel: PriceViewModel + ) -> String { + priceViewModel.integerPart + + priceViewModel.decimalSeparator + + priceViewModel.fractionalPart + + priceViewModel.currency + } + + func makeTermsOfService( + _ terms: TermsOfService, + font: UIFont, + foregroundColor: UIColor + ) -> NSMutableAttributedString { + let attributedText: NSMutableAttributedString + + let attributes: [NSAttributedString.Key: Any] = [ + .font: font, + .foregroundColor: foregroundColor, + ] + attributedText = NSMutableAttributedString( + string: "\(terms.text) ", + attributes: attributes + ) + + let linkAttributedText = NSMutableAttributedString( + string: terms.hyperlink, + attributes: attributes + ) + let linkRange = NSRange(location: 0, length: terms.hyperlink.count) + linkAttributedText.addAttribute(.link, value: terms.url, range: linkRange) + attributedText.append(linkAttributedText) + + return attributedText + } + + func makeMessage( + _ error: Error + ) -> String { + let message: String + + switch error { + case let error as PresentableError: + message = error.message + default: + message = §CommonLocalized.Error.unknown + } + + return message + } +} + +// MARK: - Localized + +private extension SberpayPresenter { + enum Localized: String { + case fee = "Contract.fee" + } +} diff --git a/YooKassaPayments/Private/Modules/Sberpay/Router/SberpayRouter.swift b/YooKassaPayments/Private/Modules/Sberpay/Router/SberpayRouter.swift new file mode 100644 index 00000000..dcd8ab03 --- /dev/null +++ b/YooKassaPayments/Private/Modules/Sberpay/Router/SberpayRouter.swift @@ -0,0 +1,22 @@ +import UIKit +import SafariServices + +final class SberpayRouter { + weak var transitionHandler: TransitionHandler? +} + +// MARK: - SberpayRouterInput + +extension SberpayRouter: SberpayRouterInput { + func presentTermsOfServiceModule( + _ url: URL + ) { + let viewController = SFSafariViewController(url: url) + viewController.modalPresentationStyle = .overFullScreen + transitionHandler?.present( + viewController, + animated: true, + completion: nil + ) + } +} diff --git a/YooKassaPayments/Private/Modules/Sberpay/SberpayInteractorIO.swift b/YooKassaPayments/Private/Modules/Sberpay/SberpayInteractorIO.swift new file mode 100644 index 00000000..831d0297 --- /dev/null +++ b/YooKassaPayments/Private/Modules/Sberpay/SberpayInteractorIO.swift @@ -0,0 +1,17 @@ +protocol SberpayInteractorInput: AnalyticsTrack { + func tokenizeSberpay() + + func makeTypeAnalyticsParameters() -> ( + authType: AnalyticsEvent.AuthType, + tokenType: AnalyticsEvent.AuthTokenType? + ) +} + +protocol SberpayInteractorOutput: class { + func didTokenize( + _ data: Tokens + ) + func didFailTokenize( + _ error: Error + ) +} diff --git a/YooKassaPayments/Private/Modules/Sberpay/SberpayModuleIO.swift b/YooKassaPayments/Private/Modules/Sberpay/SberpayModuleIO.swift new file mode 100644 index 00000000..7bb2c567 --- /dev/null +++ b/YooKassaPayments/Private/Modules/Sberpay/SberpayModuleIO.swift @@ -0,0 +1,27 @@ +import YooKassaPaymentsApi + +struct SberpayModuleInputData { + let paymentOption: PaymentOption + let clientApplicationKey: String + let tokenizationSettings: TokenizationSettings + let testModeSettings: TestModeSettings? + let isLoggingEnabled: Bool + + let shopName: String + let purchaseDescription: String + let priceViewModel: PriceViewModel + let feeViewModel: PriceViewModel? + let termsOfService: TermsOfService + let returnUrl: String + let isBackBarButtonHidden: Bool +} + +protocol SberpayModuleOutput: class { + func sberpayModule( + _ module: SberpayModuleInput, + didTokenize token: Tokens, + paymentMethodType: PaymentMethodType + ) +} + +protocol SberpayModuleInput: class {} diff --git a/YooKassaPayments/Private/Modules/Sberpay/SberpayRouterIO.swift b/YooKassaPayments/Private/Modules/Sberpay/SberpayRouterIO.swift new file mode 100644 index 00000000..394a976b --- /dev/null +++ b/YooKassaPayments/Private/Modules/Sberpay/SberpayRouterIO.swift @@ -0,0 +1,3 @@ +protocol SberpayRouterInput: class { + func presentTermsOfServiceModule(_ url: URL) +} diff --git a/YooKassaPayments/Private/Modules/Sberpay/SberpayViewIO.swift b/YooKassaPayments/Private/Modules/Sberpay/SberpayViewIO.swift new file mode 100644 index 00000000..8a288502 --- /dev/null +++ b/YooKassaPayments/Private/Modules/Sberpay/SberpayViewIO.swift @@ -0,0 +1,21 @@ +protocol SberpayViewInput: + ActivityIndicatorFullViewPresenting, + PlaceholderPresenting, + NotificationPresenting +{ + func setupViewModel( + _ viewModel: SberpayViewModel + ) + func setBackBarButtonHidden( + _ isHidden: Bool + ) + func showPlaceholder( + with message: String + ) +} + +protocol SberpayViewOutput: ActionTitleTextDialogDelegate { + func setupView() + func didTapActionButton() + func didTapTermsOfService(_ url: URL) +} diff --git a/YooKassaPayments/Private/Modules/Sberpay/View/SberpayViewController.swift b/YooKassaPayments/Private/Modules/Sberpay/View/SberpayViewController.swift new file mode 100644 index 00000000..02d2217e --- /dev/null +++ b/YooKassaPayments/Private/Modules/Sberpay/View/SberpayViewController.swift @@ -0,0 +1,315 @@ +final class SberpayViewController: UIViewController, PlaceholderProvider { + + // MARK: - VIPER + + var output: SberpayViewOutput! + + // MARK: - UI properties + + private lazy var scrollView: UIScrollView = { + $0.setStyles(UIView.Styles.grayBackground) + $0.translatesAutoresizingMaskIntoConstraints = false + $0.keyboardDismissMode = .interactive + return $0 + }(UIScrollView()) + + private lazy var contentView: UIView = { + $0.setStyles(UIView.Styles.grayBackground) + $0.translatesAutoresizingMaskIntoConstraints = false + return $0 + }(UIView()) + + private lazy var contentStackView: UIStackView = { + $0.setStyles(UIView.Styles.grayBackground) + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .vertical + return $0 + }(UIStackView()) + + private lazy var orderView: OrderView = { + $0.setStyles(UIView.Styles.grayBackground) + return $0 + }(OrderView()) + + private lazy var sberpayMethodView: LargeIconView = { + $0.setStyles( + UIView.Styles.grayBackground + ) + $0.image = PaymentMethodResources.Image.sberpay + $0.title = §Localized.paymentMethodTitle + return $0 + }(LargeIconView()) + + private lazy var actionButtonStackView: UIStackView = { + $0.setStyles(UIView.Styles.grayBackground) + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .vertical + $0.spacing = Space.double + return $0 + }(UIStackView()) + + private lazy var submitButton: Button = { + $0.tintColor = CustomizationStorage.shared.mainScheme + $0.setStyles( + UIButton.DynamicStyle.primary, + UIView.Styles.heightAsContent + ) + $0.setStyledTitle(§Localized.continue, for: .normal) + $0.addTarget( + self, + action: #selector(didPressActionButton), + for: .touchUpInside + ) + return $0 + }(Button(type: .custom)) + + private lazy var termsOfServiceLinkedTextView: LinkedTextView = { + $0.tintColor = CustomizationStorage.shared.mainScheme + $0.setStyles( + UIView.Styles.grayBackground, + UITextView.Styles.linked + ) + $0.delegate = self + return $0 + }(LinkedTextView()) + + private var activityIndicatorView: UIView? + + // MARK: - PlaceholderProvider + + lazy var placeholderView: PlaceholderView = { + $0.setStyles(UIView.Styles.defaultBackground) + $0.translatesAutoresizingMaskIntoConstraints = false + $0.contentView = self.actionTitleTextDialog + return $0 + }(PlaceholderView()) + + lazy var actionTitleTextDialog: ActionTitleTextDialog = { + $0.tintColor = CustomizationStorage.shared.mainScheme + $0.setStyles(ActionTitleTextDialog.Styles.fail) + $0.buttonTitle = §Localized.PlaceholderView.buttonTitle + $0.text = §Localized.PlaceholderView.text + $0.delegate = output + return $0 + }(ActionTitleTextDialog()) + + // MARK: - Constraints + + private lazy var scrollViewHeightConstraint = + scrollView.heightAnchor.constraint(equalToConstant: 0) + + // MARK: - Managing the View + + override func loadView() { + view = UIView() + view.setStyles(UIView.Styles.grayBackground) + navigationItem.title = §Localized.title + + setupView() + setupConstraints() + } + + override func viewDidLoad() { + super.viewDidLoad() + output.setupView() + } + + // MARK: - Setup + + private func setupView() { + [ + scrollView, + actionButtonStackView, + ].forEach(view.addSubview) + + scrollView.addSubview(contentView) + + [ + contentStackView, + ].forEach(contentView.addSubview) + + [ + orderView, + sberpayMethodView, + ].forEach(contentStackView.addArrangedSubview) + + [ + submitButton, + termsOfServiceLinkedTextView, + ].forEach(actionButtonStackView.addArrangedSubview) + } + + private func setupConstraints() { + let bottomConstraint: NSLayoutConstraint + let topConstraint: NSLayoutConstraint + if #available(iOS 11.0, *) { + bottomConstraint = actionButtonStackView.bottomAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.bottomAnchor, + constant: -Space.double + ) + topConstraint = scrollView.topAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.topAnchor + ) + } else { + bottomConstraint = actionButtonStackView.bottomAnchor.constraint( + equalTo: bottomLayoutGuide.topAnchor, + constant: -Space.double + ) + topConstraint = scrollView.topAnchor.constraint( + equalTo: topLayoutGuide.bottomAnchor + ) + } + + let constraints = [ + scrollViewHeightConstraint, + + topConstraint, + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint( + equalTo: actionButtonStackView.topAnchor, + constant: -Space.double + ), + + actionButtonStackView.leadingAnchor.constraint( + equalTo: view.leadingAnchor, + constant: Space.double + ), + actionButtonStackView.trailingAnchor.constraint( + equalTo: view.trailingAnchor, + constant: -Space.double + ), + bottomConstraint, + + contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), + contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), + + contentStackView.topAnchor.constraint(equalTo: contentView.topAnchor), + contentStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + contentStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + contentStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + ] + NSLayoutConstraint.activate(constraints) + } + + // MARK: - Configuring the View’s Layout Behavior + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + DispatchQueue.main.async { + self.fixTableViewHeight() + } + } + + private func fixTableViewHeight() { + scrollViewHeightConstraint.constant = contentStackView.bounds.height + } + + // MARK: - Action + + @objc + private func didPressActionButton( + _ sender: UIButton + ) { + output?.didTapActionButton() + } +} + +// MARK: - SberpayViewInput + +extension SberpayViewController: SberpayViewInput { + func setupViewModel( + _ viewModel: SberpayViewModel + ) { + orderView.title = viewModel.shopName + orderView.subtitle = viewModel.description + orderView.value = viewModel.priceValue + orderView.subvalue = viewModel.feeValue + termsOfServiceLinkedTextView.attributedText = viewModel.termsOfService + termsOfServiceLinkedTextView.textAlignment = .center + } + + func setBackBarButtonHidden( + _ isHidden: Bool + ) { + navigationItem.hidesBackButton = isHidden + } + + func showActivity() { + guard activityIndicatorView == nil else { return } + + let activityIndicatorView = ActivityIndicatorView() + activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false + activityIndicatorView.activity.startAnimating() + activityIndicatorView.setStyles(ActivityIndicatorView.Styles.heavyLight) + view.addSubview(activityIndicatorView) + + self.activityIndicatorView = activityIndicatorView + + let constraints = [ + activityIndicatorView.leading.constraint(equalTo: view.leading), + activityIndicatorView.trailing.constraint(equalTo: view.trailing), + activityIndicatorView.top.constraint(equalTo: view.top), + activityIndicatorView.bottom.constraint(equalTo: view.bottom), + ] + + NSLayoutConstraint.activate(constraints) + } + + func hideActivity() { + UIView.animate( + withDuration: 0.2, + animations: { + self.activityIndicatorView?.alpha = 0 + }, + completion: { _ in + self.activityIndicatorView?.removeFromSuperview() + self.activityIndicatorView = nil + } + ) + } + + func showPlaceholder( + with message: String + ) { + actionTitleTextDialog.title = message + showPlaceholder() + } +} + +// MARK: - UITextViewDelegate + +extension SberpayViewController: UITextViewDelegate { + func textView( + _ textView: UITextView, + shouldInteractWith URL: URL, + in characterRange: NSRange + ) -> Bool { + switch textView { + case termsOfServiceLinkedTextView: + output?.didTapTermsOfService(URL) + default: + assertionFailure("Unsupported textView") + } + return false + } +} + +// MARK: - Localized + +private extension SberpayViewController { + enum Localized: String { + case title = "Sberpay.Contract.Title" + case `continue` = "Contract.next" + case fee = "Contract.fee" + case paymentMethodTitle = "Sberpay.paymentMethodTitle" + + enum PlaceholderView: String { + case buttonTitle = "Common.PlaceholderView.buttonTitle" + case text = "Common.PlaceholderView.text" + } + } +} diff --git a/YooKassaPayments/Private/Modules/Sberpay/View/ViewModel/SberpayViewModel.swift b/YooKassaPayments/Private/Modules/Sberpay/View/ViewModel/SberpayViewModel.swift new file mode 100644 index 00000000..40ff4e7e --- /dev/null +++ b/YooKassaPayments/Private/Modules/Sberpay/View/ViewModel/SberpayViewModel.swift @@ -0,0 +1,7 @@ +struct SberpayViewModel { + let shopName: String + let description: String? + let priceValue: String + let feeValue: String? + let termsOfService: NSAttributedString +} diff --git a/YooKassaPayments/Private/Modules/YooMoney/Interactor/YooMoneyInteractor.swift b/YooKassaPayments/Private/Modules/YooMoney/Interactor/YooMoneyInteractor.swift index 39e15397..4f23138c 100644 --- a/YooKassaPayments/Private/Modules/YooMoney/Interactor/YooMoneyInteractor.swift +++ b/YooKassaPayments/Private/Modules/YooMoney/Interactor/YooMoneyInteractor.swift @@ -91,7 +91,7 @@ extension YooMoneyInteractor: YooMoneyInteractorInput { savePaymentMethod: savePaymentMethod, paymentMethodType: paymentMethodType, amount: amount, - tmxSessionId: tmxSessionId + tmxSessionId: tmxSessionId.value ) case let .failure(error): diff --git a/YooKassaPayments/Private/Modules/YooMoney/View/YooMoneyViewController.swift b/YooKassaPayments/Private/Modules/YooMoney/View/YooMoneyViewController.swift index a86acea1..c2b6cad3 100644 --- a/YooKassaPayments/Private/Modules/YooMoney/View/YooMoneyViewController.swift +++ b/YooKassaPayments/Private/Modules/YooMoney/View/YooMoneyViewController.swift @@ -487,7 +487,7 @@ extension YooMoneyViewController: YooMoneyViewInput { let linkAttributedText = NSMutableAttributedString(string: hyperText, attributes: attributes) let linkRange = NSRange(location: 0, length: hyperText.count) - let fakeLink = URL(string: "https://yookassa.ru") + let fakeLink = URL(string: "https://yookassa.ru")! linkAttributedText.addAttribute(.link, value: fakeLink, range: linkRange) attributedText.append(linkAttributedText) diff --git a/YooKassaPayments/Private/Services/Analytics/AnalyticsEvent.swift b/YooKassaPayments/Private/Services/Analytics/AnalyticsEvent.swift index c3a24508..42631d86 100644 --- a/YooKassaPayments/Private/Services/Analytics/AnalyticsEvent.swift +++ b/YooKassaPayments/Private/Services/Analytics/AnalyticsEvent.swift @@ -42,8 +42,15 @@ enum AnalyticsEvent { case userStartAuthorization(sdkVersion: String) case userCancelAuthorization(sdkVersion: String) - case userSuccessAuthorization(moneyAuthProcessType: MoneyAuthProcessType, sdkVersion: String) - case userFailedAuthorization(error: String, sdkVersion: String) + + case actionMoneyAuthLogin( + scheme: MoneyAuthLoginScheme, + status: MoneyAuthLoginStatus, + sdkVersion: String + ) + + /// SberPay confirmation + case actionSberPayConfirmation(sberPayConfirmationStatus: SberPayConfirmationStatus, sdkVersion: String) // MARK: - Analytic parameters. @@ -72,6 +79,7 @@ enum AnalyticsEvent { case smsSbol = "sms-sbol" case applePay = "apple-pay" case recurringCard = "recurring-card" + case sberpay = "sber-pay" var key: String { return Key.tokenizeScheme.rawValue @@ -107,21 +115,10 @@ enum AnalyticsEvent { case authType case authTokenType case authPaymentStatus - case moneyAuthProcessType case action - } - - // MARK: - Authorization - - enum MoneyAuthProcessType: String { - case enrollment - case login - case migration - case unknown - - var key: String { - return Key.moneyAuthProcessType.rawValue - } + case moneyAuthLoginScheme + case moneyAuthLoginStatus + case sberPayConfirmationStatus } // MARK: - BankCardForm actions @@ -148,6 +145,46 @@ enum AnalyticsEvent { Key.action.rawValue } } + + enum MoneyAuthLoginScheme: String { + case moneyAuthSdk + case yoomoneyApp + + var key: String { + Key.moneyAuthLoginScheme.rawValue + } + } + + enum MoneyAuthLoginStatus { + case success + case fail(String) + case canceled + + var rawValue: String { + switch self { + case .success: + return "Success" + case .fail: + return "Fail" + case .canceled: + return "Canceled" + } + } + + var key: String { + Key.moneyAuthLoginStatus.rawValue + } + } + + // MARK: - SberPayConfirmationStatus + + enum SberPayConfirmationStatus: String { + case success = "Success" + + var key: String { + return Key.sberPayConfirmationStatus.rawValue + } + } } // MARK: - Primitive type keys diff --git a/YooKassaPayments/Private/Services/Analytics/AnalyticsServiceImpl.swift b/YooKassaPayments/Private/Services/Analytics/AnalyticsServiceImpl.swift index 8d72fdfc..01e1e476 100644 --- a/YooKassaPayments/Private/Services/Analytics/AnalyticsServiceImpl.swift +++ b/YooKassaPayments/Private/Services/Analytics/AnalyticsServiceImpl.swift @@ -101,14 +101,14 @@ extension AnalyticsServiceImpl: AnalyticsService { case .userCancelAuthorization: eventName = EventKey.userCancelAuthorization.rawValue - case .userSuccessAuthorization: - eventName = EventKey.userSuccessAuthorization.rawValue - - case .userFailedAuthorization: - eventName = EventKey.userFailedAuthorization.rawValue - case .actionBankCardForm: eventName = EventKey.actionBankCardForm.rawValue + + case .actionMoneyAuthLogin: + eventName = EventKey.actionMoneyAuthLogin.rawValue + + case .actionSberPayConfirmation: + eventName = EventKey.actionSberPayConfirmation.rawValue } return eventName } @@ -200,21 +200,26 @@ extension AnalyticsServiceImpl: AnalyticsService { AnalyticsEvent.Keys.msdkVersion.rawValue: sdkVersion, ] - case let .userSuccessAuthorization(moneyAuthProcessType, sdkVersion): + case let .actionBankCardForm(action, sdkVersion): parameters = [ - moneyAuthProcessType.key: moneyAuthProcessType.rawValue, + action.key: action.rawValue, AnalyticsEvent.Keys.msdkVersion.rawValue: sdkVersion, ] - case let .userFailedAuthorization(errorLocalizedDescription, sdkVersion): + case let .actionMoneyAuthLogin(scheme, status, sdkVersion): parameters = [ - AnalyticsEvent.Keys.error.rawValue: errorLocalizedDescription, + scheme.key: scheme.rawValue, + status.key: status.rawValue, AnalyticsEvent.Keys.msdkVersion.rawValue: sdkVersion, ] - case let .actionBankCardForm(action, sdkVersion): + if case .fail(let errorLocalizedDescription) = status { + parameters?[AnalyticsEvent.Keys.error.rawValue] = errorLocalizedDescription + } + + case let .actionSberPayConfirmation(sberPayConfirmationStatus, sdkVersion): parameters = [ - action.key: action.rawValue, + sberPayConfirmationStatus.key: sberPayConfirmationStatus.rawValue, AnalyticsEvent.Keys.msdkVersion.rawValue: sdkVersion, ] } @@ -236,13 +241,13 @@ extension AnalyticsServiceImpl: AnalyticsService { case actionLogout case actionAuthWithoutWallet case actionBankCardForm + case actionMoneyAuthLogin + case actionSberPayConfirmation // MARK: - Authorization case userStartAuthorization case userCancelAuthorization - case userSuccessAuthorization - case userFailedAuthorization } } diff --git a/YooKassaPayments/Private/Services/Authorization/AuthorizationServiceImpl.swift b/YooKassaPayments/Private/Services/Authorization/AuthorizationServiceImpl.swift index 75631e36..ce2d1af7 100644 --- a/YooKassaPayments/Private/Services/Authorization/AuthorizationServiceImpl.swift +++ b/YooKassaPayments/Private/Services/Authorization/AuthorizationServiceImpl.swift @@ -72,14 +72,18 @@ extension AuthorizationServiceImpl: AuthorizationService { completion: { _ in } ) } - tokenStorage.set( - string: nil, - for: KeyValueStoringKeys.moneyCenterAuthToken - ) - tokenStorage.set( - string: nil, - for: KeyValueStoringKeys.walletToken - ) + [ + KeyValueStoringKeys.moneyCenterAuthToken, + KeyValueStoringKeys.walletToken, + Constants.Keys.walletDisplayName, + Constants.Keys.walletPhoneTitle, + Constants.Keys.walletAvatarURL, + ].forEach { + tokenStorage.set( + string: nil, + for: $0 + ) + } } func setWalletDisplayName( @@ -183,7 +187,7 @@ extension AuthorizationServiceImpl { instanceName: instanceName, singleAmountMax: amount, paymentUsageLimit: paymentUsageLimit, - tmxSessionId: tmxSessionId, + tmxSessionId: tmxSessionId.value, completion: completion ) diff --git a/YooKassaPayments/Private/Services/Payment/PaymentService.swift b/YooKassaPayments/Private/Services/Payment/PaymentService.swift index cd82a9f1..a69d760f 100644 --- a/YooKassaPayments/Private/Services/Payment/PaymentService.swift +++ b/YooKassaPayments/Private/Services/Payment/PaymentService.swift @@ -60,6 +60,15 @@ protocol PaymentService { tmxSessionId: String, completion: @escaping (Result) -> Void ) + + func tokenizeSberpay( + clientApplicationKey: String, + confirmation: Confirmation, + savePaymentMethod: Bool, + amount: MonetaryAmount?, + tmxSessionId: String, + completion: @escaping (Result) -> Void + ) func tokenizeApplePay( clientApplicationKey: String, diff --git a/YooKassaPayments/Private/Services/Payment/PaymentServiceImpl.swift b/YooKassaPayments/Private/Services/Payment/PaymentServiceImpl.swift index e138640f..6484e169 100644 --- a/YooKassaPayments/Private/Services/Payment/PaymentServiceImpl.swift +++ b/YooKassaPayments/Private/Services/Payment/PaymentServiceImpl.swift @@ -68,8 +68,7 @@ extension PaymentServiceImpl: PaymentService { paymentMethodId: paymentMethodId ) - session.perform(apiMethod: apiMethod).responseApi(queue: .global()) { [weak self] result in - guard let self = self else { return } + session.perform(apiMethod: apiMethod).responseApi(queue: .global()) { result in switch result { case let .left(error): let mappedError = mapError(error) @@ -102,8 +101,7 @@ extension PaymentServiceImpl: PaymentService { tokensRequest: tokensRequest ) - session.perform(apiMethod: apiMethod).responseApi(queue: .global()) { [weak self] result in - guard let self = self else { return } + session.perform(apiMethod: apiMethod).responseApi(queue: .global()) { result in switch result { case let .left(error): let mappedError = mapError(error) @@ -141,8 +139,7 @@ extension PaymentServiceImpl: PaymentService { tokensRequest: tokensRequest ) - session.perform(apiMethod: apiMethod).responseApi(queue: .global()) { [weak self] result in - guard let self = self else { return } + session.perform(apiMethod: apiMethod).responseApi(queue: .global()) { result in switch result { case let .left(error): let mappedError = mapError(error) @@ -184,8 +181,7 @@ extension PaymentServiceImpl: PaymentService { tokensRequest: tokensRequest ) - session.perform(apiMethod: apiMethod).responseApi(queue: .global()) { [weak self] result in - guard let self = self else { return } + session.perform(apiMethod: apiMethod).responseApi(queue: .global()) { result in switch result { case let .left(error): let mappedError = mapError(error) @@ -219,8 +215,7 @@ extension PaymentServiceImpl: PaymentService { tokensRequest: tokensRequest ) - session.perform(apiMethod: apiMethod).responseApi(queue: .global()) { [weak self] result in - guard let self = self else { return } + session.perform(apiMethod: apiMethod).responseApi(queue: .global()) { result in switch result { case let .left(error): let mappedError = mapError(error) @@ -255,6 +250,40 @@ extension PaymentServiceImpl: PaymentService { tokensRequest: tokensRequest ) + session.perform(apiMethod: apiMethod).responseApi(queue: .global()) { result in + switch result { + case let .left(error): + let mappedError = mapError(error) + completion(.failure(mappedError)) + case let .right(data): + completion(.success(data.plain)) + } + } + } + + func tokenizeSberpay( + clientApplicationKey: String, + confirmation: Confirmation, + savePaymentMethod: Bool, + amount: MonetaryAmount?, + tmxSessionId: String, + completion: @escaping (Result) -> Void + ) { + let paymentMethodData = PaymentMethodDataSberbank( + phone: nil + ) + let tokensRequest = TokensRequestPaymentMethodData( + amount: amount?.paymentsModel, + tmxSessionId: tmxSessionId, + confirmation: confirmation.paymentsModel, + savePaymentMethod: savePaymentMethod, + paymentMethodData: paymentMethodData + ) + let apiMethod = YooKassaPaymentsApi.Tokens.Method( + oauthToken: clientApplicationKey, + tokensRequest: tokensRequest + ) + session.perform(apiMethod: apiMethod).responseApi(queue: .global()) { [weak self] result in guard let self = self else { return } switch result { @@ -290,8 +319,7 @@ extension PaymentServiceImpl: PaymentService { tokensRequest: tokensRequest ) - session.perform(apiMethod: apiMethod).responseApi(queue: .global()) { [weak self] result in - guard let self = self else { return } + session.perform(apiMethod: apiMethod).responseApi(queue: .global()) { result in switch result { case let .left(error): let mappedError = mapError(error) diff --git a/YooKassaPayments/Private/Services/Payment/PaymentServiceMock.swift b/YooKassaPayments/Private/Services/Payment/PaymentServiceMock.swift index 3958414e..d92ac456 100644 --- a/YooKassaPayments/Private/Services/Payment/PaymentServiceMock.swift +++ b/YooKassaPayments/Private/Services/Payment/PaymentServiceMock.swift @@ -137,6 +137,17 @@ extension PaymentServiceMock: PaymentService { ) { makeTokensPromise(completion: completion) } + + func tokenizeSberpay( + clientApplicationKey: String, + confirmation: Confirmation, + savePaymentMethod: Bool, + amount: MonetaryAmount?, + tmxSessionId: String, + completion: @escaping (Result) -> Void + ) { + makeTokensPromise(completion: completion) + } func tokenizeApplePay( clientApplicationKey: String, diff --git a/YooKassaPayments/Private/SheetView/SheetContentViewController.swift b/YooKassaPayments/Private/SheetView/SheetContentViewController.swift index eb81d4e5..fe2f213b 100644 --- a/YooKassaPayments/Private/SheetView/SheetContentViewController.swift +++ b/YooKassaPayments/Private/SheetView/SheetContentViewController.swift @@ -13,26 +13,60 @@ final class SheetContentViewController: UIViewController { return $0 }(UIView()) - private var contentWrapperView = UIView() - private var pullBarView = UIView() - private var gripView = UIView() - private let overflowView = UIView() + private lazy var contentWrapperView: UIView = { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.layer.masksToBounds = true + if #available(iOS 11.0, *) { + $0.layer.maskedCorners = [ + .layerMaxXMinYCorner, + .layerMinXMinYCorner, + ] + } + return $0 + }(UIView()) + + private lazy var pullBarView: UIView = { + $0.translatesAutoresizingMaskIntoConstraints = false + return $0 + }(UIView()) + + private lazy var gripView: UIView = { + $0.translatesAutoresizingMaskIntoConstraints = false + return $0 + }(UIView()) + + private lazy var overflowView: UIView = { + $0.translatesAutoresizingMaskIntoConstraints = false + return $0 + }(UIView()) private lazy var childContainerView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false + $0.translatesAutoresizingMaskIntoConstraints = false if #available(iOS 13.0, *) { - view.backgroundColor = UIColor.systemBackground + $0.backgroundColor = UIColor.systemBackground } else { - view.backgroundColor = UIColor.white + $0.backgroundColor = UIColor.white } - return view - }() + $0.layer.masksToBounds = true + if #available(iOS 11.0, *) { + $0.layer.maskedCorners = [ + .layerMaxXMinYCorner, + .layerMinXMinYCorner, + ] + } + return $0 + }(UIView()) // MARK: - NSLayoutConstraint - private var contentTopConstraint: NSLayoutConstraint? - private var contentBottomConstraint: NSLayoutConstraint? + private lazy var contentTopConstraint: NSLayoutConstraint = { + return contentView.topAnchor.constraint(equalTo: view.topAnchor) + }() + + private lazy var contentBottomConstraint: NSLayoutConstraint = { + return childViewController.view.bottomAnchor.constraint(equalTo: childContainerView.bottomAnchor) + }() + private var navigationHeightConstraint: NSLayoutConstraint? private var gripSizeConstraints: [NSLayoutConstraint] = [] @@ -123,45 +157,42 @@ final class SheetContentViewController: UIViewController { private func setupContentView() { view.addSubview(contentView) + contentView.addSubview(contentWrapperView) + contentView.addSubview(overflowView) NSLayoutConstraint.activate([ - contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + contentTopConstraint, + contentView.leftAnchor.constraint(equalTo: view.leftAnchor), + contentView.rightAnchor.constraint(equalTo: view.rightAnchor), contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + contentWrapperView.leftAnchor.constraint(equalTo: contentView.leftAnchor), + contentWrapperView.rightAnchor.constraint(equalTo: contentView.rightAnchor), + contentWrapperView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + contentWrapperView.topAnchor.constraint(equalTo: contentView.topAnchor), + + overflowView.leftAnchor.constraint(equalTo: contentView.leftAnchor), + overflowView.rightAnchor.constraint(equalTo: contentView.rightAnchor), + overflowView.heightAnchor.constraint(equalToConstant: 200), + overflowView.topAnchor.constraint( + equalTo: contentView.bottomAnchor, + constant: -1 + ), ]) - - contentTopConstraint = contentView.topAnchor.constraint(equalTo: view.topAnchor) - if let contentTopConstraint = contentTopConstraint { - NSLayoutConstraint.activate([ - contentTopConstraint, - ]) - } - - contentView.addSubview(contentWrapperView) { - $0.edges.pinToSuperview() - } - - contentWrapperView.layer.masksToBounds = true - if #available(iOS 11.0, *) { - contentWrapperView.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner] - } - - contentView.addSubview(overflowView) { - $0.edges(.left, .right).pinToSuperview() - $0.height.set(200) - $0.top.align(with: contentView.al.bottom - 1) - } } private func setupChildContainerView() { contentWrapperView.addSubview(childContainerView) - - Constraints(for: childContainerView) { view in - view.top.pinToSuperview(inset: options.pullBarHeight) - view.left.pinToSuperview() - view.right.pinToSuperview() - view.bottom.pinToSuperview() - } + + NSLayoutConstraint.activate([ + childContainerView.topAnchor.constraint( + equalTo: contentWrapperView.topAnchor, + constant: options.pullBarHeight + ), + childContainerView.leftAnchor.constraint(equalTo: contentWrapperView.leftAnchor), + childContainerView.rightAnchor.constraint(equalTo: contentWrapperView.rightAnchor), + childContainerView.bottomAnchor.constraint(equalTo: contentWrapperView.bottomAnchor), + ]) } private func setupChildViewController() { @@ -175,21 +206,10 @@ final class SheetContentViewController: UIViewController { childViewController.view.leadingAnchor.constraint(equalTo: childContainerView.leadingAnchor), childViewController.view.trailingAnchor.constraint(equalTo: childContainerView.trailingAnchor), childViewController.view.topAnchor.constraint(equalTo: childContainerView.topAnchor), + contentBottomConstraint, ]) - contentBottomConstraint = childViewController.view.bottomAnchor.constraint(equalTo: childContainerView.bottomAnchor) - if let contentBottomConstraint = contentBottomConstraint { - NSLayoutConstraint.activate([ - contentBottomConstraint, - ]) - } - childViewController.didMove(toParent: self) - - childContainerView.layer.masksToBounds = true - if #available(iOS 11.0, *) { - childContainerView.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner] - } } private func setupOverflowView() { @@ -197,10 +217,12 @@ final class SheetContentViewController: UIViewController { } private func setupGripSize() { - gripSizeConstraints.forEach({ $0.isActive = false }) - Constraints(for: gripView) { - gripSizeConstraints = $0.size.set(gripSize) - } + NSLayoutConstraint.deactivate(gripSizeConstraints) + gripSizeConstraints = [ + gripView.heightAnchor.constraint(equalToConstant: gripSize.height), + gripView.widthAnchor.constraint(equalToConstant: gripSize.width), + ] + NSLayoutConstraint.activate(gripSizeConstraints) gripView.layer.cornerRadius = gripSize.height / 2 } @@ -214,12 +236,12 @@ final class SheetContentViewController: UIViewController { pullBarView.isUserInteractionEnabled = true pullBarView.backgroundColor = pullBarBackgroundColor contentWrapperView.addSubview(pullBarView) - Constraints(for: pullBarView) { - $0.top.pinToSuperview() - $0.left.pinToSuperview() - $0.right.pinToSuperview() - $0.height.set(options.pullBarHeight) - } + NSLayoutConstraint.activate([ + pullBarView.topAnchor.constraint(equalTo: contentWrapperView.topAnchor), + pullBarView.leftAnchor.constraint(equalTo: contentWrapperView.leftAnchor), + pullBarView.rightAnchor.constraint(equalTo: contentWrapperView.rightAnchor), + pullBarView.heightAnchor.constraint(equalToConstant: options.pullBarHeight), + ]) self.pullBarView = pullBarView let gripView = self.gripView @@ -227,12 +249,16 @@ final class SheetContentViewController: UIViewController { gripView.layer.cornerRadius = gripSize.height / 2 gripView.layer.masksToBounds = true pullBarView.addSubview(gripView) - gripSizeConstraints.forEach({ $0.isActive = false }) - Constraints(for: gripView) { - $0.centerY.alignWithSuperview() - $0.centerX.alignWithSuperview() - gripSizeConstraints = $0.size.set(gripSize) - } + NSLayoutConstraint.deactivate(gripSizeConstraints) + gripSizeConstraints = [ + gripView.heightAnchor.constraint(equalToConstant: gripSize.height), + gripView.widthAnchor.constraint(equalToConstant: gripSize.width), + ] + NSLayoutConstraint.activate([ + gripView.centerYAnchor.constraint(equalTo: pullBarView.centerYAnchor), + gripView.centerXAnchor.constraint(equalTo: pullBarView.centerXAnchor), + ]) + NSLayoutConstraint.activate(gripSizeConstraints) } private func setupCornerRadius() { @@ -277,7 +303,7 @@ extension SheetContentViewController { let oldPreferredHeight = preferredHeight var fittingSize = UIView.layoutFittingCompressedSize fittingSize.width = width - contentTopConstraint?.isActive = false + contentTopConstraint.isActive = false UIView.performWithoutAnimation { contentView.layoutSubviews() @@ -287,7 +313,7 @@ extension SheetContentViewController { withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow ).height - contentTopConstraint?.isActive = true + contentTopConstraint.isActive = true UIView.performWithoutAnimation { contentView.layoutSubviews() @@ -306,7 +332,7 @@ private extension SheetContentViewController { func updateNavigationControllerHeight() { guard let navigationController = childViewController as? UINavigationController else { return } navigationHeightConstraint?.isActive = false - contentTopConstraint?.isActive = false + contentTopConstraint.isActive = false if let viewController = navigationController.visibleViewController { let sizeFitting = CGSize(width: view.bounds.width, height: 0) @@ -321,13 +347,13 @@ private extension SheetContentViewController { } } navigationHeightConstraint?.isActive = true - contentTopConstraint?.isActive = true + contentTopConstraint.isActive = true } func updateChildViewControllerBottomConstraint( adjustment: CGFloat ) { - contentBottomConstraint?.constant = adjustment + contentBottomConstraint.constant = adjustment } } diff --git a/YooKassaPayments/Private/SheetView/SheetViewController.swift b/YooKassaPayments/Private/SheetView/SheetViewController.swift index 47e220b0..3bdd505a 100644 --- a/YooKassaPayments/Private/SheetView/SheetViewController.swift +++ b/YooKassaPayments/Private/SheetView/SheetViewController.swift @@ -4,6 +4,11 @@ protocol SheetViewModuleOutput: class { func start3dsProcess( requestUrl: String ) + + func startConfirmationProcess( + confirmationUrl: String, + paymentMethodType: PaymentMethodType + ) func didFinish( on module: TokenizationModuleInput, @@ -237,17 +242,11 @@ final class SheetViewController: UIViewController { ) -> CGFloat? { let convertedKeyboardFrame = view.convert(keyboardFrame, from: nil) let intersectionViewFrame = convertedKeyboardFrame.intersection(view.bounds) - var safeOffset: CGFloat = 0 - if #available(iOS 11.0, *) { - let intersectionSafeFrame = convertedKeyboardFrame.intersection(view.safeAreaLayoutGuide.layoutFrame) - safeOffset = intersectionViewFrame.height - intersectionSafeFrame.height - } let intersectionOffset = intersectionViewFrame.size.height guard convertedKeyboardFrame.minY.isInfinite == false else { return nil } - let keyboardOffset = intersectionOffset - safeOffset - return keyboardOffset + return intersectionOffset } } @@ -310,7 +309,15 @@ private extension SheetViewController { func height( for size: SheetSize ) -> CGFloat { - let fullscreenHeight = view.bounds.height + var statusBarHeight: CGFloat = 0 + if #available(iOS 13.0, *) { + let window = UIApplication.shared.windows.filter {$0.isKeyWindow}.first + statusBarHeight = window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0 + } else { + statusBarHeight = UIApplication.shared.statusBarFrame.height + } + + let fullscreenHeight = view.bounds.height - statusBarHeight let contentHeight: CGFloat switch size { @@ -596,4 +603,14 @@ extension SheetViewController: TokenizationModuleInput { func start3dsProcess(requestUrl: String) { moduleOutput?.start3dsProcess(requestUrl: requestUrl) } + + func startConfirmationProcess( + confirmationUrl: String, + paymentMethodType: PaymentMethodType + ) { + moduleOutput?.startConfirmationProcess( + confirmationUrl: confirmationUrl, + paymentMethodType: paymentMethodType + ) + } } diff --git a/YooKassaPayments/Private/SheetView/Yalta.swift b/YooKassaPayments/Private/SheetView/Yalta.swift deleted file mode 100644 index badb3140..00000000 --- a/YooKassaPayments/Private/SheetView/Yalta.swift +++ /dev/null @@ -1,410 +0,0 @@ -import UIKit - -protocol LayoutItem { // `UIView`, `UILayoutGuide` - var superview: UIView? { get } -} - -extension UIView: LayoutItem {} -extension UILayoutGuide: LayoutItem { - var superview: UIView? { return self.owningView } -} - -extension LayoutItem { // Yalta methods available via `LayoutProxy` - @nonobjc var al: LayoutProxy { return LayoutProxy(base: self) } -} - -// MARK: - LayoutProxy - -struct LayoutProxy { - let base: Base -} - -extension LayoutProxy where Base: LayoutItem { - - // MARK: SheetAnchors - - var top: SheetAnchor { return SheetAnchor(base, .top) } - var bottom: SheetAnchor { return SheetAnchor(base, .bottom) } - var left: SheetAnchor { return SheetAnchor(base, .left) } - var right: SheetAnchor { return SheetAnchor(base, .right) } - var leading: SheetAnchor { return SheetAnchor(base, .leading) } - var trailing: SheetAnchor { return SheetAnchor(base, .trailing) } - - var centerX: SheetAnchor { return SheetAnchor(base, .centerX) } - var centerY: SheetAnchor { return SheetAnchor(base, .centerY) } - - var firstBaseline: SheetAnchor { return SheetAnchor(base, .firstBaseline) } - var lastBaseline: SheetAnchor { return SheetAnchor(base, .lastBaseline) } - - var width: SheetAnchor { return SheetAnchor(base, .width) } - var height: SheetAnchor { return SheetAnchor(base, .height) } - - // MARK: SheetAnchor Collections - - func edges(_ edges: LayoutEdge...) -> SheetAnchorCollectionEdges { return SheetAnchorCollectionEdges(item: base, edges: edges) } - var edges: SheetAnchorCollectionEdges { return SheetAnchorCollectionEdges(item: base, edges: [.left, .right, .bottom, .top]) } - var center: SheetAnchorCollectionCenter { return SheetAnchorCollectionCenter(x: centerX, y: centerY) } - var size: SheetAnchorCollectionSize { return SheetAnchorCollectionSize(width: width, height: height) } -} - -extension LayoutProxy where Base: UIView { - var margins: LayoutProxy { return base.layoutMarginsGuide.al } - - @available(iOS 11.0, tvOS 11.0, *) - var safeArea: LayoutProxy { return base.safeAreaLayoutGuide.al } -} - -// MARK: - SheetAnchors - -// phantom types -enum SheetAnchorAxis { - class Horizontal {} - class Vertical {} -} - -enum SheetAnchorType { - class Dimension {} - /// Includes `center`, `edge` and `baselines` anchors. - class Alignment {} - class Center: Alignment {} - class Edge: Alignment {} - class Baseline: Alignment {} -} - -/// An anchor represents one of the view's layout attributes (e.g. `left`, -/// `centerX`, `width`, etc). Use the anchor’s methods to construct constraints. -struct SheetAnchor { // type and axis are phantom types - internal let item: LayoutItem - internal let attribute: NSLayoutConstraint.Attribute - internal let offset: CGFloat - internal let multiplier: CGFloat - - init(_ item: LayoutItem, _ attribute: NSLayoutConstraint.Attribute, offset: CGFloat = 0, multiplier: CGFloat = 1) { - self.item = item; self.attribute = attribute; self.offset = offset; self.multiplier = multiplier - } - - /// Returns a new anchor offset by a given amount. - internal func offsetting(by offset: CGFloat) -> SheetAnchor { - return SheetAnchor(item, attribute, offset: self.offset + offset, multiplier: self.multiplier) - } - - /// Returns a new anchor with a given multiplier. - internal func multiplied(by multiplier: CGFloat) -> SheetAnchor { - return SheetAnchor(item, attribute, offset: self.offset * multiplier, multiplier: self.multiplier * multiplier) - } -} - -func + (anchor: SheetAnchor, offset: CGFloat) -> SheetAnchor { - return anchor.offsetting(by: offset) -} - -func - (anchor: SheetAnchor, offset: CGFloat) -> SheetAnchor { - return anchor.offsetting(by: -offset) -} - -func * (anchor: SheetAnchor, multiplier: CGFloat) -> SheetAnchor { - return anchor.multiplied(by: multiplier) -} - -// MARK: - SheetAnchors (SheetAnchorType.Alignment) - -extension SheetAnchor where Type: SheetAnchorType.Alignment { - /// Aligns two anchors. - @discardableResult func align(with anchor: SheetAnchor, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { - return Constraints.constrain(self, anchor, relation: relation) - } -} - -// MARK: - SheetAnchors (SheetAnchorType.Edge) - -extension SheetAnchor where Type: SheetAnchorType.Edge { - /// Pins the edge to the same edge of the superview. - @discardableResult func pinToSuperview(inset: CGFloat = 0, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { - return _pin(to: item.superview!, attribute: attribute, inset: inset, relation: relation) - } - - /// Pins the edge to the respected margin of the superview. - @discardableResult func pinToSuperviewMargin(inset: CGFloat = 0, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { - return _pin(to: item.superview!, attribute: attribute.toMargin, inset: inset, relation: relation) - } - - /// Pins the edge to the respected edges of the given container. - @discardableResult func pin(to container: LayoutItem, inset: CGFloat = 0, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { - return _pin(to: container, attribute: attribute, inset: inset, relation: relation) - } - - /// Pins the edge to the safe area of the view controller. Falls back to - /// layout guides (`.topLayoutGuide` and `.bottomLayoutGuide` on iOS 10. - @discardableResult func pinToSafeArea(of vc: UIViewController, inset: CGFloat = 0, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { - let item2: Any, attr2: NSLayoutConstraint.Attribute - if #available(iOS 11, tvOS 11, *) { - // Pin to `safeAreaLayoutGuide` on iOS 11 - (item2, attr2) = (vc.view.safeAreaLayoutGuide, self.attribute) - } else { - switch self.attribute { - // Fall back to .topLayoutGuide and pin to it's .bottom. - case .top: (item2, attr2) = (vc.topLayoutGuide, .bottom) - // Fall back to .bottomLayoutGuide and pin to it's .top. - case .bottom: (item2, attr2) = (vc.bottomLayoutGuide, .top) - // There are no layout guides for .left and .right, so just pin - // to the superview instead. - default: (item2, attr2) = (vc.view!, self.attribute) - } - } - return _pin(to: item2, attribute: attr2, inset: inset, relation: relation) - } - - // Pin the anchor to another layout item. - private func _pin(to item2: Any, attribute attr2: NSLayoutConstraint.Attribute, inset: CGFloat, relation: NSLayoutConstraint.Relation) -> NSLayoutConstraint { - // Invert attribute and relation in certain cases. The `pin` semantics - // are inspired by https://github.com/PureLayout/PureLayout - let isInverted = [.trailing, .right, .bottom].contains(attribute) - return Constraints.constrain(self, toItem: item2, attribute: attr2, offset: (isInverted ? -inset : inset), relation: (isInverted ? relation.inverted : relation)) - } -} - -// MARK: - SheetAnchors (SheetAnchorType.Center) - -extension SheetAnchor where Type: SheetAnchorType.Center { - /// Aligns the axis with a superview axis. - @discardableResult func alignWithSuperview(offset: CGFloat = 0, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { - return align(with: SheetAnchor(self.item.superview!, self.attribute) + offset, relation: relation) - } -} - -// MARK: - SheetAnchors (SheetAnchorType.Dimension) - -extension SheetAnchor where Type: SheetAnchorType.Dimension { - /// Sets the dimension to a specific size. - @discardableResult func set(_ constant: CGFloat, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { - return Constraints.constrain(item: item, attribute: attribute, relatedBy: relation, constant: constant) - } - - @discardableResult func match(_ anchor: SheetAnchor, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { - return Constraints.constrain(self, anchor, relation: relation) - } -} - -// MARK: - SheetAnchorCollectionEdges - -struct SheetAnchorCollectionEdges { - internal let item: LayoutItem - internal let edges: [LayoutEdge] - private var anchors: [SheetAnchor] { return edges.map { SheetAnchor(item, $0.attribute) } } - - /// Pins the edges of the view to the edges of the superview so the the view - /// fills the available space in a container. - @discardableResult func pinToSuperview(insets: UIEdgeInsets = .zero, relation: NSLayoutConstraint.Relation = .equal) -> [NSLayoutConstraint] { - return anchors.map { $0.pinToSuperview(inset: insets.inset(for: $0.attribute), relation: relation) } - } - - /// Pins the edges of the view to the margins of the superview so the the view - /// fills the available space in a container. - @discardableResult func pinToSuperviewMargins(insets: UIEdgeInsets = .zero, relation: NSLayoutConstraint.Relation = .equal) -> [NSLayoutConstraint] { - return anchors.map { $0.pinToSuperviewMargin(inset: insets.inset(for: $0.attribute), relation: relation) } - } - - /// Pins the edges of the view to the edges of the given view so the the - /// view fills the available space in a container. - @discardableResult func pin(to item2: LayoutItem, insets: UIEdgeInsets = .zero, relation: NSLayoutConstraint.Relation = .equal) -> [NSLayoutConstraint] { - return anchors.map { $0.pin(to: item2, inset: insets.inset(for: $0.attribute), relation: relation) } - } - - /// Pins the edges to the safe area of the view controller. - /// Falls back to layout guides on iOS 10. - @discardableResult func pinToSafeArea(of vc: UIViewController, insets: UIEdgeInsets = .zero, relation: NSLayoutConstraint.Relation = .equal) -> [NSLayoutConstraint] { - return anchors.map { $0.pinToSafeArea(of: vc, inset: insets.inset(for: $0.attribute), relation: relation) } - } -} - -// MARK: - SheetAnchorCollectionCenter - -struct SheetAnchorCollectionCenter { - internal let x: SheetAnchor - internal let y: SheetAnchor - - /// Centers the view in the superview. - @discardableResult func alignWithSuperview() -> [NSLayoutConstraint] { - return [x.alignWithSuperview(), y.alignWithSuperview()] - } - - /// Makes the axis equal to the other collection of axis. - @discardableResult func align(with anchors: SheetAnchorCollectionCenter) -> [NSLayoutConstraint] { - return [x.align(with: anchors.x), y.align(with: anchors.y)] - } -} - - -// MARK: - SheetAnchorCollectionSize - -struct SheetAnchorCollectionSize { - internal let width: SheetAnchor - internal let height: SheetAnchor - - /// Set the size of item. - @discardableResult func set(_ size: CGSize, relation: NSLayoutConstraint.Relation = .equal) -> [NSLayoutConstraint] { - return [width.set(size.width, relation: relation), height.set(size.height, relation: relation)] - } - - /// Makes the size of the item equal to the size of the other item. - @discardableResult func match(_ anchors: SheetAnchorCollectionSize, insets: CGSize = .zero, multiplier: CGFloat = 1, relation: NSLayoutConstraint.Relation = .equal) -> [NSLayoutConstraint] { - return [width.match(anchors.width * multiplier - insets.width, relation: relation), - height.match(anchors.height * multiplier - insets.height, relation: relation)] - } -} - -// MARK: - Constraints - -final class Constraints { - var constraints = [NSLayoutConstraint]() - - /// All of the constraints created in the given closure are automatically - /// activated at the same time. This is more efficient then installing them - /// one-be-one. More importantly, it allows to make changes to the constraints - /// before they are installed (e.g. change `priority`). - @discardableResult init(_ closure: () -> Void) { - Constraints._stack.append(self) - closure() // create constraints - Constraints._stack.removeLast() - NSLayoutConstraint.activate(constraints) - } - - /// Creates and automatically installs a constraint. - internal static func constrain(item item1: Any, attribute attr1: NSLayoutConstraint.Attribute, relatedBy relation: NSLayoutConstraint.Relation = .equal, toItem item2: Any? = nil, attribute attr2: NSLayoutConstraint.Attribute? = nil, multiplier: CGFloat = 1, constant: CGFloat = 0) -> NSLayoutConstraint { - precondition(Thread.isMainThread, "Yalta APIs can only be used from the main thread") - (item1 as? UIView)?.translatesAutoresizingMaskIntoConstraints = false - let constraint = NSLayoutConstraint(item: item1, attribute: attr1, relatedBy: relation, toItem: item2, attribute: attr2 ?? .notAnAttribute, multiplier: multiplier, constant: constant) - _install(constraint) - return constraint - } - - /// Creates and automatically installs a constraint between two anchors. - internal static func constrain(_ lhs: SheetAnchor, _ rhs: SheetAnchor, offset: CGFloat = 0, multiplier: CGFloat = 1, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { - return constrain(item: lhs.item, attribute: lhs.attribute, relatedBy: relation, toItem: rhs.item, attribute: rhs.attribute, multiplier: (multiplier / lhs.multiplier) * rhs.multiplier, constant: offset - lhs.offset + rhs.offset) - } - - /// Creates and automatically installs a constraint between an anchor and - /// a given item. - internal static func constrain(_ lhs: SheetAnchor, toItem item2: Any?, attribute attr2: NSLayoutConstraint.Attribute?, offset: CGFloat = 0, multiplier: CGFloat = 1, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { - return constrain(item: lhs.item, attribute: lhs.attribute, relatedBy: relation, toItem: item2, attribute: attr2, multiplier: multiplier / lhs.multiplier, constant: offset - lhs.offset) - } - - private static var _stack = [Constraints]() // this is what enabled constraint auto-installing - - private static func _install(_ constraint: NSLayoutConstraint) { - if _stack.isEmpty { // not creating a group of constraints - constraint.isActive = true - } else { // remember which constaints to install when group is completed - let group = _stack.last! - group.constraints.append(constraint) - } - } -} - -// MARK: - UIView + Constraints - -extension UIView { - @discardableResult @nonobjc func addSubview(_ a: UIView, constraints: (LayoutProxy) -> Void) -> Constraints { - addSubview(a) - return Constraints(for: a, constraints) - } - - @discardableResult @nonobjc func addSubview(_ a: UIView, _ b: UIView, constraints: (LayoutProxy, LayoutProxy) -> Void) -> Constraints { - [a, b].forEach { addSubview($0) } - return Constraints(for: a, b, constraints) - } - - @discardableResult @nonobjc func addSubview(_ a: UIView, _ b: UIView, _ c: UIView, constraints: (LayoutProxy, LayoutProxy, LayoutProxy) -> Void) -> Constraints { - [a, b, c].forEach { addSubview($0) } - return Constraints(for: a, b, c, constraints) - } - - @discardableResult @nonobjc func addSubview(_ a: UIView, _ b: UIView, _ c: UIView, _ d: UIView, constraints: (LayoutProxy, LayoutProxy, LayoutProxy, LayoutProxy) -> Void) -> Constraints { - [a, b, c, d].forEach { addSubview($0) } - return Constraints(for: a, b, c, d, constraints) - } -} - -// MARK: - Constraints (Arity) - -extension Constraints { - @discardableResult convenience init(for a: A, _ closure: (LayoutProxy) -> Void) { - self.init { closure(a.al) } - } - - @discardableResult convenience init(for a: A, _ b: B, _ closure: (LayoutProxy, LayoutProxy) -> Void) { - self.init { closure(a.al, b.al) } - } - - @discardableResult convenience init(for a: A, _ b: B, _ c: C, _ closure: (LayoutProxy, LayoutProxy, LayoutProxy) -> Void) { - self.init { closure(a.al, b.al, c.al) } - } - - @discardableResult convenience init(for a: A, _ b: B, _ c: C, _ d: D, _ closure: (LayoutProxy, LayoutProxy, LayoutProxy, LayoutProxy) -> Void) { - self.init { closure(a.al, b.al, c.al, d.al) } - } -} - -// MARK: - Misc - -enum LayoutEdge { - case top, bottom, leading, trailing, left, right - - internal var attribute: NSLayoutConstraint.Attribute { - switch self { - case .top: return .top; case .bottom: return .bottom - case .leading: return .leading; case .trailing: return .trailing - case .left: return .left; case .right: return .right - } - } -} - -internal extension NSLayoutConstraint.Attribute { - var toMargin: NSLayoutConstraint.Attribute { - switch self { - case .top: return .topMargin; case .bottom: return .bottomMargin - case .leading: return .leadingMargin; case .trailing: return .trailingMargin - case .left: return .leftMargin case .right: return .rightMargin - default: return self - } - } -} - -internal extension NSLayoutConstraint.Relation { - var inverted: NSLayoutConstraint.Relation { - switch self { - case .greaterThanOrEqual: return .lessThanOrEqual - case .lessThanOrEqual: return .greaterThanOrEqual - case .equal: return self - @unknown default: - return self - } - } -} - -internal extension UIEdgeInsets { - func inset(for attribute: NSLayoutConstraint.Attribute) -> CGFloat { - switch attribute { - case .top: return top; case .bottom: return bottom - case .left, .leading: return left - case .right, .trailing: return right - default: return 0 - } - } -} - -// MARK: - Deprecated - -extension SheetAnchor where Type: SheetAnchorType.Alignment { - @available(*, deprecated, message: "Please use operators instead, e.g. `view.top.align(with: view.bottom * 2 + 10)`.") - @discardableResult func align(with anchor: SheetAnchor, offset: CGFloat = 0, multiplier: CGFloat = 1, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { - return Constraints.constrain(self, anchor, offset: offset, multiplier: multiplier, relation: relation) - } -} - -extension SheetAnchor where Type: SheetAnchorType.Dimension { - @available(*, deprecated, message: "Please use operators instead, e.g. `view.width.match(view.height * 2 + 10)`.") - @discardableResult func match(_ anchor: SheetAnchor, offset: CGFloat = 0, multiplier: CGFloat = 1, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { - return Constraints.constrain(self, anchor, offset: offset, multiplier: multiplier, relation: relation) - } -} diff --git a/YooKassaPayments/Public/InputData/BankCardRepeatModuleInputData.swift b/YooKassaPayments/Public/InputData/BankCardRepeatModuleInputData.swift index f9ea8fb4..eb6cb1c3 100644 --- a/YooKassaPayments/Public/InputData/BankCardRepeatModuleInputData.swift +++ b/YooKassaPayments/Public/InputData/BankCardRepeatModuleInputData.swift @@ -31,6 +31,10 @@ public struct BankCardRepeatModuleInputData { /// Setting for saving payment method. let savePaymentMethod: SavePaymentMethod + /// Gateway ID. Setup, is provided at check in YooKassa. + /// The cashier at the division of payment flows within a single account. + let gatewayId: String? + /// Creates instance of `BankCardRepeatModuleInputData`. /// /// - Parameters: @@ -44,6 +48,8 @@ public struct BankCardRepeatModuleInputData { /// - isLoggingEnabled: Enable logging /// - customizationSettings: Settings to customize SDK interface. /// - savePaymentMethod: Setting for saving payment method. + /// - gatewayId: Gateway ID. Setup, is provided at check in YooKassa. + /// The cashier at the division of payment flows within a single account. /// /// - Returns: Instance of `BankCardRepeatModuleInputData`. public init( @@ -56,7 +62,8 @@ public struct BankCardRepeatModuleInputData { returnUrl: String? = nil, isLoggingEnabled: Bool = false, customizationSettings: CustomizationSettings = CustomizationSettings(), - savePaymentMethod: SavePaymentMethod + savePaymentMethod: SavePaymentMethod, + gatewayId: String? = nil ) { self.clientApplicationKey = (clientApplicationKey + ":").base64Encoded() self.shopName = shopName @@ -68,5 +75,6 @@ public struct BankCardRepeatModuleInputData { self.isLoggingEnabled = isLoggingEnabled self.customizationSettings = customizationSettings self.savePaymentMethod = savePaymentMethod + self.gatewayId = gatewayId } } diff --git a/YooKassaPayments/Public/InputData/TokenizationModuleInputData.swift b/YooKassaPayments/Public/InputData/TokenizationModuleInputData.swift index 4e9dfc98..d6a39631 100644 --- a/YooKassaPayments/Public/InputData/TokenizationModuleInputData.swift +++ b/YooKassaPayments/Public/InputData/TokenizationModuleInputData.swift @@ -47,6 +47,10 @@ public struct TokenizationModuleInputData { /// Money center authorization identifier. let moneyAuthClientId: String? + + /// Application scheme for returning after opening a deeplink. + /// Example: myapplication:// + let applicationScheme: String? /// Creates instance of `TokenizationModuleInputData`. /// @@ -68,6 +72,7 @@ public struct TokenizationModuleInputData { /// - customizationSettings: Settings to customize SDK interface. /// - savePaymentMethod: Setting for saving payment method. /// - moneyAuthClientId: Money center authorization identifier + /// - applicationScheme: Application scheme for returning after opening a deeplink. /// /// - Returns: Instance of `TokenizationModuleInputData`. public init( @@ -85,7 +90,8 @@ public struct TokenizationModuleInputData { userPhoneNumber: String? = nil, customizationSettings: CustomizationSettings = CustomizationSettings(), savePaymentMethod: SavePaymentMethod, - moneyAuthClientId: String? = nil + moneyAuthClientId: String? = nil, + applicationScheme: String? = nil ) { self.clientApplicationKey = (clientApplicationKey + ":").base64Encoded() self.shopName = shopName @@ -102,5 +108,6 @@ public struct TokenizationModuleInputData { self.customizationSettings = customizationSettings self.savePaymentMethod = savePaymentMethod self.moneyAuthClientId = moneyAuthClientId + self.applicationScheme = applicationScheme } } diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/Common/avatar.imageset/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/Common/avatar.imageset/Contents.json index c9b4dfed..975e7427 100644 --- a/YooKassaPayments/Public/Resources/Media.xcassets/Common/avatar.imageset/Contents.json +++ b/YooKassaPayments/Public/Resources/Media.xcassets/Common/avatar.imageset/Contents.json @@ -3,6 +3,16 @@ { "filename" : "avatar.pdf", "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "avatar-1.pdf", + "idiom" : "universal" } ], "info" : { diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/Common/avatar.imageset/avatar-1.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/Common/avatar.imageset/avatar-1.pdf new file mode 100644 index 00000000..6ba574c9 Binary files /dev/null and b/YooKassaPayments/Public/Resources/Media.xcassets/Common/avatar.imageset/avatar-1.pdf differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.AmericanExpress.imageset/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.AmericanExpress.imageset/Contents.json index 83a66311..11ba9ac4 100644 --- a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.AmericanExpress.imageset/Contents.json +++ b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.AmericanExpress.imageset/Contents.json @@ -1,12 +1,12 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "americanExpress.pdf" + "filename" : "PaymentMethod.AmericanExpress.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.AmericanExpress.imageset/PaymentMethod.AmericanExpress.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.AmericanExpress.imageset/PaymentMethod.AmericanExpress.pdf new file mode 100644 index 00000000..a4a1ff9f Binary files /dev/null and b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.AmericanExpress.imageset/PaymentMethod.AmericanExpress.pdf differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.AmericanExpress.imageset/americanExpress.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.AmericanExpress.imageset/americanExpress.pdf deleted file mode 100644 index 939796a8..00000000 Binary files a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.AmericanExpress.imageset/americanExpress.pdf and /dev/null differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Cup.imageset/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Cup.imageset/Contents.json index 387809ed..745cf7a7 100644 --- a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Cup.imageset/Contents.json +++ b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Cup.imageset/Contents.json @@ -1,12 +1,12 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "cup.pdf" + "filename" : "PaymentMethod.Cup.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Cup.imageset/PaymentMethod.Cup.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Cup.imageset/PaymentMethod.Cup.pdf new file mode 100644 index 00000000..35b17bca Binary files /dev/null and b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Cup.imageset/PaymentMethod.Cup.pdf differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Cup.imageset/cup.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Cup.imageset/cup.pdf deleted file mode 100644 index 0bfde833..00000000 Binary files a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Cup.imageset/cup.pdf and /dev/null differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Dankort.imageset/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Dankort.imageset/Contents.json index 51c4418f..475af0fd 100644 --- a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Dankort.imageset/Contents.json +++ b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Dankort.imageset/Contents.json @@ -1,12 +1,12 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "dankort.pdf" + "filename" : "PaymentMethod.Dankort.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Dankort.imageset/PaymentMethod.Dankort.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Dankort.imageset/PaymentMethod.Dankort.pdf new file mode 100644 index 00000000..67a4a7ef Binary files /dev/null and b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Dankort.imageset/PaymentMethod.Dankort.pdf differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Dankort.imageset/dankort.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Dankort.imageset/dankort.pdf deleted file mode 100644 index 0ca7e50f..00000000 Binary files a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Dankort.imageset/dankort.pdf and /dev/null differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.DinersClub.imageset/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.DinersClub.imageset/Contents.json index 20c9a5b4..e9fee15c 100644 --- a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.DinersClub.imageset/Contents.json +++ b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.DinersClub.imageset/Contents.json @@ -1,12 +1,12 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "dinersClub.pdf" + "filename" : "PaymentMethod.DinersClub.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.DinersClub.imageset/PaymentMethod.DinersClub.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.DinersClub.imageset/PaymentMethod.DinersClub.pdf new file mode 100644 index 00000000..f7435285 Binary files /dev/null and b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.DinersClub.imageset/PaymentMethod.DinersClub.pdf differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.DinersClub.imageset/dinersClub.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.DinersClub.imageset/dinersClub.pdf deleted file mode 100644 index 33e566dc..00000000 Binary files a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.DinersClub.imageset/dinersClub.pdf and /dev/null differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.DiscoverCard.imageset/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.DiscoverCard.imageset/Contents.json index d2e8e636..38009de9 100644 --- a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.DiscoverCard.imageset/Contents.json +++ b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.DiscoverCard.imageset/Contents.json @@ -1,12 +1,12 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "discoverCard.pdf" + "filename" : "PaymentMethod.DiscoverCard.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.DiscoverCard.imageset/PaymentMethod.DiscoverCard.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.DiscoverCard.imageset/PaymentMethod.DiscoverCard.pdf new file mode 100644 index 00000000..803267d0 Binary files /dev/null and b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.DiscoverCard.imageset/PaymentMethod.DiscoverCard.pdf differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.DiscoverCard.imageset/discoverCard.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.DiscoverCard.imageset/discoverCard.pdf deleted file mode 100644 index 4dafc220..00000000 Binary files a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.DiscoverCard.imageset/discoverCard.pdf and /dev/null differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Instapay.imageset/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Instapay.imageset/Contents.json index e8df349c..0a8ab126 100644 --- a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Instapay.imageset/Contents.json +++ b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Instapay.imageset/Contents.json @@ -1,12 +1,12 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "instapay.pdf" + "filename" : "PaymentMethod.Instapay.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Instapay.imageset/PaymentMethod.Instapay.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Instapay.imageset/PaymentMethod.Instapay.pdf new file mode 100644 index 00000000..4a3f58ed Binary files /dev/null and b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Instapay.imageset/PaymentMethod.Instapay.pdf differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Instapay.imageset/instapay.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Instapay.imageset/instapay.pdf deleted file mode 100644 index 4f6acf63..00000000 Binary files a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Instapay.imageset/instapay.pdf and /dev/null differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Jcb.imageset/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Jcb.imageset/Contents.json index c28dcb66..83a2a5f3 100644 --- a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Jcb.imageset/Contents.json +++ b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Jcb.imageset/Contents.json @@ -1,12 +1,12 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "jcb.pdf" + "filename" : "PaymentMethod.Jcb.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Jcb.imageset/PaymentMethod.Jcb.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Jcb.imageset/PaymentMethod.Jcb.pdf new file mode 100644 index 00000000..4c37d59d Binary files /dev/null and b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Jcb.imageset/PaymentMethod.Jcb.pdf differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Jcb.imageset/jcb.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Jcb.imageset/jcb.pdf deleted file mode 100644 index dcd592b4..00000000 Binary files a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Jcb.imageset/jcb.pdf and /dev/null differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Lazer.imageset/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Lazer.imageset/Contents.json index 669470b3..b181ad2e 100644 --- a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Lazer.imageset/Contents.json +++ b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Lazer.imageset/Contents.json @@ -1,12 +1,12 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "lazer.pdf" + "filename" : "PaymentMethod.Lazer.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Lazer.imageset/PaymentMethod.Lazer.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Lazer.imageset/PaymentMethod.Lazer.pdf new file mode 100644 index 00000000..4cf072ee Binary files /dev/null and b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Lazer.imageset/PaymentMethod.Lazer.pdf differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Lazer.imageset/lazer.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Lazer.imageset/lazer.pdf deleted file mode 100644 index 21e6faef..00000000 Binary files a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Lazer.imageset/lazer.pdf and /dev/null differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Maestro.imageset/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Maestro.imageset/Contents.json index bbf2094c..a08c4afc 100644 --- a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Maestro.imageset/Contents.json +++ b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Maestro.imageset/Contents.json @@ -1,12 +1,12 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "maestro.pdf" + "filename" : "PaymentMethod.Maestro.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Maestro.imageset/PaymentMethod.Maestro.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Maestro.imageset/PaymentMethod.Maestro.pdf new file mode 100644 index 00000000..fb4b5b54 Binary files /dev/null and b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Maestro.imageset/PaymentMethod.Maestro.pdf differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Maestro.imageset/maestro.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Maestro.imageset/maestro.pdf deleted file mode 100644 index 0cd8ceef..00000000 Binary files a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Maestro.imageset/maestro.pdf and /dev/null differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Mastercard.imageset/PaymentMethod.Mastercard.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Mastercard.imageset/PaymentMethod.Mastercard.pdf index 3e9a98e0..000e5bee 100644 Binary files a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Mastercard.imageset/PaymentMethod.Mastercard.pdf and b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Mastercard.imageset/PaymentMethod.Mastercard.pdf differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Mir.imageset/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Mir.imageset/Contents.json index 0e5cc298..4ae3e800 100644 --- a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Mir.imageset/Contents.json +++ b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Mir.imageset/Contents.json @@ -1,12 +1,12 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "mir.pdf" + "filename" : "PaymentMethod.Mir.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Mir.imageset/PaymentMethod.Mir.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Mir.imageset/PaymentMethod.Mir.pdf new file mode 100644 index 00000000..368df429 Binary files /dev/null and b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Mir.imageset/PaymentMethod.Mir.pdf differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Mir.imageset/mir.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Mir.imageset/mir.pdf deleted file mode 100644 index cbe70086..00000000 Binary files a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Mir.imageset/mir.pdf and /dev/null differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Sberbank.imageset/PaymentMethod.Sberbank.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Sberbank.imageset/PaymentMethod.Sberbank.pdf deleted file mode 100644 index de0ec1bb..00000000 Binary files a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Sberbank.imageset/PaymentMethod.Sberbank.pdf and /dev/null differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Sberbank.imageset/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Sberpay.imageset/Contents.json similarity index 71% rename from YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Sberbank.imageset/Contents.json rename to YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Sberpay.imageset/Contents.json index 481207db..19205fa3 100644 --- a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Sberbank.imageset/Contents.json +++ b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Sberpay.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "PaymentMethod.Sberbank.pdf", + "filename" : "PaymentMethod.Sberpay.pdf", "idiom" : "universal" } ], diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Sberpay.imageset/PaymentMethod.Sberpay.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Sberpay.imageset/PaymentMethod.Sberpay.pdf new file mode 100644 index 00000000..1ad84e29 Binary files /dev/null and b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Sberpay.imageset/PaymentMethod.Sberpay.pdf differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Solo.imageset/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Solo.imageset/Contents.json index cc0d0988..1b5359bf 100644 --- a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Solo.imageset/Contents.json +++ b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Solo.imageset/Contents.json @@ -1,12 +1,12 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "solo.pdf" + "filename" : "PaymentMethod.Solo.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Solo.imageset/PaymentMethod.Solo.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Solo.imageset/PaymentMethod.Solo.pdf new file mode 100644 index 00000000..f2059f48 Binary files /dev/null and b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Solo.imageset/PaymentMethod.Solo.pdf differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Solo.imageset/solo.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Solo.imageset/solo.pdf deleted file mode 100644 index 8246c7a6..00000000 Binary files a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Solo.imageset/solo.pdf and /dev/null differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Switch.imageset/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Switch.imageset/Contents.json index fed15410..5ad74f9b 100644 --- a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Switch.imageset/Contents.json +++ b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Switch.imageset/Contents.json @@ -1,12 +1,12 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "switch.pdf" + "filename" : "PaymentMethod.Switch.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Switch.imageset/PaymentMethod.Switch.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Switch.imageset/PaymentMethod.Switch.pdf new file mode 100644 index 00000000..b5327e9a Binary files /dev/null and b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Switch.imageset/PaymentMethod.Switch.pdf differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Switch.imageset/switch.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Switch.imageset/switch.pdf deleted file mode 100644 index e695a4e4..00000000 Binary files a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Switch.imageset/switch.pdf and /dev/null differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Visa.imageset/PaymentMethod.Visa.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Visa.imageset/PaymentMethod.Visa.pdf index 574ec52b..1f8d8ceb 100644 Binary files a/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Visa.imageset/PaymentMethod.Visa.pdf and b/YooKassaPayments/Public/Resources/Media.xcassets/PaymentMethods/PaymentMethod.Visa.imageset/PaymentMethod.Visa.pdf differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/Contents.json deleted file mode 100644 index da4a164c..00000000 --- a/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_maestro_textControl.imageset/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_maestro_textControl.imageset/Contents.json deleted file mode 100644 index 88e9b6c1..00000000 --- a/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_maestro_textControl.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "paymentSystem_maestro_textControl.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_maestro_textControl.imageset/paymentSystem_maestro_textControl.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_maestro_textControl.imageset/paymentSystem_maestro_textControl.pdf deleted file mode 100644 index ef674b84..00000000 Binary files a/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_maestro_textControl.imageset/paymentSystem_maestro_textControl.pdf and /dev/null differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_masterCard_textControl.imageset/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_masterCard_textControl.imageset/Contents.json deleted file mode 100644 index ec842545..00000000 --- a/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_masterCard_textControl.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "paymentSystem_masterCard_textControl.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_masterCard_textControl.imageset/paymentSystem_masterCard_textControl.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_masterCard_textControl.imageset/paymentSystem_masterCard_textControl.pdf deleted file mode 100644 index 71a3d4da..00000000 Binary files a/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_masterCard_textControl.imageset/paymentSystem_masterCard_textControl.pdf and /dev/null differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_mir_textControl.imageset/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_mir_textControl.imageset/Contents.json deleted file mode 100644 index 34db428e..00000000 --- a/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_mir_textControl.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "paymentSystem_mir_textControl.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_mir_textControl.imageset/paymentSystem_mir_textControl.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_mir_textControl.imageset/paymentSystem_mir_textControl.pdf deleted file mode 100644 index 2357c77c..00000000 Binary files a/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_mir_textControl.imageset/paymentSystem_mir_textControl.pdf and /dev/null differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_unknownCard_textControl.imageset/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_unknownCard_textControl.imageset/Contents.json deleted file mode 100644 index 17c3b885..00000000 --- a/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_unknownCard_textControl.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "paymentSystem_unknownCard_textControl.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_unknownCard_textControl.imageset/paymentSystem_unknownCard_textControl.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_unknownCard_textControl.imageset/paymentSystem_unknownCard_textControl.pdf deleted file mode 100644 index 9eb02e0f..00000000 Binary files a/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_unknownCard_textControl.imageset/paymentSystem_unknownCard_textControl.pdf and /dev/null differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_visa_textControl.imageset/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_visa_textControl.imageset/Contents.json deleted file mode 100644 index cac6ca3c..00000000 --- a/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_visa_textControl.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "paymentSystem_visa_textControl.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_visa_textControl.imageset/paymentSystem_visa_textControl.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_visa_textControl.imageset/paymentSystem_visa_textControl.pdf deleted file mode 100644 index f1d88e03..00000000 Binary files a/YooKassaPayments/Public/Resources/Media.xcassets/paymentSystem_old/paymentSystem_visa_textControl.imageset/paymentSystem_visa_textControl.pdf and /dev/null differ diff --git a/YooKassaPayments/Public/Resources/en.lproj/Localizable.strings b/YooKassaPayments/Public/Resources/en.lproj/Localizable.strings index 5a51db03..68a4bb10 100644 --- a/YooKassaPayments/Public/Resources/en.lproj/Localizable.strings +++ b/YooKassaPayments/Public/Resources/en.lproj/Localizable.strings @@ -1,7 +1,7 @@ // Payment methods "PaymentMethods.paymentMethods" = "Payment method"; -"PaymentMethod.sberbank" = "Sberbank Online"; +"PaymentMethod.sberpay" = "SberPay"; "PaymentMethod.wallet" = "YooMoney"; "PaymentMethod.applePay" = "Apple Pay"; "PaymentMethod.bankCard" = "Bank card"; @@ -16,6 +16,7 @@ "BankCardView.inputCvcPlaceholder" = "CVC"; "BankCardDataInputView.BottomHint.invalidPan" = "Check the card number"; "BankCardDataInputView.BottomHint.invalidExpiry" = "Check the month and date"; +"BankCardDataInputView.BottomHint.invalidCvc" = "Check CVC"; "BankCard.savePaymentMethod.title" = "Link a card"; // Contract @@ -128,7 +129,7 @@ "ApplePayContract.fee" = "Commission"; -"Sberbank.Contract.Title" = "Sberbank Online"; +"Sberpay.Contract.Title" = "SberPay"; "Contract.resendSms" = "Send again"; @@ -136,6 +137,8 @@ "YooMoney.title" = "YooMoney"; +"Sberpay.paymentMethodTitle" = "Next, open the Sberbank Online application - confirm the payment"; + "LogoutConfirmation.format.title" = "Are you sure you want to sign out of account '%@'?"; "BankCardDataInput.navigationBarTitle" = "Bank card"; diff --git a/YooKassaPayments/Public/Resources/ru.lproj/Localizable.strings b/YooKassaPayments/Public/Resources/ru.lproj/Localizable.strings index 3841d1d5..bee4d175 100644 --- a/YooKassaPayments/Public/Resources/ru.lproj/Localizable.strings +++ b/YooKassaPayments/Public/Resources/ru.lproj/Localizable.strings @@ -1,7 +1,7 @@ // Payment methods "PaymentMethods.paymentMethods" = "Способ оплаты"; -"PaymentMethod.sberbank" = "Сбербанк Онлайн"; +"PaymentMethod.sberpay" = "SberPay"; "PaymentMethod.wallet" = "ЮMoney"; "PaymentMethod.applePay" = "Apple Pay"; "PaymentMethod.bankCard" = "Банковская карта"; @@ -16,6 +16,7 @@ "BankCardView.inputCvcPlaceholder" = "CVC"; "BankCardDataInputView.BottomHint.invalidPan" = "Проверьте номер карты"; "BankCardDataInputView.BottomHint.invalidExpiry" = "Проверьте месяц и год"; +"BankCardDataInputView.BottomHint.invalidCvc" = "Проверьте CVC"; "BankCard.savePaymentMethod.title" = "Привязать карту"; // Contract @@ -103,8 +104,6 @@ "Contract.Sberbank.PhoneInput.Placeholder" = "+ 7 987 654 32 10"; -"Sberbank.Contract.Title" = "СберБанк Онлайн"; - /*В процессе токенизации ApplePay произошла ошибка*/ "Error.ApplePayStrategy.failTokenizeData" = "В процессе токенизации ApplePay произошла ошибка"; @@ -130,12 +129,16 @@ "ApplePayContract.fee" = "Комиссия"; +"Sberpay.Contract.Title" = "SberPay"; + "Contract.resendSms" = "Отправить снова"; "ApplePayContract.title" = "Apple Pay"; "YooMoney.title" = "ЮMoney"; +"Sberpay.paymentMethodTitle" = "Дальше откроем приложение Сбербанк Онлайн — подтвердите оплату"; + "LogoutConfirmation.format.title" = "Уверены, что хотите выйти из аккаунта '%@'?"; "BankCardDataInput.navigationBarTitle" = "Банковская карта"; diff --git a/YooKassaPayments/Public/TokenizationAssembly.swift b/YooKassaPayments/Public/TokenizationAssembly.swift index 0e93b19d..2dc89a10 100644 --- a/YooKassaPayments/Public/TokenizationAssembly.swift +++ b/YooKassaPayments/Public/TokenizationAssembly.swift @@ -49,7 +49,14 @@ public enum TokenizationAssembly { _ inputData: TokenizationModuleInputData, moduleOutput: TokenizationModuleOutput ) -> UIViewController & TokenizationModuleInput { + YKSdk.shared.moduleOutput = moduleOutput + YKSdk.shared.applicationScheme = inputData.applicationScheme + YKSdk.shared.analyticsService = AnalyticsServiceAssembly.makeService( + isLoggingEnabled: inputData.isLoggingEnabled + ) + let paymentMethodsModuleInputData = PaymentMethodsModuleInputData( + applicationScheme: inputData.applicationScheme, clientApplicationKey: inputData.clientApplicationKey, applePayMerchantIdentifier: inputData.applePayMerchantIdentifier, gatewayId: inputData.gatewayId, @@ -90,6 +97,10 @@ public enum TokenizationAssembly { viewControllerToReturn = sheetViewController } + + YKSdk.shared.moduleOutput = moduleOutput + YKSdk.shared.applicationScheme = inputData.applicationScheme + YKSdk.shared.paymentMethodsModuleInput = moduleInput return viewControllerToReturn } diff --git a/YooKassaPayments/Public/TokenizationModuleIO.swift b/YooKassaPayments/Public/TokenizationModuleIO.swift index a488cad3..d3754c3a 100644 --- a/YooKassaPayments/Public/TokenizationModuleIO.swift +++ b/YooKassaPayments/Public/TokenizationModuleIO.swift @@ -7,7 +7,18 @@ public protocol TokenizationModuleInput: class { /// /// - Parameters: /// - requestUrl: URL string for request website. + @available(*, deprecated, message: "Use startConfirmationProcess(confirmationUrl:paymentMethodType:) instead") func start3dsProcess(requestUrl: String) + + /// Start confirmation process + /// + /// - Parameters: + /// - requestUrl: Deeplink. + /// - paymentMethodType: Type of the source of funds for the payment. + func startConfirmationProcess( + confirmationUrl: String, + paymentMethodType: PaymentMethodType + ) } /// Output for tokenization module. @@ -31,9 +42,18 @@ public protocol TokenizationModuleOutput: class { /// - module: Input for tokenization module. /// In the process of running mSDK, allows you to run processes using the /// `TokenizationModuleInput` protocol methods. + @available(*, deprecated, message: "Use didSuccessfullyConfirmation(paymentMethodType:) instead") func didSuccessfullyPassedCardSec( on module: TokenizationModuleInput ) + + /// Will be called when the confirmation process successfully passes. + /// + /// - Parameters: + /// - paymentMethodType: Type of the source of funds for the payment. + func didSuccessfullyConfirmation( + paymentMethodType: PaymentMethodType + ) /// Will be called when the tokenization process successfully passes. /// diff --git a/YooKassaPayments/Public/YKSdkService.swift b/YooKassaPayments/Public/YKSdkService.swift new file mode 100644 index 00000000..db6696ab --- /dev/null +++ b/YooKassaPayments/Public/YKSdkService.swift @@ -0,0 +1,46 @@ +/// Class for handle open url. +public final class YKSdk { + + /// Input for payment methods module. + weak var paymentMethodsModuleInput: PaymentMethodsModuleInput? + + /// Output for tokenization module. + weak var moduleOutput: TokenizationModuleOutput? + + /// Application scheme for returning after opening a deeplink. + var applicationScheme: String? + + var analyticsService: AnalyticsService? + + private init() {} + + /// Shared YooKassa sdk service. + public static let shared = YKSdk() + + public func handleOpen( + url: URL, + sourceApplication: String? + ) -> Bool { + guard let scheme = url.scheme, + let applicationScheme = applicationScheme, + "\(scheme)://" == applicationScheme, + let deeplink = DeepLinkFactory.makeDeepLink(url: url) else { + return false + } + + switch deeplink { + case .invoicingSberpay: + let event: AnalyticsEvent = .actionSberPayConfirmation( + sberPayConfirmationStatus: .success, + sdkVersion: Bundle.frameworkVersion + ) + analyticsService?.trackEvent(event) + moduleOutput?.didSuccessfullyConfirmation(paymentMethodType: .sberbank) + + case .yooMoneyExchange(let cryptogram): + paymentMethodsModuleInput?.authorizeInYooMoney(with: cryptogram) + } + + return true + } +} diff --git a/YooKassaPaymentsExample/AppDelegate.swift b/YooKassaPaymentsExample/AppDelegate.swift index a5bd16ce..e6311f9a 100644 --- a/YooKassaPaymentsExample/AppDelegate.swift +++ b/YooKassaPaymentsExample/AppDelegate.swift @@ -6,9 +6,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - func application(_ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) let viewController = UINavigationController(rootViewController: RootViewController()) window?.rootViewController = viewController @@ -48,6 +49,34 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } +// MARK: - Handle open url + +extension AppDelegate { + func application( + _ application: UIApplication, + open url: URL, + sourceApplication: String?, + annotation: Any + ) -> Bool { + return YKSdk.shared.handleOpen( + url: url, + sourceApplication: sourceApplication + ) + } + + @available(iOS 9.0, *) + func application( + _ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey: Any] = [:] + ) -> Bool { + return YKSdk.shared.handleOpen( + url: url, + sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String + ) + } +} + extension AppDelegate { private func setupAppearance() { diff --git a/YooKassaPaymentsExample/Resources/Info.plist b/YooKassaPaymentsExample/Resources/Info.plist index 7cd5af0f..a16071c4 100644 --- a/YooKassaPaymentsExample/Resources/Info.plist +++ b/YooKassaPaymentsExample/Resources/Info.plist @@ -18,8 +18,26 @@ APPL CFBundleShortVersionString 1.0 + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + ${BUNDLE_ID} + CFBundleURLSchemes + + yookassapaymentsexample + + + CFBundleVersion $(CURRENT_PROJECT_VERSION) + LSApplicationQueriesSchemes + + sberpay + yoomoneyauth + LSRequiresIPhoneOS NSAppTransportSecurity diff --git a/YooKassaPaymentsExample/Resources/en.lproj/Localizable.strings b/YooKassaPaymentsExample/Resources/en.lproj/Localizable.strings index c15c3a04..bfcb546d 100644 --- a/YooKassaPaymentsExample/Resources/en.lproj/Localizable.strings +++ b/YooKassaPaymentsExample/Resources/en.lproj/Localizable.strings @@ -16,6 +16,11 @@ "test_mode.payment_auth" = "The customer has logged into the Wallet"; "test_mode.attached_cards" = "Number of linked cards"; "test_mode.payment_error" = "Complete the payment with an error"; +"test_mode.process_type" = "Payment confirmation"; +"test_mode.process.3ds" = "3ds"; +"test_mode.process.app2app" = "app2app"; + +"process.title" = "Confirmation"; "settings.title" = "Settings"; "settings.payment_methods.title" = "Payment methods"; diff --git a/YooKassaPaymentsExample/Resources/ru.lproj/Localizable.strings b/YooKassaPaymentsExample/Resources/ru.lproj/Localizable.strings index 6d11c807..ea7893a9 100644 --- a/YooKassaPaymentsExample/Resources/ru.lproj/Localizable.strings +++ b/YooKassaPaymentsExample/Resources/ru.lproj/Localizable.strings @@ -16,6 +16,11 @@ "test_mode.payment_auth" = "Покупатель вошёл в кошелёк"; "test_mode.attached_cards" = "Количество привязанных карт"; "test_mode.payment_error" = "Завершить платёж ошибкой"; +"test_mode.process_type" = "Подтверждение платежа"; +"test_mode.process.3ds" = "3ds"; +"test_mode.process.app2app" = "app2app"; + +"process.title" = "Тип подтверждение"; "settings.title" = "Настройки"; "settings.payment_methods.title" = "Способы оплаты"; diff --git a/YooKassaPaymentsExample/RootViewController+TokenizationModuleOutput.swift b/YooKassaPaymentsExample/RootViewController+TokenizationModuleOutput.swift index 71b4f1c5..060c58aa 100644 --- a/YooKassaPaymentsExample/RootViewController+TokenizationModuleOutput.swift +++ b/YooKassaPaymentsExample/RootViewController+TokenizationModuleOutput.swift @@ -3,41 +3,82 @@ import YooKassaPayments // MARK: - TokenizationModuleOutput extension RootViewController: TokenizationModuleOutput { - func tokenizationModule(_ module: TokenizationModuleInput, - didTokenize token: Tokens, - paymentMethodType: PaymentMethodType) { - + func tokenizationModule( + _ module: TokenizationModuleInput, + didTokenize token: Tokens, + paymentMethodType: PaymentMethodType + ) { self.token = token self.paymentMethodType = paymentMethodType - + + if settings.testModeSettings.isTestModeEnadled, + let processConfirmation = settings.testModeSettings.processConfirmation { + switch processConfirmation { + case let .threeDSecure(requestUrl): + tokenizationModuleInput?.startConfirmationProcess( + confirmationUrl: requestUrl, + paymentMethodType: paymentMethodType + ) + + case let .app2app(confirmationUrl): + tokenizationModuleInput?.startConfirmationProcess( + confirmationUrl: confirmationUrl, + paymentMethodType: paymentMethodType + ) + } + } else { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + self.dismiss(animated: true) + + let successViewController = SuccessViewController() + let navigationController = UINavigationController( + rootViewController: successViewController + ) + successViewController.delegate = self + self.present(navigationController, animated: true) + } + } + } + + func didFinish( + on module: TokenizationModuleInput, + with error: YooKassaPaymentsError? + ) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } - self.dismiss(animated: true) - - let successViewController = SuccessViewController() - let navigationController = UINavigationController( - rootViewController: successViewController - ) - successViewController.delegate = self - self.present(navigationController, animated: true) } } - - func didFinish(on module: TokenizationModuleInput, - with error: YooKassaPaymentsError?) { + + func didSuccessfullyPassedCardSec( + on module: TokenizationModuleInput + ) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } + let alertController = UIAlertController( + title: "3D-Sec", + message: "Successfully passed 3d-sec", + preferredStyle: .alert + ) + let action = UIAlertAction(title: "OK", style: .default) + alertController.addAction(action) self.dismiss(animated: true) + self.present(alertController, animated: true) } } - - func didSuccessfullyPassedCardSec(on module: TokenizationModuleInput) { + + func didSuccessfullyConfirmation( + paymentMethodType: PaymentMethodType + ) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } - let alertController = UIAlertController(title: "3D-Sec", - message: "Successfully passed 3d-sec", - preferredStyle: .alert) + let alertController = UIAlertController( + title: "Confirmation", + message: "Successfully confirmation", + preferredStyle: .alert + ) let action = UIAlertAction(title: "OK", style: .default) alertController.addAction(action) self.dismiss(animated: true) diff --git a/YooKassaPaymentsExample/Source/Models/ProcessConfirmation.swift b/YooKassaPaymentsExample/Source/Models/ProcessConfirmation.swift new file mode 100644 index 00000000..27bc4707 --- /dev/null +++ b/YooKassaPaymentsExample/Source/Models/ProcessConfirmation.swift @@ -0,0 +1,71 @@ +enum ProcessConfirmation { + case threeDSecure(String) + case app2app(String) + + static var allCasesWithNil: [ProcessConfirmation?] = [ + nil, + .threeDSecure(""), + .app2app("sberpay://invoicing/v2?redirect_uri="), + ] +} + +extension ProcessConfirmation { + var description: String { + let value: String + switch self { + case .threeDSecure: + value = translate(Localized.process3ds) + case .app2app: + value = translate(Localized.processApp2App) + } + return value + } + + var url: String { + let value: String + switch self { + case let .threeDSecure(requestUrl): + value = requestUrl + case let .app2app(confirmationUrl): + value = confirmationUrl + } + return value + } + + private enum Localized: String { + case process3ds = "test_mode.process.3ds" + case processApp2App = "test_mode.process.app2app" + } +} + +extension ProcessConfirmation: Codable { + enum CodingKeys: CodingKey { + case threeDSecure + case app2app + } + + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + if let value = try? values.decode(String.self, forKey: .threeDSecure) { + self = .threeDSecure(value) + } else if let value = try? values.decode(String.self, forKey: .app2app) { + self = .app2app(value) + } else { + throw DecodingError.dataCorrupted + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .threeDSecure(requestUrl): + try container.encode(requestUrl, forKey: .threeDSecure) + case let .app2app(confirmationUrl): + try container.encode(confirmationUrl, forKey: .app2app) + } + } + + enum DecodingError: Error { + case dataCorrupted + } +} diff --git a/YooKassaPaymentsExample/Source/Models/TestSettings.swift b/YooKassaPaymentsExample/Source/Models/TestSettings.swift index b12a246f..75329a53 100644 --- a/YooKassaPaymentsExample/Source/Models/TestSettings.swift +++ b/YooKassaPaymentsExample/Source/Models/TestSettings.swift @@ -5,4 +5,5 @@ struct TestSettings { var isPaymentAuthorizationPassed = false var isPaymentWithError = false var cardsCount: Int? = 2 + var processConfirmation: ProcessConfirmation? } diff --git a/YooKassaPaymentsExample/Source/Services/SettingsService.swift b/YooKassaPaymentsExample/Source/Services/SettingsService.swift index 452b9585..3d459afd 100644 --- a/YooKassaPaymentsExample/Source/Services/SettingsService.swift +++ b/YooKassaPaymentsExample/Source/Services/SettingsService.swift @@ -1,36 +1,47 @@ import Foundation final class SettingsService { - + // MARK: - Private properties - + private let storage: KeyValueStoring - + // MARK: - Initialization/Deinitialization - + init(storage: KeyValueStoring) { self.storage = storage } - + func loadSettingsFromStorage() -> Settings? { if let isTestModeEnadled = storage.getBool(for: Constants.isTestModeEnadledKey), let isPaymentAuthorizationPassed = storage.getBool(for: Constants.isPaymentAuthorizationPassedKey), let isPaymentWithError = storage.getBool(for: Constants.isPaymentWithErrorKey) { - + let cardsCount = storage.getInt(for: Constants.cardsCountKey) - - let testSettings = TestSettings(isTestModeEnadled: isTestModeEnadled, - isPaymentAuthorizationPassed: isPaymentAuthorizationPassed, - isPaymentWithError: isPaymentWithError, - cardsCount: cardsCount) - + + var processConfirmation: ProcessConfirmation? + if let processConfirmationData = storage.getAny(for: Constants.processConfirmationKey) as? Data { + processConfirmation = try? JSONDecoder().decode( + ProcessConfirmation.self, + from: processConfirmationData + ) + } + + let testSettings = TestSettings( + isTestModeEnadled: isTestModeEnadled, + isPaymentAuthorizationPassed: isPaymentAuthorizationPassed, + isPaymentWithError: isPaymentWithError, + cardsCount: cardsCount, + processConfirmation: processConfirmation + ) + if let isYooMoneyEnabled = storage.getBool(for: Constants.isYooMoneyEnabledKey), let isBankCardEnabled = storage.getBool(for: Constants.isBankCardEnabledKey), let isApplePayEnabled = storage.getBool(for: Constants.isApplePayEnabledKey), let isSberbankEnabled = storage.getBool(for: Constants.isSberbankEnabledKey), let isShowingYooMoneyLogoEnabled = storage.getBool(for: Constants.isShowingYooMoneyLogoEnabledKey), let price = storage.getDecimal(for: Constants.priceKey) { - + return Settings( isYooMoneyEnabled: isYooMoneyEnabled, isBankCardEnabled: isBankCardEnabled, @@ -42,32 +53,56 @@ final class SettingsService { ) } } - + return nil } - + func saveSettingsToStorage(settings: Settings) { - storage.setBool(settings.isYooMoneyEnabled, - for: Constants.isYooMoneyEnabledKey) - storage.setBool(settings.isBankCardEnabled, - for: Constants.isBankCardEnabledKey) - storage.setBool(settings.isApplePayEnabled, - for: Constants.isApplePayEnabledKey) - storage.setBool(settings.isSberbankEnabled, - for: Constants.isSberbankEnabledKey) - storage.setBool(settings.isShowingYooMoneyLogoEnabled, - for: Constants.isShowingYooMoneyLogoEnabledKey) - storage.setDecimal(settings.price, - for: Constants.priceKey) - - storage.setBool(settings.testModeSettings.isTestModeEnadled, - for: Constants.isTestModeEnadledKey) - storage.setBool(settings.testModeSettings.isPaymentAuthorizationPassed, - for: Constants.isPaymentAuthorizationPassedKey) - storage.setBool(settings.testModeSettings.isPaymentWithError, - for: Constants.isPaymentWithErrorKey) - storage.setInt(settings.testModeSettings.cardsCount, - for: Constants.cardsCountKey) + storage.setBool( + settings.isYooMoneyEnabled, + for: Constants.isYooMoneyEnabledKey + ) + storage.setBool( + settings.isBankCardEnabled, + for: Constants.isBankCardEnabledKey + ) + storage.setBool( + settings.isApplePayEnabled, + for: Constants.isApplePayEnabledKey + ) + storage.setBool( + settings.isSberbankEnabled, + for: Constants.isSberbankEnabledKey + ) + storage.setBool( + settings.isShowingYooMoneyLogoEnabled, + for: Constants.isShowingYooMoneyLogoEnabledKey + ) + storage.setDecimal( + settings.price, + for: Constants.priceKey + ) + + storage.setBool( + settings.testModeSettings.isTestModeEnadled, + for: Constants.isTestModeEnadledKey + ) + storage.setBool( + settings.testModeSettings.isPaymentAuthorizationPassed, + for: Constants.isPaymentAuthorizationPassedKey + ) + storage.setBool( + settings.testModeSettings.isPaymentWithError, + for: Constants.isPaymentWithErrorKey + ) + storage.setInt( + settings.testModeSettings.cardsCount, + for: Constants.cardsCountKey + ) + storage.setAny( + try? JSONEncoder().encode(settings.testModeSettings.processConfirmation), + for: Constants.processConfirmationKey + ) } } @@ -79,10 +114,12 @@ private extension SettingsService { static let isSberbankEnabledKey = "isSberbankEnabled" static let isShowingYooMoneyLogoEnabledKey = "isShowingYooMoneyLogoEnabled" static let priceKey = "price" - + static let isTestModeEnadledKey = "isTestModeEnadled" static let isPaymentAuthorizationPassedKey = "isPaymentAuthorizationPassed" static let isPaymentWithErrorKey = "isPaymentWithError" static let cardsCountKey = "cardsCount" + + static let processConfirmationKey = "processConfirmation" } } diff --git a/YooKassaPaymentsExample/Source/UserStories/Cards/AttachedCardCountViewController.swift b/YooKassaPaymentsExample/Source/UserStories/Cards/AttachedCardCountViewController.swift index 49e0e265..26217b89 100644 --- a/YooKassaPaymentsExample/Source/UserStories/Cards/AttachedCardCountViewController.swift +++ b/YooKassaPaymentsExample/Source/UserStories/Cards/AttachedCardCountViewController.swift @@ -1,19 +1,21 @@ import UIKit protocol AttachedCardCountViewControllerDelegate: class { - func attachedCardCountViewController(_ attachedCardCountViewController: AttachedCardCountViewController, - didSaveCardCount cardCount: Int?) + func attachedCardCountViewController( + _ attachedCardCountViewController: AttachedCardCountViewController, + didSaveCardCount cardCount: Int? + ) } final class AttachedCardCountViewController: UIViewController { - public static func makeModule(cardCount: Int?, - delegate: AttachedCardCountViewControllerDelegate? = nil) -> UIViewController { - + public static func makeModule( + cardCount: Int?, + delegate: AttachedCardCountViewControllerDelegate? = nil + ) -> UIViewController { let controller = AttachedCardCountViewController() controller.delegate = delegate controller.initialCardCount = cardCount - return controller } @@ -36,10 +38,12 @@ final class AttachedCardCountViewController: UIViewController { return $0 }(UIPickerView()) - fileprivate lazy var saveBarItem = UIBarButtonItem(title: translate(CommonLocalized.save), - style: .plain, - target: self, - action: #selector(saveButtonDidPress)) + fileprivate lazy var saveBarItem = UIBarButtonItem( + title: translate(CommonLocalized.save), + style: .plain, + target: self, + action: #selector(saveButtonDidPress) + ) // MARK: - Managing the View @@ -73,34 +77,49 @@ final class AttachedCardCountViewController: UIViewController { @objc private func saveButtonDidPress() { let cardCount = countPickerView.selectedRow(inComponent: 0) - delegate?.attachedCardCountViewController(self, - didSaveCardCount: cardCount == 0 ? nil : cardCount) + delegate?.attachedCardCountViewController( + self, + didSaveCardCount: cardCount == 0 ? nil : cardCount + ) navigationController?.popViewController(animated: true) } } extension AttachedCardCountViewController: UIPickerViewDelegate { - - func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + func pickerView( + _ pickerView: UIPickerView, + titleForRow row: Int, + forComponent component: Int + ) -> String? { return cards[row] == 0 ? translate(CommonLocalized.none) : String(cards[row]) } - func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + func pickerView( + _ pickerView: UIPickerView, + rowHeightForComponent component: Int + ) -> CGFloat { return Space.quadruple } - public func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { - - } + public func pickerView( + _ pickerView: UIPickerView, + didSelectRow row: Int, + inComponent component: Int + ) {} } extension AttachedCardCountViewController: UIPickerViewDataSource { - public func numberOfComponents(in pickerView: UIPickerView) -> Int { + public func numberOfComponents( + in pickerView: UIPickerView + ) -> Int { return 1 } - public func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + public func pickerView( + _ pickerView: UIPickerView, + numberOfRowsInComponent component: Int + ) -> Int { return cards.count } } diff --git a/YooKassaPaymentsExample/Source/UserStories/Process/ProcessViewController.swift b/YooKassaPaymentsExample/Source/UserStories/Process/ProcessViewController.swift new file mode 100644 index 00000000..7cc7a9d3 --- /dev/null +++ b/YooKassaPaymentsExample/Source/UserStories/Process/ProcessViewController.swift @@ -0,0 +1,141 @@ +import UIKit + +protocol ProcessViewControllerDelegate: class { + func processViewController( + _ processViewController: ProcessViewController, + processConfirmation: ProcessConfirmation? + ) +} + +final class ProcessViewController: UIViewController { + public static func makeModule( + processConfirmation: ProcessConfirmation?, + delegate: ProcessViewControllerDelegate? = nil + ) -> UIViewController { + let controller = ProcessViewController() + controller.delegate = delegate + controller.initialProcessConfirmation = processConfirmation + return controller + } + + weak var delegate: ProcessViewControllerDelegate? + + private let processesConfirmation = ProcessConfirmation.allCasesWithNil + + // MARK: - Initial values + + private var initialProcessConfirmation: ProcessConfirmation? + + // MARK: - UI properties + + fileprivate lazy var countPickerView: UIPickerView = { + $0.dataSource = self + $0.delegate = self + $0.translatesAutoresizingMaskIntoConstraints = false + $0.setStyles(UIPickerView.Styles.defaultBackground) + $0.selectRow( + processesConfirmation.firstIndex(where: { + switch ($0, initialProcessConfirmation) { + case (.threeDSecure, .threeDSecure): return true + case (.app2app, .app2app): return true + default: return false + } + }) ?? 0, + inComponent: 0, + animated: false + ) + return $0 + }(UIPickerView()) + + fileprivate lazy var saveBarItem = UIBarButtonItem( + title: translate(CommonLocalized.save), + style: .plain, + target: self, + action: #selector(saveButtonDidPress) + ) + + // MARK: - Managing the View + + override func loadView() { + view = UIView() + view.setStyles(UIView.Styles.grayBackground) + navigationItem.title = translate(Localized.title) + + loadSubviews() + loadConstraints() + } + + private func loadSubviews() { + view.addSubview(countPickerView) + navigationItem.rightBarButtonItem = saveBarItem + } + + private func loadConstraints() { + let constraints = [ + countPickerView.leading.constraint(equalTo: view.leading), + topLayoutGuide.bottom.constraint(equalTo: countPickerView.top), + countPickerView.trailing.constraint(equalTo: view.trailing), + countPickerView.height.constraint(equalToConstant: 154), + ] + + NSLayoutConstraint.activate(constraints) + } + + // MARK: - Actions + + @objc + private func saveButtonDidPress() { + let selectedRow = countPickerView.selectedRow(inComponent: 0) + delegate?.processViewController( + self, + processConfirmation: processesConfirmation[selectedRow] + ) + navigationController?.popViewController(animated: true) + } +} + +extension ProcessViewController: UIPickerViewDelegate { + func pickerView( + _ pickerView: UIPickerView, + titleForRow row: Int, + forComponent component: Int + ) -> String? { + return processesConfirmation[row]?.description + ?? translate(CommonLocalized.none) + } + + func pickerView( + _ pickerView: UIPickerView, + rowHeightForComponent component: Int + ) -> CGFloat { + return Space.quadruple + } + + public func pickerView( + _ pickerView: UIPickerView, + didSelectRow row: Int, + inComponent component: Int + ) {} +} + +extension ProcessViewController: UIPickerViewDataSource { + + public func numberOfComponents( + in pickerView: UIPickerView + ) -> Int { + return 1 + } + + public func pickerView( + _ pickerView: UIPickerView, + numberOfRowsInComponent component: Int + ) -> Int { + return processesConfirmation.count + } +} + +private extension ProcessViewController { + enum Localized: String { + case title = "process.title" + } +} diff --git a/YooKassaPaymentsExample/Source/UserStories/Root/RootViewController.swift b/YooKassaPaymentsExample/Source/UserStories/Root/RootViewController.swift index f7f50500..c8ce71ef 100644 --- a/YooKassaPaymentsExample/Source/UserStories/Root/RootViewController.swift +++ b/YooKassaPaymentsExample/Source/UserStories/Root/RootViewController.swift @@ -86,10 +86,12 @@ final class RootViewController: UIViewController { fileprivate lazy var imageView = UIImageView(image: #imageLiteral(resourceName: "Root.Comet")) - fileprivate lazy var settingsBarItem = UIBarButtonItem(image: #imageLiteral(resourceName: "Root.Settings"), - style: .plain, - target: self, - action: #selector(settingsButtonDidPress)) + fileprivate lazy var settingsBarItem = UIBarButtonItem( + image: #imageLiteral(resourceName: "Root.Settings"), + style: .plain, + target: self, + action: #selector(settingsButtonDidPress) + ) fileprivate lazy var ratingImageView = UIImageView(image: #imageLiteral(resourceName: "Root.Rating")) @@ -103,7 +105,15 @@ final class RootViewController: UIViewController { // MARK: - Private properties - private lazy var settings: Settings = { + private let settingsService: SettingsService + + private var currentKeyboardOffset: CGFloat = 0 + + // MARK: - Data properties + + var token: Tokens? + var paymentMethodType: PaymentMethodType? + lazy var settings: Settings = { if let settings = settingsService.loadSettingsFromStorage() { return settings } else { @@ -114,15 +124,6 @@ final class RootViewController: UIViewController { } }() - private let settingsService: SettingsService - - private var currentKeyboardOffset: CGFloat = 0 - - // MARK: - Data properties - - var token: Tokens? - var paymentMethodType: PaymentMethodType? - // MARK: - Initialization/Deinitialization override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { @@ -201,8 +202,10 @@ final class RootViewController: UIViewController { contentView.addSubview(subview) } - let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, - action: #selector(rootViewPressed)) + let tap: UITapGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(rootViewPressed) + ) view.addGestureRecognizer(tap) } @@ -323,12 +326,13 @@ final class RootViewController: UIViewController { let amount = Amount(value: settings.price, currency: .rub) if settings.testModeSettings.isTestModeEnadled { - let paymentAuthorizationPassed = settings.testModeSettings.isPaymentAuthorizationPassed - testSettings = TestModeSettings(paymentAuthorizationPassed: paymentAuthorizationPassed, - cardsCount: settings.testModeSettings.cardsCount ?? 0, - charge: amount, - enablePaymentError: settings.testModeSettings.isPaymentWithError) + testSettings = TestModeSettings( + paymentAuthorizationPassed: paymentAuthorizationPassed, + cardsCount: settings.testModeSettings.cardsCount ?? 0, + charge: amount, + enablePaymentError: settings.testModeSettings.isPaymentWithError + ) } else { testSettings = nil } @@ -356,7 +360,8 @@ final class RootViewController: UIViewController { userPhoneNumber: "7", customizationSettings: CustomizationSettings(mainScheme: .blueRibbon), savePaymentMethod: .userSelects, - moneyAuthClientId: "hitm6hg51j1d3g1u3ln040bajiol903b" + moneyAuthClientId: "hitm6hg51j1d3g1u3ln040bajiol903b", + applicationScheme: "yookassapaymentsexample://" )) // let inputData: TokenizationFlow = .bankCardRepeat(BankCardRepeatModuleInputData( @@ -368,7 +373,8 @@ final class RootViewController: UIViewController { // testModeSettings: testSettings, // isLoggingEnabled: true, // customizationSettings: CustomizationSettings(mainScheme: .blueRibbon), -// savePaymentMethod: .userSelects +// savePaymentMethod: .userSelects, +// gatewayId: nil // )) let viewController = TokenizationAssembly.makeModule( @@ -403,10 +409,12 @@ final class RootViewController: UIViewController { // MARK: - Notifications private func subscribeOnNotifications() { - NotificationCenter.default.addObserver(self, - selector: #selector(accessibilityReapply), - name: UIContentSizeCategory.didChangeNotification, - object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(accessibilityReapply), + name: UIContentSizeCategory.didChangeNotification, + object: nil + ) } private func unsubscribeFromNotifications() { @@ -421,17 +429,21 @@ final class RootViewController: UIViewController { if traitCollection.horizontalSizeClass == .regular { payButtonBottomConstraint.constant = Space.fivefold nameLabelTopConstraint.constant = Constants.nameTopOffset * 2 - contentView.layoutMargins = UIEdgeInsets(top: Space.quadruple, - left: view.frame.width * Constants.widthRatio, - bottom: 0, - right: view.frame.width * Constants.widthRatio) + contentView.layoutMargins = UIEdgeInsets( + top: Space.quadruple, + left: view.frame.width * Constants.widthRatio, + bottom: 0, + right: view.frame.width * Constants.widthRatio + ) } else { payButtonBottomConstraint.constant = Space.double nameLabelTopConstraint.constant = Constants.nameTopOffset - contentView.layoutMargins = UIEdgeInsets(top: Space.single, - left: Space.double, - bottom: 0, - right: Space.double) + contentView.layoutMargins = UIEdgeInsets( + top: Space.single, + left: Space.double, + bottom: 0, + right: Space.double + ) } updatePayButtonBottomConstraint() @@ -455,17 +467,20 @@ final class RootViewController: UIViewController { paymentTypes.insert(.sberbank) } - return TokenizationSettings(paymentMethodTypes: paymentTypes, - showYooKassaLogo: settings.isShowingYooMoneyLogoEnabled) + return TokenizationSettings( + paymentMethodTypes: paymentTypes, + showYooKassaLogo: settings.isShowingYooMoneyLogoEnabled + ) } } // MARK: - SettingsViewControllerDelegate extension RootViewController: SettingsViewControllerDelegate { - func settingsViewController(_ settingsViewController: SettingsViewController, - didChangeSettings settings: Settings) { - + func settingsViewController( + _ settingsViewController: SettingsViewController, + didChangeSettings settings: Settings + ) { self.settings = settings settingsService.saveSettingsToStorage(settings: settings) } @@ -476,16 +491,19 @@ extension RootViewController: SettingsViewControllerDelegate { extension RootViewController { func startKeyboardObserving() { - - NotificationCenter.default.addObserver(self, - selector: #selector(keyboardWillShow(_:)), - name: UIResponder.keyboardWillShowNotification, - object: nil) - - NotificationCenter.default.addObserver(self, - selector: #selector(keyboardWillHide(_:)), - name: UIResponder.keyboardWillHideNotification, - object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardWillShow(_:)), + name: UIResponder.keyboardWillShowNotification, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardWillHide(_:)), + name: UIResponder.keyboardWillHideNotification, + object: nil + ) } func stopKeyboardObserving() { @@ -531,26 +549,31 @@ extension RootViewController { } extension RootViewController: MFMailComposeViewControllerDelegate { - func mailComposeController(_ controller: MFMailComposeViewController, - didFinishWith result: MFMailComposeResult, - error: Error?) { + func mailComposeController( + _ controller: MFMailComposeViewController, + didFinishWith result: MFMailComposeResult, + error: Error? + ) { controller.dismiss(animated: true) } } extension RootViewController: PriceInputViewControllerDelegate { - func priceInputViewController(_ priceInputViewController: PriceInputViewController, - didChangePrice price: Decimal?, - valid: Bool) { - + func priceInputViewController( + _ priceInputViewController: PriceInputViewController, + didChangePrice price: Decimal?, + valid: Bool + ) { if price != nil, valid == true { payButton.isEnabled = true } else { payButton.isEnabled = false } - scrollView.scrollRectToVisible(priceInputViewController.view.frame, - animated: true) + scrollView.scrollRectToVisible( + priceInputViewController.view.frame, + animated: true + ) } } @@ -598,9 +621,11 @@ extension RootViewController: SuccessViewControllerDelegate { let viewController: UIViewController if MFMailComposeViewController.canSendMail() == false { - let alertController = UIAlertController(title: "Token", - message: message, - preferredStyle: .alert) + let alertController = UIAlertController( + title: "Token", + message: message, + preferredStyle: .alert + ) let action = UIAlertAction(title: "OK", style: .default) alertController.addAction(action) viewController = alertController diff --git a/YooKassaPaymentsExample/Source/UserStories/Settings/SettingsViewController.swift b/YooKassaPaymentsExample/Source/UserStories/Settings/SettingsViewController.swift index 19c92581..205dd0f4 100644 --- a/YooKassaPaymentsExample/Source/UserStories/Settings/SettingsViewController.swift +++ b/YooKassaPaymentsExample/Source/UserStories/Settings/SettingsViewController.swift @@ -1,18 +1,20 @@ import UIKit protocol SettingsViewControllerDelegate: class { - func settingsViewController(_ settingsViewController: SettingsViewController, - didChangeSettings settings: Settings) + func settingsViewController( + _ settingsViewController: SettingsViewController, + didChangeSettings settings: Settings + ) } final class SettingsViewController: UIViewController { - public static func makeModule(settings: Settings, - delegate: SettingsViewControllerDelegate? = nil) -> UIViewController { - + public static func makeModule( + settings: Settings, + delegate: SettingsViewControllerDelegate? = nil + ) -> UIViewController { let controller = SettingsViewController(settings: settings) controller.delegate = delegate - return controller } @@ -21,11 +23,13 @@ final class SettingsViewController: UIViewController { // MARK: - UI properties private lazy var tableViewController = TableViewController(style: .grouped) - - private lazy var closeBarItem = UIBarButtonItem(image: #imageLiteral(resourceName: "Settings.Close"), - style: .plain, - target: self, - action: #selector(closeBarButtonItemDidPress)) + + private lazy var closeBarItem = UIBarButtonItem( + image: #imageLiteral(resourceName: "Settings.Close"), + style: .plain, + target: self, + action: #selector(closeBarButtonItemDidPress) + ) // MARK: - Private properties @@ -74,7 +78,12 @@ final class SettingsViewController: UIViewController { navigationItem.leftBarButtonItem = closeBarItem navigationItem.title = Localized.title - navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) + navigationItem.backBarButtonItem = UIBarButtonItem( + title: "", + style: .plain, + target: nil, + action: nil + ) tableViewController.sections = sectionDescriptors tableViewController.reload() @@ -104,7 +113,10 @@ final class SettingsViewController: UIViewController { @objc private func closeBarButtonItemDidPress() { - navigationController?.presentingViewController?.dismiss(animated: true, completion: nil) + navigationController?.presentingViewController?.dismiss( + animated: true, + completion: nil + ) } } @@ -122,47 +134,71 @@ extension SettingsViewController { } private var paymentMethodsSection: SectionDescriptor { - let yooMoneyCell = switchCellWith(title: Localized.yooMoney, - initialValue: { $0.isYooMoneyEnabled }, - settingHandler: { $0.isYooMoneyEnabled = $1 }) - - let sberbankCell = switchCellWith(title: Localized.sberbank, - initialValue: { $0.isSberbankEnabled }, - settingHandler: { $0.isSberbankEnabled = $1 }) - - let bankCardCell = switchCellWith(title: Localized.bankCard, - initialValue: { $0.isBankCardEnabled }, - settingHandler: { $0.isBankCardEnabled = $1 }) - - let applePayCell = switchCellWith(title: Localized.applePay, - initialValue: { $0.isApplePayEnabled }, - settingHandler: { $0.isApplePayEnabled = $1 }) - - return SectionDescriptor(headerText: Localized.paymentMethods, - rows: [yooMoneyCell, sberbankCell, bankCardCell, applePayCell]) + let yooMoneyCell = switchCellWith( + title: Localized.yooMoney, + initialValue: { $0.isYooMoneyEnabled }, + settingHandler: { $0.isYooMoneyEnabled = $1 } + ) + + let sberbankCell = switchCellWith( + title: Localized.sberbank, + initialValue: { $0.isSberbankEnabled }, + settingHandler: { $0.isSberbankEnabled = $1 } + ) + + let bankCardCell = switchCellWith( + title: Localized.bankCard, + initialValue: { $0.isBankCardEnabled }, + settingHandler: { $0.isBankCardEnabled = $1 } + ) + + let applePayCell = switchCellWith( + title: Localized.applePay, + initialValue: { $0.isApplePayEnabled }, + settingHandler: { $0.isApplePayEnabled = $1 } + ) + + return SectionDescriptor( + headerText: Localized.paymentMethods, + rows: [ + yooMoneyCell, + sberbankCell, + bankCardCell, + applePayCell, + ] + ) } private var uiCustomizationSection: SectionDescriptor { - let yooMoneyLogoCell = switchCellWith(title: Localized.yooMoneyLogo, - initialValue: { $0.isShowingYooMoneyLogoEnabled }, - settingHandler: { $0.isShowingYooMoneyLogoEnabled = $1 }) - - return SectionDescriptor(rows: [yooMoneyLogoCell]) + let yooMoneyLogoCell = switchCellWith( + title: Localized.yooMoneyLogo, + initialValue: { $0.isShowingYooMoneyLogoEnabled }, + settingHandler: { $0.isShowingYooMoneyLogoEnabled = $1 } + ) + + return SectionDescriptor( + rows: [ + yooMoneyLogoCell, + ] + ) } private var testModeSection: SectionDescriptor { let testMode = CellDescriptor(configuration: { [unowned self] (cell: ContainerTableViewCell) in cell.containedView.title = Localized.test_mode - cell.containedView.value = self.settings.testModeSettings.isTestModeEnadled ? - translate(CommonLocalized.on) : translate(CommonLocalized.off) + cell.containedView.value = self.settings.testModeSettings.isTestModeEnadled + ? translate(CommonLocalized.on) + : translate(CommonLocalized.off) cell.accessoryType = .disclosureIndicator }, selection: { [unowned self] (indexPath) in self.tableViewController.tableView.deselectRow(at: indexPath, animated: true) - let controller = TestSettingsViewController.makeModule(settings: self.settings.testModeSettings, - delegate: self) + let controller = TestSettingsViewController.makeModule( + settings: self.settings.testModeSettings, + delegate: self + ) let navigation = UINavigationController(rootViewController: controller) if #available(iOS 11.0, *) { @@ -177,27 +213,29 @@ extension SettingsViewController { return SectionDescriptor(rows: [testMode]) } - private func switchCellWith(title: String, - initialValue: @escaping (Settings) -> Bool, - settingHandler: @escaping (inout Settings, Bool) -> Void) - -> CellDescriptor { - return CellDescriptor(configuration: { [unowned self] (cell: ContainerTableViewCell) in - - cell.containedView.title = title - cell.containedView.isOn = initialValue(self.settings) - cell.containedView.valueChangeHandler = { - settingHandler(&self.settings, $0) - } - }) + private func switchCellWith( + title: String, + initialValue: @escaping (Settings) -> Bool, + settingHandler: @escaping (inout Settings, Bool) -> Void + ) -> CellDescriptor { + return CellDescriptor(configuration: { [unowned self] (cell: ContainerTableViewCell) in + + cell.containedView.title = title + cell.containedView.isOn = initialValue(self.settings) + cell.containedView.valueChangeHandler = { + settingHandler(&self.settings, $0) + } + }) } } // MARK: - TestSettingsViewControllerDelegate extension SettingsViewController: TestSettingsViewControllerDelegate { - func testSettingsViewController(_ testSettingsViewController: TestSettingsViewController, - didChangeSettings settings: TestSettings) { - + func testSettingsViewController( + _ testSettingsViewController: TestSettingsViewController, + didChangeSettings settings: TestSettings + ) { self.settings.testModeSettings = settings tableViewController.reloadTable() } diff --git a/YooKassaPaymentsExample/Source/UserStories/Test Settings/TestSettingsViewController.swift b/YooKassaPaymentsExample/Source/UserStories/Test Settings/TestSettingsViewController.swift index d89bf7bc..9869e89d 100644 --- a/YooKassaPaymentsExample/Source/UserStories/Test Settings/TestSettingsViewController.swift +++ b/YooKassaPaymentsExample/Source/UserStories/Test Settings/TestSettingsViewController.swift @@ -2,18 +2,20 @@ import UIKit import YooKassaPayments protocol TestSettingsViewControllerDelegate: class { - func testSettingsViewController(_ testSettingsViewController: TestSettingsViewController, - didChangeSettings settings: TestSettings) + func testSettingsViewController( + _ testSettingsViewController: TestSettingsViewController, + didChangeSettings settings: TestSettings + ) } final class TestSettingsViewController: UIViewController { - public static func makeModule(settings: TestSettings, - delegate: TestSettingsViewControllerDelegate? = nil) -> UIViewController { - + public static func makeModule( + settings: TestSettings, + delegate: TestSettingsViewControllerDelegate? = nil + ) -> UIViewController { let controller = TestSettingsViewController(settings: settings) controller.delegate = delegate - return controller } @@ -22,15 +24,19 @@ final class TestSettingsViewController: UIViewController { // MARK: - UI properties private lazy var tableViewController = TableViewController(style: .grouped) - - private lazy var closeBarItem = UIBarButtonItem(image: #imageLiteral(resourceName: "Settings.Close"), - style: .plain, - target: self, - action: #selector(closeBarButtonItemDidPress)) - - private lazy var testModeCell = switchCellWith(title: translate(Localized.title), - initialValue: { $0.isTestModeEnadled }, - settingHandler: { $0.isTestModeEnadled = $1 }) + + private lazy var closeBarItem = UIBarButtonItem( + image: #imageLiteral(resourceName: "Settings.Close"), + style: .plain, + target: self, + action: #selector(closeBarButtonItemDidPress) + ) + + private lazy var testModeCell = switchCellWith( + title: translate(Localized.title), + initialValue: { $0.isTestModeEnadled }, + settingHandler: { $0.isTestModeEnadled = $1 } + ) // MARK: - Private properties @@ -85,6 +91,13 @@ final class TestSettingsViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + + let tapGesture = UITapGestureRecognizer( + target: self, + action: #selector(hideKeyboard) + ) + tapGesture.cancelsTouchesInView = false + view.addGestureRecognizer(tapGesture) navigationItem.leftBarButtonItem = closeBarItem navigationItem.title = translate(Localized.title) @@ -92,6 +105,19 @@ final class TestSettingsViewController: UIViewController { updateSections(for: settings.isTestModeEnadled) tableViewController.reload(force: true) + + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardWillShow), + name: UIResponder.keyboardWillShowNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardWillHide), + name: UIResponder.keyboardWillHideNotification, + object: nil + ) } // MARK: - Initialization/Deinitialization @@ -115,12 +141,30 @@ final class TestSettingsViewController: UIViewController { } // MARK: - Action handlers + + @objc + private func hideKeyboard( + _ gestureRecognizer: UITapGestureRecognizer + ) { + view.endEditing(true) + } @objc private func closeBarButtonItemDidPress() { navigationController?.presentingViewController?.dismiss(animated: true, completion: nil) } + @objc + func keyboardWillShow(notification: NSNotification) { + if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue { + tableViewController.tableView.contentInset.bottom = keyboardSize.height + } + } + + @objc + func keyboardWillHide(notification: NSNotification) { + tableViewController.tableView.contentInset.bottom = 0 + } } // MARK: - TableView data @@ -129,7 +173,11 @@ extension TestSettingsViewController { private func updateSections(for testModeEnabled: Bool) { if testModeEnabled { - tableViewController.sections = [testModeSection, testModeOptionsSection] + tableViewController.sections = [ + testModeSection, + testModeOptionsSection, + processOptionsSection, + ] } else { tableViewController.sections = [testModeSection] } @@ -140,33 +188,33 @@ extension TestSettingsViewController { } private var testModeOptionsSection: SectionDescriptor { - - // MARK: OBIOS-105 temporarily remove -// let payment3DSCell = switchCellWith(title: translate(Localized.check3ds), -// initialValue: { $0.is3DSEnabled }, -// settingHandler: { $0.is3DSEnabled = $1 }) - - let paymentAuthCell = switchCellWith(title: translate(Localized.paymentAuth), - initialValue: { $0.isPaymentAuthorizationPassed }, - settingHandler: { $0.isPaymentAuthorizationPassed = $1 }) - - let paymentErrorCell = switchCellWith(title: translate(Localized.paymentError), - initialValue: { $0.isPaymentWithError }, - settingHandler: { $0.isPaymentWithError = $1 }) + let paymentAuthCell = switchCellWith( + title: translate(Localized.paymentAuth), + initialValue: { $0.isPaymentAuthorizationPassed }, + settingHandler: { $0.isPaymentAuthorizationPassed = $1 } + ) + + let paymentErrorCell = switchCellWith( + title: translate(Localized.paymentError), + initialValue: { $0.isPaymentWithError }, + settingHandler: { $0.isPaymentWithError = $1 } + ) let cards = CellDescriptor(configuration: { [unowned self] (cell: ContainerTableViewCell) in - cell.containedView.title = translate(Localized.attached) cell.containedView.value = self.settings.cardsCount.flatMap(String.init) ?? translate(CommonLocalized.none) cell.accessoryType = .disclosureIndicator - }, selection: { [unowned self] (indexPath) in - - self.tableViewController.tableView.deselectRow(at: indexPath, animated: true) - - let controller = AttachedCardCountViewController.makeModule(cardCount: self.settings.cardsCount, - delegate: self) - self.navigationController?.pushViewController(controller, - animated: true) + }, selection: { [unowned self] (indexPath) in + self.tableViewController.tableView.deselectRow(at: indexPath, animated: true) + + let controller = AttachedCardCountViewController.makeModule( + cardCount: self.settings.cardsCount, + delegate: self + ) + self.navigationController?.pushViewController( + controller, + animated: true + ) }) return SectionDescriptor(rows: [ @@ -175,39 +223,97 @@ extension TestSettingsViewController { paymentErrorCell, ]) } - - private func switchCellWith(title: String, - initialValue: @escaping (TestSettings) -> Bool, - settingHandler: @escaping (inout TestSettings, Bool) -> Void) - -> CellDescriptor { - return CellDescriptor(configuration: { [unowned self] (cell: ContainerTableViewCell) in - - cell.containedView.title = title - cell.containedView.isOn = initialValue(self.settings) + + private var processOptionsSection: SectionDescriptor { + var rows: [CellDescriptor] = [] + + rows.append(CellDescriptor(configuration: { + [unowned self] (cell: ContainerTableViewCell) in + cell.containedView.title = translate(Localized.processType) + cell.containedView.value = self.settings.processConfirmation?.description + ?? translate(CommonLocalized.none) + cell.accessoryType = .disclosureIndicator + }, selection: { [unowned self] (indexPath) in + self.tableViewController.tableView.deselectRow(at: indexPath, animated: true) + + let controller = ProcessViewController.makeModule( + processConfirmation: self.settings.processConfirmation, + delegate: self + ) + self.navigationController?.pushViewController( + controller, + animated: true + ) + })) + + if let processConfirmation = settings.processConfirmation { + rows.append(CellDescriptor(configuration: { + [unowned self] (cell: ContainerTableViewCell) in + cell.containedView.placeholder = processConfirmation.description + cell.containedView.text = processConfirmation.url cell.containedView.valueChangeHandler = { - settingHandler(&self.settings, $0) + switch processConfirmation { + case .threeDSecure: + self.settings.processConfirmation = .threeDSecure($0 ?? "") + + case .app2app: + self.settings.processConfirmation = .app2app($0 ?? "") + } } - }) + })) + } + + return SectionDescriptor(rows: rows) + } + + private func switchCellWith( + title: String, + initialValue: @escaping (TestSettings) -> Bool, + settingHandler: @escaping (inout TestSettings, Bool) -> Void + ) -> CellDescriptor { + return CellDescriptor(configuration: { [unowned self] (cell: ContainerTableViewCell) in + cell.containedView.title = title + cell.containedView.isOn = initialValue(self.settings) + cell.containedView.valueChangeHandler = { + settingHandler(&self.settings, $0) + } + }) } } -extension TestSettingsViewController: AttachedCardCountViewControllerDelegate { - func attachedCardCountViewController(_ attachedCardCountViewController: AttachedCardCountViewController, - didSaveCardCount сardCount: Int?) { +// MARK: - AttachedCardCountViewControllerDelegate +extension TestSettingsViewController: AttachedCardCountViewControllerDelegate { + func attachedCardCountViewController( + _ attachedCardCountViewController: AttachedCardCountViewController, + didSaveCardCount сardCount: Int? + ) { settings.cardsCount = сardCount tableViewController.reloadTable() } } +// MARK: - ProcessViewControllerDelegate + +extension TestSettingsViewController: ProcessViewControllerDelegate { + func processViewController( + _ processViewController: ProcessViewController, + processConfirmation: ProcessConfirmation? + ) { + settings.processConfirmation = processConfirmation + updateSections(for: settings.isTestModeEnadled) + tableViewController.reloadTable() + } +} + // MARK: - Localization -extension TestSettingsViewController { +extension TestSettingsViewController { private enum Localized: String { case title = "test_mode.title" - case check3ds = "test_mode.3ds" case paymentAuth = "test_mode.payment_auth" case attached = "test_mode.attached_cards" case paymentError = "test_mode.payment_error" + case processType = "test_mode.process_type" } } diff --git a/YooKassaPaymentsExample/Source/ViewControllers/CellDescriptor.swift b/YooKassaPaymentsExample/Source/ViewControllers/CellDescriptor.swift index a8f249ff..8bf6d34a 100644 --- a/YooKassaPaymentsExample/Source/ViewControllers/CellDescriptor.swift +++ b/YooKassaPaymentsExample/Source/ViewControllers/CellDescriptor.swift @@ -8,13 +8,12 @@ final class CellDescriptor { let configuration: (UITableViewCell) -> Void let selection: ((IndexPath) -> Void)? - init(configuration: @escaping (Cell) -> Void, - selection: ((IndexPath) -> Void)? = nil) { - + init( + configuration: @escaping (Cell) -> Void, + selection: ((IndexPath) -> Void)? = nil + ) { self.cellClass = Cell.self - self.selection = selection - self.configuration = { cell in guard let cell = cell as? Cell else { assertionFailure("Cell and selection types mismatch") @@ -24,5 +23,4 @@ final class CellDescriptor { configuration(cell) } } - } diff --git a/YooKassaPaymentsExample/Source/ViewControllers/TableViewController.swift b/YooKassaPaymentsExample/Source/ViewControllers/TableViewController.swift index b5ad9b9f..cbd47159 100644 --- a/YooKassaPaymentsExample/Source/ViewControllers/TableViewController.swift +++ b/YooKassaPaymentsExample/Source/ViewControllers/TableViewController.swift @@ -34,6 +34,7 @@ class TableViewController: UITableViewController { tableView.register(TextHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: TextHeaderFooterView.identifier) + tableView.keyboardDismissMode = .interactive tableView.rowHeight = UITableView.automaticDimension tableView.sectionHeaderHeight = UITableView.automaticDimension tableView.sectionFooterHeight = UITableView.automaticDimension diff --git a/YooKassaPaymentsExample/Source/Views/ContainerTableViewCell.swift b/YooKassaPaymentsExample/Source/Views/ContainerTableViewCell.swift index ddbb333f..e5c033d8 100644 --- a/YooKassaPaymentsExample/Source/Views/ContainerTableViewCell.swift +++ b/YooKassaPaymentsExample/Source/Views/ContainerTableViewCell.swift @@ -14,7 +14,6 @@ class ContainerTableViewCell Void)? + + // MARK: - Public properties + + var text: String? { + get { + return textField.text + } + set { + textField.text = newValue + } + } + + var placeholder: String? { + get { + return textField.placeholder + } + set { + textField.placeholder = newValue + } + } + + // MARK: - UI properties + + private lazy var textField: UITextField = { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.addTarget( + self, + action: #selector(textFieldDidChange), + for: .editingChanged + ) + return $0 + }(UITextField()) + + // MARK: - Initialization/Deinitialization + + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init?(coder:) is not implemented") + } + + // MARK: - Private methods + + private func setupUI() { + [ + textField, + ].forEach(addSubview) + setupConstraints() + } + + private func setupConstraints() { + let constraints = [ + textField.top.constraint(equalTo: topMargin, constant: Space.single), + textField.leading.constraint(equalTo: leadingMargin), + textField.trailing.constraint(equalTo: trailingMargin), + textField.bottom.constraint(equalTo: bottomMargin, constant: -Space.single), + ] + NSLayoutConstraint.activate(constraints) + } + + // MARK: - Actions + + @objc + private func textFieldDidChange(textField: UITextField) { + valueChangeHandler?(textField.text) + } +} + +// MARK: - TableViewCellDataProviderSupport + +extension TextFieldView: TableViewCellDataProviderSupport { + class var estimatedCellHeight: CGFloat { + return 56.0 + } +}