diff --git a/.rubocop.yml b/.rubocop.yml
index 18399d5..df6ceb0 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -249,6 +249,8 @@ Naming/AccessorMethodName:
# ( ) for method calls
Style/MethodCallWithArgsParentheses:
Enabled: true
+ Exclude:
+ - "**/*.podspec"
IgnoredMethods:
- 'require'
- 'require_relative'
diff --git a/Example/App/AppDelegate.swift b/Example/App/AppDelegate.swift
index dc19b12..6f2fdbd 100644
--- a/Example/App/AppDelegate.swift
+++ b/Example/App/AppDelegate.swift
@@ -33,18 +33,20 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
let factory = TinkoffIDFactory(
clientId: clientId,
- callbackUrl: callbackUrl
+ callbackUrl: callbackUrl,
+ webViewSourceProvider: self
)
return factory.build()
}()
+ lazy var authController: AuthViewController = {
+ AuthViewController(signInInitializer: tinkoffId,
+ credentialsRefresher: tinkoffId,
+ signOutInitializer: tinkoffId)
+ }()
+
func applicationDidFinishLaunching(_ application: UIApplication) {
- let authController = AuthViewController(
- signInInitializer: tinkoffId,
- credentialsRefresher: tinkoffId,
- signOutInitializer: tinkoffId
- )
authController.tabBarItem = UITabBarItem(title: "Auth", image: nil, tag: 0)
let tinkoffButtonsController = TinkoffButtonsViewController()
@@ -65,6 +67,14 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
- tinkoffId.handleCallbackUrl(url)
+ return tinkoffId.handleCallbackUrl(url)
+ }
+}
+
+// MARK: - AuthWebView Usage
+
+extension AppDelegate: IAuthWebViewSourceProvider {
+ func getSourceViewController() -> UIViewController {
+ authController
}
}
diff --git a/Example/App/Resources/Info.plist b/Example/App/Resources/Info.plist
index 091780c..efbc803 100644
--- a/Example/App/Resources/Info.plist
+++ b/Example/App/Resources/Info.plist
@@ -58,5 +58,34 @@
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+ NSExceptionDomains
+
+ certs.tinkoff.ru
+
+ NSExceptionAllowsInsecureHTTPLoads
+
+ NSIncludesSubdomains
+
+
+ tcsbank.ru
+
+ NSExceptionAllowsInsecureHTTPLoads
+
+ NSIncludesSubdomains
+
+
+ tinkoff.ru
+
+ NSExceptionAllowsInsecureHTTPLoads
+
+ NSIncludesSubdomains
+
+
+
+
diff --git a/Example/Podfile b/Example/Podfile
index 68edf55..9edbbb7 100644
--- a/Example/Podfile
+++ b/Example/Podfile
@@ -1,3 +1,5 @@
+source 'https://github.com/CocoaPods/Specs.git'
+
use_frameworks!
target 'TinkoffIDExample' do
diff --git a/Example/Podfile.lock b/Example/Podfile.lock
index 79bf1fc..4a5e3e4 100644
--- a/Example/Podfile.lock
+++ b/Example/Podfile.lock
@@ -1,7 +1,12 @@
PODS:
- - SnapKit (5.0.1)
- - TinkoffID (1.1.0)
- - TinkoffID/Tests (1.1.0)
+ - SnapKit (5.6.0)
+ - TCSSSLPinningPublic (4.0.0):
+ - TrustKit (= 1.6.3)
+ - TinkoffID (1.2.0):
+ - TCSSSLPinningPublic (~> 4.0)
+ - TinkoffID/Tests (1.2.0):
+ - TCSSSLPinningPublic (~> 4.0)
+ - TrustKit (1.6.3)
DEPENDENCIES:
- SnapKit
@@ -9,17 +14,21 @@ DEPENDENCIES:
- TinkoffID/Tests (from `../`)
SPEC REPOS:
- trunk:
+ https://github.com/CocoaPods/Specs.git:
- SnapKit
+ - TCSSSLPinningPublic
+ - TrustKit
EXTERNAL SOURCES:
TinkoffID:
:path: "../"
SPEC CHECKSUMS:
- SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb
- TinkoffID: 5f8c16ed1ec6ab3c055d76b162d5770a324ad3db
+ SnapKit: e01d52ebb8ddbc333eefe2132acf85c8227d9c25
+ TCSSSLPinningPublic: 7d8aed728c4a1ff66feb2323f686a389b868f1e5
+ TinkoffID: c78e0033baeeefcf1c3782496fe4e1b65159f6d1
+ TrustKit: a2f0c3a926f0a3ce3c082db9a39f1f540dbb04cb
-PODFILE CHECKSUM: ebbf3aa204a2c47033aee78dc783acf8bc5423bd
+PODFILE CHECKSUM: afca0ae77c379b2ea123dd308359330458e3b198
COCOAPODS: 1.10.1
diff --git a/Example/TinkoffIDExample.xcodeproj/project.pbxproj b/Example/TinkoffIDExample.xcodeproj/project.pbxproj
index 44dc21d..3d21761 100644
--- a/Example/TinkoffIDExample.xcodeproj/project.pbxproj
+++ b/Example/TinkoffIDExample.xcodeproj/project.pbxproj
@@ -353,7 +353,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = "$(SRCROOT)/App/Resources/Info.plist";
- IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -378,7 +378,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = "$(SRCROOT)/App/Resources/Info.plist";
- IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
diff --git a/README.md b/README.md
index d45d75c..60be2db 100755
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@
* [Отзыв авторизационных данных](#Отзыв-авторизационных-данных)
* [Структура TinkoffTokenPayload](#Структура-TinkoffTokenPayload)
* [Хранение Refresh Token](#Хранение-Refresh-Token)
+* [Авторизация через WebView](#Авторизация-через-WebView)
* [UI](#UI)
* [Отладка без приложения Тинькофф](#Отладка-без-приложения-Тинькофф)
* [Настройка приложения](#Настройка-приложения)
@@ -65,6 +66,40 @@ pod 'TinkoffID'
tinkoffbank
```
++ Добавленная запись в `plist`, позволяющая Вашему приложению получать запасные сертификаты SSL Тинькофф.
+
+```
+NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+ NSExceptionDomains
+
+ certs.tinkoff.ru
+
+ NSExceptionAllowsInsecureHTTPLoads
+
+ NSIncludesSubdomains
+
+
+ tcsbank.ru
+
+ NSExceptionAllowsInsecureHTTPLoads
+
+ NSIncludesSubdomains
+
+
+ tinkoff.ru
+
+ NSExceptionAllowsInsecureHTTPLoads
+
+ NSIncludesSubdomains
+
+
+
+
+
+```
## Структура публичной части SDK
@@ -75,6 +110,7 @@ pod 'TinkoffID'
+ `ITinkoffAuthCallbackHandler` - обработчик возврата в приложение из приложения Тинькофф
+ `ITinkoffCredentialsRefresher` - объект, умеющий обновлять `Credentials` по их `Refresh token`
+ `ITinkoffSignOutInitiator` - инициатор отзыва авторизационных данных
++ `ITinkoffWebViewPresentationProvider` - провайдет источника для показа [WebView](#Авторизация-через-WebView)
В зависимости от архитектуры приложения можно использовать непосредственно`ITinkoffID` или каждый подпротокол отдельно в требуемой части системы.
@@ -197,6 +233,23 @@ tinkoffId.signOut(with: credentials.accessToken, tokenTypeHint: .access, complet
При получении `TinkoffTokenPayload` и наличии у него поля `refreshToken` имеет смысл сохранить значение этого поля чтобы иметь возможность запросить новый `accessToken`, когда прежний станет неактивным. Рекомендуемый способ хранения токена - [Keychain Services](https://developer.apple.com/documentation/security/keychain_services)
+## Авторизация через WebView
+
+В некоторых случаях система не открывает приложения по universal link, а ведет в браузер. Для того чтобы избежать этого, добавлен fallback на открытие WebView.
+
+В данном сценарии при отсутствии установленного на телефоне приложения Тиннькофф или невозможности открыть приложение по universal link, SDK попробует открыть WebView с веб-авторизацией. Пользователю будет предложена авторизация по номеру телефону. После успешной авторизации WebView автоматически передаст управление SDK для продлолжения процесса.
+
+Для использования необходимо установить значение `usesUniversalLinks` в `true` у `TargetAppConfiguration` (для `TinkoffApp` настроено по умолчанию), и передать провайдер `IAuthWebViewSourceProvider`.
+
+Если же использовать дефолтную конфигурацию фабрики:
+```
+let factory = TinkoffIDFactory(
+ clientId: clientId,
+ callbackUrl: callbackUrl)
+```
+открытие WebView будет происходить от `keyWindow`.
+
+
## UI
SDK поставляет два варианта фирменных кнопок входа через Тинькофф.
Первый вариант - стандартная прямоугольная кнопка с текстом, с возможностью задать текст, радиус скругления и шрифт. Так же можно выбрать один из трех вариантов цветового стиля и размера. Есть возможность добавить дополнительный текст для привлечения клиентов.
@@ -299,13 +352,7 @@ SDK поставляется с примером приложения. Для з
### AuthViewController
-`AuthViewController` инициируется ссылками на объекты, реализующими `ITinkoffAuthInitiator`, `ITinkoffCredentialsRefresher` и `ITinkoffSignOutInitiator` соответственно.
-
-В текущей реализации все эти ссылки указывают на один и тот же экземпляр объекта `TinkoffID`, реализующий интерфейс `ITinkoffID`. Такой подход был выбран для демонстрации возможности использования подинтерфейсов `ITinkoffID` в той или иной части системы. Пользователь SDK вправе сам решать использовать ли ему единый интерфейс `ITinkoffID` или необходимый подинтерфейс в зависимости от архитектуры приложения.
-
-Подробнее с подинтерфесами `ITinkoffID` можно ознакомиться в разделе `Структура публичной части SDK`.
-
-После загрузки `view` контроллер добавляет на него кнопку входа через Тинькофф, по нажатию на которую будет инициирована авторизация.
+`AuthViewController` инициируется ссылками на объекты, реализующими `ITinkoffAuthInitiator`, `ITinkoffCredentialsRefresher`, `ITinkoffSignOutInitiator` и `ITinkoffWebViewPresentationProvider` соответственно.
## Поддержка
Сообщать об ошибках и запрашивать новый функционал можно в разделе [Issues](https://github.com/tinkoff-mobile-tech/TinkoffID-iOS/issues)
diff --git a/Sources/API/PinningDelegate/PinningDelegate.swift b/Sources/API/PinningDelegate/PinningDelegate.swift
new file mode 100644
index 0000000..30afdc7
--- /dev/null
+++ b/Sources/API/PinningDelegate/PinningDelegate.swift
@@ -0,0 +1,67 @@
+//
+// PinningDelegate.swift
+// Pods-TinkoffIDExample
+//
+// Created by Aleksandr Moskalyuk on 18.05.2023.
+//
+
+import Foundation
+import TCSSSLPinning
+import WebKit
+
+protocol IPinningDelegate {}
+
+final class PinningDelegate: NSObject, IPinningDelegate {
+
+ // MARK: - Dependencies
+
+ private var httpPublicKeyPinningService: IHTTPPublicKeyPinningService
+
+ // MARK: - Lifestyle
+
+ init(hostAndPinsURL: String?) {
+ let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
+ let bundleID = Bundle.main.bundleIdentifier?.description ?? "ru.tinkoff.id"
+ let configuration = HPKPServiceConfiguration(hostAndPinsURL: URL(string: hostAndPinsURL ?? "") ?? HPKPServiceConstants.Configuration.productionHostAndPinsURL,
+ untrustedConnectionPolicy: .continue,
+ cachedHostsAndPinsDefaultsKey: "\(bundleID).hostsandpins",
+ appParameters: AppParameters(version: version ?? "1.0", origin: "origin"))
+ self.httpPublicKeyPinningService = HPKPServiceAssembly.createHPKPPinningService(with: configuration)
+
+ self.httpPublicKeyPinningService.configure()
+ self.httpPublicKeyPinningService.updateHostsAndPins()
+
+ super.init()
+ }
+}
+
+// MARK: - URLSessionDelegate
+
+extension PinningDelegate: URLSessionDelegate {
+ func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
+ httpPublicKeyPinningService.urlSession?(session, didBecomeInvalidWithError: error)
+ }
+
+ func urlSession(_ session: URLSession,
+ didReceive challenge: URLAuthenticationChallenge,
+ completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
+ httpPublicKeyPinningService.urlSession?(session,
+ didReceive: challenge,
+ completionHandler: completionHandler)
+
+ }
+
+ func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
+ httpPublicKeyPinningService.urlSessionDidFinishEvents?(forBackgroundURLSession: session)
+ }
+}
+
+// MARK: - WKNavigationDelegate
+
+extension PinningDelegate: WKNavigationDelegate {
+ public func webView(_ webView: WKWebView,
+ didReceive challenge: URLAuthenticationChallenge,
+ completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
+ httpPublicKeyPinningService.webView?(webView, didReceive: challenge, completionHandler: completionHandler)
+ }
+}
\ No newline at end of file
diff --git a/Sources/AppLaunching/IAppLauncher/IAppLauncher.swift b/Sources/AppLaunching/IAppLauncher/IAppLauncher.swift
index e9841bd..a607200 100644
--- a/Sources/AppLaunching/IAppLauncher/IAppLauncher.swift
+++ b/Sources/AppLaunching/IAppLauncher/IAppLauncher.swift
@@ -25,5 +25,5 @@ protocol IAppLauncher {
var canLaunchApp: Bool { get }
/// Запускает приложение с заданными опциями
- func launchApp(with options: AppLaunchOptions) throws
+ func launchApp(with options: AppLaunchOptions, universalLinksOnly: Bool, completion: @escaping ((Bool) -> Void)) throws
}
diff --git a/Sources/AppLaunching/IAppLauncher/URLSchemeAppLauncher.swift b/Sources/AppLaunching/IAppLauncher/URLSchemeAppLauncher.swift
index 79a0705..bea0a94 100644
--- a/Sources/AppLaunching/IAppLauncher/URLSchemeAppLauncher.swift
+++ b/Sources/AppLaunching/IAppLauncher/URLSchemeAppLauncher.swift
@@ -39,11 +39,10 @@ final class URLSchemeAppLauncher: IAppLauncher {
.map(router.canOpenURL) ?? false
}
- func launchApp(with options: AppLaunchOptions) throws {
+ func launchApp(with options: AppLaunchOptions, universalLinksOnly: Bool, completion: @escaping ((Bool) -> Void)) throws {
let appUrl = try builder.buildUrlScheme(with: options)
- if !router.open(appUrl) {
- // Не удалось запустить приложение
+ if !router.openWithFallback(appUrl, universalLinksOnly: universalLinksOnly, completion: completion) {
throw Error.launchFailure
}
}
diff --git a/Sources/AppLaunching/IAppLauncher/Utils/Router/IURLRouter.swift b/Sources/AppLaunching/IAppLauncher/Utils/Router/IURLRouter.swift
index 9f9a034..d5a3495 100644
--- a/Sources/AppLaunching/IAppLauncher/Utils/Router/IURLRouter.swift
+++ b/Sources/AppLaunching/IAppLauncher/Utils/Router/IURLRouter.swift
@@ -26,6 +26,9 @@ protocol IURLRouter {
/// Открывает заданный URL и возвращает `true` если открытие удалось
func open(_ url: URL) -> Bool
+
+ /// Открывает заданный URL c фоллбэком на открытие вебвью в случае если приложение не установлено
+ func openWithFallback(_ url: URL, universalLinksOnly: Bool, completion: @escaping ((Bool) -> Void)) -> Bool
}
extension UIApplication: IURLRouter {
@@ -36,4 +39,12 @@ extension UIApplication: IURLRouter {
return true
}
+
+ func openWithFallback(_ url: URL, universalLinksOnly: Bool, completion: @escaping ((Bool) -> Void)) -> Bool {
+ guard canOpenURL(url) else { return false }
+
+ open(url, options: [.universalLinksOnly : universalLinksOnly], completionHandler: completion)
+
+ return true
+ }
}
diff --git a/Sources/Extensions/Environment+EnvironmentConfiguration.swift b/Sources/Extensions/Environment+EnvironmentConfiguration.swift
index c11bb61..31c89d5 100644
--- a/Sources/Extensions/Environment+EnvironmentConfiguration.swift
+++ b/Sources/Extensions/Environment+EnvironmentConfiguration.swift
@@ -17,8 +17,13 @@
// limitations under the License.
import Foundation
+import TCSSSLPinning
extension TinkoffEnvironment: EnvironmentConfiguration {
+ public var hostAndPinsUrl: String? {
+ return HPKPServiceConstants.Configuration.productionHostAndPinsURL.absoluteString
+ }
+
public var apiBaseUrl: String {
switch self {
case .production:
diff --git a/Sources/Extensions/TinkoffApp+TargetAppConfiguration.swift b/Sources/Extensions/TinkoffApp+TargetAppConfiguration.swift
index 0a35d2c..6dd7c06 100644
--- a/Sources/Extensions/TinkoffApp+TargetAppConfiguration.swift
+++ b/Sources/Extensions/TinkoffApp+TargetAppConfiguration.swift
@@ -19,6 +19,7 @@
import Foundation
extension TinkoffApp: TargetAppConfiguration {
+
public var urlScheme: String {
switch self {
case .bank:
@@ -26,6 +27,13 @@ extension TinkoffApp: TargetAppConfiguration {
}
}
+ public var usesUniversalLinks: Bool {
+ switch self {
+ case .bank:
+ return true
+ }
+ }
+
public var authUrl: String {
switch self {
case .bank:
diff --git a/Sources/Resources/TinkoffID.xcassets/reload.imageset/Contents.json b/Sources/Resources/TinkoffID.xcassets/reload.imageset/Contents.json
new file mode 100644
index 0000000..8f259c4
--- /dev/null
+++ b/Sources/Resources/TinkoffID.xcassets/reload.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "reload.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Sources/Resources/TinkoffID.xcassets/reload.imageset/reload.pdf b/Sources/Resources/TinkoffID.xcassets/reload.imageset/reload.pdf
new file mode 100644
index 0000000..45088d6
Binary files /dev/null and b/Sources/Resources/TinkoffID.xcassets/reload.imageset/reload.pdf differ
diff --git a/Sources/SDK/Concrete/DebugTinkoffID/DebugTinkoffID.swift b/Sources/SDK/Concrete/DebugTinkoffID/DebugTinkoffID.swift
index 58d9457..2323baa 100644
--- a/Sources/SDK/Concrete/DebugTinkoffID/DebugTinkoffID.swift
+++ b/Sources/SDK/Concrete/DebugTinkoffID/DebugTinkoffID.swift
@@ -17,6 +17,7 @@
// limitations under the License.
import Foundation
+import UIKit
final class DebugTinkoffID: ITinkoffID {
@@ -92,6 +93,10 @@ final class DebugTinkoffID: ITinkoffID {
}
}
+ func getSourceViewController() -> UIViewController {
+ UIViewController()
+ }
+
// MARK: - Private
private func resolveDebugAppResult(_ url: URL) -> DebugAppResult? {
diff --git a/Sources/SDK/Concrete/DebugTinkoffID/IDebugAppLauncher/DebugAppLauncher.swift b/Sources/SDK/Concrete/DebugTinkoffID/IDebugAppLauncher/DebugAppLauncher.swift
index ebad6a3..a152040 100644
--- a/Sources/SDK/Concrete/DebugTinkoffID/IDebugAppLauncher/DebugAppLauncher.swift
+++ b/Sources/SDK/Concrete/DebugTinkoffID/IDebugAppLauncher/DebugAppLauncher.swift
@@ -25,6 +25,6 @@ final class DebugAppLauncher: IDebugAppLauncher {
}
func launchDebugApp() {
- _ = debugAppUrl.map(router.open(_:))
+// _ = debugAppUrl.map(router.open(_:))
}
}
diff --git a/Sources/SDK/Concrete/TinkoffID/Configuration/EnvironmentConfiguration.swift b/Sources/SDK/Concrete/TinkoffID/Configuration/EnvironmentConfiguration.swift
index 11cf853..6cd4241 100644
--- a/Sources/SDK/Concrete/TinkoffID/Configuration/EnvironmentConfiguration.swift
+++ b/Sources/SDK/Concrete/TinkoffID/Configuration/EnvironmentConfiguration.swift
@@ -22,4 +22,6 @@ import Foundation
public protocol EnvironmentConfiguration {
/// Базовый URL API
var apiBaseUrl: String { get }
+ /// URL Host and Pins для работы с кастомными сертификатами TLS/SSL
+ var hostAndPinsUrl: String? { get }
}
diff --git a/Sources/SDK/Concrete/TinkoffID/Configuration/TargetAppConfiguration.swift b/Sources/SDK/Concrete/TinkoffID/Configuration/TargetAppConfiguration.swift
index 40ee41a..82bd602 100644
--- a/Sources/SDK/Concrete/TinkoffID/Configuration/TargetAppConfiguration.swift
+++ b/Sources/SDK/Concrete/TinkoffID/Configuration/TargetAppConfiguration.swift
@@ -25,4 +25,7 @@ public protocol TargetAppConfiguration {
/// Ссылка для проведения авторизации
var authUrl: String { get }
+
+ /// Признак того является ли `authUrl` универсальной ссылкой (https://...)
+ var usesUniversalLinks: Bool { get }
}
diff --git a/Sources/SDK/Concrete/TinkoffID/TinkoffID.swift b/Sources/SDK/Concrete/TinkoffID/TinkoffID.swift
index 6ff4553..0c95247 100644
--- a/Sources/SDK/Concrete/TinkoffID/TinkoffID.swift
+++ b/Sources/SDK/Concrete/TinkoffID/TinkoffID.swift
@@ -26,9 +26,13 @@ final class TinkoffID: ITinkoffID {
let appLauncher: IAppLauncher
let callbackUrlParser: ICallbackURLParser
let api: IAPI
+ let authWebViewBuilder: IAuthWebViewBuilder
+ let webViewSourceProvider: IAuthWebViewSourceProvider?
+ let universalLinksOnly: Bool
// MARK: - State
private var currentProcess: AuthProcess?
+ private var authWebViewSourceController: UIViewController?
// MARK: - Properties
let clientId: String
@@ -38,14 +42,20 @@ final class TinkoffID: ITinkoffID {
appLauncher: IAppLauncher,
callbackUrlParser: ICallbackURLParser,
api: IAPI,
+ authWebViewBuilder: IAuthWebViewBuilder,
+ webViewSourceProvider: IAuthWebViewSourceProvider?,
clientId: String,
- callbackUrl: String) {
+ callbackUrl: String,
+ universalLinksOnly: Bool) {
self.payloadGenerator = payloadGenerator
self.appLauncher = appLauncher
self.callbackUrlParser = callbackUrlParser
self.api = api
+ self.authWebViewBuilder = authWebViewBuilder
+ self.webViewSourceProvider = webViewSourceProvider
self.clientId = clientId
self.callbackUrl = callbackUrl
+ self.universalLinksOnly = universalLinksOnly
}
// MARK: - ITinkoffAuthInitiator
@@ -63,7 +73,19 @@ final class TinkoffID: ITinkoffID {
let process = AuthProcess(appLaunchOptions: options,
completion: completion)
- try appLauncher.launchApp(with: options)
+ try appLauncher.launchApp(with: options,
+ universalLinksOnly: self.universalLinksOnly,
+ completion: { [weak self] didLaunchMobileApp in
+ guard let self = self else { return }
+
+ guard !didLaunchMobileApp else { return }
+
+ if self.universalLinksOnly, let _ = self.webViewSourceProvider {
+ self.openWebView(options: options)
+ } else {
+ completion(.failure(.failedToLaunchApp))
+ }
+ })
currentProcess = process
} catch {
@@ -71,6 +93,13 @@ final class TinkoffID: ITinkoffID {
}
}
+
+ func openWebView(options: AppLaunchOptions) {
+ let authWebView = authWebViewBuilder.build(with: options)
+ authWebView.delegate = self
+ authWebView.open(from: webViewSourceProvider?.getSourceViewController())
+ }
+
// MARK: - ITinkoffAuthCallbackHandler
public func handleCallbackUrl(_ url: URL) -> Bool {
@@ -132,3 +161,12 @@ final class TinkoffID: ITinkoffID {
}
}
}
+
+extension TinkoffID: IAuthWebViewDelegate {
+ func authWebView(_ webView: IAuthWebView, didOpen url: URL) {
+ let handled = handleCallbackUrl(url)
+ if handled {
+ webView.dismiss()
+ }
+ }
+}
diff --git a/Sources/SDK/Factory/Concrete/TinkoffIDFactory.swift b/Sources/SDK/Factory/Concrete/TinkoffIDFactory.swift
index f881d7a..3bc2007 100644
--- a/Sources/SDK/Factory/Concrete/TinkoffIDFactory.swift
+++ b/Sources/SDK/Factory/Concrete/TinkoffIDFactory.swift
@@ -27,6 +27,7 @@ public final class TinkoffIDFactory: ITinkoffIDFactory {
private let callbackUrl: String
private let appConfiguration: TargetAppConfiguration
private let environmentConfiguration: EnvironmentConfiguration
+ private let webViewSourceProvider: IAuthWebViewSourceProvider?
// MARK: - Initialization
@@ -41,13 +42,15 @@ public final class TinkoffIDFactory: ITinkoffIDFactory {
clientId: String,
callbackUrl: String,
app: TinkoffApp = .bank,
- environment: TinkoffEnvironment = .production
+ environment: TinkoffEnvironment = .production,
+ webViewSourceProvider: IAuthWebViewSourceProvider? = DefaultAuthWebViewSourceProvider.instance
) {
self.init(
clientId: clientId,
callbackUrl: callbackUrl,
appConfiguration: app,
- environmentConfiguration: environment
+ environmentConfiguration: environment,
+ webViewSourceProvider: webViewSourceProvider
)
}
@@ -62,12 +65,14 @@ public final class TinkoffIDFactory: ITinkoffIDFactory {
clientId: String,
callbackUrl: String,
appConfiguration: TargetAppConfiguration,
- environmentConfiguration: EnvironmentConfiguration
+ environmentConfiguration: EnvironmentConfiguration,
+ webViewSourceProvider: IAuthWebViewSourceProvider? = DefaultAuthWebViewSourceProvider.instance
) {
self.clientId = clientId
self.callbackUrl = callbackUrl
self.environmentConfiguration = environmentConfiguration
self.appConfiguration = appConfiguration
+ self.webViewSourceProvider = webViewSourceProvider
}
// MARK: - ITinkoffIDFactory
@@ -80,10 +85,14 @@ public final class TinkoffIDFactory: ITinkoffIDFactory {
router: UIApplication.shared
)
+ let pinningDelegate = PinningDelegate(hostAndPinsURL: environmentConfiguration.hostAndPinsUrl)
+ let urlSession = URLSession(configuration: URLSessionConfiguration.default,
+ delegate: pinningDelegate,
+ delegateQueue: nil)
let requestBuilder = RequestBuilder(baseUrl: environmentConfiguration.apiBaseUrl)
let api = API(
requestBuilder: requestBuilder,
- requestProcessor: URLSession.shared,
+ requestProcessor: urlSession,
responseDispatcher: DispatchQueue.main
)
@@ -96,14 +105,19 @@ public final class TinkoffIDFactory: ITinkoffIDFactory {
)
let callbackUrlParser = CallbackURLParser()
+ let authWebViewBuilder = AuthWebViewBuilder(baseUrl: environmentConfiguration.apiBaseUrl,
+ pinningDelegate: pinningDelegate)
return TinkoffID(
payloadGenerator: payloadGenerator,
appLauncher: appLauncher,
callbackUrlParser: callbackUrlParser,
api: api,
+ authWebViewBuilder: authWebViewBuilder,
+ webViewSourceProvider: webViewSourceProvider,
clientId: clientId,
- callbackUrl: callbackUrl
+ callbackUrl: callbackUrl,
+ universalLinksOnly: appConfiguration.usesUniversalLinks
)
}
}
diff --git a/Sources/WebView/AuthWebView.swift b/Sources/WebView/AuthWebView.swift
new file mode 100644
index 0000000..c074c2e
--- /dev/null
+++ b/Sources/WebView/AuthWebView.swift
@@ -0,0 +1,173 @@
+//
+// AuthWebView.swift
+// TinkoffID
+//
+// Copyright (c) 2023 Tinkoff
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+import WebKit
+
+protocol IAuthWebViewDelegate: AnyObject {
+ func authWebView(_ webView: IAuthWebView, didOpen url: URL)
+}
+
+protocol IAuthWebView: AnyObject {
+ var delegate: IAuthWebViewDelegate? { get set }
+
+ func open(from: UIViewController?)
+ func dismiss()
+}
+
+final class AuthWebView: UIViewController {
+
+ weak var delegate: IAuthWebViewDelegate?
+
+ private let webView: WKWebView = {
+ let configuration = WKWebViewConfiguration()
+ configuration.websiteDataStore = .nonPersistent()
+ return WKWebView(frame: .zero, configuration: configuration)
+ }()
+ private let options: AppLaunchOptions
+ private var baseUrl: String
+ private let pinningDelegate: WKNavigationDelegate
+
+ init(pinningDelegate: PinningDelegate,
+ options: AppLaunchOptions,
+ baseUrl: String) {
+ self.pinningDelegate = pinningDelegate
+ self.options = options
+ self.baseUrl = baseUrl
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Закрыть",
+ style: .plain,
+ target: self,
+ action: #selector(closeButtonClicked))
+ navigationItem.rightBarButtonItem = UIBarButtonItem(image: Bundle.resourcesBundle?.imageNamed("reload"),
+ style: .plain,
+ target: self,
+ action: #selector(reloadButtonClicked))
+
+ view.addSubview(webView)
+ webView.navigationDelegate = self
+
+ webView.translatesAutoresizingMaskIntoConstraints = false
+ webView.topAnchor.constraint(equalTo: view.topAnchor, constant: navigationController?.navigationBar.frame.size.height ?? 44).isActive = true
+ webView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
+ webView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
+ webView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
+
+ if #available(iOS 13.0, *) {
+ view.backgroundColor = UIColor.systemBackground
+ } else {
+ view.backgroundColor = .white
+ }
+
+ loadWebView()
+ }
+
+ // MARK: - Private
+
+ private func loadWebView() {
+ do {
+ let url = try buildWebViewURL(with: options)
+ let request = URLRequest(url: url)
+ DispatchQueue.main.async {
+ self.webView.load(request)
+ }
+ } catch {
+ fatalError("Invalid URL provided to WebView")
+ }
+ }
+
+ private func buildWebViewURL(with options: AppLaunchOptions) throws -> URL {
+ let params = [
+ "client_id": options.clientId,
+ "code_verifier": options.payload.verifier,
+ "code_challenge_method": options.payload.challengeMethod,
+ "code_challenge": options.payload.challenge,
+ "redirect_uri": options.callbackUrl,
+ "response_type": "code",
+ "response_mode": "query"
+ ]
+
+ var components = URLComponents(string: "\(baseUrl)/auth/authorize")
+ components?.queryItems = params.map {
+ URLQueryItem(name: $0.key, value: $0.value)
+ }
+
+ enum Error: Swift.Error {
+ case unableToInitializeUrl
+ }
+
+ guard let url = components?.url else {
+ throw Error.unableToInitializeUrl
+ }
+
+ return url
+ }
+
+ @objc private func closeButtonClicked() {
+ dismiss(animated: true)
+ }
+
+ @objc private func reloadButtonClicked() {
+ loadWebView()
+ }
+}
+
+// MARK: - IAuthWebView
+
+extension AuthWebView: IAuthWebView {
+
+ func open(from: UIViewController?) {
+ let navigationController = UINavigationController(rootViewController: self)
+ from?.present(navigationController, animated: true)
+ }
+
+ func dismiss() {
+ dismiss(animated: true)
+ }
+}
+
+// MARK: - WKNavigationDelegate
+
+extension AuthWebView: WKNavigationDelegate {
+ func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
+ guard let url = navigationAction.request.url else { return }
+
+ if url.absoluteString == "https://www.tinkoff.ru/" {
+ decisionHandler(.cancel)
+ return
+ }
+ decisionHandler(.allow)
+ delegate?.authWebView(self, didOpen: url)
+ }
+
+ public func webView(_ webView: WKWebView,
+ didReceive challenge: URLAuthenticationChallenge,
+ completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
+
+ pinningDelegate.webView?(webView, didReceive: challenge, completionHandler: completionHandler)
+ }
+}
diff --git a/Sources/WebView/AuthWebViewBuilder.swift b/Sources/WebView/AuthWebViewBuilder.swift
new file mode 100644
index 0000000..2c143a2
--- /dev/null
+++ b/Sources/WebView/AuthWebViewBuilder.swift
@@ -0,0 +1,39 @@
+//
+// AuthWebViewBuilder.swift
+// TinkoffID
+//
+// Copyright (c) 2023 Tinkoff
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+
+protocol IAuthWebViewBuilder {
+ func build(with options: AppLaunchOptions) -> IAuthWebView
+}
+
+final class AuthWebViewBuilder: IAuthWebViewBuilder {
+
+ private var baseUrl: String
+ private var pinningDelegate: PinningDelegate
+
+ init(baseUrl: String,
+ pinningDelegate: PinningDelegate) {
+ self.baseUrl = baseUrl
+ self.pinningDelegate = pinningDelegate
+ }
+
+ func build(with options: AppLaunchOptions) -> IAuthWebView {
+ return AuthWebView(pinningDelegate: pinningDelegate, options: options, baseUrl: baseUrl)
+ }
+}
diff --git a/Sources/WebView/DefaultAuthWebViewSourceProvider.swift b/Sources/WebView/DefaultAuthWebViewSourceProvider.swift
new file mode 100644
index 0000000..8fb62da
--- /dev/null
+++ b/Sources/WebView/DefaultAuthWebViewSourceProvider.swift
@@ -0,0 +1,50 @@
+//
+// DefaultAuthWebViewSourceProvider.swift
+// TinkoffID
+//
+// Copyright (c) 2023 Tinkoff
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+
+public final class DefaultAuthWebViewSourceProvider: IAuthWebViewSourceProvider {
+
+ public static var instance: IAuthWebViewSourceProvider {
+ DefaultAuthWebViewSourceProvider()
+ }
+
+ public func getSourceViewController() -> UIViewController {
+ guard let topViewController = UIApplication.topViewController() else {
+ fatalError("Ошибка в иерархии вью")
+ }
+ return topViewController
+ }
+}
+
+extension UIApplication {
+ class func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
+ if let navigationController = controller as? UINavigationController {
+ return topViewController(controller: navigationController.visibleViewController)
+ }
+ if let tabController = controller as? UITabBarController {
+ if let selected = tabController.selectedViewController {
+ return topViewController(controller: selected)
+ }
+ }
+ if let presented = controller?.presentedViewController {
+ return topViewController(controller: presented)
+ }
+ return controller
+ }
+}
diff --git a/Sources/WebView/Public/IAuthWebViewSourceProvider.swift b/Sources/WebView/Public/IAuthWebViewSourceProvider.swift
new file mode 100644
index 0000000..2275401
--- /dev/null
+++ b/Sources/WebView/Public/IAuthWebViewSourceProvider.swift
@@ -0,0 +1,23 @@
+//
+// IAuthWebViewProvider.swift
+// TinkoffID
+//
+// Copyright (c) 2023 Tinkoff
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+
+public protocol IAuthWebViewSourceProvider {
+ func getSourceViewController() -> UIViewController
+}
diff --git a/Tests/Mocks/MockedAppLauncher.swift b/Tests/Mocks/MockedAppLauncher.swift
index 38761f0..b8ed808 100644
--- a/Tests/Mocks/MockedAppLauncher.swift
+++ b/Tests/Mocks/MockedAppLauncher.swift
@@ -22,16 +22,19 @@ import Foundation
final class MockedAppLauncher: IAppLauncher {
var stubbedCanLaunchApp: Bool!
var stubbedLaunchAppError: Error?
+ var stubbedLaunchAppCompletionResult: Bool!
var lastLaunchAppOptions: AppLaunchOptions?
var canLaunchApp: Bool {
stubbedCanLaunchApp
}
-
- func launchApp(with options: AppLaunchOptions) throws {
+
+ func launchApp(with options: AppLaunchOptions, universalLinksOnly: Bool = false, completion: @escaping ((Bool) -> Void)) throws {
lastLaunchAppOptions = options
-
+
+ completion(stubbedLaunchAppCompletionResult)
+
if let error = stubbedLaunchAppError {
throw error
}
diff --git a/Tests/Mocks/MockedURLRouter.swift b/Tests/Mocks/MockedURLRouter.swift
index 9773e40..965788e 100644
--- a/Tests/Mocks/MockedURLRouter.swift
+++ b/Tests/Mocks/MockedURLRouter.swift
@@ -45,4 +45,16 @@ final class MockedURLRouter: IURLRouter {
return nextOpenResult
}
+
+ func openWithFallback(_ url: URL, universalLinksOnly: Bool, completion: @escaping ((Bool) -> Void)) -> Bool {
+ lastOpenedURL = url
+
+ defer {
+ nextOpenResult = nil
+ }
+
+ completion(nextOpenResult)
+
+ return nextOpenResult
+ }
}
diff --git a/Tests/Mocks/MockedWebView.swift b/Tests/Mocks/MockedWebView.swift
new file mode 100644
index 0000000..b3e28ed
--- /dev/null
+++ b/Tests/Mocks/MockedWebView.swift
@@ -0,0 +1,33 @@
+//
+// MockedWebView.swift
+// TinkoffID
+//
+// Copyright (c) 2021 Tinkoff
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Foundation
+@testable import TinkoffID
+
+final class MockedWebView: IAuthWebView {
+
+ weak var delegate: IAuthWebViewDelegate?
+
+ var subbedDidOpenURLResult: URL!
+
+ func open(from: UIViewController?) {
+ delegate?.authWebView(self, didOpen: subbedDidOpenURLResult)
+ }
+
+ func dismiss() {}
+}
diff --git a/Tests/Mocks/MockedWebViewBuilder.swift b/Tests/Mocks/MockedWebViewBuilder.swift
new file mode 100644
index 0000000..fedc0d6
--- /dev/null
+++ b/Tests/Mocks/MockedWebViewBuilder.swift
@@ -0,0 +1,28 @@
+//
+// MockedWebViewBuilder.swift
+// TinkoffID
+//
+// Copyright (c) 2021 Tinkoff
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+@testable import TinkoffID
+
+final class MockedWebViewBuilder: IAuthWebViewBuilder {
+
+ var stubbedAuthWebViewResult: IAuthWebView!
+
+ func build(with options: AppLaunchOptions) -> IAuthWebView {
+ stubbedAuthWebViewResult
+ }
+}
diff --git a/Tests/TinkoffIDTests.swift b/Tests/TinkoffIDTests.swift
index 72397d2..b4e66c8 100644
--- a/Tests/TinkoffIDTests.swift
+++ b/Tests/TinkoffIDTests.swift
@@ -25,6 +25,9 @@ class TinkoffIDTests: XCTestCase {
private var appLauncher: MockedAppLauncher!
private var callbackParser: MockedCallbackURLParser!
private var api: MockedAPI!
+ private var authWebView: MockedWebView!
+ private var authWebViewBuilder: MockedWebViewBuilder!
+
private let clientId = "some_client"
private let callbackUrl = "nowhere://"
@@ -33,13 +36,18 @@ class TinkoffIDTests: XCTestCase {
appLauncher = MockedAppLauncher()
callbackParser = MockedCallbackURLParser()
api = MockedAPI()
+ authWebView = MockedWebView()
+ authWebViewBuilder = MockedWebViewBuilder()
sdk = TinkoffID(payloadGenerator: payloadGenerator,
appLauncher: appLauncher,
callbackUrlParser: callbackParser,
api: api,
+ authWebViewBuilder: authWebViewBuilder,
+ webViewSourceProvider: nil,
clientId: clientId,
- callbackUrl: callbackUrl)
+ callbackUrl: callbackUrl,
+ universalLinksOnly: false)
}
func testThatIsTinkoffAuthAvailableValueDependsOnAppLauncher() {
@@ -56,6 +64,7 @@ class TinkoffIDTests: XCTestCase {
func testThatAppLauncherWillLaunchAppWithCorrectOptionsWhenStartingTinkoffAuth() {
// Given
payloadGenerator.stubbedPayload = .stub
+ appLauncher.stubbedLaunchAppCompletionResult = true
let expectedOptions = AppLaunchOptions(clientId: clientId,
callbackUrl: callbackUrl,
@@ -95,6 +104,7 @@ class TinkoffIDTests: XCTestCase {
func testThatHandleCallbackUrlWillReturnFalseIfCallbackUrlDoesNotMatchExpectedOne() {
// Given
payloadGenerator.stubbedPayload = .stub
+ appLauncher.stubbedLaunchAppCompletionResult = true
let callbackUrl = incorrectCallbackUrl
@@ -113,6 +123,7 @@ class TinkoffIDTests: XCTestCase {
// Given
payloadGenerator.stubbedPayload = .stub
callbackParser.stubbedParseResult = .cancelled
+ appLauncher.stubbedLaunchAppCompletionResult = true
// When
var callbackUrlHandlingResult: Bool!
@@ -129,6 +140,7 @@ class TinkoffIDTests: XCTestCase {
// Given
payloadGenerator.stubbedPayload = .stub
callbackParser.stubbedParseResult = CallbackURLParseResult?.none
+ appLauncher.stubbedLaunchAppCompletionResult = true
// When
@@ -144,6 +156,7 @@ class TinkoffIDTests: XCTestCase {
// Given
callbackParser.stubbedParseResult = .cancelled
payloadGenerator.stubbedPayload = .stub
+ appLauncher.stubbedLaunchAppCompletionResult = true
// When
let result = startTinkoffAuth { _ in
@@ -160,6 +173,7 @@ class TinkoffIDTests: XCTestCase {
// Given
callbackParser.stubbedParseResult = .unavailable
payloadGenerator.stubbedPayload = .stub
+ appLauncher.stubbedLaunchAppCompletionResult = true
// When
let result = startTinkoffAuth { _ in
@@ -180,6 +194,7 @@ class TinkoffIDTests: XCTestCase {
api.obtainCredentialsResult = .failure(ErrorStub.foo)
callbackParser.stubbedParseResult = .codeObtained(code)
payloadGenerator.stubbedPayload = payload
+ appLauncher.stubbedLaunchAppCompletionResult = true
// When
_ = startTinkoffAuth { _ in
@@ -201,6 +216,7 @@ class TinkoffIDTests: XCTestCase {
api.obtainCredentialsResult = .failure(ErrorStub.foo)
callbackParser.stubbedParseResult = .codeObtained(code)
payloadGenerator.stubbedPayload = payload
+ appLauncher.stubbedLaunchAppCompletionResult = true
// When
let result = startTinkoffAuth { _ in
@@ -221,6 +237,7 @@ class TinkoffIDTests: XCTestCase {
api.obtainCredentialsResult = .success(.stub)
callbackParser.stubbedParseResult = .codeObtained(code)
payloadGenerator.stubbedPayload = payload
+ appLauncher.stubbedLaunchAppCompletionResult = true
// When
let result = startTinkoffAuth { _ in
diff --git a/Tests/URLSchemeAppLauncherTests.swift b/Tests/URLSchemeAppLauncherTests.swift
index 9dfd445..e01002f 100644
--- a/Tests/URLSchemeAppLauncherTests.swift
+++ b/Tests/URLSchemeAppLauncherTests.swift
@@ -55,7 +55,7 @@ class URLSchemeAppLauncherTests: XCTestCase {
router.nextOpenResult = true
// When
- try! launcher.launchApp(with: expectedOptions)
+ try! launcher.launchApp(with: expectedOptions, universalLinksOnly: false, completion: { _ in })
// Then
XCTAssertEqual(expectedOptions, schemeBuilder.lastOptions)
@@ -70,7 +70,7 @@ class URLSchemeAppLauncherTests: XCTestCase {
// When
assertNoError(message: "App has to be launched") {
- try launcher.launchApp(with: .stub)
+ try launcher.launchApp(with: .stub, universalLinksOnly: false) { _ in }
}
// Then
@@ -83,7 +83,7 @@ class URLSchemeAppLauncherTests: XCTestCase {
router.nextOpenResult = false
// When
- let when = { try self.launcher.launchApp(with: .stub) }
+ let when = { try self.launcher.launchApp(with: .stub, universalLinksOnly: false) { _ in } }
// Then
assertErrorEqual(URLSchemeAppLauncher.Error.launchFailure, when)
diff --git a/TinkoffID.podspec b/TinkoffID.podspec
index 6de0cca..6102afb 100644
--- a/TinkoffID.podspec
+++ b/TinkoffID.podspec
@@ -6,10 +6,11 @@ Pod::Spec.new do |s|
s.homepage = 'https://github.com/tinkoff-mobile-tech/TinkoffID'
s.license = { type: 'MIT', file: 'LICENSE' }
s.source = { git: 'https://github.com/tinkoff-mobile-tech/TinkoffID.git', tag: s.version.to_s }
- s.ios.deployment_target = '10.0'
+ s.ios.deployment_target = '12.0'
s.swift_version = '5.0'
s.source_files = 'Sources/**/*.swift'
s.resources = 'Sources/**/*.{xcassets,lproj}'
+ s.dependency 'TCSSSLPinningPublic', '~> 4.0'
s.test_spec('Tests') do |test_spec|
test_spec.source_files = 'Tests/**/*.{swift}'