diff --git a/.ruby-version b/.ruby-version index f967d3c1..30c59922 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -ruby-2.5.5 \ No newline at end of file +ruby-2.7.4 diff --git a/.version b/.version index c0be8a79..cd802a1e 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -6.4.0 \ No newline at end of file +6.6.0 \ No newline at end of file diff --git a/.version_code b/.version_code index c38d19af..2497dbba 100644 --- a/.version_code +++ b/.version_code @@ -1 +1 @@ -6040000 \ No newline at end of file +6060000 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a0c3141..c7bf42ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ ### NEXT_VERSION_DESCRIPTION_BEGIN ### NEXT_VERSION_DESCRIPTION_END +## [6.6.0] (29-12-2021) + +* Обновление метрик. Использование удалённого файла конфигурации. + +## [6.5.0] (21-12-2021) + +* Обновлены зависимости, убраны ворнинги. Убраны deprecated методы для CardSecModuleOutput. + ## [6.4.0] (25-09-2021) * Добалена возможность рекурентных SberPay платежей diff --git a/Cartfile b/Cartfile index eff0a917..cf61e082 100644 --- a/Cartfile +++ b/Cartfile @@ -2,6 +2,6 @@ github "yoomoney/functional-swift" ~> 1.7.3 github "yoomoney/core-api-swift" ~> 2.0.1 github "yoomoney/yookassa-wallet-api-swift" ~> 2.3.1 github "yoomoney/yookassa-payments-api-swift" ~> 2.11.0 -binary "https://raw.githubusercontent.com/yoomoney/yooid-sdk-ios/master/MoneyAuth.json" ~> 2.34.1 +binary "https://raw.githubusercontent.com/yoomoney/yooid-sdk-ios/master/MoneyAuth.json" ~> 3.3.0 binary "https://raw.githubusercontent.com/yandexmobile/metrica-sdk-ios/master/YandexMobileMetrica.json" ~> 3.0 -binary "https://raw.githubusercontent.com/yoomoney/yookassa-threat-metrix-adapter-ios/main/ThreatMetrixAdapter.json" ~> 3.3.0 +binary "https://raw.githubusercontent.com/yoomoney/yookassa-threat-metrix-adapter-ios/main/ThreatMetrixAdapter.json" ~> 3.3.3 diff --git a/Cartfile.resolved b/Cartfile.resolved index e5d6509f..92b3b584 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,5 +1,5 @@ binary "https://raw.githubusercontent.com/yandexmobile/metrica-sdk-ios/master/YandexMobileMetrica.json" "3.17.0" -binary "https://raw.githubusercontent.com/yoomoney/yooid-sdk-ios/master/MoneyAuth.json" "2.34.1" +binary "https://raw.githubusercontent.com/yoomoney/yooid-sdk-ios/master/MoneyAuth.json" "3.3.0" binary "https://raw.githubusercontent.com/yoomoney/yookassa-threat-metrix-adapter-ios/main/ThreatMetrixAdapter.json" "3.3.3" github "AliSoftware/OHHTTPStubs" "8.0.0" github "yoomoney/core-api-swift" "2.0.1" diff --git a/Podfile b/Podfile index fada4dc8..e96616b8 100644 --- a/Podfile +++ b/Podfile @@ -8,7 +8,6 @@ project 'YooKassaPaymentsDemoApp.xcodeproj' workspace 'YooKassaPayments.xcworkspace' target 'YooKassaPaymentsDemoApp' do - pod 'CardIO' pod 'SwiftLint' pod 'Reveal-SDK', :configurations => ['Debug'] diff --git a/Podfile.lock b/Podfile.lock index 7f5d7f95..b77ffdc6 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,7 +1,6 @@ PODS: - - CardIO (5.4.1) - FunctionalSwift (1.7.3) - - MoneyAuth (2.34.1): + - MoneyAuth (3.3.0): - FunctionalSwift - ThreatMetrixAdapter - YooMoneyCoreApi @@ -14,9 +13,9 @@ PODS: - YandexMobileMetrica/Dynamic/Core (3.17.0) - YandexMobileMetrica/Dynamic/Crashes (3.17.0): - YandexMobileMetrica/Dynamic/Core - - YooKassaPayments (6.4.0): - - MoneyAuth (~> 2.34.1) - - ThreatMetrixAdapter (~> 3.3.0) + - YooKassaPayments (6.6.0): + - MoneyAuth (~> 3.3.0) + - ThreatMetrixAdapter (~> 3.3.3) - YandexMobileMetrica/Dynamic (~> 3.0) - YooKassaPaymentsApi (~> 2.11.0) - YooKassaWalletApi (~> 2.3.1) @@ -31,7 +30,6 @@ PODS: - FunctionalSwift (~> 1.7.3) DEPENDENCIES: - - CardIO - Reveal-SDK - SwiftLint - YooKassaPayments (from `./`) @@ -46,7 +44,6 @@ SPEC REPOS: - YooKassaWalletApi - YooMoneyCoreApi https://github.com/CocoaPods/Specs.git: - - CardIO - Reveal-SDK - SwiftLint - YandexMobileMetrica @@ -56,18 +53,17 @@ EXTERNAL SOURCES: :path: "./" SPEC CHECKSUMS: - CardIO: 56983b39b62f495fc6dae9ad7cf875143df06443 FunctionalSwift: 856da67cf3fb812341445d4e28f05875904d8da0 - MoneyAuth: 6a008f183c29d195704053ff2f98fc181f905074 + MoneyAuth: 74ba8e8fadbdabe2be706dcad1d0b9de96152897 Reveal-SDK: effba1c940b8337195563c425a6b5862ec875caa SwiftLint: 06ac37e4d38c7068e0935bb30cda95f093bec761 ThreatMetrixAdapter: 1b31f0afe02eb68be52945e160cc9c0fd117b06c YandexMobileMetrica: 9e713c16bb6aca0ba63b84c8d7b8b86d32f4ecc4 - YooKassaPayments: 41e95ab8d3297816189ba7c64869325df79dc6a6 + YooKassaPayments: a682124f853fce382ae3b4e2a115cd6e79500c06 YooKassaPaymentsApi: f76c84ec94ace98e8babc6996cf804840d93f8d8 YooKassaWalletApi: 817d511330332aaec84490f8335ab8c94285d9be YooMoneyCoreApi: 80f30f988f1dc19a397baf246db64cbbca077fd6 -PODFILE CHECKSUM: e3d8979584d168459cceff74a485647132ad28ae +PODFILE CHECKSUM: 09cf283b8b86edb3f9c9eb3e422765473b150274 COCOAPODS: 1.11.2 diff --git a/YooKassaPayments.podspec b/YooKassaPayments.podspec index 7631f83b..d41bf663 100644 --- a/YooKassaPayments.podspec +++ b/YooKassaPayments.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'YooKassaPayments' - s.version = '6.4.0' + s.version = '6.6.0' s.homepage = 'https://github.com/yoomoney/yookassa-payments-swift' s.license = { :type => "MIT", @@ -32,8 +32,8 @@ Pod::Spec.new do |s| s.ios.dependency 'YooMoneyCoreApi', '~> 2.0.1' s.ios.dependency 'YooKassaPaymentsApi', '~> 2.11.0' s.ios.dependency 'YooKassaWalletApi', '~> 2.3.1' - s.ios.dependency 'MoneyAuth', '~> 2.34.1' - s.ios.dependency 'ThreatMetrixAdapter', '~> 3.3.0' + s.ios.dependency 'MoneyAuth', '~> 3.3.0' + s.ios.dependency 'ThreatMetrixAdapter', '~> 3.3.3' s.ios.dependency 'YandexMobileMetrica/Dynamic', '~> 3.0' diff --git a/YooKassaPayments.xcodeproj/project.pbxproj b/YooKassaPayments.xcodeproj/project.pbxproj index 8bb9e488..29707ea2 100644 --- a/YooKassaPayments.xcodeproj/project.pbxproj +++ b/YooKassaPayments.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 2519BAC926F35BBA0013DEFD /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2519BAC826F35BBA0013DEFD /* Config.swift */; }; + 2519BACC26F878520013DEFD /* defaultConfig_ru.json in Resources */ = {isa = PBXBuildFile; fileRef = 2519BACB26F878520013DEFD /* defaultConfig_ru.json */; }; + 2519BAD226FAF7630013DEFD /* ConfigMediator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2519BAD126FAF7630013DEFD /* ConfigMediator.swift */; }; + 2519BAD426FAF78A0013DEFD /* ConfigService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2519BAD326FAF78A0013DEFD /* ConfigService.swift */; }; 252982A426AC38B300174692 /* TMXProfiling.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 30D7527E261C6D3C00EF3231 /* TMXProfiling.xcframework */; }; 252982A626AC38B300174692 /* TMXProfilingConnections.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 30D7527D261C6D3C00EF3231 /* TMXProfilingConnections.xcframework */; }; 252982B026AE928C00174692 /* Cartfile in Resources */ = {isa = PBXBuildFile; fileRef = 252982AF26AE928C00174692 /* Cartfile */; }; @@ -18,6 +22,7 @@ 252982C426AEB3B500174692 /* YandexMobileMetrica.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 252982C226AEB3B500174692 /* YandexMobileMetrica.xcframework */; }; 252982C526AEB3B500174692 /* YandexMobileMetricaCrashes.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 252982C326AEB3B500174692 /* YandexMobileMetricaCrashes.xcframework */; }; 252982C726AEB6A800174692 /* ThreatMetrixAdapter.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 252982C626AEB6A800174692 /* ThreatMetrixAdapter.xcframework */; }; + 252ED94A2773B9F800547007 /* AnalyticsTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 252ED9492773B9F700547007 /* AnalyticsTracking.swift */; }; 25347BE126BADBE800FDD1DA /* CardSettingsRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25347BD626BADBE800FDD1DA /* CardSettingsRouter.swift */; }; 25347BE226BADBE800FDD1DA /* CardSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25347BD726BADBE800FDD1DA /* CardSettingsPresenter.swift */; }; 25347BE326BADBE800FDD1DA /* CardSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25347BD826BADBE800FDD1DA /* CardSettingsViewController.swift */; }; @@ -29,6 +34,19 @@ 25347BE926BADBE800FDD1DA /* CardSettingsInteractorIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25347BE026BADBE800FDD1DA /* CardSettingsInteractorIO.swift */; }; 25347BEB26BADC2E00FDD1DA /* LargeActionInformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25347BEA26BADC2E00FDD1DA /* LargeActionInformer.swift */; }; 25347BED26BADC6800FDD1DA /* ActionTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25347BEC26BADC6800FDD1DA /* ActionTemplate.swift */; }; + 2548049E2727F53F0067777C /* AnalyticsEventContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2548049D2727F53F0067777C /* AnalyticsEventContext.swift */; }; + 254804A0272956280067777C /* TooManyRequestsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2548049F272956280067777C /* TooManyRequestsError.swift */; }; + 255F2A9126FB5A9C00BF4354 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 255F2A9026FB5A9C00BF4354 /* Logger.swift */; }; + 255F2A9326FC5C3500BF4354 /* defaultConfig_en.json in Resources */ = {isa = PBXBuildFile; fileRef = 255F2A9226FC5C2500BF4354 /* defaultConfig_en.json */; }; + 255F2A952701D73200BF4354 /* ConfigMediatorAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 255F2A942701D73200BF4354 /* ConfigMediatorAssembly.swift */; }; + 255F2A972701D99F00BF4354 /* ConfigServiceAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 255F2A962701D99F00BF4354 /* ConfigServiceAssembly.swift */; }; + 255F2A9927032ED700BF4354 /* ConfigMediatorImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 255F2A9827032ED700BF4354 /* ConfigMediatorImpl.swift */; }; + 255F2A9B27032EE500BF4354 /* ConfigServiceImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 255F2A9A27032EE500BF4354 /* ConfigServiceImpl.swift */; }; + 25911A9B27073B930033F0B1 /* PlistStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25911A9A27073B930033F0B1 /* PlistStorage.swift */; }; + 25911A9D270B512C0033F0B1 /* DataLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25911A9C270B512C0033F0B1 /* DataLoader.swift */; }; + 25911AB6270EB8A30033F0B1 /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25911AB5270EB8A30033F0B1 /* LoadingViewController.swift */; }; + 2596771A275A67FE0043D10A /* HTMLUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25967719275A67FE0043D10A /* HTMLUtils.swift */; }; + 25CA4F8F27635D71006C257E /* Hosts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 25CA4F8E27635D70006C257E /* Hosts.plist */; }; 25FD881326CD4AD70032B5FD /* PaymentRecurrencyAndDataSavingSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25FD881226CD4AD70032B5FD /* PaymentRecurrencyAndDataSavingSection.swift */; }; 25FD881526CD4B820032B5FD /* PaymentRecurrencyAndDataSavingSectionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25FD881426CD4B820032B5FD /* PaymentRecurrencyAndDataSavingSectionFactory.swift */; }; 3089EF4923846F6300CB7319 /* SwitcherSavePaymentMethodViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3089EF4823846F6300CB7319 /* SwitcherSavePaymentMethodViewModel.swift */; }; @@ -105,19 +123,16 @@ 402EB461D1438596B3505A77 /* PhoneNumberInputAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB45088875FC4B1B83754 /* PhoneNumberInputAssembly.swift */; }; 402EB469B4798293A88ABAF4 /* ActivityIndicatorPresenting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB63E55B21BF12B8CD2C4 /* ActivityIndicatorPresenting.swift */; }; 402EB481012F5C629D018C8A /* SavePaymentMethodViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB96DFA8C9D871DE124D0 /* SavePaymentMethodViewModelFactory.swift */; }; - 402EB4A38D9458D726EF2973 /* AnalyticsProviderAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBA44464C238E00928819 /* AnalyticsProviderAssembly.swift */; }; 402EB4DB7D89DA8CBED9BBBB /* AuthTypeStatesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBFF57151C710CF91D0E6 /* AuthTypeStatesService.swift */; }; 402EB4DECF9836134B8DFF2C /* SberbankViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBD38199403D8E554BCA3 /* SberbankViewController.swift */; }; 402EB4E93D7A00A973BB93FA /* UITableViewHeaderFooterView+Identifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB47AB6115E5DB414156B /* UITableViewHeaderFooterView+Identifier.swift */; }; 402EB4FEA877102C24D0AEE2 /* UnderlinedTextField+InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB51EED8568C831E1F9B6 /* UnderlinedTextField+InputView.swift */; }; 402EB51D8B988D695E8DB719 /* ApiSessionAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB572A001C6A59C33858D /* ApiSessionAssembly.swift */; }; - 402EB52500117CC0F9F7337E /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBC4E71E76A1046320E32 /* AnalyticsService.swift */; }; 402EB52D11504C419695D49F /* UserDefaultsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB0D93DE0D413280F555E /* UserDefaultsStorage.swift */; }; 402EB52D1B69D9D12BA05F02 /* UIFont+Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB21194794FB1D5FFEFF9 /* UIFont+Style.swift */; }; 402EB5371BE9F336FC06AA3A /* Anchor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB179E2E8A2381A0AEBAD /* Anchor.swift */; }; 402EB53AE18A55F18C9BDCA9 /* UIView+SpecificLayoutGuide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB226DE34051D5952382B /* UIView+SpecificLayoutGuide.swift */; }; 402EB55BA25BA4C0946AE0D8 /* KeyValueStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB009CEDEC5DB870537E1 /* KeyValueStoring.swift */; }; - 402EB5609877054CD489DF14 /* AnalyticsTrack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBD98D680460920FAFF25 /* AnalyticsTrack.swift */; }; 402EB566B44670E5D7F5558B /* CardSecModuleIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB1CA83A927FE69ED0790 /* CardSecModuleIO.swift */; }; 402EB571173B519C0F71C65A /* InputCvcView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB9A63FFF964EED27D4DE /* InputCvcView.swift */; }; 402EB5745ABA5C4E1A40294A /* WalletLoginServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBAD1F924C381F5EF1B45 /* WalletLoginServiceMock.swift */; }; @@ -126,7 +141,6 @@ 402EB5D6E79FE22EFE162375 /* PaymentMethodsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB6F0F343B8BCBDECB5C7 /* PaymentMethodsViewController.swift */; }; 402EB5EFF251B3E8A8C0C35B /* PriceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBB761608F1C750B0D45F /* PriceView.swift */; }; 402EB6121E6DE1E810A074A9 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 402EB434B2C5611C56511AC1 /* Localizable.strings */; }; - 402EB62CDBDD5CBC28E68446 /* Hosts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 402EB1F6B2E1B5879D2BCCC8 /* Hosts.plist */; }; 402EB636AA2031BCCDFB4139 /* SberbankInteractorIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB9A46DDCE237E4AB1B00 /* SberbankInteractorIO.swift */; }; 402EB636FC3E40E0EDE5A096 /* LogoutConfirmationModuleIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB021B3ABFDEC19285694 /* LogoutConfirmationModuleIO.swift */; }; 402EB65A2699AF0F54E79C59 /* BankCardDataInputInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB8AAAF53AE99712DC15E /* BankCardDataInputInteractor.swift */; }; @@ -169,7 +183,6 @@ 402EB90A3E9FD8733C4EBEA9 /* LoginConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB7CFB5576396563D966E /* LoginConfirmationViewController.swift */; }; 402EB9182B2015B619CAAF81 /* SberbankInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBB751AD176B1868469FB /* SberbankInteractor.swift */; }; 402EB91BEC54829B7D570406 /* PhoneNumberInputModuleOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBAC288F1B43084EA43BC /* PhoneNumberInputModuleOutput.swift */; }; - 402EB92B510A1A0BFB6EB464 /* TermsOfService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBDA93CCB741604CDF4AF /* TermsOfService.swift */; }; 402EB933D69EDDFED2DDAB44 /* Amount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB94E653C072F4708CB50 /* Amount.swift */; }; 402EB948B1832C8B0A29FBC2 /* UILayoutPriority+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBA3026D26E32EFFB6024 /* UILayoutPriority+Constants.swift */; }; 402EB952EA7B9CD954D7654E /* KeyboardObservingAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBCCE907967D5F3300495 /* KeyboardObservingAccessoryView.swift */; }; @@ -182,7 +195,6 @@ 402EB9C37ECFBE9D2B402043 /* PaymentMethodViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB584E224B50E412AB1FB /* PaymentMethodViewModel.swift */; }; 402EB9C4BBC1F6EF4241A9C9 /* CardData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBA04C707C5EA568200DE /* CardData.swift */; }; 402EB9CF3E1CEA91F61C39DE /* PaymentMethodResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBBEF9B96FD16F8D2F809 /* PaymentMethodResources.swift */; }; - 402EB9E3F15C7DAD7D254D24 /* AnalyticsProviderImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBEC76B2AB7515C0B5B91 /* AnalyticsProviderImpl.swift */; }; 402EB9E7626589A984FC9F72 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBF30547C0D27015F777A /* Settings.swift */; }; 402EB9EA1CCAA11BF60B5F05 /* PaymentMethodsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB03E078236001308568D /* PaymentMethodsInteractor.swift */; }; 402EBA1C3AD52A9F54713245 /* IdentifyCountryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBF6A37743D45F45F9F0A /* IdentifyCountryService.swift */; }; @@ -191,13 +203,12 @@ 402EBAA666772AAB2FA2149D /* InputPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB8DEF09A97F555305770 /* InputPresenter.swift */; }; 402EBAAC323B2686A6C43781 /* BankCardDataInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBC4B72D66A2A1BDFB78B /* BankCardDataInputView.swift */; }; 402EBAAF81518608501DBE6D /* BankCardAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBC98CF5119E39D7C3A36 /* BankCardAssembly.swift */; }; - 402EBACF1C5CFC5C064DFA36 /* AnalyticsServiceImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB5091FBD0903A0369764 /* AnalyticsServiceImpl.swift */; }; + 402EBACF1C5CFC5C064DFA36 /* CommonTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB5091FBD0903A0369764 /* CommonTracker.swift */; }; 402EBAE5A1E3FF3F36EA7C71 /* PhoneNumberInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB3B3E075F5039FB3D646 /* PhoneNumberInputView.swift */; }; - 402EBB04E607A7D25433D1A8 /* AnalyticsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBBE3BE1580D80C891092 /* AnalyticsProvider.swift */; }; 402EBB0834948CFD4278DF96 /* UILabel+Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB371C94C403A2BA9E933 /* UILabel+Style.swift */; }; 402EBB0D7303FCBCDB26B74B /* Stylable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB607BBBCF0EDB2F7A639 /* Stylable.swift */; }; 402EBB1D932FB9AA14055422 /* PanInputPresenterStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBA599B2479B086E7EC2A /* PanInputPresenterStyle.swift */; }; - 402EBB302AB46E50088A5B40 /* AnalyticsServiceAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBF4B20C030067340B10D /* AnalyticsServiceAssembly.swift */; }; + 402EBB302AB46E50088A5B40 /* AnalyticsTrackingAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EBF4B20C030067340B10D /* AnalyticsTrackingAssembly.swift */; }; 402EBB42DE308BDC3E21CA74 /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB15B276B126AC1A628AD /* NavigationController.swift */; }; 402EBB5BA886912050991DEC /* PaymentMethodTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB487A00F4E5CBA7498D9 /* PaymentMethodTypes.swift */; }; 402EBB63E95FBBA1BA1B59BE /* InternalStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402EB1EEE3DFE557080259B2 /* InternalStyle.swift */; }; @@ -403,6 +414,10 @@ 244F7D88849ACD598FFD45ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.info; path = Info.plist; sourceTree = ""; }; 244F7DBBF9F3523FC00B2357 /* YooKassaPayments.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = YooKassaPayments.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 244F7F4FDAEC4CA756655499 /* YooKassaPayments.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = YooKassaPayments.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + 2519BAC826F35BBA0013DEFD /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; + 2519BACB26F878520013DEFD /* defaultConfig_ru.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = defaultConfig_ru.json; sourceTree = ""; }; + 2519BAD126FAF7630013DEFD /* ConfigMediator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigMediator.swift; sourceTree = ""; }; + 2519BAD326FAF78A0013DEFD /* ConfigService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigService.swift; sourceTree = ""; }; 252982AF26AE928C00174692 /* Cartfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Cartfile; sourceTree = ""; }; 252982B826AEB01200174692 /* YooKassaWalletApi.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = YooKassaWalletApi.xcframework; path = Carthage/Build/YooKassaWalletApi.xcframework; sourceTree = ""; }; 252982B926AEB01200174692 /* FunctionalSwift.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = FunctionalSwift.xcframework; path = Carthage/Build/FunctionalSwift.xcframework; sourceTree = ""; }; @@ -413,6 +428,7 @@ 252982C326AEB3B500174692 /* YandexMobileMetricaCrashes.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = YandexMobileMetricaCrashes.xcframework; path = Carthage/Build/YandexMobileMetricaCrashes.xcframework; sourceTree = ""; }; 252982C626AEB6A800174692 /* ThreatMetrixAdapter.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ThreatMetrixAdapter.xcframework; path = Carthage/Build/ThreatMetrixAdapter.xcframework; sourceTree = ""; }; 252982C826AEB70300174692 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.3.sdk/System/iOSSupport/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; + 252ED9492773B9F700547007 /* AnalyticsTracking.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyticsTracking.swift; sourceTree = ""; }; 25347BD626BADBE800FDD1DA /* CardSettingsRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardSettingsRouter.swift; sourceTree = ""; }; 25347BD726BADBE800FDD1DA /* CardSettingsPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardSettingsPresenter.swift; sourceTree = ""; }; 25347BD826BADBE800FDD1DA /* CardSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardSettingsViewController.swift; sourceTree = ""; }; @@ -424,6 +440,19 @@ 25347BE026BADBE800FDD1DA /* CardSettingsInteractorIO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardSettingsInteractorIO.swift; sourceTree = ""; }; 25347BEA26BADC2E00FDD1DA /* LargeActionInformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LargeActionInformer.swift; sourceTree = ""; }; 25347BEC26BADC6800FDD1DA /* ActionTemplate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionTemplate.swift; sourceTree = ""; }; + 2548049D2727F53F0067777C /* AnalyticsEventContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsEventContext.swift; sourceTree = ""; }; + 2548049F272956280067777C /* TooManyRequestsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooManyRequestsError.swift; sourceTree = ""; }; + 255F2A9026FB5A9C00BF4354 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + 255F2A9226FC5C2500BF4354 /* defaultConfig_en.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = defaultConfig_en.json; sourceTree = ""; }; + 255F2A942701D73200BF4354 /* ConfigMediatorAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigMediatorAssembly.swift; sourceTree = ""; }; + 255F2A962701D99F00BF4354 /* ConfigServiceAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigServiceAssembly.swift; sourceTree = ""; }; + 255F2A9827032ED700BF4354 /* ConfigMediatorImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigMediatorImpl.swift; sourceTree = ""; }; + 255F2A9A27032EE500BF4354 /* ConfigServiceImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigServiceImpl.swift; sourceTree = ""; }; + 25911A9A27073B930033F0B1 /* PlistStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlistStorage.swift; sourceTree = ""; }; + 25911A9C270B512C0033F0B1 /* DataLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataLoader.swift; sourceTree = ""; }; + 25911AB5270EB8A30033F0B1 /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = ""; }; + 25967719275A67FE0043D10A /* HTMLUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLUtils.swift; sourceTree = ""; }; + 25CA4F8E27635D70006C257E /* Hosts.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Hosts.plist; sourceTree = ""; }; 25CD87AF26B90641002F4769 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; 25FD881226CD4AD70032B5FD /* PaymentRecurrencyAndDataSavingSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentRecurrencyAndDataSavingSection.swift; sourceTree = ""; }; 25FD881426CD4B820032B5FD /* PaymentRecurrencyAndDataSavingSectionFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentRecurrencyAndDataSavingSectionFactory.swift; sourceTree = ""; }; @@ -474,7 +503,6 @@ 402EB1D0205199AC3C6DE886 /* StyleStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StyleStorage.swift; sourceTree = ""; }; 402EB1E94429DDB4E260133C /* YooKassaPaymentsError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YooKassaPaymentsError.swift; sourceTree = ""; }; 402EB1EEE3DFE557080259B2 /* InternalStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InternalStyle.swift; sourceTree = ""; }; - 402EB1F6B2E1B5879D2BCCC8 /* Hosts.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = Hosts.plist; sourceTree = ""; }; 402EB1FE652CC66389E53BE1 /* BankCardDataInputRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BankCardDataInputRouter.swift; sourceTree = ""; }; 402EB21194794FB1D5FFEFF9 /* UIFont+Style.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIFont+Style.swift"; sourceTree = ""; }; 402EB2267FCDA994A8449002 /* AuthTypeStatesProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthTypeStatesProvider.swift; sourceTree = ""; }; @@ -517,7 +545,7 @@ 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 = ""; }; - 402EB5091FBD0903A0369764 /* AnalyticsServiceImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyticsServiceImpl.swift; sourceTree = ""; }; + 402EB5091FBD0903A0369764 /* CommonTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommonTracker.swift; sourceTree = ""; }; 402EB50F92BB073BCF76DB45 /* ApplePayAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplePayAssembly.swift; sourceTree = ""; }; 402EB5183B4C5075C6903F81 /* UIView+Style.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Style.swift"; sourceTree = ""; }; 402EB51EED8568C831E1F9B6 /* UnderlinedTextField+InputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UnderlinedTextField+InputView.swift"; sourceTree = ""; }; @@ -591,7 +619,6 @@ 402EBA3026D26E32EFFB6024 /* UILayoutPriority+Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UILayoutPriority+Constants.swift"; sourceTree = ""; }; 402EBA38EC646E32C0BA9D59 /* Button.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; 402EBA3F8618D17FB47AC873 /* BankSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BankSettings.swift; sourceTree = ""; }; - 402EBA44464C238E00928819 /* AnalyticsProviderAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyticsProviderAssembly.swift; sourceTree = ""; }; 402EBA4C0B33E4DDCE4D1EB9 /* CustomizationSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizationSettings.swift; sourceTree = ""; }; 402EBA518CC45BC28F50C847 /* BankCardRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BankCardRouter.swift; sourceTree = ""; }; 402EBA599B2479B086E7EC2A /* PanInputPresenterStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PanInputPresenterStyle.swift; sourceTree = ""; }; @@ -624,7 +651,6 @@ 402EBBB86E3224388A362ECF /* CardSecInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardSecInteractor.swift; sourceTree = ""; }; 402EBBCB42F9BB4505322D12 /* UIButton+Style.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIButton+Style.swift"; sourceTree = ""; }; 402EBBD082F2801D1ED650A4 /* TempAmount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TempAmount.swift; sourceTree = ""; }; - 402EBBE3BE1580D80C891092 /* AnalyticsProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyticsProvider.swift; sourceTree = ""; }; 402EBBEF9B96FD16F8D2F809 /* PaymentMethodResources.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentMethodResources.swift; sourceTree = ""; }; 402EBBF290DDF9A3AE12231E /* PaymentMethodsPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentMethodsPresenter.swift; sourceTree = ""; }; 402EBC0AAB711E47B4FDF1E4 /* BankCardInteractorIO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BankCardInteractorIO.swift; sourceTree = ""; }; @@ -632,7 +658,6 @@ 402EBC149878F940133678B1 /* SberbankAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SberbankAssembly.swift; sourceTree = ""; }; 402EBC22917722A03346114F /* BankCardDataInputViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BankCardDataInputViewModel.swift; sourceTree = ""; }; 402EBC4B72D66A2A1BDFB78B /* BankCardDataInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BankCardDataInputView.swift; sourceTree = ""; }; - 402EBC4E71E76A1046320E32 /* AnalyticsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = ""; }; 402EBC5278F1B7CBEE0256A1 /* CscInputPresenterStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CscInputPresenterStyle.swift; sourceTree = ""; }; 402EBC65C7C9A4599E39EEC2 /* KeyboardNotificationInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardNotificationInfo.swift; sourceTree = ""; }; 402EBC6E1AC8DE3F0701FE15 /* HostsConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HostsConfig.swift; sourceTree = ""; }; @@ -652,8 +677,6 @@ 402EBD3F5BF15C3FEC34A8BE /* PhoneNumberStyleWithAutoCorrection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhoneNumberStyleWithAutoCorrection.swift; sourceTree = ""; }; 402EBD5D02739AA84ECC7981 /* SberbankRouterIO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SberbankRouterIO.swift; sourceTree = ""; }; 402EBD6AC87739A790C03D95 /* ActionTextDialog+Style.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ActionTextDialog+Style.swift"; sourceTree = ""; }; - 402EBD98D680460920FAFF25 /* AnalyticsTrack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyticsTrack.swift; sourceTree = ""; }; - 402EBDA93CCB741604CDF4AF /* TermsOfService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TermsOfService.swift; sourceTree = ""; }; 402EBDAAC92273068B52009E /* UINavigationBar+Style.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Style.swift"; sourceTree = ""; }; 402EBDB3EFE7969B76F4C0A4 /* LogoutConfirmationAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogoutConfirmationAssembly.swift; sourceTree = ""; }; 402EBDB815101786067EB6B2 /* UIColor+Style.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Style.swift"; sourceTree = ""; }; @@ -666,7 +689,6 @@ 402EBE5FF1DF14A2198A90F5 /* WebBrowserViewIO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebBrowserViewIO.swift; sourceTree = ""; }; 402EBE6D7C56606D0812B6B1 /* BankCardDataInputViewIO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BankCardDataInputViewIO.swift; sourceTree = ""; }; 402EBE9D384C19774E01A03F /* LargeIconButtonItemViewIO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LargeIconButtonItemViewIO.swift; sourceTree = ""; }; - 402EBEC76B2AB7515C0B5B91 /* AnalyticsProviderImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyticsProviderImpl.swift; sourceTree = ""; }; 402EBED6C43E33F60A00D47A /* KeychainStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = ""; }; 402EBEF2D9C85D1F6D213635 /* YooKassaPaymentsExamplePods.debug.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = YooKassaPaymentsExamplePods.debug.entitlements; sourceTree = ""; }; 402EBEF70767863315609E30 /* UIScreen+SafeAreaInsets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIScreen+SafeAreaInsets.swift"; sourceTree = ""; }; @@ -674,7 +696,7 @@ 402EBF0B61189D45DE05B5DD /* Localization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Localization.swift; sourceTree = ""; }; 402EBF30547C0D27015F777A /* Settings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; 402EBF3A00DEE5F5D61B5F98 /* PhoneNumberInputViewIO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhoneNumberInputViewIO.swift; sourceTree = ""; }; - 402EBF4B20C030067340B10D /* AnalyticsServiceAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyticsServiceAssembly.swift; sourceTree = ""; }; + 402EBF4B20C030067340B10D /* AnalyticsTrackingAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyticsTrackingAssembly.swift; sourceTree = ""; }; 402EBF6A37743D45F45F9F0A /* IdentifyCountryService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentifyCountryService.swift; sourceTree = ""; }; 402EBF7AC88FCC8EA3154C56 /* UserAgentFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserAgentFactory.swift; sourceTree = ""; }; 402EBFB7B00CE2B4D01875E7 /* BankSettingsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BankSettingsService.swift; sourceTree = ""; }; @@ -861,6 +883,20 @@ ); sourceTree = ""; }; + 2519BAC726F35B9F0013DEFD /* Config */ = { + isa = PBXGroup; + children = ( + 2519BAC826F35BBA0013DEFD /* Config.swift */, + 2519BAD126FAF7630013DEFD /* ConfigMediator.swift */, + 255F2A9827032ED700BF4354 /* ConfigMediatorImpl.swift */, + 255F2A942701D73200BF4354 /* ConfigMediatorAssembly.swift */, + 2519BAD326FAF78A0013DEFD /* ConfigService.swift */, + 255F2A9A27032EE500BF4354 /* ConfigServiceImpl.swift */, + 255F2A962701D99F00BF4354 /* ConfigServiceAssembly.swift */, + ); + path = Config; + sourceTree = ""; + }; 25347BD426BADBE800FDD1DA /* CardSettings */ = { isa = PBXGroup; children = ( @@ -1133,6 +1169,8 @@ 402EBFC3A3FDF23F3A7607E3 /* UIViewController+TransitionHandler.swift */, 402EB47AB6115E5DB414156B /* UITableViewHeaderFooterView+Identifier.swift */, 402EB9AEA4AE184596686453 /* UITableViewCell+Identifier.swift */, + 25911A9C270B512C0033F0B1 /* DataLoader.swift */, + 25967719275A67FE0043D10A /* HTMLUtils.swift */, ); path = Helpers; sourceTree = ""; @@ -1140,7 +1178,7 @@ 402EB1C8E83F34A1D7D30CE0 /* Configurations */ = { isa = PBXGroup; children = ( - 402EB1F6B2E1B5879D2BCCC8 /* Hosts.plist */, + 25CA4F8E27635D70006C257E /* Hosts.plist */, 402EB43CDEA9F5F132A2CF9A /* banks.json */, ); path = Configurations; @@ -1285,6 +1323,7 @@ 402EB8437EC5A9EC41268B10 /* Sberbank */, 78E1BA6325EEA99000E5F275 /* Sberpay */, 7811066625C2CD4D004DB71D /* YooMoney */, + 25911AB5270EB8A30033F0B1 /* LoadingViewController.swift */, ); path = Modules; sourceTree = ""; @@ -1310,6 +1349,8 @@ 402EB412B667877AE545EBD1 /* Resources */ = { isa = PBXGroup; children = ( + 255F2A9226FC5C2500BF4354 /* defaultConfig_en.json */, + 2519BACB26F878520013DEFD /* defaultConfig_ru.json */, 402EBC9ABFBF24C573DC88A4 /* Media.xcassets */, 402EB1C8E83F34A1D7D30CE0 /* Configurations */, 402EB8D687A8CDBF62ECC5E9 /* Entitlements */, @@ -1643,6 +1684,7 @@ isa = PBXGroup; children = ( 402EB7CFD97FD6FA875CB83F /* PaymentProcessingError.swift */, + 2548049F272956280067777C /* TooManyRequestsError.swift */, 402EB9D50FF6F6597287AA66 /* PaymentService.swift */, 402EBB5972E9BA97E5350DCC /* PaymentServiceAssembly.swift */, 789C374025AF285C00BA94D1 /* PaymentServiceImpl.swift */, @@ -1759,7 +1801,6 @@ 402EBBEF9B96FD16F8D2F809 /* PaymentMethodResources.swift */, 402EBF30547C0D27015F777A /* Settings.swift */, 402EBBD082F2801D1ED650A4 /* TempAmount.swift */, - 402EBDA93CCB741604CDF4AF /* TermsOfService.swift */, 784A1AC025B9C57A00637CB5 /* Payment */, 784A1AC925B9C57B00637CB5 /* WalletLogin */, ); @@ -1973,11 +2014,11 @@ 402EBF2ECE99022B226CDBE6 /* Analytics */ = { isa = PBXGroup; children = ( + 252ED9492773B9F700547007 /* AnalyticsTracking.swift */, 402EB0D033CC079B2E741512 /* AnalyticsEvent.swift */, - 402EBC4E71E76A1046320E32 /* AnalyticsService.swift */, - 402EBF4B20C030067340B10D /* AnalyticsServiceAssembly.swift */, - 402EB5091FBD0903A0369764 /* AnalyticsServiceImpl.swift */, - 402EBD98D680460920FAFF25 /* AnalyticsTrack.swift */, + 2548049D2727F53F0067777C /* AnalyticsEventContext.swift */, + 402EBF4B20C030067340B10D /* AnalyticsTrackingAssembly.swift */, + 402EB5091FBD0903A0369764 /* CommonTracker.swift */, ); path = Analytics; sourceTree = ""; @@ -1993,11 +2034,12 @@ 402EBF67858F805C0168A633 /* Services */ = { isa = PBXGroup; children = ( + 255F2A9026FB5A9C00BF4354 /* Logger.swift */, 402EB40E45FD50302AF2C546 /* ApiLogger.swift */, 402EB0E1A17D557739552F70 /* CardService.swift */, 402EB6287F7DD3A3CC35816B /* HostProvider.swift */, + 2519BAC726F35B9F0013DEFD /* Config */, 402EBF2ECE99022B226CDBE6 /* Analytics */, - 784A1A9725B9779500637CB5 /* AnalyticsProvider */, 784A1A8925B5A42900637CB5 /* ApplePay */, 402EBEFAE69A8355E0DE0E81 /* Authorization */, 402EBBE1ED7002E267CEBE6A /* AuthTypeStates */, @@ -2243,6 +2285,7 @@ 402EBED6C43E33F60A00D47A /* KeychainStorage.swift */, 402EBC8C5F2E04C722215410 /* KeychainStorageMock.swift */, 402EB0D93DE0D413280F555E /* UserDefaultsStorage.swift */, + 25911A9A27073B930033F0B1 /* PlistStorage.swift */, ); path = Storage; sourceTree = ""; @@ -2267,16 +2310,6 @@ path = WebLogger; sourceTree = ""; }; - 784A1A9725B9779500637CB5 /* AnalyticsProvider */ = { - isa = PBXGroup; - children = ( - 402EBBE3BE1580D80C891092 /* AnalyticsProvider.swift */, - 402EBA44464C238E00928819 /* AnalyticsProviderAssembly.swift */, - 402EBEC76B2AB7515C0B5B91 /* AnalyticsProviderImpl.swift */, - ); - path = AnalyticsProvider; - sourceTree = ""; - }; 784A1AC025B9C57A00637CB5 /* Payment */ = { isa = PBXGroup; children = ( @@ -2684,8 +2717,10 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 25CA4F8F27635D71006C257E /* Hosts.plist in Resources */, 252982B026AE928C00174692 /* Cartfile in Resources */, - 402EB62CDBDD5CBC28E68446 /* Hosts.plist in Resources */, + 255F2A9326FC5C3500BF4354 /* defaultConfig_en.json in Resources */, + 2519BACC26F878520013DEFD /* defaultConfig_ru.json in Resources */, 402EBE58A36A98F06C98DD57 /* banks.json in Resources */, 3093D831268475980048C8D9 /* LocalizedResources.strings in Resources */, 402EB20489B718B1F3042183 /* Media.xcassets in Resources */, @@ -2700,6 +2735,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 254804A0272956280067777C /* TooManyRequestsError.swift in Sources */, 7811067A25C2D561004DB71D /* YooMoneyViewIO.swift in Sources */, 7811067F25C419D2004DB71D /* PaymentMethodsRouter.swift in Sources */, 781CDD7D25D408F600C34912 /* ApplePayContractInteractor.swift in Sources */, @@ -2726,6 +2762,7 @@ 402EBE713055C909AA9126E6 /* PhoneNumberInputInteractorIO.swift in Sources */, 402EB91BEC54829B7D570406 /* PhoneNumberInputModuleOutput.swift in Sources */, 402EB5EFF251B3E8A8C0C35B /* PriceView.swift in Sources */, + 255F2A9927032ED700BF4354 /* ConfigMediatorImpl.swift in Sources */, 7860291D25CC56DC00B9E961 /* LargeIconButtonItemView+Style.swift in Sources */, 25347BEB26BADC2E00FDD1DA /* LargeActionInformer.swift in Sources */, 7860293425D1231F00B9E961 /* ActionTitleTextDialog+Style.swift in Sources */, @@ -2796,6 +2833,7 @@ 402EB760A3EDD767C76A1AE9 /* UILabel+Stylable.swift in Sources */, 784A1AD225B9C57B00637CB5 /* Service.swift in Sources */, 7860290825C982E600B9E961 /* PaymentAuthorizationViewController.swift in Sources */, + 2519BAD226FAF7630013DEFD /* ConfigMediator.swift in Sources */, 78997A2425D562160093CAE2 /* LargeIconView.swift in Sources */, 7811067425C2D212004DB71D /* YooMoneyInteractorIO.swift in Sources */, 25347BE726BADBE800FDD1DA /* CardSettingsModuleIO.swift in Sources */, @@ -2840,10 +2878,12 @@ 402EB06BF631E107D84C1D80 /* CustomizationSettings.swift in Sources */, 402EBFC348B3BCF7D33FCAB5 /* CustomizationColors.swift in Sources */, 7860292A25CD406500B9E961 /* ImageDownloadServiceImpl.swift in Sources */, + 2519BAC926F35BBA0013DEFD /* Config.swift in Sources */, 7811066E25C2CD91004DB71D /* YooMoneyInteractor.swift in Sources */, 781FB66E260235AD00567AFA /* BankCardImageFactory.swift in Sources */, 7860294525D1673800B9E961 /* LinkedCardInteractor.swift in Sources */, 402EB9C4BBC1F6EF4241A9C9 /* CardData.swift in Sources */, + 255F2A9B27032EE500BF4354 /* ConfigServiceImpl.swift in Sources */, 7811066C25C2CD7B004DB71D /* YooMoneyAssembly.swift in Sources */, 402EBDEA03C1BFD97AE64116 /* TempAmount.swift in Sources */, 7860293925D1670400B9E961 /* LinkedCardModuleIO.swift in Sources */, @@ -2851,7 +2891,6 @@ 402EB9CF3E1CEA91F61C39DE /* PaymentMethodResources.swift in Sources */, 402EB9E7626589A984FC9F72 /* Settings.swift in Sources */, 402EBB95FB1B114F31BD9725 /* HostsConfig.swift in Sources */, - 402EB92B510A1A0BFB6EB464 /* TermsOfService.swift in Sources */, 402EB2E1934B6B55DE54F193 /* BankSettings.swift in Sources */, 30EDA3CA26398E6D00FAD14D /* AmountNumberFormatter.swift in Sources */, 402EBB85276075DF17470C04 /* HostProviderAssembly.swift in Sources */, @@ -2901,6 +2940,7 @@ 7860293725D166FC00B9E961 /* LinkedCardInteractorIO.swift in Sources */, 402EBD28BA87CC3549C6FE49 /* ApplePayContractPresenter.swift in Sources */, 308E4180238581CB00B44490 /* SavePaymentMethodInfoPresenter.swift in Sources */, + 255F2A972701D99F00BF4354 /* ConfigServiceAssembly.swift in Sources */, 789C373D25AF260400BA94D1 /* PaymentMethodHandlerServiceImpl.swift in Sources */, 784A1A8B25B5A44600637CB5 /* ApplePayService.swift in Sources */, 402EBC41089B09F83AC6D4CA /* ApplePayContractModuleIO.swift in Sources */, @@ -2933,10 +2973,12 @@ 402EBE309FF80B279847B66D /* BankCardRepeatPresenter.swift in Sources */, 402EB8D43DDD23DC7A645AA1 /* BankCardRepeatInteractor.swift in Sources */, 402EB1F0EAA3B2E1422FD95D /* BankCardRepeatInteractorIO.swift in Sources */, + 252ED94A2773B9F800547007 /* AnalyticsTracking.swift in Sources */, 3089EF4D23846F7400CB7319 /* SavePaymentMethodViewModel.swift in Sources */, 781FB671260235EF00567AFA /* BankCardImageFactoryAssembly.swift in Sources */, 402EB38B0B0D9EA3988F9922 /* PaymentService.swift in Sources */, 402EBC0B94C8D1B32962BBA8 /* PaymentProcessingError.swift in Sources */, + 255F2A9126FB5A9C00BF4354 /* Logger.swift in Sources */, 402EBD95FD7CAFB6AD2F29FC /* PaymentServiceMock.swift in Sources */, 402EBB74CB98BDB6DE11B17C /* PaymentServiceAssembly.swift in Sources */, 402EB8203E2273B472E5819C /* HostProvider.swift in Sources */, @@ -2944,6 +2986,7 @@ 402EB96B6F077D9807AD790B /* PaymentMethodHandlerService.swift in Sources */, 78C1BD2525EFA13C0058080F /* SberpayPresenter.swift in Sources */, 7811067C25C3FCB8004DB71D /* OrderView.swift in Sources */, + 25911A9D270B512C0033F0B1 /* DataLoader.swift in Sources */, 402EB8D9C4C6674F176C1E2C /* AuthorizationService.swift in Sources */, 402EBCB6D5E6441798A2DC59 /* AuthorizationServiceImpl.swift in Sources */, 402EBC79F23AF96FD348A6DE /* AuthorizationServiceAssembly.swift in Sources */, @@ -2965,18 +3008,14 @@ 784A1ADD25B9C57B00637CB5 /* AuthTypeState.swift in Sources */, 780D0D3725DC0DE800CF15D1 /* BankCardRepeatViewIO.swift in Sources */, 402EBF134B3899D2D4F57F13 /* DeviceInfoServiceAssembly.swift in Sources */, - 402EBACF1C5CFC5C064DFA36 /* AnalyticsServiceImpl.swift in Sources */, - 402EB9E3F15C7DAD7D254D24 /* AnalyticsProviderImpl.swift in Sources */, + 402EBACF1C5CFC5C064DFA36 /* CommonTracker.swift in Sources */, 7811067825C2D223004DB71D /* YooMoneyRouterIO.swift in Sources */, 402EB0D571A0BE68455B56D8 /* AnalyticsEvent.swift in Sources */, - 402EBB302AB46E50088A5B40 /* AnalyticsServiceAssembly.swift in Sources */, - 402EB52500117CC0F9F7337E /* AnalyticsService.swift in Sources */, + 402EBB302AB46E50088A5B40 /* AnalyticsTrackingAssembly.swift in Sources */, 25347BED26BADC6800FDD1DA /* ActionTemplate.swift in Sources */, - 402EB5609877054CD489DF14 /* AnalyticsTrack.swift in Sources */, 784A1AD925B9C57B00637CB5 /* PaymentUsageLimit.swift in Sources */, - 402EBB04E607A7D25433D1A8 /* AnalyticsProvider.swift in Sources */, - 402EB4A38D9458D726EF2973 /* AnalyticsProviderAssembly.swift in Sources */, 402EB7B158726C9D36615F9A /* WebLoggerServiceImpl.swift in Sources */, + 2519BAD426FAF78A0013DEFD /* ConfigService.swift in Sources */, 402EB10E2B1F8A4E8BD2DEB8 /* BankSettingsService.swift in Sources */, 402EB324EC0352C1CF0A8A59 /* BankSettingsServiceImpl.swift in Sources */, 7860292C25CD406500B9E961 /* ImageCacheImpl.swift in Sources */, @@ -2994,10 +3033,13 @@ 402EB0FBD2A8AFE719A2A013 /* TokenizationSettings.swift in Sources */, 78C1BDF725F2305F0058080F /* Deeplink.swift in Sources */, 308E417A2385501E00B44490 /* SavePaymentMethodInfoViewController.swift in Sources */, + 2548049E2727F53F0067777C /* AnalyticsEventContext.swift in Sources */, 78CF11E025D438AC00F7154E /* ApplePayContractViewModel.swift in Sources */, 784A1AEA25B9CFA700637CB5 /* PaymentMethodBankCard.swift in Sources */, + 2596771A275A67FE0043D10A /* HTMLUtils.swift in Sources */, 402EBB5BA886912050991DEC /* PaymentMethodTypes.swift in Sources */, 7860294F25D1726800B9E961 /* LinkedCardRouterIO.swift in Sources */, + 25911AB6270EB8A30033F0B1 /* LoadingViewController.swift in Sources */, 402EB1162640933D8CD20B33 /* Currency.swift in Sources */, 402EB32E6A6837771A082A37 /* BankCardRepeatModuleInputData.swift in Sources */, 7860294725D1673E00B9E961 /* LinkedCardPresenter.swift in Sources */, @@ -3041,6 +3083,7 @@ 402EBF9EAC29349F6CD640C2 /* BankCardInteractorIO.swift in Sources */, 402EBCE0D0434D717711E283 /* BankCardViewIO.swift in Sources */, 402EBAAF81518608501DBE6D /* BankCardAssembly.swift in Sources */, + 25911A9B27073B930033F0B1 /* PlistStorage.swift in Sources */, 402EBA44D91DC3FCB1ADE11E /* BankCardModuleIO.swift in Sources */, 402EB2528EDA388F36ABD38A /* BankCardRouterIO.swift in Sources */, 402EB0A73D181CBCD8CD6466 /* BankCardViewController.swift in Sources */, @@ -3051,6 +3094,7 @@ 402EB571173B519C0F71C65A /* InputCvcView.swift in Sources */, 402EB420CBCEF91141DDBD23 /* InputPanCardView.swift in Sources */, 78C1BD2125EFA1280058080F /* SberpayViewController.swift in Sources */, + 255F2A952701D73200BF4354 /* ConfigMediatorAssembly.swift in Sources */, 402EBD521E90D0EE32F6611D /* InputExpiryDateView.swift in Sources */, 402EBAAC323B2686A6C43781 /* BankCardDataInputView.swift in Sources */, 402EBC52D30F0A1A37263A97 /* BankCardDataInputAssembly.swift in Sources */, diff --git a/YooKassaPayments.xcodeproj/xcshareddata/xcschemes/YooKassaPayments.xcscheme b/YooKassaPayments.xcodeproj/xcshareddata/xcschemes/YooKassaPayments.xcscheme index aa3c1634..e8ceedee 100644 --- a/YooKassaPayments.xcodeproj/xcshareddata/xcschemes/YooKassaPayments.xcscheme +++ b/YooKassaPayments.xcodeproj/xcshareddata/xcschemes/YooKassaPayments.xcscheme @@ -1,6 +1,6 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 6.4.0 + 6.6.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass diff --git a/YooKassaPayments/Private/Atomic Design/Controls/FixedLengthCodeControl/FixedLengthCodeControl.swift b/YooKassaPayments/Private/Atomic Design/Controls/FixedLengthCodeControl/FixedLengthCodeControl.swift index a41ca982..dfa4320c 100644 --- a/YooKassaPayments/Private/Atomic Design/Controls/FixedLengthCodeControl/FixedLengthCodeControl.swift +++ b/YooKassaPayments/Private/Atomic Design/Controls/FixedLengthCodeControl/FixedLengthCodeControl.swift @@ -26,7 +26,7 @@ final class FixedLengthCodeControl: UIView { return view }() - private var singleCharacterViews: [SingleCharacterView] = [] + private(set) var singleCharacterViews: [SingleCharacterView] = [] // MARK: - Initializers @@ -52,8 +52,7 @@ final class FixedLengthCodeControl: UIView { } override func becomeFirstResponder() -> Bool { - guard let newFirstResponder = singleCharacterViews.first(where: { $0.character == nil }) else { return false } - return newFirstResponder.becomeFirstResponder() + return singleCharacterViews.first?.becomeFirstResponder() ?? false } } diff --git a/YooKassaPayments/Private/Atomic Design/Molecules/ItemViews/IconButtonItemView/IconButtonItemTableViewCell.swift b/YooKassaPayments/Private/Atomic Design/Molecules/ItemViews/IconButtonItemView/IconButtonItemTableViewCell.swift index 47a5d093..1cd499c6 100644 --- a/YooKassaPayments/Private/Atomic Design/Molecules/ItemViews/IconButtonItemView/IconButtonItemTableViewCell.swift +++ b/YooKassaPayments/Private/Atomic Design/Molecules/ItemViews/IconButtonItemView/IconButtonItemTableViewCell.swift @@ -35,6 +35,7 @@ class IconButtonItemTableViewCell: UITableViewCell { private lazy var itemView: IconButtonItemView = { $0.translatesAutoresizingMaskIntoConstraints = false $0.output = self + $0.imageView.contentMode = .scaleAspectFit return $0 }(IconButtonItemView()) diff --git a/YooKassaPayments/Private/Atomic Design/Molecules/ItemViews/IconButtonItemView/IconButtonItemView.swift b/YooKassaPayments/Private/Atomic Design/Molecules/ItemViews/IconButtonItemView/IconButtonItemView.swift index 422aea71..e273cb52 100644 --- a/YooKassaPayments/Private/Atomic Design/Molecules/ItemViews/IconButtonItemView/IconButtonItemView.swift +++ b/YooKassaPayments/Private/Atomic Design/Molecules/ItemViews/IconButtonItemView/IconButtonItemView.swift @@ -210,7 +210,8 @@ class IconButtonItemView: UIView { } activeConstraints += [ - imageView.width.constraint(equalTo: imageView.height), + imageView.heightAnchor.constraint(equalToConstant: Space.fivefold), + imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor), ] activeConstraints += [ diff --git a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Assembly/BankCardDataInputAssembly.swift b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Assembly/BankCardDataInputAssembly.swift index 08871d92..31d2f676 100644 --- a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Assembly/BankCardDataInputAssembly.swift +++ b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Assembly/BankCardDataInputAssembly.swift @@ -12,7 +12,7 @@ enum BankCardDataInputAssembly { let view = makeView(inputData: inputData) let presenter = makePresenter(inputData: inputData) let interactor = makeInteractor(inputData: inputData) - let router = makeRouter(inputData: inputData) + let router = BankCardDataInputRouter(cardScanner: inputData.cardScanner) view.output = presenter @@ -62,9 +62,7 @@ enum BankCardDataInputAssembly { ) -> BankCardDataInputInteractor { let cardService = CardService() let bankSettingsService = BankSettingsServiceAssembly.makeService() - let analyticsService = AnalyticsServiceAssembly.makeService( - isLoggingEnabled: inputData.isLoggingEnabled - ) + let analyticsService = AnalyticsTrackingAssembly.make(isLoggingEnabled: inputData.isLoggingEnabled) let interactor = BankCardDataInputInteractor( cardService: cardService, bankSettingsService: bankSettingsService, @@ -72,13 +70,4 @@ enum BankCardDataInputAssembly { ) return interactor } - - private static func makeRouter( - inputData: BankCardDataInputModuleInputData - ) -> BankCardDataInputRouter { - let router = BankCardDataInputRouter( - cardScanner: inputData.cardScanner - ) - return router - } } diff --git a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/BankCardDataInputInteractorIO.swift b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/BankCardDataInputInteractorIO.swift index 7e2c4c08..982f473c 100644 --- a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/BankCardDataInputInteractorIO.swift +++ b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/BankCardDataInputInteractorIO.swift @@ -1,25 +1,12 @@ -protocol BankCardDataInputInteractorInput: AnalyticsTrack { - func validate( - cardData: CardData, - shouldMoveFocus: Bool - ) - func fetchBankCardSettings( - _ cardMask: String - ) +protocol BankCardDataInputInteractorInput { + func validate(cardData: CardData, shouldMoveFocus: Bool) + func fetchBankCardSettings(_ cardMask: String) + func track(event: AnalyticsEvent) } protocol BankCardDataInputInteractorOutput: AnyObject { - func didSuccessValidateCardData( - _ cardData: CardData - ) - func didFailValidateCardData( - errors: [CardService.ValidationError], - shouldMoveFocus: Bool - ) - func didFetchBankSettings( - _ bankSettings: BankSettings - ) - func didFailFetchBankSettings( - _ cardMask: String - ) + func didSuccessValidateCardData(_ cardData: CardData) + func didFailValidateCardData(errors: [CardService.ValidationError], shouldMoveFocus: Bool) + func didFetchBankSettings(_ bankSettings: BankSettings) + func didFailFetchBankSettings(_ cardMask: String) } diff --git a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Interactor/BankCardDataInputInteractor.swift b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Interactor/BankCardDataInputInteractor.swift index 0b3f092e..ccc9b3ea 100644 --- a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Interactor/BankCardDataInputInteractor.swift +++ b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Interactor/BankCardDataInputInteractor.swift @@ -8,12 +8,12 @@ final class BankCardDataInputInteractor { private let cardService: CardService private let bankSettingsService: BankSettingsService - private let analyticsService: AnalyticsService + private let analyticsService: AnalyticsTracking init( cardService: CardService, bankSettingsService: BankSettingsService, - analyticsService: AnalyticsService + analyticsService: AnalyticsTracking ) { self.cardService = cardService self.bankSettingsService = bankSettingsService @@ -52,9 +52,7 @@ extension BankCardDataInputInteractor: BankCardDataInputInteractorInput { output?.didFetchBankSettings(bankSettings) } - func trackEvent( - _ event: AnalyticsEvent - ) { - analyticsService.trackEvent(event) + func track(event: AnalyticsEvent) { + analyticsService.track(event: event) } } diff --git a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Presenter/BankCardDataInputPresenter.swift b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Presenter/BankCardDataInputPresenter.swift index 0c924ad2..524c1a34 100644 --- a/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Presenter/BankCardDataInputPresenter.swift +++ b/YooKassaPayments/Private/Atomic Design/Views/BankCardDataInput/Presenter/BankCardDataInputPresenter.swift @@ -395,73 +395,40 @@ private extension BankCardDataInputPresenter { private extension BankCardDataInputPresenter { func trackScanBankCardAction() { - let event: AnalyticsEvent = .actionBankCardForm( - action: .scanBankCardAction, - sdkVersion: Bundle.frameworkVersion - ) - trackEvent(event) + track(event: .actionBankCardForm(action: .scanBankCardAction)) } func trackCardNumberInputError() { - let event: AnalyticsEvent = .actionBankCardForm( - action: .cardNumberInputError, - sdkVersion: Bundle.frameworkVersion - ) - trackEvent(event) + track(event: .actionBankCardForm(action: .cardNumberInputError)) } func trackCardExpiryInputError() { - let event: AnalyticsEvent = .actionBankCardForm( - action: .cardExpiryInputError, - sdkVersion: Bundle.frameworkVersion - ) - trackEvent(event) + track(event: .actionBankCardForm(action: .cardExpiryInputError)) } func trackCardCvcInputError() { - let event: AnalyticsEvent = .actionBankCardForm( - action: .cardCvcInputError, - sdkVersion: Bundle.frameworkVersion - ) - trackEvent(event) + track(event: .actionBankCardForm(action: .cardCvcInputError)) } func trackCardNumberClearAction() { - let event: AnalyticsEvent = .actionBankCardForm( - action: .cardNumberClearAction, - sdkVersion: Bundle.frameworkVersion - ) - trackEvent(event) + track(event: .actionBankCardForm(action: .cardNumberClearAction)) } func trackCardNumberInputSuccess() { - let event: AnalyticsEvent = .actionBankCardForm( - action: .cardNumberInputSuccess, - sdkVersion: Bundle.frameworkVersion - ) - trackEvent(event) + track(event: .actionBankCardForm(action: .cardNumberInputSuccess)) } func trackCardNumberContinueAction() { - let event: AnalyticsEvent = .actionBankCardForm( - action: .cardNumberContinueAction, - sdkVersion: Bundle.frameworkVersion - ) - trackEvent(event) + track(event: .actionBankCardForm(action: .cardNumberContinueAction)) } func trackCardNumberReturnToEdit() { - let event: AnalyticsEvent = .actionBankCardForm( - action: .cardNumberReturnToEdit, - sdkVersion: Bundle.frameworkVersion - ) - trackEvent(event) + track(event: .actionBankCardForm(action: .cardNumberReturnToEdit)) } - func trackEvent(_ event: AnalyticsEvent) { + func track(event: AnalyticsEvent) { DispatchQueue.global().async { [weak self] in - guard let self = self else { return } - self.interactor.trackEvent(event) + self?.interactor.track(event: event) } } } diff --git a/YooKassaPayments/Private/Constants/GlobalConstants.swift b/YooKassaPayments/Private/Constants/GlobalConstants.swift index f67ab565..e101d96c 100644 --- a/YooKassaPayments/Private/Constants/GlobalConstants.swift +++ b/YooKassaPayments/Private/Constants/GlobalConstants.swift @@ -3,5 +3,6 @@ enum GlobalConstants { enum Hosts { static let moneyAuth = "moneyAuth" + static let config = "config" } } diff --git a/YooKassaPayments/Private/Factory/ApiSessionAssembly.swift b/YooKassaPayments/Private/Factory/ApiSessionAssembly.swift index a757ef99..5af1b7bf 100644 --- a/YooKassaPayments/Private/Factory/ApiSessionAssembly.swift +++ b/YooKassaPayments/Private/Factory/ApiSessionAssembly.swift @@ -1,9 +1,7 @@ import YooMoneyCoreApi enum ApiSessionAssembly { - static func makeApiSession( - isLoggingEnabled: Bool - ) -> ApiSession { + static func makeApiSession(isLoggingEnabled: Bool) -> ApiSession { let configuration: URLSessionConfiguration = .default configuration.httpAdditionalHeaders = [ "User-Agent": UserAgentFactory.makeHeaderValue(), @@ -14,18 +12,28 @@ enum ApiSessionAssembly { logger: isLoggingEnabled ? ApiLogger() : nil ) - let isDevHost = KeyValueStoringAssembly.makeSettingsStorage().getBool(for: Settings.Keys.devHost) ?? false + let isDevHost = KeyValueStoringAssembly.makeUserDefaultsStorage().getBool(for: Settings.Keys.devHost) ?? false if isDevHost { - session.taskDidReceiveChallengeWithCompletion = { (session, challenge, completionHandler) in - if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, - let trust = challenge.protectionSpace.serverTrust { - let credential = URLCredential(trust: trust) - completionHandler(.useCredential, credential) - } - } + session.taskDidReceiveChallengeWithCompletion = Self.challengeHandler() } return session } + + typealias ChallengeHandler = ( + URLSession, + URLAuthenticationChallenge, + @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) -> Void + + static func challengeHandler() -> ChallengeHandler { + return { (session, challenge, completion) in + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, + let trust = challenge.protectionSpace.serverTrust { + let credential = URLCredential(trust: trust) + completion(.useCredential, credential) + } + } + } } diff --git a/YooKassaPayments/Private/Factory/HostProviderAssembly.swift b/YooKassaPayments/Private/Factory/HostProviderAssembly.swift index 204f91c6..680d2757 100644 --- a/YooKassaPayments/Private/Factory/HostProviderAssembly.swift +++ b/YooKassaPayments/Private/Factory/HostProviderAssembly.swift @@ -2,6 +2,10 @@ import YooMoneyCoreApi enum HostProviderAssembly { static func makeHostProvider() -> YooMoneyCoreApi.HostProvider { - return HostProvider(settingStorage: KeyValueStoringAssembly.makeSettingsStorage()) + return HostProvider( + settingStorage: KeyValueStoringAssembly.makeUserDefaultsStorage(), + configStorage: KeyValueStoringAssembly.makeSettingsStorage(), + defaultConfig: ConfigMediatorImpl.defaultConfig + ) } } diff --git a/YooKassaPayments/Private/Factory/MoneyAuthAssembly.swift b/YooKassaPayments/Private/Factory/MoneyAuthAssembly.swift index c244b4d7..586b478d 100644 --- a/YooKassaPayments/Private/Factory/MoneyAuthAssembly.swift +++ b/YooKassaPayments/Private/Factory/MoneyAuthAssembly.swift @@ -5,7 +5,7 @@ enum MoneyAuthAssembly { moneyAuthClientId: String, loggingEnabled: Bool ) -> MoneyAuth.Config { - let keyValueStorage = KeyValueStoringAssembly.makeSettingsStorage() + let keyValueStorage = KeyValueStoringAssembly.makeUserDefaultsStorage() let isDevHost = keyValueStorage.getBool(for: Settings.Keys.devHost) ?? false let authenticationChallengeHandler = makeAuthenticationChallengeHandler( isDevHost: isDevHost @@ -35,9 +35,13 @@ enum MoneyAuthAssembly { } static func makeMoneyAuthCustomization() -> MoneyAuth.Customization { + let configStorage = KeyValueStoringAssembly.makeSettingsStorage() + let config: Config = (try? configStorage.readValue(for: StorageKeys.configKey)) + ?? ConfigMediatorImpl.defaultConfig + let customization = MoneyAuth.Customization( restorePasswordEnabled: Constants.restorePasswordEnabled, - userAgreementTitle: Localized.userAgreementTitle, + userAgreementTitle: config.userAgreementUrl, userWithEmailAgreementTitle: Localized.userWithEmailAgreementTitle, emailCheckboxVisible: Constants.emailCheckboxVisible, emailCheckboxTitle: Localized.emailCheckboxTitle, diff --git a/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactory.swift b/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactory.swift index 5dcd0609..ffca85f9 100644 --- a/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactory.swift +++ b/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactory.swift @@ -31,4 +31,6 @@ protocol PaymentMethodViewModelFactory { func replaceBullets(_ pan: String) -> String func makeMaskedPan(_ cardMask: String) -> String + + func yooLogoImage() -> UIImage } diff --git a/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactoryAssembly.swift b/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactoryAssembly.swift index 529c176f..3a796aa2 100644 --- a/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactoryAssembly.swift +++ b/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactoryAssembly.swift @@ -1,7 +1,11 @@ enum PaymentMethodViewModelFactoryAssembly { - static func makeFactory() -> PaymentMethodViewModelFactory { + static func makeFactory(isLoggingEnabled: Bool) -> PaymentMethodViewModelFactory { return PaymentMethodViewModelFactoryImpl( - bankSettingsService: BankServiceSettingsImpl.shared + bankSettingsService: BankServiceSettingsImpl.shared, + configMediator: ConfigMediatorImpl( + service: ConfigServiceAssembly.make(isLoggingEnabled: isLoggingEnabled), + storage: KeyValueStoringAssembly.makeSettingsStorage() + ) ) } } diff --git a/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactoryImpl.swift b/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactoryImpl.swift index c6ed286c..7bd88f55 100644 --- a/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactoryImpl.swift +++ b/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactoryImpl.swift @@ -6,13 +6,16 @@ final class PaymentMethodViewModelFactoryImpl { // MARK: - Init data private let bankSettingsService: BankSettingsService + private let configMediator: ConfigMediator // MARK: - Init init( - bankSettingsService: BankSettingsService + bankSettingsService: BankSettingsService, + configMediator: ConfigMediator ) { self.bankSettingsService = bankSettingsService + self.configMediator = configMediator } // MARK: - Stored properties @@ -33,6 +36,9 @@ final class PaymentMethodViewModelFactoryImpl { // MARK: - PaymentMethodViewModelFactory extension PaymentMethodViewModelFactoryImpl: PaymentMethodViewModelFactory { + func yooLogoImage() -> UIImage { + configMediator.asset(for: .logo) + } // MARK: - Transform ViewModel from PaymentOption @@ -169,7 +175,7 @@ private extension PaymentMethodViewModelFactoryImpl { return PaymentMethodViewModel( id: nil, isShopLinkedCard: false, - image: PaymentMethodResources.Image.yooMoney, + image: makePaymentMethodTypeImage(.yooMoney), title: walletDisplayName ?? paymentOption.accountId, subtitle: makeBalanceText(paymentOption.balance) ) @@ -217,24 +223,29 @@ private extension PaymentMethodViewModelFactoryImpl { // MARK: - Making ViewModel from PaymentMethodType private extension PaymentMethodViewModelFactoryImpl { - func makePaymentMethodTypeTitle( - _ paymentMethodType: YooKassaPaymentsApi.PaymentMethodType - ) -> String { - let name: String + func makePaymentMethodTypeTitle(_ paymentMethodType: YooKassaPaymentsApi.PaymentMethodType) -> String { + let kind: Config.PaymentMethod.Kind switch paymentMethodType { - case .bankCard: - name = PaymentMethodResources.Localized.bankCard - case .yooMoney: - name = PaymentMethodResources.Localized.wallet - case .applePay: - name = PaymentMethodResources.Localized.applePay - case .sberbank: - name = PaymentMethodResources.Localized.sberpay + case .bankCard: kind = .bankCard + case .yooMoney: kind = .yoomoney + case .applePay: kind = .applePay + case .sberbank: kind = .sberbank default: assertionFailure("Unsupported PaymentMethodType") - name = "Unsupported" + return "Unsupported" + } + let defaultTitle: String + switch kind { + case .bankCard: defaultTitle = PaymentMethodResources.Localized.bankCard + case .yoomoney: defaultTitle = PaymentMethodResources.Localized.wallet + case .applePay: defaultTitle = PaymentMethodResources.Localized.applePay + case .sberbank: defaultTitle = PaymentMethodResources.Localized.sberpay + case .unknown: + assertionFailure("Unsupported kind") + defaultTitle = "Unsupported" } - return name + return configMediator.storedConfig().paymentMethods.first { $0.kind == kind }?.title + ?? defaultTitle } } @@ -248,13 +259,13 @@ private extension PaymentMethodViewModelFactoryImpl { let image: UIImage switch paymentMethodType { case .bankCard: - image = PaymentMethodResources.Image.unknown + image = configMediator.asset(for: .bankCard) case .yooMoney: - image = PaymentMethodResources.Image.yooMoney + image = configMediator.asset(for: .yoomoney) case .applePay: - image = PaymentMethodResources.Image.applePay + image = configMediator.asset(for: .applePay) case .sberbank: - image = PaymentMethodResources.Image.sberpay + image = configMediator.asset(for: .sberbank) default: assertionFailure("Unsupported PaymentMethodType") image = UIImage() @@ -283,6 +294,8 @@ private extension PaymentMethodViewModelFactoryImpl { case .solo: image = PaymentMethodResources.Image.solo case .switch: image = PaymentMethodResources.Image.switch case .unknown: image = PaymentMethodResources.Image.unknown + @unknown default: + image = PaymentMethodResources.Image.unknown } return image } diff --git a/YooKassaPayments/Private/Helpers/DataLoader.swift b/YooKassaPayments/Private/Helpers/DataLoader.swift new file mode 100644 index 00000000..a15f4c2b --- /dev/null +++ b/YooKassaPayments/Private/Helpers/DataLoader.swift @@ -0,0 +1,47 @@ +import Foundation + +class DataLoader { + let urls: [URL] + private let loadingGroup = DispatchGroup() + private let session = URLSession.shared + private var loadingResult: [URL: Result] = [:] + private(set) var isLoading = false + + enum LoadingError: Error { + case unknown + } + + init(urls: [URL]) { + self.urls = urls + } + + private var tasks: [URLSessionDataTask] = [] + + func load(completion: @escaping ([URL: Result]) -> Void) { + guard !isLoading else { + PrintLogger.debugWarn("Multiple load calls", info: ["object": String(describing: self)] ) + return + } + + isLoading = true + urls.forEach { targetUrl in + loadingGroup.enter() + let task = session.dataTask(with: targetUrl) { [weak self] data, _, error in + guard let self = self else { return } + self.loadingGroup.leave() + let result: Result = data.map { .success($0) } + ?? error.map { .failure($0) } + ?? .failure(LoadingError.unknown) + self.loadingResult[targetUrl] = result + } + tasks.append(task) + task.resume() + } + + loadingGroup.notify(queue: .global()) { [weak self] in + guard let self = self else { return } + self.tasks = [] + completion(self.loadingResult) + } + } +} diff --git a/YooKassaPayments/Private/Helpers/HTMLUtils.swift b/YooKassaPayments/Private/Helpers/HTMLUtils.swift new file mode 100644 index 00000000..3f686029 --- /dev/null +++ b/YooKassaPayments/Private/Helpers/HTMLUtils.swift @@ -0,0 +1,119 @@ +import UIKit + +enum HTMLUtils { + /// Take html and hightlight any text between `` + /// - tag attributes ignored, link is extracted from href, `` is considered as closing `` + /// - html attributes other than `` are handled by default NSAttributedString behavior + /// - Example: `some` will highlight `some` with .link attribute + static func highlightHyperlinks(html: String) -> NSAttributedString { + do { + // Capture text between into capture group #3 + // Range#0: full match + // Range#1: opening tag , tag attributes ignored + // Range#2: tagged text + // Range#3: closing . missing 'a' tolerance, is treated as close tag + let regex = try NSRegularExpression( + pattern: #"(]{0,}>)([^<]+)(<\/a{0,1} {0,}>)"#, + options: .anchorsMatchLines + ) + + var matches: [(NSRange, [String])] = [] + + regex.enumerateMatches( + in: html, + options: .withTransparentBounds, + range: NSRange(location: 0, length: (html as NSString).length) + ) { result, flags, ptr in + if let result = result { + var captured: [String] = [] + for rangeIndex in 0 ..< result.numberOfRanges { + captured.append((html as NSString).substring(with: result.range(at: rangeIndex))) + } + + matches.append((result.range, captured)) + } + } + + guard !matches.isEmpty else { + return NSAttributedString( + string: htmlToPlain(html), + attributes: [.foregroundColor: UIColor.AdaptiveColors.secondary] + ) + } + var ranges: [NSRange] = [] + let plainStart = htmlToPlain( + (html as NSString).substring(with: NSRange(location: 0, length: matches[0].0.location)) + ) + let resulting = NSMutableAttributedString( + string: plainStart, + attributes: [.foregroundColor: UIColor.AdaptiveColors.secondary] + ) + ranges.append(NSRange(location: 0, length: matches[0].0.location)) + matches.forEach { + guard let last = ranges.last else { return } + let distance = $0.0.location - (last.location + last.length) + if distance > 0 { + let range = NSRange(location: (last.location + last.length) + 1, length: distance - 1) + let plain = htmlToPlain((html as NSString).substring(with: range)) + resulting.append( + NSAttributedString( + string: plain, + attributes: [.foregroundColor: UIColor.AdaptiveColors.secondary] + ) + ) + ranges.append(range) + } + ranges.append($0.0) + let fulllMatch = $0.1[0] + var url: String? + if let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) { + let urlMatches = detector.matches( + in: fulllMatch, + options: [], + range: NSRange(location: 0, length: fulllMatch.utf16.count) + ) + for match in urlMatches { + guard let range = Range(match.range, in: fulllMatch) else { continue } + let found = String(fulllMatch[range]) + url = found + } + } + resulting.append(NSAttributedString(string: $0.1[2], attributes: [.link: url ?? "yookassa://"])) + } + if let last = matches.last { + let location = last.0.location + last.0.length + 1 + let lenght = (html as NSString).length - location + // if there is any text after last , append plain text to resulting + if lenght > 0 { + let range = NSRange(location: location, length: lenght) + let plain = htmlToPlain((html as NSString).substring(with: range)) + resulting.append( + NSAttributedString( + string: plain, + attributes: [.foregroundColor: UIColor.AdaptiveColors.secondary] + ) + ) + } + } + + return resulting + } catch { + PrintLogger.trace("regex failure", info: ["error": error.localizedDescription]) + } + + return NSAttributedString(string: html) + } + + /// Convert
-> \n and other html text formatting to native `String` + static func htmlToPlain(_ html: String) -> String { + guard + let data = html.data(using: .utf16), + let converted = try? NSAttributedString( + data: data, + options: [.documentType: NSAttributedString.DocumentType.html], + documentAttributes: nil + ) + else { return html } + return converted.string + } +} diff --git a/YooKassaPayments/Private/Models/HostsConfig.swift b/YooKassaPayments/Private/Models/HostsConfig.swift index aefa918e..6f1aeff8 100644 --- a/YooKassaPayments/Private/Models/HostsConfig.swift +++ b/YooKassaPayments/Private/Models/HostsConfig.swift @@ -2,4 +2,5 @@ struct HostsConfig { let wallet: String let payments: String let moneyAuth: String + let config: String } diff --git a/YooKassaPayments/Private/Models/Payment/ConfirmationType.swift b/YooKassaPayments/Private/Models/Payment/ConfirmationType.swift index 6cfa9610..443144f8 100644 --- a/YooKassaPayments/Private/Models/Payment/ConfirmationType.swift +++ b/YooKassaPayments/Private/Models/Payment/ConfirmationType.swift @@ -28,6 +28,8 @@ extension ConfirmationType { self = .external case .mobileApplication: self = .mobileApplication + @unknown default: + fatalError("unsupported confirmationType") } } diff --git a/YooKassaPayments/Private/Models/TermsOfService.swift b/YooKassaPayments/Private/Models/TermsOfService.swift deleted file mode 100644 index 2ec50b9f..00000000 --- a/YooKassaPayments/Private/Models/TermsOfService.swift +++ /dev/null @@ -1,42 +0,0 @@ -enum TermsOfServiceFactory { - static func makeTermsOfService() -> TermsOfService { - return TermsOfService( - text: Localized.TermsOfService.text, - hyperlink: Localized.TermsOfService.hyperlink, - url: Constants.termsOfServiceUrl - ) - } -} - -struct TermsOfService { - let text: String - let hyperlink: String - let url: URL -} - -// MARK: - Localized - -private enum Localized { - enum TermsOfService { - static let text = NSLocalizedString( - "TermsOfService.Text", - bundle: Bundle.framework, - value: "Нажимая кнопку, вы принимаете", - comment: "Текст `Нажимая кнопку, вы принимаете` https://yadi.sk/i/Z2oi1Uun7nS-jA" - ) - static let hyperlink = NSLocalizedString( - "TermsOfService.Hyperlink", - bundle: Bundle.framework, - value: "условия сервиса", - comment: "Текст `условия сервиса` https://yadi.sk/i/Z2oi1Uun7nS-jA" - ) - } -} - -// MARK: - Constants - -private enum Constants { - // swiftlint:disable force_unwrapping - static let termsOfServiceUrl = URL(string: "https://yoomoney.ru/page?id=526623")! - // swiftlint:enable force_unwrapping -} diff --git a/YooKassaPayments/Private/Modules/3DS/Assembly/CardSecAssembly.swift b/YooKassaPayments/Private/Modules/3DS/Assembly/CardSecAssembly.swift index 3072800f..f29c300e 100644 --- a/YooKassaPayments/Private/Modules/3DS/Assembly/CardSecAssembly.swift +++ b/YooKassaPayments/Private/Modules/3DS/Assembly/CardSecAssembly.swift @@ -5,13 +5,9 @@ enum CardSecAssembly { inputData: CardSecModuleInputData, moduleOutput: CardSecModuleOutput ) -> UIViewController { - let presenter = CardSecPresenter( - isConfirmation: inputData.isConfirmation - ) + let presenter = CardSecPresenter() - let analyticsService = AnalyticsServiceAssembly.makeService( - isLoggingEnabled: inputData.isLoggingEnabled - ) + let analyticsService = AnalyticsTrackingAssembly.make(isLoggingEnabled: inputData.isLoggingEnabled) let logger = WebLoggerServiceAssembly.makeService( isLoggingEnabled: inputData.isLoggingEnabled ) diff --git a/YooKassaPayments/Private/Modules/3DS/CardSecInteractorIO.swift b/YooKassaPayments/Private/Modules/3DS/CardSecInteractorIO.swift index 27599da3..a1034701 100644 --- a/YooKassaPayments/Private/Modules/3DS/CardSecInteractorIO.swift +++ b/YooKassaPayments/Private/Modules/3DS/CardSecInteractorIO.swift @@ -1,4 +1,6 @@ -protocol CardSecInteractorInput: AnalyticsTrack {} +protocol CardSecInteractorInput { + func track(event: AnalyticsEvent) +} protocol CardSecInteractorOutput: AnyObject { func didSuccessfullyPassedCardSec() diff --git a/YooKassaPayments/Private/Modules/3DS/CardSecModuleIO.swift b/YooKassaPayments/Private/Modules/3DS/CardSecModuleIO.swift index 8bb2e547..c8a63582 100644 --- a/YooKassaPayments/Private/Modules/3DS/CardSecModuleIO.swift +++ b/YooKassaPayments/Private/Modules/3DS/CardSecModuleIO.swift @@ -5,30 +5,24 @@ struct CardSecModuleInputData { let requestUrl: String let redirectUrl: String let isLoggingEnabled: Bool - let isConfirmation: Bool // MARK: - Init init( requestUrl: String, redirectUrl: String, - isLoggingEnabled: Bool, - isConfirmation: Bool + isLoggingEnabled: Bool ) { self.requestUrl = requestUrl self.redirectUrl = redirectUrl self.isLoggingEnabled = isLoggingEnabled - self.isConfirmation = isConfirmation } } protocol CardSecModuleInput: AnyObject {} protocol CardSecModuleOutput: AnyObject { - func didSuccessfullyPassedCardSec( - on module: CardSecModuleInput, - isConfirmation: Bool - ) + func didSuccessfullyPassedCardSec(on module: CardSecModuleInput) func didPressCloseButton(on module: CardSecModuleInput) func viewWillDisappear() } diff --git a/YooKassaPayments/Private/Modules/3DS/Interactor/CardSecInteractor.swift b/YooKassaPayments/Private/Modules/3DS/Interactor/CardSecInteractor.swift index 9bb57b5e..b738901d 100644 --- a/YooKassaPayments/Private/Modules/3DS/Interactor/CardSecInteractor.swift +++ b/YooKassaPayments/Private/Modules/3DS/Interactor/CardSecInteractor.swift @@ -9,7 +9,7 @@ class CardSecInteractor { // MARK: - Init data - private let analyticsService: AnalyticsService + private let analyticsService: AnalyticsTracking private let requestUrl: String private let redirectUrl: String private let logger: WebLoggerService @@ -17,7 +17,7 @@ class CardSecInteractor { // MARK: - Init init( - analyticsService: AnalyticsService, + analyticsService: AnalyticsTracking, requestUrl: String, redirectUrl: String, logger: WebLoggerService @@ -62,7 +62,7 @@ extension CardSecInteractor: WebBrowserInteractorInput { // MARK: - CardSecInteractorInput extension CardSecInteractor: CardSecInteractorInput { - func trackEvent(_ event: AnalyticsEvent) { - analyticsService.trackEvent(event) + func track(event: AnalyticsEvent) { + analyticsService.track(event: event) } } diff --git a/YooKassaPayments/Private/Modules/3DS/Presenter/CardSecPresenter.swift b/YooKassaPayments/Private/Modules/3DS/Presenter/CardSecPresenter.swift index 10c0fc95..c258f9a3 100644 --- a/YooKassaPayments/Private/Modules/3DS/Presenter/CardSecPresenter.swift +++ b/YooKassaPayments/Private/Modules/3DS/Presenter/CardSecPresenter.swift @@ -9,16 +9,6 @@ final class CardSecPresenter: WebBrowserPresenter { private var shouldCallDidSuccessfullyPassedCardSec = true - // MARK: - Init data - - private let isConfirmation: Bool - - // MARK: - Init - - init(isConfirmation: Bool) { - self.isConfirmation = isConfirmation - } - // MARK: - Overridden funcs override func setupView() { @@ -28,6 +18,9 @@ final class CardSecPresenter: WebBrowserPresenter { override func didPressCloseButton() { cardSecModuleOutput?.didPressCloseButton(on: self) + if shouldCallDidSuccessfullyPassedCardSec { + cardSecInteractor.track(event: .screen3dsClose(success: false)) + } } override func viewWillDisappear() { @@ -35,10 +28,7 @@ final class CardSecPresenter: WebBrowserPresenter { } private func trackAnalyticsEvent() { - let event: AnalyticsEvent = .screen3ds( - sdkVersion: Bundle.frameworkVersion - ) - cardSecInteractor.trackEvent(event) + cardSecInteractor.track(event: .screen3ds) } } @@ -48,10 +38,8 @@ extension CardSecPresenter: CardSecInteractorOutput { func didSuccessfullyPassedCardSec() { guard shouldCallDidSuccessfullyPassedCardSec else { return } shouldCallDidSuccessfullyPassedCardSec = false - cardSecModuleOutput?.didSuccessfullyPassedCardSec( - on: self, - isConfirmation: isConfirmation - ) + cardSecInteractor.track(event: .screen3dsClose(success: true)) + cardSecModuleOutput?.didSuccessfullyPassedCardSec(on: self) } } diff --git a/YooKassaPayments/Private/Modules/ApplePayContract/ApplePayContractInteractorIO.swift b/YooKassaPayments/Private/Modules/ApplePayContract/ApplePayContractInteractorIO.swift index e2044f0c..81a60ba1 100644 --- a/YooKassaPayments/Private/Modules/ApplePayContract/ApplePayContractInteractorIO.swift +++ b/YooKassaPayments/Private/Modules/ApplePayContract/ApplePayContractInteractorIO.swift @@ -1,16 +1,10 @@ -protocol ApplePayContractInteractorInput: AnalyticsTrack, AnalyticsProvider { - func tokenize( - paymentData: String, - savePaymentMethod: Bool, - amount: MonetaryAmount - ) +protocol ApplePayContractInteractorInput { + func tokenize(paymentData: String, savePaymentMethod: Bool, amount: MonetaryAmount) + func track(event: AnalyticsEvent) + func analyticsAuthType() -> AnalyticsEvent.AuthType } protocol ApplePayContractInteractorOutput: AnyObject { - func didTokenize( - _ token: Tokens - ) - func failTokenize( - _ error: Error - ) + func didTokenize(_ token: Tokens) + func failTokenize(_ error: Error) } diff --git a/YooKassaPayments/Private/Modules/ApplePayContract/ApplePayContractModuleIO.swift b/YooKassaPayments/Private/Modules/ApplePayContract/ApplePayContractModuleIO.swift index 76e5f548..d49a088a 100644 --- a/YooKassaPayments/Private/Modules/ApplePayContract/ApplePayContractModuleIO.swift +++ b/YooKassaPayments/Private/Modules/ApplePayContract/ApplePayContractModuleIO.swift @@ -11,13 +11,14 @@ struct ApplePayContractModuleInputData { let price: PriceViewModel let fee: PriceViewModel? let paymentOption: PaymentOption - let termsOfService: TermsOfService + let termsOfService: NSAttributedString let merchantIdentifier: String? let savePaymentMethodViewModel: SavePaymentMethodViewModel? let initialSavePaymentMethod: Bool let isBackBarButtonHidden: Bool let customerId: String? let isSafeDeal: Bool + let paymentOptionTitle: String? } protocol ApplePayContractModuleInput: AnyObject {} diff --git a/YooKassaPayments/Private/Modules/ApplePayContract/Assembly/ApplePayContractAssembly.swift b/YooKassaPayments/Private/Modules/ApplePayContract/Assembly/ApplePayContractAssembly.swift index a8534e8a..44fca204 100644 --- a/YooKassaPayments/Private/Modules/ApplePayContract/Assembly/ApplePayContractAssembly.swift +++ b/YooKassaPayments/Private/Modules/ApplePayContract/Assembly/ApplePayContractAssembly.swift @@ -18,25 +18,27 @@ enum ApplePayContractAssembly { savePaymentMethodViewModel: inputData.savePaymentMethodViewModel, initialSavePaymentMethod: inputData.initialSavePaymentMethod, isBackBarButtonHidden: inputData.isBackBarButtonHidden, - isSafeDeal: inputData.isSafeDeal + isSafeDeal: inputData.isSafeDeal, + paymentOptionTitle: inputData.paymentOptionTitle ) - let analyticsService = AnalyticsServiceAssembly.makeService( - isLoggingEnabled: inputData.isLoggingEnabled - ) + let analyticsService = AnalyticsTrackingAssembly.make(isLoggingEnabled: inputData.isLoggingEnabled) let paymentService = PaymentServiceAssembly.makeService( tokenizationSettings: inputData.tokenizationSettings, testModeSettings: inputData.testModeSettings, isLoggingEnabled: inputData.isLoggingEnabled ) - let analyticsProvider = AnalyticsProviderAssembly.makeProvider( - testModeSettings: inputData.testModeSettings - ) let threatMetrixService = ThreatMetrixServiceFactory.makeService() + let authService = AuthorizationServiceAssembly.makeService( + isLoggingEnabled: inputData.isLoggingEnabled, + testModeSettings: inputData.testModeSettings, + moneyAuthClientId: nil + ) + let interactor = ApplePayContractInteractor( paymentService: paymentService, analyticsService: analyticsService, - analyticsProvider: analyticsProvider, + authorizationService: authService, threatMetrixService: threatMetrixService, clientApplicationKey: inputData.clientApplicationKey, customerId: inputData.customerId diff --git a/YooKassaPayments/Private/Modules/ApplePayContract/Interactor/ApplePayContractInteractor.swift b/YooKassaPayments/Private/Modules/ApplePayContract/Interactor/ApplePayContractInteractor.swift index feba372d..e4baf5dc 100644 --- a/YooKassaPayments/Private/Modules/ApplePayContract/Interactor/ApplePayContractInteractor.swift +++ b/YooKassaPayments/Private/Modules/ApplePayContract/Interactor/ApplePayContractInteractor.swift @@ -9,9 +9,9 @@ final class ApplePayContractInteractor { // MARK: - Init data private let paymentService: PaymentService - private let analyticsService: AnalyticsService - private let analyticsProvider: AnalyticsProvider + private let analyticsService: AnalyticsTracking private let threatMetrixService: ThreatMetrixService + private let authorizationService: AuthorizationService private let clientApplicationKey: String private let customerId: String? @@ -20,15 +20,15 @@ final class ApplePayContractInteractor { init( paymentService: PaymentService, - analyticsService: AnalyticsService, - analyticsProvider: AnalyticsProvider, + analyticsService: AnalyticsTracking, + authorizationService: AuthorizationService, threatMetrixService: ThreatMetrixService, clientApplicationKey: String, customerId: String? ) { self.paymentService = paymentService self.analyticsService = analyticsService - self.analyticsProvider = analyticsProvider + self.authorizationService = authorizationService self.threatMetrixService = threatMetrixService self.clientApplicationKey = clientApplicationKey @@ -39,13 +39,12 @@ final class ApplePayContractInteractor { // MARK: - ApplePayContractInteractorInput extension ApplePayContractInteractor: ApplePayContractInteractorInput { - func trackEvent(_ event: AnalyticsEvent) { - analyticsService.trackEvent(event) + func track(event: AnalyticsEvent) { + analyticsService.track(event: event) } - func makeTypeAnalyticsParameters() -> (authType: AnalyticsEvent.AuthType, - tokenType: AnalyticsEvent.AuthTokenType?) { - return analyticsProvider.makeTypeAnalyticsParameters() + func analyticsAuthType() -> AnalyticsEvent.AuthType { + authorizationService.analyticsAuthType() } func tokenize( diff --git a/YooKassaPayments/Private/Modules/ApplePayContract/Presenter/ApplePayContractPresenter.swift b/YooKassaPayments/Private/Modules/ApplePayContract/Presenter/ApplePayContractPresenter.swift index 220efaae..4acde130 100644 --- a/YooKassaPayments/Private/Modules/ApplePayContract/Presenter/ApplePayContractPresenter.swift +++ b/YooKassaPayments/Private/Modules/ApplePayContract/Presenter/ApplePayContractPresenter.swift @@ -24,12 +24,13 @@ final class ApplePayContractPresenter: NSObject { private let price: PriceViewModel private let fee: PriceViewModel? private let paymentOption: PaymentOption - private let termsOfService: TermsOfService + private let termsOfService: NSAttributedString private let merchantIdentifier: String? private let savePaymentMethodViewModel: SavePaymentMethodViewModel? private var initialSavePaymentMethod: Bool private let isBackBarButtonHidden: Bool private let isSafeDeal: Bool + private let paymentOptionTitle: String? // MARK: - Init @@ -39,12 +40,13 @@ final class ApplePayContractPresenter: NSObject { price: PriceViewModel, fee: PriceViewModel?, paymentOption: PaymentOption, - termsOfService: TermsOfService, + termsOfService: NSAttributedString, merchantIdentifier: String?, savePaymentMethodViewModel: SavePaymentMethodViewModel?, initialSavePaymentMethod: Bool, isBackBarButtonHidden: Bool, - isSafeDeal: Bool + isSafeDeal: Bool, + paymentOptionTitle: String? ) { self.shopName = shopName self.purchaseDescription = purchaseDescription @@ -57,6 +59,7 @@ final class ApplePayContractPresenter: NSObject { self.initialSavePaymentMethod = initialSavePaymentMethod self.isBackBarButtonHidden = isBackBarButtonHidden self.isSafeDeal = isSafeDeal + self.paymentOptionTitle = paymentOptionTitle } // MARK: - Stored properties @@ -77,7 +80,8 @@ extension ApplePayContractPresenter: ApplePayContractViewOutput { price: price, fee: fee, terms: termsOfService, - safeDealText: isSafeDeal ? PaymentMethodResources.Localized.safeDealInfoLink : nil + safeDealText: isSafeDeal ? PaymentMethodResources.Localized.safeDealInfoLink : nil, + paymentOptionTitle: paymentOptionTitle ) view.setupViewModel(viewModel) @@ -131,31 +135,27 @@ extension ApplePayContractPresenter: ApplePayContractViewOutput { initialSavePaymentMethod = state } - private func trackScreenErrorAnalytics(scheme: AnalyticsEvent.TokenizeScheme?) { + private func trackScreenErrorAnalytics(scheme: AnalyticsEvent.TokenizeScheme?, savePaymentMethod: Bool?) { DispatchQueue.global().async { [weak self] in - guard let interactor = self?.interactor else { return } - let (authType, _) = interactor.makeTypeAnalyticsParameters() - let event = AnalyticsEvent.screenError( - authType: authType, - scheme: scheme, - sdkVersion: Bundle.frameworkVersion + guard let self = self else { return } + self.interactor.track(event: + .screenError( + scheme: .applePay, + currentAuthType: self.interactor.analyticsAuthType() + ) ) - interactor.trackEvent(event) } } - private func trackScreenPaymentAnalytics( - scheme: AnalyticsEvent.TokenizeScheme - ) { + private func trackScreenPaymentAnalytics(scheme: AnalyticsEvent.TokenizeScheme) { DispatchQueue.global().async { [weak self] in - guard let interactor = self?.interactor else { return } - let (authType, _) = interactor.makeTypeAnalyticsParameters() - let event = AnalyticsEvent.screenPaymentContract( - authType: authType, - scheme: scheme, - sdkVersion: Bundle.frameworkVersion + guard let self = self else { return } + self.interactor.track(event: + .screenPaymentContract( + scheme: .applePay, + currentAuthType: self.interactor.analyticsAuthType() + ) ) - interactor.trackEvent(event) } } } @@ -163,27 +163,17 @@ extension ApplePayContractPresenter: ApplePayContractViewOutput { // MARK: - ContractInteractorOutput extension ApplePayContractPresenter: ApplePayContractInteractorOutput { - func didTokenize( - _ token: Tokens - ) { - guard applePayState == .success else { - return - } + func didTokenize(_ token: Tokens) { + guard applePayState == .success else { return } applePayCompletion?(.success) - - let parameters = interactor.makeTypeAnalyticsParameters() - let event: AnalyticsEvent = .actionTokenize( - scheme: .applePay, - authType: parameters.authType, - tokenType: parameters.tokenType, - sdkVersion: Bundle.frameworkVersion + interactor.track(event: + .actionTokenize( + scheme: .applePay, + currentAuthType: interactor.analyticsAuthType() + ) ) - interactor.trackEvent(event) - - DispatchQueue.main.asyncAfter( - deadline: .now() + Constants.dismissTimeout - ) { [weak self] in + DispatchQueue.main.asyncAfter(deadline: .now() + Constants.dismissTimeout) { [weak self] in guard let self = self else { return } self.router.closeApplePay { self.moduleOutput?.tokenizationModule( @@ -195,19 +185,16 @@ extension ApplePayContractPresenter: ApplePayContractInteractorOutput { } } - func failTokenize( - _ error: Error - ) { - guard applePayState == .success else { - return - } + func failTokenize(_ error: Error) { + guard applePayState == .success else { return } - trackScreenErrorAnalytics(scheme: .applePay) + trackScreenErrorAnalytics(scheme: .applePay, savePaymentMethod: paymentOption.savePaymentMethod == .allowed) applePayCompletion?(.failure) + interactor.track( + event: .screenErrorContract(scheme: .applePay, currentAuthType: interactor.analyticsAuthType()) + ) - DispatchQueue.main.asyncAfter( - deadline: .now() + Constants.dismissTimeout - ) { [weak self] in + DispatchQueue.main.asyncAfter(deadline: .now() + Constants.dismissTimeout) { [weak self] in guard let self = self else { return } self.router.closeApplePay { self.view?.presentError(with: CommonLocalized.ApplePay.failTokenizeData) @@ -226,7 +213,9 @@ extension ApplePayContractPresenter: ApplePayModuleOutput { func didFailPresentApplePayModule() { applePayState = .idle - trackScreenErrorAnalytics(scheme: .applePay) + interactor.track( + event: .screenErrorContract(scheme: .applePay, currentAuthType: interactor.analyticsAuthType()) + ) DispatchQueue.main.async { [weak self] in guard let view = self?.view else { return } @@ -258,6 +247,8 @@ extension ApplePayContractPresenter: ApplePayModuleOutput { applePayState = .success applePayCompletion = completion + interactor.track(event: .actionTryTokenize(scheme: .applePay, currentAuthType: interactor.analyticsAuthType())) + DispatchQueue.global().async { [weak self] in guard let self = self else { return } @@ -269,9 +260,7 @@ extension ApplePayContractPresenter: ApplePayModuleOutput { } } - func paymentAuthorizationViewControllerDidFinish( - _ controller: PKPaymentAuthorizationViewController - ) { + func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) { router.closeApplePay(completion: nil) applePayState = .cancel } diff --git a/YooKassaPayments/Private/Modules/ApplePayContract/Router/ApplePayContractRouter.swift b/YooKassaPayments/Private/Modules/ApplePayContract/Router/ApplePayContractRouter.swift index b9f89e12..4f7c85dd 100644 --- a/YooKassaPayments/Private/Modules/ApplePayContract/Router/ApplePayContractRouter.swift +++ b/YooKassaPayments/Private/Modules/ApplePayContract/Router/ApplePayContractRouter.swift @@ -8,6 +8,7 @@ final class ApplePayContractRouter { extension ApplePayContractRouter: ApplePayContractRouterInput { func presentTermsOfServiceModule(_ url: URL) { + guard url.scheme == "http" || url.scheme == "https" else { return } let viewController = SFSafariViewController(url: url) viewController.modalPresentationStyle = .overFullScreen transitionHandler?.present( diff --git a/YooKassaPayments/Private/Modules/ApplePayContract/View/ApplePayContractViewController.swift b/YooKassaPayments/Private/Modules/ApplePayContract/View/ApplePayContractViewController.swift index 57468273..0ba66f4e 100644 --- a/YooKassaPayments/Private/Modules/ApplePayContract/View/ApplePayContractViewController.swift +++ b/YooKassaPayments/Private/Modules/ApplePayContract/View/ApplePayContractViewController.swift @@ -72,7 +72,6 @@ final class ApplePayContractViewController: UIViewController { submitButton.translatesAutoresizingMaskIntoConstraints = false view.addSubview(submitButton) let defaultHeight = submitButton.heightAnchor.constraint(equalToConstant: Space.triple * 2) - defaultHeight.priority = .defaultLow + 1 NSLayoutConstraint.activate([ submitButton.leadingAnchor.constraint(equalTo: view.leadingAnchor), submitButton.topAnchor.constraint(equalTo: view.topAnchor), @@ -86,6 +85,7 @@ final class ApplePayContractViewController: UIViewController { private let termsOfServiceLinkedTextView: LinkedTextView = { let view = LinkedTextView() + view.setContentCompressionResistancePriority(.required, for: .vertical) view.tintColor = CustomizationStorage.shared.mainScheme view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) return view @@ -93,6 +93,7 @@ final class ApplePayContractViewController: UIViewController { private let safeDealLinkedTextView: LinkedTextView = { let view = LinkedTextView() + view.setContentCompressionResistancePriority(.required, for: .vertical) view.tintColor = CustomizationStorage.shared.mainScheme view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) return view @@ -158,7 +159,7 @@ final class ApplePayContractViewController: UIViewController { private lazy var scrollViewHeightConstraint: NSLayoutConstraint = { let constraint = scrollView.heightAnchor.constraint(equalToConstant: 0) - constraint.priority = .defaultLow + constraint.priority = .defaultHigh + 1 return constraint }() @@ -274,7 +275,7 @@ final class ApplePayContractViewController: UIViewController { } private func fixTableViewHeight() { - scrollViewHeightConstraint.constant = contentStackView.bounds.height + scrollViewHeightConstraint.constant = ceil(scrollView.contentSize.height) + Space.triple * 2 } // MARK: - Action @@ -300,11 +301,7 @@ extension ApplePayContractViewController: ApplePayContractViewInput { orderView.subvalue = nil } - termsOfServiceLinkedTextView.attributedText = makeTermsOfService( - viewModel.terms, - font: UIFont.dynamicCaption2, - foregroundColor: UIColor.AdaptiveColors.secondary - ) + termsOfServiceLinkedTextView.attributedText = viewModel.terms termsOfServiceLinkedTextView.textAlignment = .center safeDealLinkedTextView.attributedText = viewModel.safeDealText @@ -312,6 +309,7 @@ extension ApplePayContractViewController: ApplePayContractViewInput { termsOfServiceLinkedTextView.textAlignment = .center safeDealLinkedTextView.textAlignment = .center + viewModel.paymentOptionTitle.map { navigationItem.title = $0 } } func setSavePaymentMethodViewModel( @@ -360,33 +358,6 @@ extension ApplePayContractViewController: ApplePayContractViewInput { + price.currency } - private 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 - } - private func makeSavePaymentMethodAttributedString( text: String, hyperText: String, diff --git a/YooKassaPayments/Private/Modules/ApplePayContract/View/ViewModel/ApplePayContractViewModel.swift b/YooKassaPayments/Private/Modules/ApplePayContract/View/ViewModel/ApplePayContractViewModel.swift index 95a70c68..f1986a8b 100644 --- a/YooKassaPayments/Private/Modules/ApplePayContract/View/ViewModel/ApplePayContractViewModel.swift +++ b/YooKassaPayments/Private/Modules/ApplePayContract/View/ViewModel/ApplePayContractViewModel.swift @@ -3,6 +3,7 @@ struct ApplePayContractViewModel { let description: String? let price: PriceViewModel let fee: PriceViewModel? - let terms: TermsOfService + let terms: NSAttributedString let safeDealText: NSAttributedString? + let paymentOptionTitle: String? } diff --git a/YooKassaPayments/Private/Modules/BankCard/Assembly/BankCardAssembly.swift b/YooKassaPayments/Private/Modules/BankCard/Assembly/BankCardAssembly.swift index c3f82ca6..cac9df71 100644 --- a/YooKassaPayments/Private/Modules/BankCard/Assembly/BankCardAssembly.swift +++ b/YooKassaPayments/Private/Modules/BankCard/Assembly/BankCardAssembly.swift @@ -55,8 +55,11 @@ enum BankCardAssembly { canSaveInstrument: inputData.canSaveInstrument, apiSavePaymentMethod: inputData.apiSavePaymentMethod, clientSavePaymentMethod: inputData.savePaymentMethod, - paymentMethodViewModelFactory: PaymentMethodViewModelFactoryAssembly.makeFactory(), - isSafeDeal: inputData.isSafeDeal + paymentMethodViewModelFactory: PaymentMethodViewModelFactoryAssembly.makeFactory( + isLoggingEnabled: inputData.isLoggingEnabled + ), + isSafeDeal: inputData.isSafeDeal, + config: inputData.config ) return presenter } @@ -69,18 +72,18 @@ enum BankCardAssembly { testModeSettings: inputData.testModeSettings, isLoggingEnabled: inputData.isLoggingEnabled ) - let analyticsService = AnalyticsServiceAssembly.makeService( - isLoggingEnabled: inputData.isLoggingEnabled - ) - let analyticsProvider = AnalyticsProviderAssembly.makeProvider( - testModeSettings: inputData.testModeSettings - ) + let analyticsService = AnalyticsTrackingAssembly.make(isLoggingEnabled: inputData.isLoggingEnabled) let threatMetrixService = ThreatMetrixServiceFactory.makeService() + let authService = AuthorizationServiceAssembly.makeService( + isLoggingEnabled: inputData.isLoggingEnabled, + testModeSettings: inputData.testModeSettings, + moneyAuthClientId: nil + ) let interactor = BankCardInteractor( + authService: authService, paymentService: paymentService, analyticsService: analyticsService, - analyticsProvider: analyticsProvider, threatMetrixService: threatMetrixService, clientApplicationKey: inputData.clientApplicationKey, amount: inputData.paymentOption.charge.plain, diff --git a/YooKassaPayments/Private/Modules/BankCard/BankCardInteractorIO.swift b/YooKassaPayments/Private/Modules/BankCard/BankCardInteractorIO.swift index fe8a4c5e..65189b00 100644 --- a/YooKassaPayments/Private/Modules/BankCard/BankCardInteractorIO.swift +++ b/YooKassaPayments/Private/Modules/BankCard/BankCardInteractorIO.swift @@ -1,7 +1,8 @@ -protocol BankCardInteractorInput: AnalyticsTrack { +protocol BankCardInteractorInput { func tokenizeInstrument(id: String, csc: String?, savePaymentMethod: Bool) func tokenizeBankCard(cardData: CardData, savePaymentMethod: Bool, savePaymentInstrument: Bool?) - func makeTypeAnalyticsParameters() -> (authType: AnalyticsEvent.AuthType, tokenType: AnalyticsEvent.AuthTokenType?) + func track(event: AnalyticsEvent) + func analyticsAuthType() -> AnalyticsEvent.AuthType } protocol BankCardInteractorOutput: AnyObject { diff --git a/YooKassaPayments/Private/Modules/BankCard/BankCardModuleIO.swift b/YooKassaPayments/Private/Modules/BankCard/BankCardModuleIO.swift index f76225a1..2acebd61 100644 --- a/YooKassaPayments/Private/Modules/BankCard/BankCardModuleIO.swift +++ b/YooKassaPayments/Private/Modules/BankCard/BankCardModuleIO.swift @@ -10,7 +10,7 @@ struct BankCardModuleInputData { let priceViewModel: PriceViewModel let feeViewModel: PriceViewModel? let paymentOption: PaymentOption - let termsOfService: TermsOfService + let termsOfService: NSAttributedString let cardScanning: CardScanning? let returnUrl: String let savePaymentMethod: SavePaymentMethod @@ -20,6 +20,7 @@ struct BankCardModuleInputData { let customerId: String? let instrument: PaymentInstrumentBankCard? let isSafeDeal: Bool + let config: Config } protocol BankCardModuleOutput: AnyObject { diff --git a/YooKassaPayments/Private/Modules/BankCard/Interactor/BankCardInteractor.swift b/YooKassaPayments/Private/Modules/BankCard/Interactor/BankCardInteractor.swift index 558d6e99..7d245a6c 100644 --- a/YooKassaPayments/Private/Modules/BankCard/Interactor/BankCardInteractor.swift +++ b/YooKassaPayments/Private/Modules/BankCard/Interactor/BankCardInteractor.swift @@ -8,9 +8,9 @@ final class BankCardInteractor { // MARK: - Initialization + private let authService: AuthorizationService private let paymentService: PaymentService - private let analyticsService: AnalyticsService - private let analyticsProvider: AnalyticsProvider + private let analyticsService: AnalyticsTracking private let threatMetrixService: ThreatMetrixService private let clientApplicationKey: String private let amount: MonetaryAmount @@ -18,18 +18,18 @@ final class BankCardInteractor { private let customerId: String? init( + authService: AuthorizationService, paymentService: PaymentService, - analyticsService: AnalyticsService, - analyticsProvider: AnalyticsProvider, + analyticsService: AnalyticsTracking, threatMetrixService: ThreatMetrixService, clientApplicationKey: String, amount: MonetaryAmount, returnUrl: String, customerId: String? ) { + self.authService = authService self.paymentService = paymentService self.analyticsService = analyticsService - self.analyticsProvider = analyticsProvider self.threatMetrixService = threatMetrixService self.clientApplicationKey = clientApplicationKey self.amount = amount @@ -110,17 +110,12 @@ extension BankCardInteractor: BankCardInteractorInput { } } - func makeTypeAnalyticsParameters() -> ( - authType: AnalyticsEvent.AuthType, - tokenType: AnalyticsEvent.AuthTokenType? - ) { - analyticsProvider.makeTypeAnalyticsParameters() + func analyticsAuthType() -> AnalyticsEvent.AuthType { + authService.analyticsAuthType() } - func trackEvent( - _ event: AnalyticsEvent - ) { - analyticsService.trackEvent(event) + func track(event: AnalyticsEvent) { + analyticsService.track(event: event) } } diff --git a/YooKassaPayments/Private/Modules/BankCard/Presenter/BankCardPresenter.swift b/YooKassaPayments/Private/Modules/BankCard/Presenter/BankCardPresenter.swift index 97906efe..ebad79e9 100644 --- a/YooKassaPayments/Private/Modules/BankCard/Presenter/BankCardPresenter.swift +++ b/YooKassaPayments/Private/Modules/BankCard/Presenter/BankCardPresenter.swift @@ -22,7 +22,7 @@ final class BankCardPresenter { private let purchaseDescription: String private let priceViewModel: PriceViewModel private let feeViewModel: PriceViewModel? - private let termsOfService: TermsOfService + private let termsOfService: NSAttributedString private let cardScanning: CardScanning? private let clientSavePaymentMethod: SavePaymentMethod private let isBackBarButtonHidden: Bool @@ -31,6 +31,7 @@ final class BankCardPresenter { private let apiSavePaymentMethod: YooKassaPaymentsApi.SavePaymentMethod private let paymentMethodViewModelFactory: PaymentMethodViewModelFactory private let isSafeDeal: Bool + private let config: Config init( cardService: CardService, @@ -38,7 +39,7 @@ final class BankCardPresenter { purchaseDescription: String, priceViewModel: PriceViewModel, feeViewModel: PriceViewModel?, - termsOfService: TermsOfService, + termsOfService: NSAttributedString, cardScanning: CardScanning?, isBackBarButtonHidden: Bool, instrument: PaymentInstrumentBankCard?, @@ -46,7 +47,8 @@ final class BankCardPresenter { apiSavePaymentMethod: YooKassaPaymentsApi.SavePaymentMethod, clientSavePaymentMethod: SavePaymentMethod, paymentMethodViewModelFactory: PaymentMethodViewModelFactory, - isSafeDeal: Bool + isSafeDeal: Bool, + config: Config ) { self.cardService = cardService self.shopName = shopName @@ -62,6 +64,7 @@ final class BankCardPresenter { self.clientSavePaymentMethod = clientSavePaymentMethod self.paymentMethodViewModelFactory = paymentMethodViewModelFactory self.isSafeDeal = isSafeDeal + self.config = config } // MARK: - Stored properties @@ -82,12 +85,6 @@ extension BankCardPresenter: BankCardViewOutput { feeValue = "\(CommonLocalized.Contract.fee) " + makePrice(feeViewModel) } - let termsOfServiceValue = makeTermsOfService( - termsOfService, - font: UIFont.dynamicCaption2, - foregroundColor: UIColor.AdaptiveColors.secondary - ) - let maskedNumber = instrument .map { ($0.first6 ?? "") + "******" + $0.last4 } .map(paymentMethodViewModelFactory.replaceBullets) @@ -111,11 +108,13 @@ extension BankCardPresenter: BankCardViewOutput { case .on: section = PaymentRecurrencyAndDataSavingSectionFactory.make( mode: .requiredRecurring, + texts: config.savePaymentMethodOptionTexts, output: self ) case .userSelects: section = PaymentRecurrencyAndDataSavingSectionFactory.make( mode: .allowRecurring, + texts: config.savePaymentMethodOptionTexts, output: self ) case .off: @@ -126,6 +125,7 @@ extension BankCardPresenter: BankCardViewOutput { clientSavePaymentMethod: clientSavePaymentMethod, apiSavePaymentMethod: apiSavePaymentMethod, canSavePaymentInstrument: canSaveInstrument, + texts: config.savePaymentMethodOptionTexts, output: self ) } @@ -135,12 +135,13 @@ extension BankCardPresenter: BankCardViewOutput { description: purchaseDescription, priceValue: priceValue, feeValue: feeValue, - termsOfService: termsOfServiceValue, + termsOfService: termsOfService, instrumentMode: instrument != nil, maskedNumber: maskedNumber.splitEvery(4, separator: " "), cardLogo: logo, safeDealText: isSafeDeal ? PaymentMethodResources.Localized.safeDealInfoLink : nil, - recurrencyAndDataSavingSection: section + recurrencyAndDataSavingSection: section, + paymentOptionTitle: config.paymentMethods.first { $0.kind == .bankCard }?.title ) if let section = section { @@ -163,18 +164,12 @@ extension BankCardPresenter: BankCardViewOutput { DispatchQueue.global().async { [weak self] in guard let self = self else { return } - let parameters = self.interactor.makeTypeAnalyticsParameters() - let form: AnalyticsEvent = .screenBankCardForm( - authType: parameters.authType, - sdkVersion: Bundle.frameworkVersion - ) - self.interactor.trackEvent(form) - let contract = AnalyticsEvent.screenPaymentContract( - authType: parameters.authType, - scheme: .bankCard, - sdkVersion: Bundle.frameworkVersion + self.interactor.track(event: + .screenPaymentContract( + scheme: .bankCard, + currentAuthType: self.interactor.analyticsAuthType() + ) ) - self.interactor.trackEvent(contract) } } @@ -191,12 +186,11 @@ extension BankCardPresenter: BankCardViewOutput { saveMethod = true case (.userSelects, .allowed): saveMethod = saveInstrument ?? false - case (_, .unknown(_)): - saveMethod = false - default: + case (_, _): saveMethod = false } let savePaymentInstrument = canSaveInstrument ? saveInstrument : false + interactor.track(event: .actionTryTokenize(scheme: .bankCard, currentAuthType: interactor.analyticsAuthType())) DispatchQueue.global().async { [weak self] in guard let self = self, let interactor = self.interactor else { return } @@ -308,29 +302,21 @@ extension BankCardPresenter: BankCardInteractorOutput { } DispatchQueue.global().async { [weak self] in - guard let self = self, let interactor = self.interactor else { return } - let type = interactor.makeTypeAnalyticsParameters() - let event: AnalyticsEvent = .actionTokenize( - scheme: scheme, - authType: type.authType, - tokenType: type.tokenType, - sdkVersion: Bundle.frameworkVersion + guard let self = self else { return } + self.interactor.track(event: + .actionTokenize( + scheme: scheme, + currentAuthType: self.interactor.analyticsAuthType() + ) ) - interactor.trackEvent(event) } } } - func didFailTokenize( - _ error: Error - ) { - let parameters = interactor.makeTypeAnalyticsParameters() - let event: AnalyticsEvent = .screenError( - authType: parameters.authType, - scheme: .bankCard, - sdkVersion: Bundle.frameworkVersion + func didFailTokenize(_ error: Error) { + interactor.track( + event: .screenErrorContract(scheme: .bankCard, currentAuthType: interactor.analyticsAuthType()) ) - interactor.trackEvent(event) let message = makeMessage(error) @@ -387,23 +373,38 @@ extension BankCardPresenter: PaymentRecurrencyAndDataSavingSectionOutput { switch mode { case .allowRecurring, .requiredRecurring: router.presentSafeDealInfo( - title: CommonLocalized.CardSettingsDetails.autopayInfoTitle, - body: CommonLocalized.CardSettingsDetails.autopayInfoDetails + title: htmlOut(source: config.savePaymentMethodOptionTexts.screenRecurrentOnBindOffTitle), + body: htmlOut(source: config.savePaymentMethodOptionTexts.screenRecurrentOnBindOffText) ) case .savePaymentData, .requiredSaveData: router.presentSafeDealInfo( - title: CommonLocalized.RecurrencyAndSavePaymentData.saveDataInfoTitle, - body: CommonLocalized.RecurrencyAndSavePaymentData.saveDataInfoMessage + title: htmlOut(source: config.savePaymentMethodOptionTexts.screenRecurrentOffBindOnTitle), + body: htmlOut(source: config.savePaymentMethodOptionTexts.screenRecurrentOffBindOnText) ) case .allowRecurringAndSaveData, .requiredRecurringAndSaveData: router.presentSafeDealInfo( - title: CommonLocalized.RecurrencyAndSavePaymentData.saveDataAndAutopaymentsInfoTitle, - body: CommonLocalized.RecurrencyAndSavePaymentData.saveDataAndAutopaymentsInfoMessage + title: htmlOut(source: config.savePaymentMethodOptionTexts.screenRecurrentOnBindOnTitle), + body: htmlOut(source: config.savePaymentMethodOptionTexts.screenRecurrentOnBindOnText) ) default: break } } + + /// Convert
-> \n and other html text formatting to native `String` + private func htmlOut(source: String) -> String { + guard let data = source.data(using: .utf16) else { return source } + do { + let html = try NSAttributedString( + data: data, + options: [.documentType: NSAttributedString.DocumentType.html], + documentAttributes: nil + ) + return html.string + } catch { + return source + } + } } // MARK: - Private global helpers @@ -431,30 +432,3 @@ private func makePrice( + priceViewModel.fractionalPart + priceViewModel.currency } - -private 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 -} diff --git a/YooKassaPayments/Private/Modules/BankCard/Router/BankCardRouter.swift b/YooKassaPayments/Private/Modules/BankCard/Router/BankCardRouter.swift index 6b99b54f..23bbbd13 100644 --- a/YooKassaPayments/Private/Modules/BankCard/Router/BankCardRouter.swift +++ b/YooKassaPayments/Private/Modules/BankCard/Router/BankCardRouter.swift @@ -8,6 +8,7 @@ final class BankCardRouter { extension BankCardRouter: BankCardRouterInput { func presentTermsOfServiceModule(_ url: URL) { + guard url.scheme == "http" || url.scheme == "https" else { return } let viewController = SFSafariViewController(url: url) viewController.modalPresentationStyle = .overFullScreen transitionHandler?.present( diff --git a/YooKassaPayments/Private/Modules/BankCard/View/BankCardViewController.swift b/YooKassaPayments/Private/Modules/BankCard/View/BankCardViewController.swift index 93b6d317..2e3809ee 100644 --- a/YooKassaPayments/Private/Modules/BankCard/View/BankCardViewController.swift +++ b/YooKassaPayments/Private/Modules/BankCard/View/BankCardViewController.swift @@ -114,7 +114,6 @@ final class BankCardViewController: UIViewController { submitButton.translatesAutoresizingMaskIntoConstraints = false view.addSubview(submitButton) let defaultHeight = submitButton.heightAnchor.constraint(equalToConstant: Space.triple * 2) - defaultHeight.priority = .defaultLow + 1 NSLayoutConstraint.activate([ submitButton.leadingAnchor.constraint(equalTo: view.leadingAnchor), submitButton.topAnchor.constraint(equalTo: view.topAnchor), @@ -128,6 +127,7 @@ final class BankCardViewController: UIViewController { private let termsOfServiceLinkedTextView: LinkedTextView = { let view = LinkedTextView() + view.setContentCompressionResistancePriority(.required, for: .vertical) view.tintColor = CustomizationStorage.shared.mainScheme view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) return view @@ -135,6 +135,7 @@ final class BankCardViewController: UIViewController { private let safeDealLinkedTextView: LinkedTextView = { let view = LinkedTextView() + view.setContentCompressionResistancePriority(.required, for: .vertical) view.tintColor = CustomizationStorage.shared.mainScheme view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) return view @@ -144,7 +145,7 @@ final class BankCardViewController: UIViewController { private lazy var scrollViewHeightConstraint: NSLayoutConstraint = { let constraint = scrollView.heightAnchor.constraint(equalToConstant: 0) - constraint.priority = .defaultLow + constraint.priority = .defaultHigh + 1 return constraint }() @@ -274,7 +275,7 @@ final class BankCardViewController: UIViewController { } private func updateContentHeight() { - scrollViewHeightConstraint.constant = contentStackView.bounds.height + scrollViewHeightConstraint.constant = ceil(scrollView.contentSize.height) + Space.triple * 2 } } @@ -291,6 +292,7 @@ extension BankCardViewController: BankCardViewInput { safeDealLinkedTextView.attributedText = viewModel.safeDealText termsOfServiceLinkedTextView.textAlignment = .center safeDealLinkedTextView.textAlignment = .center + viewModel.paymentOptionTitle.map { navigationItem.title = $0 } if viewModel.instrumentMode { bankCardDataInputView.isHidden = true diff --git a/YooKassaPayments/Private/Modules/BankCard/View/PaymentRecurrencyAndDataSavingSection.swift b/YooKassaPayments/Private/Modules/BankCard/View/PaymentRecurrencyAndDataSavingSection.swift index 1ac71f02..6a9c2e3c 100644 --- a/YooKassaPayments/Private/Modules/BankCard/View/PaymentRecurrencyAndDataSavingSection.swift +++ b/YooKassaPayments/Private/Modules/BankCard/View/PaymentRecurrencyAndDataSavingSection.swift @@ -26,8 +26,11 @@ class PaymentRecurrencyAndDataSavingSection: UIView, SwitchItemViewOutput, Linke var switchValue: Bool { switchSection.state } - init(mode: Mode, frame: CGRect = .zero) { + private let texts: Config.SavePaymentMethodOptionTexts + + init(mode: Mode, texts: Config.SavePaymentMethodOptionTexts, frame: CGRect = .zero) { self.mode = mode + self.texts = texts super.init(frame: frame) accessibilityIdentifier = "PaymentRecurrencyAndDataSavingSection" @@ -88,58 +91,29 @@ class PaymentRecurrencyAndDataSavingSection: UIView, SwitchItemViewOutput, Linke innerContainer.arrangedSubviews.forEach { $0.isHidden = true } case .savePaymentData: headerSection.isHidden = true - switchSection.title = HeaderText.optionalSaveDataHeader + switchSection.title = texts.switchRecurrentOffBindOnTitle switchSection.state = true - linkSection.attributedString = makeLink( - text: LinkText.Optional.saveDataLink, - interactive: LinkText.Optional.saveDataLinkInteractive - ) + linkSection.attributedString = HTMLUtils.highlightHyperlinks(html: texts.switchRecurrentOffBindOnSubtitle) case .allowRecurring: headerSection.isHidden = true - switchSection.title = HeaderText.optionalAutopaymentsHeader - linkSection.attributedString = makeLink( - text: LinkText.Optional.autopaymentsLink, - interactive: LinkText.Optional.autopaymentsInteractive - ) + switchSection.title = texts.switchRecurrentOnBindOffTitle + linkSection.attributedString = HTMLUtils.highlightHyperlinks(html: texts.switchRecurrentOnBindOffSubtitle) case .allowRecurringAndSaveData: headerSection.isHidden = true - switchSection.title = HeaderText.optionalSaveDataAndAutopaymentsHeader - linkSection.attributedString = makeLink( - text: LinkText.Optional.autopaymentsAndSaveDataLink, - interactive: LinkText.Optional.autopaymentsAndSaveDataInteractive - ) + switchSection.title = texts.switchRecurrentOnBindOnTitle + linkSection.attributedString = HTMLUtils.highlightHyperlinks(html: texts.switchRecurrentOnBindOnSubtitle) case .requiredRecurringAndSaveData: switchSection.isHidden = true - headerSection.title = HeaderText.requiredSaveDataAndAutopaymentsHeader - linkSection.attributedString = makeLink( - text: LinkText.Required.autopaymentsAndSaveDataLink, - interactive: LinkText.Required.autopaymentsAndSaveDataInteractive - ) + headerSection.title = texts.messageRecurrentOnBindOnTitle + linkSection.attributedString = HTMLUtils.highlightHyperlinks(html: texts.messageRecurrentOnBindOnSubtitle) case .requiredRecurring: switchSection.isHidden = true - headerSection.title = HeaderText.requiredAutopaymentsHeader - linkSection.attributedString = makeLink( - text: LinkText.Required.autopaymentsLink, - interactive: LinkText.Required.autopaymentsInteractive - ) + headerSection.title = texts.messageRecurrentOnBindOffTitle + linkSection.attributedString = HTMLUtils.highlightHyperlinks(html: texts.messageRecurrentOnBindOffSubtitle) case .requiredSaveData: switchSection.isHidden = true - headerSection.title = HeaderText.requiredSaveDataHeader - linkSection.attributedString = makeLink( - text: LinkText.Required.saveDataLink, - interactive: LinkText.Required.saveDataLinkInteractive - ) + headerSection.title = texts.messageRecurrentOffBindOnTitle + linkSection.attributedString = HTMLUtils.highlightHyperlinks(html: texts.messageRecurrentOffBindOnSubtitle) } } - - private func makeLink(text: String, interactive: String) -> NSAttributedString { - let range = (text as NSString).range(of: interactive) - let result = NSMutableAttributedString(string: text) - result.setAttributes( - [.foregroundColor: UIColor.AdaptiveColors.secondary], - range: NSRange(location: 0, length: (result.string as NSString).length) - ) - result.addAttribute(.link, value: "yookassapayments://", range: range) - return result - } } diff --git a/YooKassaPayments/Private/Modules/BankCard/View/PaymentRecurrencyAndDataSavingSectionFactory.swift b/YooKassaPayments/Private/Modules/BankCard/View/PaymentRecurrencyAndDataSavingSectionFactory.swift index cc052dfb..9cf7abaa 100644 --- a/YooKassaPayments/Private/Modules/BankCard/View/PaymentRecurrencyAndDataSavingSectionFactory.swift +++ b/YooKassaPayments/Private/Modules/BankCard/View/PaymentRecurrencyAndDataSavingSectionFactory.swift @@ -6,6 +6,7 @@ enum PaymentRecurrencyAndDataSavingSectionFactory { clientSavePaymentMethod: SavePaymentMethod, apiSavePaymentMethod: YooKassaPaymentsApi.SavePaymentMethod, canSavePaymentInstrument: Bool, + texts: Config.SavePaymentMethodOptionTexts, output: PaymentRecurrencyAndDataSavingSectionOutput ) -> PaymentRecurrencyAndDataSavingSection? { let view: PaymentRecurrencyAndDataSavingSection? @@ -15,22 +16,22 @@ enum PaymentRecurrencyAndDataSavingSectionFactory { view = nil case (.off, .forbidden, true), (.off, .allowed, true), (.userSelects, .forbidden, true): - view = PaymentRecurrencyAndDataSavingSection(mode: .savePaymentData) + view = PaymentRecurrencyAndDataSavingSection(mode: .savePaymentData, texts: texts) case (.userSelects, .allowed, false): - view = PaymentRecurrencyAndDataSavingSection(mode: .allowRecurring) + view = PaymentRecurrencyAndDataSavingSection(mode: .allowRecurring, texts: texts) case (.userSelects, .allowed, true): - view = PaymentRecurrencyAndDataSavingSection(mode: .allowRecurringAndSaveData) + view = PaymentRecurrencyAndDataSavingSection(mode: .allowRecurringAndSaveData, texts: texts) case (.on, .allowed, true): - view = PaymentRecurrencyAndDataSavingSection(mode: .requiredRecurringAndSaveData) + view = PaymentRecurrencyAndDataSavingSection(mode: .requiredRecurringAndSaveData, texts: texts) case (.on, .allowed, false): - view = PaymentRecurrencyAndDataSavingSection(mode: .requiredRecurring) + view = PaymentRecurrencyAndDataSavingSection(mode: .requiredRecurring, texts: texts) case (.on, .forbidden, true): - view = PaymentRecurrencyAndDataSavingSection(mode: .savePaymentData) + view = PaymentRecurrencyAndDataSavingSection(mode: .savePaymentData, texts: texts) default: view = nil @@ -41,9 +42,10 @@ enum PaymentRecurrencyAndDataSavingSectionFactory { static func make( mode: PaymentRecurrencyAndDataSavingSection.Mode, + texts: Config.SavePaymentMethodOptionTexts, output: PaymentRecurrencyAndDataSavingSectionOutput ) -> PaymentRecurrencyAndDataSavingSection { - let view = PaymentRecurrencyAndDataSavingSection(mode: mode) + let view = PaymentRecurrencyAndDataSavingSection(mode: mode, texts: texts) view.output = output return view } diff --git a/YooKassaPayments/Private/Modules/BankCard/View/ViewModel/BankCardViewModel.swift b/YooKassaPayments/Private/Modules/BankCard/View/ViewModel/BankCardViewModel.swift index f8af8749..b42c6600 100644 --- a/YooKassaPayments/Private/Modules/BankCard/View/ViewModel/BankCardViewModel.swift +++ b/YooKassaPayments/Private/Modules/BankCard/View/ViewModel/BankCardViewModel.swift @@ -11,4 +11,5 @@ struct BankCardViewModel { let cardLogo: UIImage let safeDealText: NSAttributedString? let recurrencyAndDataSavingSection: UIView? + let paymentOptionTitle: String? } diff --git a/YooKassaPayments/Private/Modules/BankCardRepeat/Assembly/BankCardRepeatAssembly.swift b/YooKassaPayments/Private/Modules/BankCardRepeat/Assembly/BankCardRepeatAssembly.swift index 9d54f850..68c03d0a 100644 --- a/YooKassaPayments/Private/Modules/BankCardRepeat/Assembly/BankCardRepeatAssembly.swift +++ b/YooKassaPayments/Private/Modules/BankCardRepeat/Assembly/BankCardRepeatAssembly.swift @@ -11,9 +11,12 @@ enum BankCardRepeatAssembly { let view = BankCardRepeatViewController() let cardService = CardService() - let paymentMethodViewModelFactory = PaymentMethodViewModelFactoryAssembly.makeFactory() + let paymentMethodViewModelFactory = PaymentMethodViewModelFactoryAssembly.makeFactory( + isLoggingEnabled: inputData.isLoggingEnabled + ) + let configMediator = ConfigMediatorAssembly.make(isLoggingEnabled: inputData.isLoggingEnabled) let priceViewModelFactory = PriceViewModelFactoryAssembly.makeFactory() - let termsOfService = TermsOfServiceFactory.makeTermsOfService() + let termsOfService = HTMLUtils.highlightHyperlinks(html: configMediator.storedConfig().userAgreementUrl) let initialSavePaymentMethod = makeInitialSavePaymentMethod(inputData.savePaymentMethod) let savePaymentMethodViewModel = SavePaymentMethodViewModelFactory.makeSavePaymentMethodViewModel( inputData.savePaymentMethod, @@ -34,22 +37,23 @@ enum BankCardRepeatAssembly { isSafeDeal: inputData.isSafeDeal ) - let analyticsService = AnalyticsServiceAssembly.makeService( - isLoggingEnabled: inputData.isLoggingEnabled - ) + let analyticsService = AnalyticsTrackingAssembly.make(isLoggingEnabled: inputData.isLoggingEnabled) let paymentService = PaymentServiceAssembly.makeService( tokenizationSettings: TokenizationSettings(), testModeSettings: inputData.testModeSettings, isLoggingEnabled: inputData.isLoggingEnabled ) - let analyticsProvider = AnalyticsProviderAssembly.makeProvider( - testModeSettings: inputData.testModeSettings + let authService = AuthorizationServiceAssembly.makeService( + isLoggingEnabled: inputData.isLoggingEnabled, + testModeSettings: inputData.testModeSettings, + moneyAuthClientId: nil ) + let threatMetrixService = ThreatMetrixServiceFactory.makeService() let amountNumberFormatter = AmountNumberFormatterAssembly.makeAmountNumberFormatter() let interactor = BankCardRepeatInteractor( + authService: authService, analyticsService: analyticsService, - analyticsProvider: analyticsProvider, paymentService: paymentService, threatMetrixService: threatMetrixService, amountNumberFormatter: amountNumberFormatter, diff --git a/YooKassaPayments/Private/Modules/BankCardRepeat/BankCardRepeatInteractorIO.swift b/YooKassaPayments/Private/Modules/BankCardRepeat/BankCardRepeatInteractorIO.swift index e9191c3b..f841bb1d 100644 --- a/YooKassaPayments/Private/Modules/BankCardRepeat/BankCardRepeatInteractorIO.swift +++ b/YooKassaPayments/Private/Modules/BankCardRepeat/BankCardRepeatInteractorIO.swift @@ -1,9 +1,7 @@ import YooKassaPaymentsApi -protocol BankCardRepeatInteractorInput: AnalyticsTrack, AnalyticsProvider { - func fetchPaymentMethod( - paymentMethodId: String - ) +protocol BankCardRepeatInteractorInput { + func fetchPaymentMethod(paymentMethodId: String) func tokenize( amount: MonetaryAmount, confirmation: Confirmation, @@ -13,30 +11,17 @@ protocol BankCardRepeatInteractorInput: AnalyticsTrack, AnalyticsProvider { ) func fetchPaymentMethods() - - func startAnalyticsService() - func stopAnalyticsService() + func track(event: AnalyticsEvent) + func analyticsAuthType() -> AnalyticsEvent.AuthType } protocol BankCardRepeatInteractorOutput: AnyObject { - func didFetchPaymentMethod( - _ paymentMethod: PaymentMethod - ) - func didFailFetchPaymentMethod( - _ error: Error - ) + func didFetchPaymentMethod(_ paymentMethod: PaymentMethod) + func didFailFetchPaymentMethod(_ error: Error) - func didTokenize( - _ tokens: Tokens - ) - func didFailTokenize( - _ error: Error - ) + func didTokenize(_ tokens: Tokens) + func didFailTokenize(_ error: Error) - func didFetchPaymentMethods( - _ paymentMethods: [PaymentOption] - ) - func didFetchPaymentMethods( - _ 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 c0858894..138946c9 100644 --- a/YooKassaPayments/Private/Modules/BankCardRepeat/Interactor/BankCardRepeatInteractor.swift +++ b/YooKassaPayments/Private/Modules/BankCardRepeat/Interactor/BankCardRepeatInteractor.swift @@ -8,8 +8,8 @@ final class BankCardRepeatInteractor { // MARK: - Init data - private let analyticsService: AnalyticsService - private let analyticsProvider: AnalyticsProvider + private let authService: AuthorizationService + private let analyticsService: AnalyticsTracking private let paymentService: PaymentService private let threatMetrixService: ThreatMetrixService private let amountNumberFormatter: AmountNumberFormatter @@ -22,8 +22,8 @@ final class BankCardRepeatInteractor { // MARK: - Init init( - analyticsService: AnalyticsService, - analyticsProvider: AnalyticsProvider, + authService: AuthorizationService, + analyticsService: AnalyticsTracking, paymentService: PaymentService, threatMetrixService: ThreatMetrixService, amountNumberFormatter: AmountNumberFormatter, @@ -32,8 +32,8 @@ final class BankCardRepeatInteractor { amount: Amount, customerId: String? ) { + self.authService = authService self.analyticsService = analyticsService - self.analyticsProvider = analyticsProvider self.paymentService = paymentService self.threatMetrixService = threatMetrixService self.amountNumberFormatter = amountNumberFormatter @@ -123,23 +123,12 @@ extension BankCardRepeatInteractor: BankCardRepeatInteractorInput { } } - func trackEvent(_ event: AnalyticsEvent) { - analyticsService.trackEvent(event) + func track(event: AnalyticsEvent) { + analyticsService.track(event: event) } - func makeTypeAnalyticsParameters() -> ( - authType: AnalyticsEvent.AuthType, - tokenType: AnalyticsEvent.AuthTokenType? - ) { - return analyticsProvider.makeTypeAnalyticsParameters() - } - - func startAnalyticsService() { - analyticsService.start() - } - - func stopAnalyticsService() { - analyticsService.stop() + func analyticsAuthType() -> AnalyticsEvent.AuthType { + authService.analyticsAuthType() } } diff --git a/YooKassaPayments/Private/Modules/BankCardRepeat/Presenter/BankCardRepeatPresenter.swift b/YooKassaPayments/Private/Modules/BankCardRepeat/Presenter/BankCardRepeatPresenter.swift index 3e0e115d..b27bb2bf 100644 --- a/YooKassaPayments/Private/Modules/BankCardRepeat/Presenter/BankCardRepeatPresenter.swift +++ b/YooKassaPayments/Private/Modules/BankCardRepeat/Presenter/BankCardRepeatPresenter.swift @@ -22,7 +22,7 @@ final class BankCardRepeatPresenter { private let paymentMethodId: String private let shopName: String private let purchaseDescription: String - private let termsOfService: TermsOfService + private let termsOfService: NSAttributedString private let savePaymentMethodViewModel: SavePaymentMethodViewModel? private var initialSavePaymentMethod: Bool private let isSafeDeal: Bool @@ -38,7 +38,7 @@ final class BankCardRepeatPresenter { paymentMethodId: String, shopName: String, purchaseDescription: String, - termsOfService: TermsOfService, + termsOfService: NSAttributedString, savePaymentMethodViewModel: SavePaymentMethodViewModel?, initialSavePaymentMethod: Bool, isSafeDeal: Bool @@ -73,19 +73,17 @@ extension BankCardRepeatPresenter: BankCardRepeatViewOutput { guard let view = view else { return } view.showActivity() - interactor.startAnalyticsService() 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: .recurringCard, - sdkVersion: Bundle.frameworkVersion + guard let self = self else { return } + + self.interactor.track(event: + .screenPaymentContract( + scheme: .recurringCard, + currentAuthType: self.interactor.analyticsAuthType() + ) ) - interactor.trackEvent(event) - interactor.fetchPaymentMethods() + self.interactor.fetchPaymentMethods() } } @@ -232,23 +230,13 @@ extension BankCardRepeatPresenter: BankCardRepeatInteractorOutput { if let savePaymentMethodViewModel = self.savePaymentMethodViewModel { view.setSavePaymentMethodViewModel(savePaymentMethodViewModel) } - - DispatchQueue.global().async { [weak self] in - let event: AnalyticsEvent = .screenRecurringCardForm( - sdkVersion: Bundle.frameworkVersion - ) - self?.interactor.trackEvent(event) - } } } func didFailFetchPaymentMethod(_ error: Error) { - let event = AnalyticsEvent.screenError( - authType: .withoutAuth, - scheme: .recurringCard, - sdkVersion: Bundle.frameworkVersion + interactor.track( + event: .screenErrorContract(scheme: .recurringCard, currentAuthType: interactor.analyticsAuthType()) ) - interactor.trackEvent(event) let message = makeMessage(error) @@ -260,29 +248,26 @@ extension BankCardRepeatPresenter: BankCardRepeatInteractorOutput { } func didTokenize(_ tokens: Tokens) { - interactor.stopAnalyticsService() - moduleOutput?.tokenizationModule( - self, - didTokenize: tokens, - paymentMethodType: .bankCard - ) + moduleOutput?.tokenizationModule(self, didTokenize: tokens, paymentMethodType: .bankCard) DispatchQueue.global().async { [weak self] in - guard let self = self, let interactor = self.interactor else { return } - let type = interactor.makeTypeAnalyticsParameters() - let event: AnalyticsEvent = .actionTokenize( - scheme: .recurringCard, - authType: type.authType, - tokenType: type.tokenType, - sdkVersion: Bundle.frameworkVersion + guard let self = self else { return } + self.interactor.track(event: + .actionTokenize( + scheme: .recurringCard, + currentAuthType: self.interactor.analyticsAuthType() + ) ) - interactor.trackEvent(event) } } func didFailTokenize(_ error: Error) { let message = makeMessage(error) + interactor.track( + event: .screenErrorContract(scheme: .recurringCard, currentAuthType: interactor.analyticsAuthType()) + ) + DispatchQueue.main.async { [weak self] in guard let view = self?.view else { return } view.hideActivity() @@ -311,6 +296,10 @@ extension BankCardRepeatPresenter: BankCardRepeatInteractorOutput { func didFetchPaymentMethods(_ error: Error) { let message = makeMessage(error) + interactor.track( + event: .screenErrorContract(scheme: .recurringCard, currentAuthType: interactor.analyticsAuthType()) + ) + DispatchQueue.main.async { [weak self] in guard let view = self?.view else { return } view.hideActivity() @@ -331,6 +320,9 @@ extension BankCardRepeatPresenter: BankCardRepeatInteractorOutput { return } + interactor.track( + event: .actionTryTokenize(scheme: .recurringCard, currentAuthType: interactor.analyticsAuthType()) + ) interactor.tokenize( amount: paymentOption.charge.plain, confirmation: confirmation, @@ -375,8 +367,7 @@ extension BankCardRepeatPresenter: TokenizationModuleInput { let moduleInputData = CardSecModuleInputData( requestUrl: requestUrl, redirectUrl: returnUrl ?? GlobalConstants.returnUrl, - isLoggingEnabled: isLoggingEnabled, - isConfirmation: false + isLoggingEnabled: isLoggingEnabled ) DispatchQueue.main.async { [weak self] in guard let self = self else { return } @@ -394,8 +385,7 @@ extension BankCardRepeatPresenter: TokenizationModuleInput { let moduleInputData = CardSecModuleInputData( requestUrl: confirmationUrl, redirectUrl: returnUrl ?? GlobalConstants.returnUrl, - isLoggingEnabled: isLoggingEnabled, - isConfirmation: true + isLoggingEnabled: isLoggingEnabled ) DispatchQueue.main.async { [weak self] in guard let self = self else { return } @@ -410,19 +400,8 @@ extension BankCardRepeatPresenter: TokenizationModuleInput { // MARK: - CardSecModuleOutput extension BankCardRepeatPresenter: CardSecModuleOutput { - func didSuccessfullyPassedCardSec( - on module: CardSecModuleInput, - isConfirmation: Bool - ) { - if isConfirmation { - moduleOutput?.didSuccessfullyConfirmation( - paymentMethodType: .bankCard - ) - } else { - moduleOutput?.didSuccessfullyPassedCardSec( - on: self - ) - } + func didSuccessfullyPassedCardSec(on module: CardSecModuleInput) { + moduleOutput?.didSuccessfullyConfirmation(paymentMethodType: .bankCard) } func didPressCloseButton(on module: CardSecModuleInput) { diff --git a/YooKassaPayments/Private/Modules/BankCardRepeat/Router/BankCardRepeatRouter.swift b/YooKassaPayments/Private/Modules/BankCardRepeat/Router/BankCardRepeatRouter.swift index 7f588250..1e2f2f78 100644 --- a/YooKassaPayments/Private/Modules/BankCardRepeat/Router/BankCardRepeatRouter.swift +++ b/YooKassaPayments/Private/Modules/BankCardRepeat/Router/BankCardRepeatRouter.swift @@ -8,6 +8,7 @@ final class BankCardRepeatRouter { extension BankCardRepeatRouter: BankCardRepeatRouterInput { func presentTermsOfServiceModule(_ url: URL) { + guard url.scheme == "http" || url.scheme == "https" else { return } let viewController = SFSafariViewController(url: url) viewController.modalPresentationStyle = .overFullScreen transitionHandler?.present( diff --git a/YooKassaPayments/Private/Modules/BankCardRepeat/View/BankCardRepeatViewController.swift b/YooKassaPayments/Private/Modules/BankCardRepeat/View/BankCardRepeatViewController.swift index 61820ccc..12896224 100644 --- a/YooKassaPayments/Private/Modules/BankCardRepeat/View/BankCardRepeatViewController.swift +++ b/YooKassaPayments/Private/Modules/BankCardRepeat/View/BankCardRepeatViewController.swift @@ -112,7 +112,6 @@ final class BankCardRepeatViewController: UIViewController, PlaceholderProvider submitButton.translatesAutoresizingMaskIntoConstraints = false view.addSubview(submitButton) let defaultHeight = submitButton.heightAnchor.constraint(equalToConstant: Space.triple * 2) - defaultHeight.priority = .defaultLow + 1 NSLayoutConstraint.activate([ submitButton.leadingAnchor.constraint(equalTo: view.leadingAnchor), submitButton.topAnchor.constraint(equalTo: view.topAnchor), @@ -324,7 +323,7 @@ final class BankCardRepeatViewController: UIViewController, PlaceholderProvider ) } - scrollViewHeightConstraint.priority = .defaultLow + scrollViewHeightConstraint.priority = .defaultHigh + 1 let constraints = [ scrollViewHeightConstraint, @@ -451,11 +450,7 @@ extension BankCardRepeatViewController: BankCardRepeatViewInput { maskedCardView.cardNumber = viewModel.cardMask maskedCardView.cardLogo = viewModel.cardLogo - termsOfServiceLinkedTextView.attributedText = makeTermsOfService( - viewModel.terms, - font: UIFont.dynamicCaption2, - foregroundColor: UIColor.AdaptiveColors.secondary - ) + termsOfServiceLinkedTextView.attributedText = viewModel.terms safeDealLinkedTextView.attributedText = viewModel.safeDealText safeDealLinkedTextView.isHidden = viewModel.safeDealText?.string.isEmpty ?? true termsOfServiceLinkedTextView.textAlignment = .center @@ -522,33 +517,6 @@ extension BankCardRepeatViewController: BankCardRepeatViewInput { + price.currency } - private 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 - } - private func makeSavePaymentMethodAttributedString( text: String, hyperText: String, diff --git a/YooKassaPayments/Private/Modules/BankCardRepeat/View/ViewModel/BankCardRepeatViewModel.swift b/YooKassaPayments/Private/Modules/BankCardRepeat/View/ViewModel/BankCardRepeatViewModel.swift index d45ef6c3..33cab2a1 100644 --- a/YooKassaPayments/Private/Modules/BankCardRepeat/View/ViewModel/BankCardRepeatViewModel.swift +++ b/YooKassaPayments/Private/Modules/BankCardRepeat/View/ViewModel/BankCardRepeatViewModel.swift @@ -7,6 +7,6 @@ struct BankCardRepeatViewModel { let fee: PriceViewModel? let cardMask: String let cardLogo: UIImage - let terms: TermsOfService + let terms: NSAttributedString let safeDealText: NSAttributedString? } diff --git a/YooKassaPayments/Private/Modules/CardSettings/Assembly/CardSettingsAssembly.swift b/YooKassaPayments/Private/Modules/CardSettings/Assembly/CardSettingsAssembly.swift index 0151a832..13bd58ff 100644 --- a/YooKassaPayments/Private/Modules/CardSettings/Assembly/CardSettingsAssembly.swift +++ b/YooKassaPayments/Private/Modules/CardSettings/Assembly/CardSettingsAssembly.swift @@ -5,7 +5,9 @@ enum CardSettingsAssembly { let view = CardSettingsViewController(nibName: nil, bundle: nil) let presenter = CardSettingsPresenter( data: data, - paymentMethodViewModelFactory: PaymentMethodViewModelFactoryAssembly.makeFactory() + paymentMethodViewModelFactory: PaymentMethodViewModelFactoryAssembly.makeFactory( + isLoggingEnabled: data.isLoggingEnabled + ) ) let interactor = CardSettingsInteractor( clientId: data.clientId, @@ -14,7 +16,7 @@ enum CardSettingsAssembly { testModeSettings: data.testModeSettings, isLoggingEnabled: data.isLoggingEnabled ), - analyticsService: AnalyticsServiceAssembly.makeService(isLoggingEnabled: data.isLoggingEnabled) + analyticsService: AnalyticsTrackingAssembly.make(isLoggingEnabled: data.isLoggingEnabled) ) let router = CardSettingsRouter(transitionHandler: view) diff --git a/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsInteractor.swift b/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsInteractor.swift index aff5361b..28b9687b 100644 --- a/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsInteractor.swift +++ b/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsInteractor.swift @@ -3,18 +3,18 @@ import Foundation class CardSettingsInteractor: CardSettingsInteractorInput { var output: CardSettingsInteractorOutput! - private let analyticsService: AnalyticsService + private let analyticsService: AnalyticsTracking private let paymentService: PaymentService private let clientId: String - init(clientId: String, paymentService: PaymentService, analyticsService: AnalyticsService) { + init(clientId: String, paymentService: PaymentService, analyticsService: AnalyticsTracking) { self.analyticsService = analyticsService self.clientId = clientId self.paymentService = paymentService } func track(event: AnalyticsEvent) { - analyticsService.trackEvent(event) + analyticsService.track(event: event) } func unbind(id: String) { diff --git a/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsPresenter.swift b/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsPresenter.swift index 108ee661..dc181357 100644 --- a/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsPresenter.swift +++ b/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsPresenter.swift @@ -27,14 +27,14 @@ final class CardSettingsPresenter: CardSettingsViewOutput, CardSettingsInteracto canUnbind = false cardMaskHint = PaymentMethodResources.Localized.yooMoneyCard view.hideSubmit(true) - interactor.track(event: AnalyticsEvent.screenUnbindCard(cardType: .wallet)) + interactor.track(event: .screenUnbindCard(cardType: .wallet)) case .card(let name, _): displayName = name cardTitle = PaymentMethodResources.Localized.linkedCard canUnbind = true cardMaskHint = PaymentMethodResources.Localized.bankCard view.hideSubmit(false) - interactor.track(event: AnalyticsEvent.screenUnbindCard(cardType: .bankCard)) + interactor.track(event: .screenUnbindCard(cardType: .bankCard)) } view.set( @@ -73,7 +73,7 @@ final class CardSettingsPresenter: CardSettingsViewOutput, CardSettingsInteracto title: CommonLocalized.CardSettingsDetails.unbindInfoTitle, details: CommonLocalized.CardSettingsDetails.unbindInfoDetails ) - interactor.track(event: .screenDetailsUnbindWalletCard(sdkVersion: Bundle.frameworkVersion)) + interactor.track(event: .screenDetailsUnbindWalletCard) case .card: router.openInfo( title: CommonLocalized.CardSettingsDetails.autopayInfoTitle, @@ -85,7 +85,7 @@ final class CardSettingsPresenter: CardSettingsViewOutput, CardSettingsInteracto // MARK: - CardSettingsInteractorOutput func didFailUnbind(error: Error, id: String) { - interactor.track(event: .actionUnbindBankCard(actionUnbindCardStatus: .fail)) + interactor.track(event: .actionUnbindBankCard(success: false)) DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.view.hideActivity() @@ -97,7 +97,7 @@ final class CardSettingsPresenter: CardSettingsViewOutput, CardSettingsInteracto } func didUnbind(id: String) { - interactor.track(event: .actionUnbindBankCard(actionUnbindCardStatus: .success)) + interactor.track(event: .actionUnbindBankCard(success: true)) DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.view.enableSubmit() diff --git a/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsViewController.swift b/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsViewController.swift index f9f631ca..7155ff85 100644 --- a/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsViewController.swift +++ b/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsViewController.swift @@ -70,8 +70,13 @@ final class CardSettingsViewController: UIViewController, CardSettingsViewInput output.setupView() } + // TODO: - fix layout race condition; remove this constraint + private lazy var contentHeight = self.view.heightAnchor.constraint(equalToConstant: 400) + private func setupConstraints() { + NSLayoutConstraint.activate([ + contentHeight, contentContainer.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), contentContainer.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), view.layoutMarginsGuide.trailingAnchor.constraint(equalTo: contentContainer.trailingAnchor), @@ -127,6 +132,8 @@ final class CardSettingsViewController: UIViewController, CardSettingsViewInput func hideSubmit(_ hide: Bool) { submitButton.isHidden = hide + // TODO: - fix layout race condition; remove this constraint + contentHeight.constant = hide ? 300 : 400 } } diff --git a/YooKassaPayments/Private/Modules/LinkedCard/Assembly/LinkedCardAssembly.swift b/YooKassaPayments/Private/Modules/LinkedCard/Assembly/LinkedCardAssembly.swift index 82995d40..c8b154dd 100644 --- a/YooKassaPayments/Private/Modules/LinkedCard/Assembly/LinkedCardAssembly.swift +++ b/YooKassaPayments/Private/Modules/LinkedCard/Assembly/LinkedCardAssembly.swift @@ -9,7 +9,7 @@ enum LinkedCardAssembly { let cardService = CardService() let paymentMethodViewModelFactory = - PaymentMethodViewModelFactoryAssembly.makeFactory() + PaymentMethodViewModelFactoryAssembly.makeFactory(isLoggingEnabled: inputData.isLoggingEnabled) let presenter = LinkedCardPresenter( cardService: cardService, paymentMethodViewModelFactory: paymentMethodViewModelFactory, @@ -35,22 +35,16 @@ enum LinkedCardAssembly { testModeSettings: inputData.testModeSettings, moneyAuthClientId: inputData.moneyAuthClientId ) - let analyticsService = AnalyticsServiceAssembly.makeService( - isLoggingEnabled: inputData.isLoggingEnabled - ) + let analyticsService = AnalyticsTrackingAssembly.make(isLoggingEnabled: inputData.isLoggingEnabled) let paymentService = PaymentServiceAssembly.makeService( tokenizationSettings: inputData.tokenizationSettings, testModeSettings: inputData.testModeSettings, isLoggingEnabled: inputData.isLoggingEnabled ) - let analyticsProvider = AnalyticsProviderAssembly.makeProvider( - testModeSettings: inputData.testModeSettings - ) let threatMetrixService = ThreatMetrixServiceFactory.makeService() let interactor = LinkedCardInteractor( authorizationService: authorizationService, analyticsService: analyticsService, - analyticsProvider: analyticsProvider, paymentService: paymentService, threatMetrixService: threatMetrixService, clientApplicationKey: inputData.clientApplicationKey, diff --git a/YooKassaPayments/Private/Modules/LinkedCard/Interactor/LinkedCardInteractor.swift b/YooKassaPayments/Private/Modules/LinkedCard/Interactor/LinkedCardInteractor.swift index f57b5cdf..47e6d33a 100644 --- a/YooKassaPayments/Private/Modules/LinkedCard/Interactor/LinkedCardInteractor.swift +++ b/YooKassaPayments/Private/Modules/LinkedCard/Interactor/LinkedCardInteractor.swift @@ -9,8 +9,7 @@ final class LinkedCardInteractor { // MARK: - Init data private let authorizationService: AuthorizationService - private let analyticsService: AnalyticsService - private let analyticsProvider: AnalyticsProvider + private let analyticsService: AnalyticsTracking private let paymentService: PaymentService private let threatMetrixService: ThreatMetrixService @@ -21,8 +20,7 @@ final class LinkedCardInteractor { init( authorizationService: AuthorizationService, - analyticsService: AnalyticsService, - analyticsProvider: AnalyticsProvider, + analyticsService: AnalyticsTracking, paymentService: PaymentService, threatMetrixService: ThreatMetrixService, clientApplicationKey: String, @@ -30,7 +28,6 @@ final class LinkedCardInteractor { ) { self.authorizationService = authorizationService self.analyticsService = analyticsService - self.analyticsProvider = analyticsProvider self.paymentService = paymentService self.threatMetrixService = threatMetrixService @@ -42,6 +39,14 @@ final class LinkedCardInteractor { // MARK: - LinkedCardInteractorInput extension LinkedCardInteractor: LinkedCardInteractorInput { + func track(event: AnalyticsEvent) { + analyticsService.track(event: event) + } + + func analyticsAuthType() -> AnalyticsEvent.AuthType { + authorizationService.analyticsAuthType() + } + func loginInWallet( amount: MonetaryAmount, reusableToken: Bool, @@ -112,17 +117,6 @@ extension LinkedCardInteractor: LinkedCardInteractorInput { return authorizationService.hasReusableWalletToken() } - func trackEvent(_ event: AnalyticsEvent) { - analyticsService.trackEvent(event) - } - - func makeTypeAnalyticsParameters() -> ( - authType: AnalyticsEvent.AuthType, - tokenType: AnalyticsEvent.AuthTokenType? - ) { - return analyticsProvider.makeTypeAnalyticsParameters() - } - private func tokenizeWithTMXSessionId( id: String, csc: String, diff --git a/YooKassaPayments/Private/Modules/LinkedCard/LinkedCardInteractorIO.swift b/YooKassaPayments/Private/Modules/LinkedCard/LinkedCardInteractorIO.swift index c9feb37f..4eb1eeba 100644 --- a/YooKassaPayments/Private/Modules/LinkedCard/LinkedCardInteractorIO.swift +++ b/YooKassaPayments/Private/Modules/LinkedCard/LinkedCardInteractorIO.swift @@ -1,4 +1,4 @@ -protocol LinkedCardInteractorInput: AnalyticsTrack, AnalyticsProvider { +protocol LinkedCardInteractorInput { func loginInWallet( amount: MonetaryAmount, reusableToken: Bool, @@ -16,6 +16,9 @@ protocol LinkedCardInteractorInput: AnalyticsTrack, AnalyticsProvider { ) func hasReusableWalletToken() -> Bool + + func track(event: AnalyticsEvent) + func analyticsAuthType() -> AnalyticsEvent.AuthType } protocol LinkedCardInteractorOutput: AnyObject { diff --git a/YooKassaPayments/Private/Modules/LinkedCard/LinkedCardModuleIO.swift b/YooKassaPayments/Private/Modules/LinkedCard/LinkedCardModuleIO.swift index e38aa556..a4601fa0 100644 --- a/YooKassaPayments/Private/Modules/LinkedCard/LinkedCardModuleIO.swift +++ b/YooKassaPayments/Private/Modules/LinkedCard/LinkedCardModuleIO.swift @@ -12,7 +12,7 @@ struct LinkedCardModuleInputData { let price: PriceViewModel let fee: PriceViewModel? let paymentOption: PaymentInstrumentYooMoneyLinkedBankCard - let termsOfService: TermsOfService + let termsOfService: NSAttributedString let returnUrl: String let tmxSessionId: String? let initialSavePaymentMethod: Bool diff --git a/YooKassaPayments/Private/Modules/LinkedCard/Presenter/LinkedCardPresenter.swift b/YooKassaPayments/Private/Modules/LinkedCard/Presenter/LinkedCardPresenter.swift index cc955774..f5ac82ee 100644 --- a/YooKassaPayments/Private/Modules/LinkedCard/Presenter/LinkedCardPresenter.swift +++ b/YooKassaPayments/Private/Modules/LinkedCard/Presenter/LinkedCardPresenter.swift @@ -1,3 +1,4 @@ +import Foundation import YooKassaPaymentsApi final class LinkedCardPresenter { @@ -25,7 +26,7 @@ final class LinkedCardPresenter { private let price: PriceViewModel private let fee: PriceViewModel? private let paymentOption: PaymentInstrumentYooMoneyLinkedBankCard - private let termsOfService: TermsOfService + private let termsOfService: NSAttributedString private let returnUrl: String? private let tmxSessionId: String? private var initialSavePaymentMethod: Bool @@ -46,7 +47,7 @@ final class LinkedCardPresenter { price: PriceViewModel, fee: PriceViewModel?, paymentOption: PaymentInstrumentYooMoneyLinkedBankCard, - termsOfService: TermsOfService, + termsOfService: NSAttributedString, returnUrl: String?, tmxSessionId: String?, initialSavePaymentMethod: Bool, @@ -115,15 +116,13 @@ extension LinkedCardPresenter: LinkedCardViewOutput { DispatchQueue.global().async { [weak self] in guard let self = self else { return } - let form: AnalyticsEvent = .screenLinkedCardForm(sdkVersion: Bundle.frameworkVersion) - self.interactor.trackEvent(form) - let contract = AnalyticsEvent.screenPaymentContract( - authType: self.interactor.makeTypeAnalyticsParameters().authType, - scheme: .bankCard, - sdkVersion: Bundle.frameworkVersion + self.interactor.track(event: + .screenPaymentContract( + scheme: .linkedCard, + currentAuthType: self.interactor.analyticsAuthType() + ) ) - self.interactor.trackEvent(contract) } } @@ -237,6 +236,7 @@ extension LinkedCardPresenter: LinkedCardInteractorOutput { ) { switch response { case .authorized: + interactor.track(event: .actionAuthFinished) tokenize() case let .notAuthorized( authTypeState: authTypeState, @@ -277,13 +277,9 @@ extension LinkedCardPresenter: LinkedCardInteractorOutput { DispatchQueue.global().async { [weak self] in guard let self = self, let interactor = self.interactor else { return } - let (authType, _) = interactor.makeTypeAnalyticsParameters() - let event: AnalyticsEvent = .screenError( - authType: authType, - scheme: .linkedCard, - sdkVersion: Bundle.frameworkVersion + interactor.track( + event: .screenErrorContract(scheme: .linkedCard, currentAuthType: interactor.analyticsAuthType()) ) - interactor.trackEvent(event) } } } @@ -297,20 +293,19 @@ extension LinkedCardPresenter: LinkedCardInteractorOutput { DispatchQueue.global().async { [weak self] in guard let self = self, let interactor = self.interactor else { return } - let type = interactor.makeTypeAnalyticsParameters() - let event: AnalyticsEvent = .actionTokenize( - scheme: .linkedCard, - authType: type.authType, - tokenType: type.tokenType, - sdkVersion: Bundle.frameworkVersion + interactor.track( + event: .actionTokenize( + scheme: .linkedCard, + currentAuthType: self.interactor.analyticsAuthType()) ) - interactor.trackEvent(event) } } func failTokenizeData(_ error: Error) { let message = makeMessage(error) - + interactor.track( + event: .screenErrorContract(scheme: .linkedCard, currentAuthType: interactor.analyticsAuthType()) + ) DispatchQueue.main.async { [weak self] in guard let view = self?.view else { return } view.hideActivity() @@ -321,6 +316,9 @@ extension LinkedCardPresenter: LinkedCardInteractorOutput { private func tokenize() { guard let csc = csc else { return } + interactor.track( + event: .actionTryTokenize(scheme: .linkedCard, currentAuthType: interactor.analyticsAuthType()) + ) interactor.tokenize( id: paymentOption.cardId, csc: csc, diff --git a/YooKassaPayments/Private/Modules/LinkedCard/Router/LinkedCardRouter.swift b/YooKassaPayments/Private/Modules/LinkedCard/Router/LinkedCardRouter.swift index 82c00046..1b4b27c4 100644 --- a/YooKassaPayments/Private/Modules/LinkedCard/Router/LinkedCardRouter.swift +++ b/YooKassaPayments/Private/Modules/LinkedCard/Router/LinkedCardRouter.swift @@ -8,6 +8,7 @@ final class LinkedCardRouter { extension LinkedCardRouter: LinkedCardRouterInput { func presentTermsOfServiceModule(_ url: URL) { + guard url.scheme == "http" || url.scheme == "https" else { return } let viewController = SFSafariViewController(url: url) viewController.modalPresentationStyle = .overFullScreen transitionHandler?.present( diff --git a/YooKassaPayments/Private/Modules/LinkedCard/View/LinkedCardViewController.swift b/YooKassaPayments/Private/Modules/LinkedCard/View/LinkedCardViewController.swift index ffc4d215..ba1db576 100644 --- a/YooKassaPayments/Private/Modules/LinkedCard/View/LinkedCardViewController.swift +++ b/YooKassaPayments/Private/Modules/LinkedCard/View/LinkedCardViewController.swift @@ -112,7 +112,6 @@ final class LinkedCardViewController: UIViewController, PlaceholderProvider { submitButton.translatesAutoresizingMaskIntoConstraints = false view.addSubview(submitButton) let defaultHeight = submitButton.heightAnchor.constraint(equalToConstant: Space.triple * 2) - defaultHeight.priority = .defaultLow + 1 NSLayoutConstraint.activate([ submitButton.leadingAnchor.constraint(equalTo: view.leadingAnchor), submitButton.topAnchor.constraint(equalTo: view.topAnchor), @@ -126,6 +125,7 @@ final class LinkedCardViewController: UIViewController, PlaceholderProvider { private let termsOfServiceLinkedTextView: LinkedTextView = { let view = LinkedTextView() + view.setContentCompressionResistancePriority(.required, for: .vertical) view.tintColor = CustomizationStorage.shared.mainScheme view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) return view @@ -133,6 +133,7 @@ final class LinkedCardViewController: UIViewController, PlaceholderProvider { private let safeDealLinkedTextView: LinkedTextView = { let view = LinkedTextView() + view.setContentCompressionResistancePriority(.required, for: .vertical) view.tintColor = CustomizationStorage.shared.mainScheme view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) return view @@ -218,7 +219,7 @@ final class LinkedCardViewController: UIViewController, PlaceholderProvider { private lazy var scrollViewHeightConstraint: NSLayoutConstraint = { let constraint = scrollView.heightAnchor.constraint(equalToConstant: 0) - constraint.priority = .defaultLow + constraint.priority = .defaultHigh + 1 return constraint }() @@ -371,7 +372,7 @@ final class LinkedCardViewController: UIViewController, PlaceholderProvider { } private func fixTableViewHeight() { - scrollViewHeightConstraint.constant = contentStackView.bounds.height + scrollViewHeightConstraint.constant = ceil(scrollView.contentSize.height) + Space.triple * 2 } // MARK: - Action @@ -422,11 +423,7 @@ extension LinkedCardViewController: LinkedCardViewInput { maskedCardView.cardNumber = viewModel.cardMask maskedCardView.cardLogo = viewModel.cardLogo - termsOfServiceLinkedTextView.attributedText = makeTermsOfService( - viewModel.terms, - font: UIFont.dynamicCaption2, - foregroundColor: UIColor.AdaptiveColors.secondary - ) + termsOfServiceLinkedTextView.attributedText = viewModel.terms safeDealLinkedTextView.isHidden = viewModel.safeDealText?.string.isEmpty ?? true safeDealLinkedTextView.attributedText = viewModel.safeDealText termsOfServiceLinkedTextView.textAlignment = .center @@ -472,33 +469,6 @@ extension LinkedCardViewController: LinkedCardViewInput { + price.fractionalPart + price.currency } - - private 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 - } } // MARK: - ActivityIndicatorFullViewPresenting diff --git a/YooKassaPayments/Private/Modules/LinkedCard/View/ViewModel/LinkedCardViewModel.swift b/YooKassaPayments/Private/Modules/LinkedCard/View/ViewModel/LinkedCardViewModel.swift index e8b75da2..dd5ea239 100644 --- a/YooKassaPayments/Private/Modules/LinkedCard/View/ViewModel/LinkedCardViewModel.swift +++ b/YooKassaPayments/Private/Modules/LinkedCard/View/ViewModel/LinkedCardViewModel.swift @@ -7,6 +7,6 @@ struct LinkedCardViewModel { let fee: PriceViewModel? let cardMask: String let cardLogo: UIImage - let terms: TermsOfService + let terms: NSAttributedString let safeDealText: NSAttributedString? } diff --git a/YooKassaPayments/Private/Modules/LoadingViewController.swift b/YooKassaPayments/Private/Modules/LoadingViewController.swift new file mode 100644 index 00000000..a45d069b --- /dev/null +++ b/YooKassaPayments/Private/Modules/LoadingViewController.swift @@ -0,0 +1,41 @@ +import UIKit + +class LoadingViewController: UIViewController, PlaceholderProvider, ActivityIndicatorPresenting { + var reloadHandler: (() -> Void)? + + // MARK: - PlaceholderProvider + + lazy var placeholderView: PlaceholderView = { + $0.setStyles(UIView.Styles.defaultBackground) + $0.translatesAutoresizingMaskIntoConstraints = false + $0.contentView = self.actionTitleTextDialog + return $0 + }(PlaceholderView()) + + private lazy var actionTitleTextDialog: ActionTitleTextDialog = { + $0.tintColor = CustomizationStorage.shared.mainScheme + $0.setStyles(ActionTitleTextDialog.Styles.fail) + $0.text = CommonLocalized.PlaceholderView.text + $0.buttonTitle = CommonLocalized.PlaceholderView.buttonTitle + $0.delegate = self + return $0 + }(ActionTitleTextDialog()) +} + +// MARK: - ActivityIndicatorFullViewPresenting + +extension LoadingViewController: ActivityIndicatorFullViewPresenting { + func showActivity() { + showFullViewActivity(style: ActivityIndicatorView.Styles.heavyLight) + } + + func hideActivity() { + hideFullViewActivity() + } +} + +extension LoadingViewController: ActionTitleTextDialogDelegate { + func didPressButton(in actionTitleTextDialog: ActionTitleTextDialog) { + reloadHandler?() + } +} diff --git a/YooKassaPayments/Private/Modules/PaymentAuthorization/Assembly/PaymentAuthorizationAssembly.swift b/YooKassaPayments/Private/Modules/PaymentAuthorization/Assembly/PaymentAuthorizationAssembly.swift index 7f98afe2..9a2d33b6 100644 --- a/YooKassaPayments/Private/Modules/PaymentAuthorization/Assembly/PaymentAuthorizationAssembly.swift +++ b/YooKassaPayments/Private/Modules/PaymentAuthorization/Assembly/PaymentAuthorizationAssembly.swift @@ -19,16 +19,10 @@ enum PaymentAuthorizationAssembly { testModeSettings: inputData.testModeSettings, moneyAuthClientId: inputData.moneyAuthClientId ) - let analyticsService = AnalyticsServiceAssembly.makeService( - isLoggingEnabled: inputData.isLoggingEnabled - ) - let analyticsProvider = AnalyticsProviderAssembly.makeProvider( - testModeSettings: inputData.testModeSettings - ) + let analyticsService = AnalyticsTrackingAssembly.make(isLoggingEnabled: inputData.isLoggingEnabled) let interactor = PaymentAuthorizationInteractor( authorizationService: authorizationService, analyticsService: analyticsService, - analyticsProvider: analyticsProvider, clientApplicationKey: inputData.clientApplicationKey ) diff --git a/YooKassaPayments/Private/Modules/PaymentAuthorization/Interactor/PaymentAuthorizationInteractor.swift b/YooKassaPayments/Private/Modules/PaymentAuthorization/Interactor/PaymentAuthorizationInteractor.swift index 0bdeb638..126144c3 100644 --- a/YooKassaPayments/Private/Modules/PaymentAuthorization/Interactor/PaymentAuthorizationInteractor.swift +++ b/YooKassaPayments/Private/Modules/PaymentAuthorization/Interactor/PaymentAuthorizationInteractor.swift @@ -9,21 +9,18 @@ final class PaymentAuthorizationInteractor { // MARK: - Init data private let authorizationService: AuthorizationService - private let analyticsService: AnalyticsService - private let analyticsProvider: AnalyticsProvider + private let analyticsService: AnalyticsTracking private let clientApplicationKey: String // MARK: - Init init( authorizationService: AuthorizationService, - analyticsService: AnalyticsService, - analyticsProvider: AnalyticsProvider, + analyticsService: AnalyticsTracking, clientApplicationKey: String ) { self.authorizationService = authorizationService self.analyticsService = analyticsService - self.analyticsProvider = analyticsProvider self.clientApplicationKey = clientApplicationKey } } @@ -78,13 +75,12 @@ extension PaymentAuthorizationInteractor: PaymentAuthorizationInteractorInput { return authorizationService.getWalletPhoneTitle() } - func trackEvent(_ event: AnalyticsEvent) { - analyticsService.trackEvent(event) + func track(event: AnalyticsEvent) { + analyticsService.track(event: event) } - func makeTypeAnalyticsParameters() -> (authType: AnalyticsEvent.AuthType, - tokenType: AnalyticsEvent.AuthTokenType?) { - return analyticsProvider.makeTypeAnalyticsParameters() + func analyticsAuthType() -> AnalyticsEvent.AuthType { + authorizationService.analyticsAuthType() } } diff --git a/YooKassaPayments/Private/Modules/PaymentAuthorization/PaymentAuthorizationInteractorIO.swift b/YooKassaPayments/Private/Modules/PaymentAuthorization/PaymentAuthorizationInteractorIO.swift index 0fb5cdee..0e3688cd 100644 --- a/YooKassaPayments/Private/Modules/PaymentAuthorization/PaymentAuthorizationInteractorIO.swift +++ b/YooKassaPayments/Private/Modules/PaymentAuthorization/PaymentAuthorizationInteractorIO.swift @@ -1,9 +1,6 @@ /// Interactor input protocol -protocol PaymentAuthorizationInteractorInput: AnalyticsTrack, AnalyticsProvider { - func resendCode( - authContextId: String, - authType: AuthType - ) +protocol PaymentAuthorizationInteractorInput { + func resendCode(authContextId: String, authType: AuthType) func checkUserAnswer( authContextId: String, @@ -13,11 +10,13 @@ protocol PaymentAuthorizationInteractorInput: AnalyticsTrack, AnalyticsProvider ) func getWalletPhoneTitle() -> String? + + func track(event: AnalyticsEvent) + func analyticsAuthType() -> AnalyticsEvent.AuthType } /// Interactor output protocol protocol PaymentAuthorizationInteractorOutput: AnyObject { - func didResendCode(authTypeState: AuthTypeState) func didFailResendCode(_ error: Error) diff --git a/YooKassaPayments/Private/Modules/PaymentAuthorization/Presenter/PaymentAuthorizationPresenter.swift b/YooKassaPayments/Private/Modules/PaymentAuthorization/Presenter/PaymentAuthorizationPresenter.swift index 30391920..2a1af0ca 100644 --- a/YooKassaPayments/Private/Modules/PaymentAuthorization/Presenter/PaymentAuthorizationPresenter.swift +++ b/YooKassaPayments/Private/Modules/PaymentAuthorization/Presenter/PaymentAuthorizationPresenter.swift @@ -188,14 +188,13 @@ extension PaymentAuthorizationPresenter: PaymentAuthorizationInteractorOutput { authTypeState: AuthTypeState ) { DispatchQueue.main.async { [weak self] in - guard let self = self, - let view = self.view else { return } - view.clearCode() + guard let self = self, let view = self.view else { return } + view.hideActivity() view.setCodeError(nil) + self.restartTimer(authTypeState: authTypeState) self.setupDescription() - view.hideActivity() + view.clearCode() self.authTypeState = authTypeState - self.restartTimer(authTypeState: authTypeState) } } @@ -216,40 +215,28 @@ extension PaymentAuthorizationPresenter: PaymentAuthorizationInteractorOutput { DispatchQueue.global().async { [weak self] in guard let self = self, let interactor = self.interactor else { return } - let (authType, _) = interactor.makeTypeAnalyticsParameters() - let event: AnalyticsEvent = .screenError( - authType: authType, - scheme: self.tokenizeScheme, - sdkVersion: Bundle.frameworkVersion + interactor.track(event: + .screenError( + scheme: self.tokenizeScheme, + currentAuthType: interactor.analyticsAuthType() + ) ) - interactor.trackEvent(event) } } } - func didCheckUserAnswer( - _ response: WalletLoginResponse - ) { - moduleOutput?.didCheckUserAnswer( - self, - response: response - ) + func didCheckUserAnswer(_ response: WalletLoginResponse) { + moduleOutput?.didCheckUserAnswer(self, response: response) if case .authorized = response { DispatchQueue.global().async { [weak self] in guard let interactor = self?.interactor else { return } - let event: AnalyticsEvent = .actionPaymentAuthorization( - authPaymentStatus: .success, - sdkVersion: Bundle.frameworkVersion - ) - interactor.trackEvent(event) + interactor.track(event: .actionPaymentAuthorization(success: true)) } } } - func didFailCheckUserAnswer( - _ error: Error - ) { + func didFailCheckUserAnswer(_ error: Error) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.view?.clearCode() @@ -258,11 +245,7 @@ extension PaymentAuthorizationPresenter: PaymentAuthorizationInteractorOutput { DispatchQueue.global().async { [weak self] in guard let interactor = self?.interactor else { return } - let event: AnalyticsEvent = .actionPaymentAuthorization( - authPaymentStatus: .fail, - sdkVersion: Bundle.frameworkVersion - ) - interactor.trackEvent(event) + interactor.track(event: .actionPaymentAuthorization(success: false)) } } } diff --git a/YooKassaPayments/Private/Modules/PaymentAuthorization/View/PaymentAuthorizationViewController.swift b/YooKassaPayments/Private/Modules/PaymentAuthorization/View/PaymentAuthorizationViewController.swift index e19c4101..4712dad1 100644 --- a/YooKassaPayments/Private/Modules/PaymentAuthorization/View/PaymentAuthorizationViewController.swift +++ b/YooKassaPayments/Private/Modules/PaymentAuthorization/View/PaymentAuthorizationViewController.swift @@ -13,6 +13,7 @@ final class PaymentAuthorizationViewController: UIViewController, PlaceholderPro }() private lazy var titleLabel: UILabel = { + $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.translatesAutoresizingMaskIntoConstraints = false $0.styledText = Localized.smsCodePlaceholder $0.setStyles( @@ -30,6 +31,7 @@ final class PaymentAuthorizationViewController: UIViewController, PlaceholderPro private lazy var codeErrorLabel: UILabel = { $0.translatesAutoresizingMaskIntoConstraints = false + $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.setStyles( UILabel.DynamicStyle.caption2, UILabel.ColorStyle.alert, @@ -41,6 +43,7 @@ final class PaymentAuthorizationViewController: UIViewController, PlaceholderPro private lazy var descriptionLabel: UILabel = { $0.translatesAutoresizingMaskIntoConstraints = false + $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.setStyles( UILabel.DynamicStyle.body, UILabel.ColorStyle.secondary, @@ -85,25 +88,31 @@ final class PaymentAuthorizationViewController: UIViewController, PlaceholderPro // MARK: - Constraints private lazy var descriptionLabelTopConstraint = descriptionLabel.topAnchor.constraint( - equalTo: codeControl.bottomAnchor, - constant: Space.quadruple + equalTo: codeErrorLabel.bottomAnchor, + constant: Space.double ) - private lazy var resendButtonBottomConstraint = resendCodeButton.bottomAnchor.constraint( - equalTo: bottomLayoutGuide.topAnchor, - constant: -Space.double + private lazy var resendButtonBottomConstraint = view.layoutMarginsGuide.bottomAnchor.constraint( + equalTo: resendCodeButton.bottomAnchor, + constant: Space.triple * 2 ) private lazy var placeholderViewBottomConstraint = placeholderView.bottomAnchor.constraint( - equalTo: bottomLayoutGuide.topAnchor + equalTo: view.layoutMarginsGuide.topAnchor, + constant: -Space.fivefold ) // MARK: - Managing the View + private lazy var defaultViewHeight = view.heightAnchor.constraint(equalToConstant: 320) + override func loadView() { view = UIView() view.setStyles(UIView.Styles.grayBackground) + defaultViewHeight.priority = .defaultHigh + 1 + defaultViewHeight.isActive = true + setupView() setupConstraints() @@ -124,7 +133,9 @@ final class PaymentAuthorizationViewController: UIViewController, PlaceholderPro override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - _ = codeControl.becomeFirstResponder() + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) { [weak self] in + _ = self?.codeControl.becomeFirstResponder() + } } override func viewWillDisappear(_ animated: Bool) { @@ -150,22 +161,30 @@ final class PaymentAuthorizationViewController: UIViewController, PlaceholderPro private func setupConstraints() { let topConstraint: NSLayoutConstraint + if shouldShowTitleOnNavBar { + defaultViewHeight.constant = 220 + } if #available(iOS 11.0, *) { if shouldShowTitleOnNavBar { topConstraint = codeControl.topAnchor.constraint( equalTo: view.safeAreaLayoutGuide.topAnchor, - constant: 2 * Space.triple + constant: Space.double ) } else { topConstraint = titleLabel.topAnchor.constraint( equalTo: view.safeAreaLayoutGuide.topAnchor, constant: Space.single / 4 ) + + codeControl.topAnchor.constraint( + equalTo: titleLabel.bottomAnchor, + constant: Space.quadruple + ).isActive = true } resendButtonBottomConstraint = resendCodeButton.bottomAnchor.constraint( equalTo: view.safeAreaLayoutGuide.bottomAnchor, - constant: -Space.double + constant: -Space.quadruple ) placeholderViewBottomConstraint = placeholderView.bottomAnchor.constraint( equalTo: view.safeAreaLayoutGuide.bottomAnchor @@ -173,22 +192,26 @@ final class PaymentAuthorizationViewController: UIViewController, PlaceholderPro } else { if shouldShowTitleOnNavBar { topConstraint = codeControl.topAnchor.constraint( - equalTo: topLayoutGuide.bottomAnchor, - constant: 2 * Space.triple + equalTo: view.layoutMarginsGuide.topAnchor, + constant: Space.double ) } else { topConstraint = titleLabel.topAnchor.constraint( - equalTo: topLayoutGuide.bottomAnchor, + equalTo: view.layoutMarginsGuide.topAnchor, constant: Space.single / 4 ) + codeControl.topAnchor.constraint( + equalTo: titleLabel.bottomAnchor, + constant: Space.quadruple + ).isActive = true } resendButtonBottomConstraint = resendCodeButton.bottomAnchor.constraint( - equalTo: bottomLayoutGuide.topAnchor, - constant: -Space.double + equalTo: view.layoutMarginsGuide.bottomAnchor, + constant: -Space.quadruple ) placeholderViewBottomConstraint = placeholderView.bottomAnchor.constraint( - equalTo: bottomLayoutGuide.topAnchor + equalTo: view.layoutMarginsGuide.bottomAnchor ) } @@ -198,7 +221,7 @@ final class PaymentAuthorizationViewController: UIViewController, PlaceholderPro codeErrorLabel.topAnchor.constraint( equalTo: codeControl.bottomAnchor, - constant: Space.double + constant: Space.single ), codeErrorLabel.leadingAnchor.constraint( equalTo: view.leadingAnchor, @@ -228,6 +251,7 @@ final class PaymentAuthorizationViewController: UIViewController, PlaceholderPro constant: -Space.double ), resendButtonBottomConstraint, + resendCodeButton.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: Space.single), placeholderView.topAnchor.constraint(equalTo: view.topAnchor), placeholderView.leadingAnchor.constraint(equalTo: view.leadingAnchor), @@ -245,11 +269,6 @@ final class PaymentAuthorizationViewController: UIViewController, PlaceholderPro equalTo: view.trailingAnchor, constant: -Space.double ), - - codeControl.topAnchor.constraint( - equalTo: titleLabel.bottomAnchor, - constant: 2 * Space.triple - ), ] } @@ -275,17 +294,15 @@ extension PaymentAuthorizationViewController: KeyboardObserver { updateBottomConstraint(keyboardInfo) } - func keyboardDidShow(with keyboardInfo: KeyboardNotificationInfo) {} + func keyboardDidShow(with keyboardInfo: KeyboardNotificationInfo) { + view.setNeedsUpdateConstraints() + } func keyboardDidHide(with keyboardInfo: KeyboardNotificationInfo) {} func keyboardDidUpdateFrame(_ keyboardFrame: CGRect) {} private func updateBottomConstraint( _ keyboardInfo: KeyboardNotificationInfo ) { - guard let keyboardOffset = keyboardYOffset(from: keyboardInfo.endKeyboardFrame) else { - return - } - let duration = keyboardInfo.animationDuration ?? 0.3 var options: UIView.AnimationOptions = [] @@ -297,10 +314,7 @@ extension PaymentAuthorizationViewController: KeyboardObserver { withDuration: duration, delay: 0, options: options, - animations: { [weak self] in - guard let self = self else { return } - self.resendButtonBottomConstraint.constant = -keyboardOffset - Space.double - self.placeholderViewBottomConstraint.constant = -keyboardOffset - Space.double + animations: { self.view.layoutIfNeeded() }, completion: nil @@ -330,18 +344,16 @@ extension PaymentAuthorizationViewController: PaymentAuthorizationViewInput { func setCodeLength(_ length: Int) { codeControl.setLength(length) - _ = codeControl.becomeFirstResponder() } func clearCode() { codeControl.clear() + codeControl.setIsEditable(true) + _ = codeControl.becomeFirstResponder() } func setCodeError(_ error: String?) { codeErrorLabel.styledText = error - descriptionLabelTopConstraint.constant = error == nil - ? Space.quadruple - : Space.triple * 2 } func setDescription(_ description: String) { @@ -397,12 +409,14 @@ extension PaymentAuthorizationViewController: FixedLengthCodeControlDelegate { extension PaymentAuthorizationViewController { func showActivity() { codeControl.setIsEditable(false) - showFullViewActivity(style: ActivityIndicatorView.Styles.cloudy) + showFullViewActivity(style: ActivityIndicatorView.Styles.heavyLight) } func hideActivity() { hideFullViewActivity() - codeControl.setIsEditable(true) + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) { [weak self] in + self?.codeControl.clear() + } } } diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/Assembly/PaymentMethodsAssembly.swift b/YooKassaPayments/Private/Modules/PaymentMethods/Assembly/PaymentMethodsAssembly.swift index dfbdae49..8db135bb 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/Assembly/PaymentMethodsAssembly.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/Assembly/PaymentMethodsAssembly.swift @@ -19,7 +19,9 @@ enum PaymentMethodsAssembly { let moneyAuthCustomization = MoneyAuthAssembly.makeMoneyAuthCustomization() - let paymentMethodViewModelFactory = PaymentMethodViewModelFactoryAssembly.makeFactory() + let paymentMethodViewModelFactory = PaymentMethodViewModelFactoryAssembly.makeFactory( + isLoggingEnabled: inputData.isLoggingEnabled + ) let priceViewModelFactory = PriceViewModelFactoryAssembly.makeFactory() let presenter = PaymentMethodsPresenter( isLogoVisible: inputData.tokenizationSettings.showYooKassaLogo, @@ -40,7 +42,8 @@ enum PaymentMethodsAssembly { savePaymentMethod: inputData.savePaymentMethod, userPhoneNumber: inputData.userPhoneNumber, cardScanning: inputData.cardScanning, - customerId: inputData.customerId + customerId: inputData.customerId, + config: inputData.config ) let paymentService = PaymentServiceAssembly.makeService( @@ -53,15 +56,10 @@ enum PaymentMethodsAssembly { testModeSettings: inputData.testModeSettings, moneyAuthClientId: inputData.moneyAuthClientId ) - let analyticsService = AnalyticsServiceAssembly.makeService( - isLoggingEnabled: inputData.isLoggingEnabled - ) + let analyticsService = AnalyticsTrackingAssembly.make(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( @@ -73,10 +71,10 @@ enum PaymentMethodsAssembly { authorizationService: authorizationService, analyticsService: analyticsService, accountService: accountService, - analyticsProvider: analyticsProvider, threatMetrixService: threatMetrixService, amountNumberFormatter: amountNumberFormatter, appDataTransferMediator: appDataTransferMediator, + configMediator: ConfigMediatorAssembly.make(isLoggingEnabled: inputData.isLoggingEnabled), 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 23eed4dd..fc3d93c4 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/Interactor/PaymentMethodsInteractor.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/Interactor/PaymentMethodsInteractor.swift @@ -12,12 +12,12 @@ class PaymentMethodsInteractor { private let paymentService: PaymentService private let authorizationService: AuthorizationService - private let analyticsService: AnalyticsService + private let analyticsService: AnalyticsTracking private let accountService: AccountService - private let analyticsProvider: AnalyticsProvider private let threatMetrixService: ThreatMetrixService private let amountNumberFormatter: AmountNumberFormatter private let appDataTransferMediator: AppDataTransferMediator + private let configMediator: ConfigMediator private let clientApplicationKey: String private let gatewayId: String? @@ -30,12 +30,12 @@ class PaymentMethodsInteractor { init( paymentService: PaymentService, authorizationService: AuthorizationService, - analyticsService: AnalyticsService, + analyticsService: AnalyticsTracking, accountService: AccountService, - analyticsProvider: AnalyticsProvider, threatMetrixService: ThreatMetrixService, amountNumberFormatter: AmountNumberFormatter, appDataTransferMediator: AppDataTransferMediator, + configMediator: ConfigMediator, clientApplicationKey: String, gatewayId: String?, amount: Amount, @@ -46,10 +46,10 @@ class PaymentMethodsInteractor { self.authorizationService = authorizationService self.analyticsService = analyticsService self.accountService = accountService - self.analyticsProvider = analyticsProvider self.threatMetrixService = threatMetrixService self.amountNumberFormatter = amountNumberFormatter self.appDataTransferMediator = appDataTransferMediator + self.configMediator = configMediator self.clientApplicationKey = clientApplicationKey self.gatewayId = gatewayId @@ -73,8 +73,8 @@ extension PaymentMethodsInteractor: PaymentMethodsInteractorInput { } } - func fetchPaymentMethods() { - let authorizationToken = authorizationService.getMoneyCenterAuthToken() + func fetchShop() { + let authorizationToken = self.authorizationService.getMoneyCenterAuthToken() paymentService.fetchPaymentOptions( clientApplicationKey: clientApplicationKey, @@ -86,11 +86,12 @@ extension PaymentMethodsInteractor: PaymentMethodsInteractorInput { customerId: customerId ) { [weak self] result in guard let output = self?.output else { return } + switch result { case let .success(data): output.didFetchShop(data) case let .failure(error): - output.didFailFetchShop(error) + output.didFailFetchShop(mapError(error)) } } } @@ -160,21 +161,12 @@ extension PaymentMethodsInteractor: PaymentMethodsInteractorInput { authorizationService.setWalletAvatarURL(account.avatar.url?.absoluteString) } - func trackEvent(_ event: AnalyticsEvent) { - analyticsService.trackEvent(event) - } - - func makeTypeAnalyticsParameters() -> (authType: AnalyticsEvent.AuthType, - tokenType: AnalyticsEvent.AuthTokenType?) { - return analyticsProvider.makeTypeAnalyticsParameters() - } - - func startAnalyticsService() { - analyticsService.start() + func track(event: AnalyticsEvent) { + analyticsService.track(event: event) } - func stopAnalyticsService() { - analyticsService.stop() + func analyticsAuthType() -> AnalyticsEvent.AuthType { + authorizationService.analyticsAuthType() } } @@ -276,6 +268,8 @@ private func mapError(_ error: Error) -> Error { return PaymentProcessingError.internetConnection case let error as NSError where error.domain == NSURLErrorDomain: return PaymentProcessingError.internetConnection + case let error as NSError where error.code == 429: + return TooManyRequestsError() default: return error } diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsInteractorIO.swift b/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsInteractorIO.swift index 0b7a094e..4e5742c2 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsInteractorIO.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsInteractorIO.swift @@ -1,15 +1,15 @@ import MoneyAuth import YooKassaPaymentsApi -protocol PaymentMethodsInteractorInput: AnalyticsTrack, AnalyticsProvider { - func fetchPaymentMethods() +protocol PaymentMethodsInteractorInput { + func fetchShop() func fetchYooMoneyPaymentMethods(moneyCenterAuthToken: String) func fetchAccount(oauthToken: String) func decryptCryptogram(_ cryptogram: String) func getWalletDisplayName() -> String? func setAccount(_ account: UserAccount) - func startAnalyticsService() - func stopAnalyticsService() + func track(event: AnalyticsEvent) + func analyticsAuthType() -> AnalyticsEvent.AuthType // MARK: - Apple Pay Tokenize diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsModuleIO.swift b/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsModuleIO.swift index 7f230aae..de94cbda 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsModuleIO.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsModuleIO.swift @@ -18,6 +18,7 @@ struct PaymentMethodsModuleInputData { let userPhoneNumber: String? let cardScanning: CardScanning? let customerId: String? + let config: Config } protocol PaymentMethodsModuleInput: SheetViewModuleOutput { diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsViewIO.swift b/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsViewIO.swift index 4786126b..7292f63c 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsViewIO.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsViewIO.swift @@ -2,7 +2,7 @@ import UIKit.UIImage protocol PaymentMethodsViewInput: ActivityIndicatorFullViewPresenting, NotificationPresenting { func reloadData() - func setLogoVisible(_ isVisible: Bool) + func setLogoVisible(image: UIImage, isVisible: Bool) func showPlaceholder(message: String) func hidePlaceholder() diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/Presenter/PaymentMethodsPresenter.swift b/YooKassaPayments/Private/Modules/PaymentMethods/Presenter/PaymentMethodsPresenter.swift index ca0aa9ef..268d6032 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/Presenter/PaymentMethodsPresenter.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/Presenter/PaymentMethodsPresenter.swift @@ -49,6 +49,7 @@ final class PaymentMethodsPresenter: NSObject { private let userPhoneNumber: String? private let cardScanning: CardScanning? private let customerId: String? + private let config: Config // MARK: - Init @@ -71,7 +72,8 @@ final class PaymentMethodsPresenter: NSObject { savePaymentMethod: SavePaymentMethod, userPhoneNumber: String?, cardScanning: CardScanning?, - customerId: String? + customerId: String?, + config: Config ) { self.isLogoVisible = isLogoVisible self.paymentMethodViewModelFactory = paymentMethodViewModelFactory @@ -95,6 +97,7 @@ final class PaymentMethodsPresenter: NSObject { self.userPhoneNumber = userPhoneNumber self.cardScanning = cardScanning self.customerId = customerId + self.config = config } // MARK: - Stored properties @@ -105,8 +108,9 @@ final class PaymentMethodsPresenter: NSObject { private var shop: Shop? private var viewModel: (models: [PaymentMethodViewModel], indexMap: ([Int: Int])) = ([], [:]) - private lazy var termsOfService: TermsOfService = { - TermsOfServiceFactory.makeTermsOfService() + private lazy var termsOfService: NSAttributedString = { + let html = ConfigMediatorAssembly.make(isLoggingEnabled: isLoggingEnabled).storedConfig().userAgreementUrl + return HTMLUtils.highlightHyperlinks(html: html) }() private var shouldReloadOnViewDidAppear = false @@ -128,11 +132,11 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { func setupView() { guard let view = view else { return } view.showActivity() - view.setLogoVisible(isLogoVisible) - interactor.startAnalyticsService() + let logo = paymentMethodViewModelFactory.yooLogoImage() + view.setLogoVisible(image: logo, isVisible: isLogoVisible) DispatchQueue.global().async { [weak self] in - self?.interactor.fetchPaymentMethods() + self?.interactor.fetchShop() } } @@ -141,7 +145,7 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { shouldReloadOnViewDidAppear = false DispatchQueue.global().async { [weak self] in guard let interactor = self?.interactor else { return } - interactor.fetchPaymentMethods() + interactor.fetchShop() } } @@ -263,8 +267,7 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { openYooMoneyAuthorization() case let paymentOption where paymentOption.paymentMethodType == .sberbank: - if shouldOpenSberpay(paymentOption), - let returnUrl = makeSberpayReturnUrl() { + if shouldOpenSberpay(paymentOption), let returnUrl = makeSberpayReturnUrl() { openSberpayModule( paymentOption: paymentOption, clientSavePaymentMethod: savePaymentMethod, @@ -318,23 +321,16 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { customization: moneyAuthCustomization, output: self ) - let event = AnalyticsEvent.userStartAuthorization( - sdkVersion: Bundle.frameworkVersion - ) - interactor.trackEvent(event) + interactor.track(event: .userStartAuthorization) } 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() + self?.interactor.fetchShop() } } - - let event = AnalyticsEvent.userCancelAuthorization( - sdkVersion: Bundle.frameworkVersion - ) - interactor.trackEvent(event) + interactor.track(event: .userCancelAuthorization) } } @@ -375,7 +371,8 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { initialSavePaymentMethod: initialSavePaymentMethod, isBackBarButtonHidden: needReplace, customerId: customerId, - isSafeDeal: isSafeDeal + isSafeDeal: isSafeDeal, + paymentOptionTitle: config.paymentMethods.first { $0.kind == .yoomoney }?.title ) router?.presentYooMoney( inputData: inputData, @@ -452,7 +449,8 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { initialSavePaymentMethod: initialSavePaymentMethod, isBackBarButtonHidden: needReplace, customerId: customerId, - isSafeDeal: isSafeDeal + isSafeDeal: isSafeDeal, + paymentOptionTitle: config.paymentMethods.first { $0.kind == .applePay }?.title ) router.presentApplePayContractModule( inputData: inputData, @@ -498,7 +496,8 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { isBackBarButtonHidden: needReplace, customerId: customerId, isSafeDeal: isSafeDeal, - clientSavePaymentMethod: savePaymentMethod + clientSavePaymentMethod: savePaymentMethod, + config: config ) router.openSberbankModule( inputData: inputData, @@ -530,7 +529,8 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { returnUrl: returnUrl, isBackBarButtonHidden: needReplace, customerId: customerId, - isSafeDeal: isSafeDeal + isSafeDeal: isSafeDeal, + config: config ) router.openSberpayModule( inputData: inputData, @@ -588,7 +588,8 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { isBackBarButtonHidden: needReplace, customerId: customerId, instrument: instrument, - isSafeDeal: isSafeDeal + isSafeDeal: isSafeDeal, + config: config ) router.openBankCardModule( inputData: inputData, @@ -689,7 +690,7 @@ extension PaymentMethodsPresenter: ActionTitleTextDialogDelegate { DispatchQueue.global().async { [weak self] in guard let self = self else { return } - self.interactor.fetchPaymentMethods() + self.interactor.fetchShop() } } } @@ -729,7 +730,7 @@ extension PaymentMethodsPresenter: PaymentMethodsModuleInput { extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { func didUnbindCard(id: String) { - interactor.fetchPaymentMethods() + interactor.fetchShop() DispatchQueue.main.async { self.unbindCompletion?(true) self.unbindCompletion = nil @@ -746,12 +747,7 @@ extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { } func didFetchShop(_ shop: Shop) { - let (authType, _) = interactor.makeTypeAnalyticsParameters() - let event: AnalyticsEvent = .screenPaymentOptions( - authType: authType, - sdkVersion: Bundle.frameworkVersion - ) - interactor.trackEvent(event) + interactor.track(event: .screenPaymentOptions(currentAuthType: interactor.analyticsAuthType())) DispatchQueue.main.async { [weak self] in guard let self = self, let view = self.view else { return } @@ -790,7 +786,7 @@ extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { } func didFailFetchShop(_ error: Error) { - presentError(error) + presentError(error, savePaymentMethod: nil) } func didFetchYooMoneyPaymentMethods(_ paymentMethods: [PaymentOption], shopProperties: ShopProperties) { @@ -808,22 +804,19 @@ extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { self.shouldReloadOnViewDidAppear = true } } else if paymentMethods.contains(where: condition) == false { - let event: AnalyticsEvent = .actionAuthWithoutWallet( - sdkVersion: Bundle.frameworkVersion - ) - interactor.trackEvent(event) - interactor.fetchPaymentMethods() + interactor.track(event: .actionAuthWithoutWallet) + interactor.fetchShop() DispatchQueue.main.async { [weak self] in guard let view = self?.view else { return } view.presentError(with: Localized.Error.noWalletTitle) } } else { - interactor.fetchPaymentMethods() + interactor.fetchShop() } } func didFetchYooMoneyPaymentMethods(_ error: Error) { - presentError(error) + presentError(error, savePaymentMethod: nil) } func didFetchAccount(_ account: UserAccount) { @@ -847,23 +840,13 @@ extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { 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.track(event: .actionMoneyAuthLogin(scheme: .yoomoneyApp, status: .success)) 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) + interactor.track(event: .actionMoneyAuthLogin(scheme: .yoomoneyApp, status: .fail(error))) DispatchQueue.main.async { [weak self] in guard let view = self?.view else { return } view.hideActivity() @@ -878,14 +861,11 @@ extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { applePayCompletion?(.success) - let parameters = interactor.makeTypeAnalyticsParameters() - let event: AnalyticsEvent = .actionTokenize( - scheme: .applePay, - authType: parameters.authType, - tokenType: parameters.tokenType, - sdkVersion: Bundle.frameworkVersion + interactor.track(event: + .actionTokenize( + scheme: .applePay, + currentAuthType: interactor.analyticsAuthType()) ) - interactor.trackEvent(event) DispatchQueue.main.asyncAfter( deadline: .now() + Constants.dismissApplePayTimeout @@ -902,11 +882,12 @@ extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { } func failTokenizeApplePay(_ error: Error) { - guard applePayState == .success else { - return - } + guard applePayState == .success else { return } - trackScreenErrorAnalytics(scheme: .applePay) + let savePaymentMethod: Bool? = shop?.options + .first { $0.paymentMethodType == .applePay } + .map { $0.savePaymentMethod == .allowed } + trackScreenErrorAnalytics(scheme: .applePay, savePaymentMethod: savePaymentMethod) applePayCompletion?(.failure) DispatchQueue.main.asyncAfter( @@ -927,7 +908,7 @@ extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { } } - private func presentError(_ error: Error) { + private func presentError(_ error: Error, savePaymentMethod: Bool?) { let message: String switch error { @@ -942,33 +923,37 @@ extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { view.hideActivity() view.showPlaceholder(message: message) - self.trackScreenErrorAnalytics(scheme: nil) + self.trackScreenErrorAnalytics(scheme: nil, savePaymentMethod: savePaymentMethod) } } - private func trackScreenErrorAnalytics(scheme: AnalyticsEvent.TokenizeScheme?) { + private func trackScreenErrorAnalytics( + scheme: AnalyticsEvent.TokenizeScheme?, + savePaymentMethod: Bool? + ) { DispatchQueue.global().async { [weak self] in guard let interactor = self?.interactor else { return } - let (authType, _) = interactor.makeTypeAnalyticsParameters() - let event: AnalyticsEvent = .screenError( - authType: authType, - scheme: scheme, - sdkVersion: Bundle.frameworkVersion + interactor.track(event: + .screenError( + scheme: scheme, + currentAuthType: interactor.analyticsAuthType() + ) ) - interactor.trackEvent(event) } } - private func trackScreenPaymentAnalytics(scheme: AnalyticsEvent.TokenizeScheme) { + private func trackScreenPaymentAnalytics( + scheme: AnalyticsEvent.TokenizeScheme, + savePaymentMethod: Bool? + ) { DispatchQueue.global().async { [weak self] in guard let interactor = self?.interactor else { return } - let (authType, _) = interactor.makeTypeAnalyticsParameters() - let event: AnalyticsEvent = .screenPaymentContract( - authType: authType, - scheme: scheme, - sdkVersion: Bundle.frameworkVersion + interactor.track(event: + .screenPaymentContract( + scheme: scheme, + currentAuthType: interactor.analyticsAuthType() + ) ) - interactor.trackEvent(event) } } @@ -984,27 +969,23 @@ extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { self.didTokenize(tokens: tokens, paymentMethodType: .bankCard, scheme: scheme) DispatchQueue.global().async { [weak self] in - guard let self = self, let interactor = self.interactor else { return } - let type = interactor.makeTypeAnalyticsParameters() - let event: AnalyticsEvent = .actionTokenize( - scheme: scheme, - authType: type.authType, - tokenType: type.tokenType, - sdkVersion: Bundle.frameworkVersion + guard let self = self else { return } + self.interactor.track(event: + .actionTokenize( + scheme: scheme, + currentAuthType: self.interactor.analyticsAuthType()) ) - interactor.trackEvent(event) } } } func didFailTokenizeInstrument(error: Error) { - let parameters = interactor.makeTypeAnalyticsParameters() - let event: AnalyticsEvent = .screenError( - authType: parameters.authType, - scheme: .bankCard, - sdkVersion: Bundle.frameworkVersion + interactor.track( + event: .screenError( + scheme: .bankCard, + currentAuthType: interactor.analyticsAuthType() + ) ) - interactor.trackEvent(event) let message: String switch error { @@ -1054,23 +1035,14 @@ extension PaymentMethodsPresenter: AuthorizationCoordinatorDelegate { moneyCenterAuthToken: token ) - let event: AnalyticsEvent = .actionMoneyAuthLogin( - scheme: .moneyAuthSdk, - status: .success, - sdkVersion: Bundle.frameworkVersion - ) - self.interactor.trackEvent(event) + self.interactor.track(event: .actionMoneyAuthLogin(scheme: .moneyAuthSdk, status: .success)) } } } func authorizationCoordinatorDidCancel(_ coordinator: AuthorizationCoordinator) { self.moneyAuthCoordinator = nil - - let event = AnalyticsEvent.userCancelAuthorization( - sdkVersion: Bundle.frameworkVersion - ) - interactor.trackEvent(event) + interactor.track(event: .userCancelAuthorization) DispatchQueue.main.async { [weak self] in guard let self = self else { return } @@ -1085,20 +1057,14 @@ extension PaymentMethodsPresenter: AuthorizationCoordinatorDelegate { func authorizationCoordinator(_ coordinator: AuthorizationCoordinator, didFailureWith error: Error) { self.moneyAuthCoordinator = nil - - let event: AnalyticsEvent = .actionMoneyAuthLogin( - scheme: .moneyAuthSdk, - status: .fail(error.localizedDescription), - sdkVersion: Bundle.frameworkVersion - ) - interactor.trackEvent(event) + interactor.track(event: .actionMoneyAuthLogin(scheme: .moneyAuthSdk, status: .fail(error))) DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.router.closeAuthorizationModule() self.view?.showActivity() DispatchQueue.global().async { [weak self] in - self?.interactor.fetchPaymentMethods() + self?.interactor.fetchShop() } } } @@ -1136,7 +1102,7 @@ extension PaymentMethodsPresenter: YooMoneyModuleOutput { self.router.closeYooMoneyModule() self.view?.showActivity() DispatchQueue.global().async { [weak self] in - self?.interactor.fetchPaymentMethods() + self?.interactor.fetchShop() } } } @@ -1177,12 +1143,18 @@ extension PaymentMethodsPresenter: LinkedCardModuleOutput { extension PaymentMethodsPresenter: ApplePayModuleOutput { func didPresentApplePayModule() { applePayState = .idle - trackScreenPaymentAnalytics(scheme: .applePay) + let savePaymentMethod = shop?.options + .first { $0.paymentMethodType == .applePay } + .map { $0.savePaymentMethod == .allowed } + trackScreenPaymentAnalytics(scheme: .applePay, savePaymentMethod: savePaymentMethod) } func didFailPresentApplePayModule() { applePayState = .idle - trackScreenErrorAnalytics(scheme: .applePay) + let savePaymentMethod = shop?.options + .first { $0.paymentMethodType == .applePay } + .map { $0.savePaymentMethod == .allowed } + trackScreenErrorAnalytics(scheme: .applePay, savePaymentMethod: savePaymentMethod) DispatchQueue.main.async { [weak self] in guard let self = self, @@ -1321,8 +1293,7 @@ extension PaymentMethodsPresenter: TokenizationModuleInput { let inputData = CardSecModuleInputData( requestUrl: requestUrl, redirectUrl: GlobalConstants.returnUrl, - isLoggingEnabled: isLoggingEnabled, - isConfirmation: false + isLoggingEnabled: isLoggingEnabled ) DispatchQueue.main.async { [weak self] in guard let self = self else { return } @@ -1355,8 +1326,7 @@ extension PaymentMethodsPresenter: TokenizationModuleInput { let inputData = CardSecModuleInputData( requestUrl: confirmationUrl, redirectUrl: GlobalConstants.returnUrl, - isLoggingEnabled: isLoggingEnabled, - isConfirmation: true + isLoggingEnabled: isLoggingEnabled ) DispatchQueue.main.async { [weak self] in guard let self = self else { return } @@ -1372,20 +1342,8 @@ extension PaymentMethodsPresenter: TokenizationModuleInput { // MARK: - CardSecModuleOutput extension PaymentMethodsPresenter: CardSecModuleOutput { - func didSuccessfullyPassedCardSec( - on module: CardSecModuleInput, - isConfirmation: Bool - ) { - interactor.stopAnalyticsService() - if isConfirmation { - tokenizationModuleOutput?.didSuccessfullyConfirmation( - paymentMethodType: .bankCard - ) - } else { - tokenizationModuleOutput?.didSuccessfullyPassedCardSec( - on: self - ) - } + func didSuccessfullyPassedCardSec(on module: CardSecModuleInput) { + tokenizationModuleOutput?.didSuccessfullyConfirmation(paymentMethodType: .bankCard) } func didPressCloseButton( @@ -1422,12 +1380,13 @@ extension PaymentMethodsPresenter: CardSettingsModuleOutput { DispatchQueue.main.async { self.view?.present(notification) self.view?.showActivity() - self.view?.setLogoVisible(self.isLogoVisible) + let logo = self.paymentMethodViewModelFactory.yooLogoImage() + self.view?.setLogoVisible(image: logo, isVisible: self.isLogoVisible) self.router.closeCardSettingsModule() } DispatchQueue.global().async { [weak self] in - self?.interactor.fetchPaymentMethods() + self?.interactor.fetchShop() } } } @@ -1440,7 +1399,6 @@ private extension PaymentMethodsPresenter { paymentMethodType: PaymentMethodType, scheme: AnalyticsEvent.TokenizeScheme ) { - interactor.stopAnalyticsService() DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.tokenizationModuleOutput?.tokenizationModule( @@ -1451,15 +1409,8 @@ private extension PaymentMethodsPresenter { } } - func didFinish( - module: TokenizationModuleInput, - error: YooKassaPaymentsError? - ) { - interactor.stopAnalyticsService() - tokenizationModuleOutput?.didFinish( - on: module, - with: error - ) + func didFinish(module: TokenizationModuleInput, error: YooKassaPaymentsError?) { + tokenizationModuleOutput?.didFinish(on: module, with: error) } } diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/View/PaymentMethodsViewController.swift b/YooKassaPayments/Private/Modules/PaymentMethods/View/PaymentMethodsViewController.swift index e44f297c..39bf9f23 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/View/PaymentMethodsViewController.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/View/PaymentMethodsViewController.swift @@ -108,6 +108,7 @@ final class PaymentMethodsViewController: UIViewController, PlaceholderProvider private func setupNavigationBar() { guard let navigationBar = navigationController?.navigationBar else { return } navigationBar.shadowImage = UIImage() + navigationBar.isTranslucent = false navigationBar.barTintColor = UIColor.AdaptiveColors.systemBackground navigationBar.tintColor = CustomizationStorage.shared.mainScheme @@ -250,13 +251,18 @@ extension PaymentMethodsViewController: PaymentMethodsViewInput { tableView.reloadData() } - func setLogoVisible(_ isVisible: Bool) { + func setLogoVisible(image: UIImage, isVisible: Bool) { guard isVisible else { navigationItem.rightBarButtonItem = nil return } - let image = UIImageView(image: Resources.kassaLogo) - let rightItem = UIBarButtonItem(customView: image) + let imageView = UIImageView(image: image) + imageView.contentMode = .scaleAspectFit + NSLayoutConstraint.activate([ + imageView.heightAnchor.constraint(equalToConstant: Space.double), + imageView.widthAnchor.constraint(lessThanOrEqualToConstant: 104), + ]) + let rightItem = UIBarButtonItem(customView: imageView) navigationItem.rightBarButtonItem = rightItem } @@ -354,10 +360,6 @@ private extension PaymentMethodsViewController { comment: "Текст кнопки отвязать " ) } - - enum Resources { - static let kassaLogo = UIImage.localizedImage("image.logo") - } } // MARK: - Constants diff --git a/YooKassaPayments/Private/Modules/Sberbank/Assembly/SberbankAssembly.swift b/YooKassaPayments/Private/Modules/Sberbank/Assembly/SberbankAssembly.swift index 8889535a..3a0fe51e 100644 --- a/YooKassaPayments/Private/Modules/Sberbank/Assembly/SberbankAssembly.swift +++ b/YooKassaPayments/Private/Modules/Sberbank/Assembly/SberbankAssembly.swift @@ -16,23 +16,25 @@ enum SberbankAssembly { isBackBarButtonHidden: inputData.isBackBarButtonHidden, isSafeDeal: inputData.isSafeDeal, clientSavePaymentMethod: inputData.clientSavePaymentMethod, - isSavePaymentMethodAllowed: inputData.paymentOption.savePaymentMethod == .allowed + isSavePaymentMethodAllowed: inputData.paymentOption.savePaymentMethod == .allowed, + config: inputData.config ) 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 analyticsService = AnalyticsTrackingAssembly.make(isLoggingEnabled: inputData.isLoggingEnabled) let threatMetrixService = ThreatMetrixServiceFactory.makeService() + let authService = AuthorizationServiceAssembly.makeService( + isLoggingEnabled: inputData.isLoggingEnabled, + testModeSettings: inputData.testModeSettings, + moneyAuthClientId: nil + ) let interactor = SberbankInteractor( + authService: authService, paymentService: paymentService, - analyticsProvider: analyticsProvider, analyticsService: analyticsService, threatMetrixService: threatMetrixService, clientApplicationKey: inputData.clientApplicationKey, diff --git a/YooKassaPayments/Private/Modules/Sberbank/Interactor/SberbankInteractor.swift b/YooKassaPayments/Private/Modules/Sberbank/Interactor/SberbankInteractor.swift index 9734d252..28ab19cc 100644 --- a/YooKassaPayments/Private/Modules/Sberbank/Interactor/SberbankInteractor.swift +++ b/YooKassaPayments/Private/Modules/Sberbank/Interactor/SberbankInteractor.swift @@ -8,25 +8,25 @@ final class SberbankInteractor { // MARK: - Init + private let authService: AuthorizationService private let paymentService: PaymentService - private let analyticsProvider: AnalyticsProvider - private let analyticsService: AnalyticsService + private let analyticsService: AnalyticsTracking private let threatMetrixService: ThreatMetrixService private let clientApplicationKey: String private let amount: MonetaryAmount private let customerId: String? init( + authService: AuthorizationService, paymentService: PaymentService, - analyticsProvider: AnalyticsProvider, - analyticsService: AnalyticsService, + analyticsService: AnalyticsTracking, threatMetrixService: ThreatMetrixService, clientApplicationKey: String, amount: MonetaryAmount, customerId: String? ) { + self.authService = authService self.paymentService = paymentService - self.analyticsProvider = analyticsProvider self.analyticsService = analyticsService self.threatMetrixService = threatMetrixService self.clientApplicationKey = clientApplicationKey @@ -73,15 +73,12 @@ extension SberbankInteractor: SberbankInteractorInput { } } - func makeTypeAnalyticsParameters() -> ( - authType: AnalyticsEvent.AuthType, - tokenType: AnalyticsEvent.AuthTokenType? - ) { - analyticsProvider.makeTypeAnalyticsParameters() + func analyticsAuthType() -> AnalyticsEvent.AuthType { + authService.analyticsAuthType() } - func trackEvent(_ event: AnalyticsEvent) { - analyticsService.trackEvent(event) + func track(event: AnalyticsEvent) { + analyticsService.track(event: event) } } diff --git a/YooKassaPayments/Private/Modules/Sberbank/Presenter/SberbankPresenter.swift b/YooKassaPayments/Private/Modules/Sberbank/Presenter/SberbankPresenter.swift index c8510689..ecb109f1 100644 --- a/YooKassaPayments/Private/Modules/Sberbank/Presenter/SberbankPresenter.swift +++ b/YooKassaPayments/Private/Modules/Sberbank/Presenter/SberbankPresenter.swift @@ -19,7 +19,7 @@ final class SberbankPresenter { private let purchaseDescription: String private let priceViewModel: PriceViewModel private let feeViewModel: PriceViewModel? - private let termsOfService: TermsOfService + private let termsOfService: NSAttributedString private let userPhoneNumber: String? private let isBackBarButtonHidden: Bool private let isSafeDeal: Bool @@ -27,18 +27,20 @@ final class SberbankPresenter { private var recurrencySectionSwitchValue: Bool? private let isSavePaymentMethodAllowed: Bool + private let config: Config init( shopName: String, purchaseDescription: String, priceViewModel: PriceViewModel, feeViewModel: PriceViewModel?, - termsOfService: TermsOfService, + termsOfService: NSAttributedString, userPhoneNumber: String?, isBackBarButtonHidden: Bool, isSafeDeal: Bool, clientSavePaymentMethod: SavePaymentMethod, - isSavePaymentMethodAllowed: Bool + isSavePaymentMethodAllowed: Bool, + config: Config ) { self.shopName = shopName self.purchaseDescription = purchaseDescription @@ -50,6 +52,7 @@ final class SberbankPresenter { self.isSafeDeal = isSafeDeal self.clientSavePaymentMethod = clientSavePaymentMethod self.isSavePaymentMethodAllowed = isSavePaymentMethodAllowed + self.config = config } // MARK: - Stored properties @@ -69,20 +72,24 @@ extension SberbankPresenter: SberbankViewOutput { feeValue = "\(CommonLocalized.Contract.fee) " + makePrice(feeViewModel) } - let termsOfServiceValue = makeTermsOfService( - termsOfService, - font: UIFont.dynamicCaption2, - foregroundColor: UIColor.AdaptiveColors.secondary - ) + let termsOfServiceValue = termsOfService var section: PaymentRecurrencyAndDataSavingSection? if isSavePaymentMethodAllowed { switch clientSavePaymentMethod { case .userSelects: - section = PaymentRecurrencyAndDataSavingSectionFactory.make(mode: .allowRecurring, output: self) + section = PaymentRecurrencyAndDataSavingSectionFactory.make( + mode: .allowRecurring, + texts: config.savePaymentMethodOptionTexts, + output: self + ) recurrencySectionSwitchValue = section?.switchValue case .on: - section = PaymentRecurrencyAndDataSavingSectionFactory.make(mode: .requiredRecurring, output: self) + section = PaymentRecurrencyAndDataSavingSectionFactory.make( + mode: .requiredRecurring, + texts: config.savePaymentMethodOptionTexts, + output: self + ) recurrencySectionSwitchValue = true case .off: section = nil @@ -96,7 +103,8 @@ extension SberbankPresenter: SberbankViewOutput { feeValue: feeValue, termsOfService: termsOfServiceValue, safeDealText: isSafeDeal ? PaymentMethodResources.Localized.safeDealInfoLink : nil, - recurrencyAndDataSavingSection: section + recurrencyAndDataSavingSection: section, + paymentOptionTitle: config.paymentMethods.first { $0.kind == .sberbank }?.title ) view.setViewModel(viewModel) @@ -112,15 +120,13 @@ extension SberbankPresenter: SberbankViewOutput { 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: .smsSbol, - sdkVersion: Bundle.frameworkVersion + guard let self = self else { return } + self.interactor.track(event: + .screenPaymentContract( + scheme: .smsSbol, + currentAuthType: self.interactor.analyticsAuthType() + ) ) - interactor.trackEvent(event) } } @@ -153,33 +159,20 @@ extension SberbankPresenter: SberbankViewOutput { // MARK: - SberbankInteractorOutput extension SberbankPresenter: SberbankInteractorOutput { - func didTokenize( - _ data: Tokens - ) { - let analyticsParameters = interactor.makeTypeAnalyticsParameters() - let event: AnalyticsEvent = .actionTokenize( - scheme: .smsSbol, - authType: analyticsParameters.authType, - tokenType: analyticsParameters.tokenType, - sdkVersion: Bundle.frameworkVersion - ) - interactor.trackEvent(event) - moduleOutput?.sberbankModule( - self, didTokenize: data, - paymentMethodType: .sberbank + func didTokenize(_ data: Tokens) { + interactor.track(event: + .actionTokenize( + scheme: .smsSbol, + currentAuthType: interactor.analyticsAuthType() + ) ) + moduleOutput?.sberbankModule(self, didTokenize: data, paymentMethodType: .sberbank) } - func didFailTokenize( - _ error: Error - ) { - let parameters = interactor.makeTypeAnalyticsParameters() - let event: AnalyticsEvent = .screenError( - authType: parameters.authType, - scheme: .smsSbol, - sdkVersion: Bundle.frameworkVersion + func didFailTokenize(_ error: Error) { + interactor.track( + event: .screenErrorContract(scheme: .smsSbol, currentAuthType: interactor.analyticsAuthType()) ) - interactor.trackEvent(event) let message = makeMessage(error) @@ -201,8 +194,10 @@ extension SberbankPresenter: ActionTitleTextDialogDelegate { view.hidePlaceholder() view.showActivity() DispatchQueue.global().async { [weak self] in - guard let self = self, - let interactor = self.interactor else { return } + guard let self = self, let interactor = self.interactor else { return } + interactor.track( + event: .actionTryTokenize(scheme: .smsSbol, currentAuthType: interactor.analyticsAuthType()) + ) interactor.tokenizeSberbank( phoneNumber: self.phoneNumber, savePaymentMethod: self.recurrencySectionSwitchValue ?? false @@ -265,33 +260,6 @@ private extension SberbankPresenter { + 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 { diff --git a/YooKassaPayments/Private/Modules/Sberbank/Router/SberbankRouter.swift b/YooKassaPayments/Private/Modules/Sberbank/Router/SberbankRouter.swift index e7a950bf..18ddbae8 100644 --- a/YooKassaPayments/Private/Modules/Sberbank/Router/SberbankRouter.swift +++ b/YooKassaPayments/Private/Modules/Sberbank/Router/SberbankRouter.swift @@ -9,6 +9,7 @@ final class SberbankRouter { extension SberbankRouter: SberbankRouterInput { func presentTermsOfServiceModule(_ url: URL) { + guard url.scheme == "http" || url.scheme == "https" else { return } let viewController = SFSafariViewController(url: url) viewController.modalPresentationStyle = .overFullScreen transitionHandler?.present( diff --git a/YooKassaPayments/Private/Modules/Sberbank/SberbankInteractorIO.swift b/YooKassaPayments/Private/Modules/Sberbank/SberbankInteractorIO.swift index 383d5a72..4e771662 100644 --- a/YooKassaPayments/Private/Modules/Sberbank/SberbankInteractorIO.swift +++ b/YooKassaPayments/Private/Modules/Sberbank/SberbankInteractorIO.swift @@ -1,17 +1,11 @@ -protocol SberbankInteractorInput: AnalyticsTrack { +protocol SberbankInteractorInput { func tokenizeSberbank(phoneNumber: String, savePaymentMethod: Bool) - func makeTypeAnalyticsParameters() -> ( - authType: AnalyticsEvent.AuthType, - tokenType: AnalyticsEvent.AuthTokenType? - ) + func analyticsAuthType() -> AnalyticsEvent.AuthType + func track(event: AnalyticsEvent) } protocol SberbankInteractorOutput: AnyObject { - func didTokenize( - _ data: Tokens - ) - func didFailTokenize( - _ error: Error - ) + func didTokenize(_ data: Tokens) + func didFailTokenize(_ error: Error) } diff --git a/YooKassaPayments/Private/Modules/Sberbank/SberbankModuleIO.swift b/YooKassaPayments/Private/Modules/Sberbank/SberbankModuleIO.swift index 2d5d5bc2..35dc0b1a 100644 --- a/YooKassaPayments/Private/Modules/Sberbank/SberbankModuleIO.swift +++ b/YooKassaPayments/Private/Modules/Sberbank/SberbankModuleIO.swift @@ -11,12 +11,13 @@ struct SberbankModuleInputData { let purchaseDescription: String let priceViewModel: PriceViewModel let feeViewModel: PriceViewModel? - let termsOfService: TermsOfService + let termsOfService: NSAttributedString let userPhoneNumber: String? let isBackBarButtonHidden: Bool let customerId: String? let isSafeDeal: Bool let clientSavePaymentMethod: SavePaymentMethod + let config: Config } protocol SberbankModuleOutput: AnyObject { diff --git a/YooKassaPayments/Private/Modules/Sberbank/View/SberbankViewController.swift b/YooKassaPayments/Private/Modules/Sberbank/View/SberbankViewController.swift index 5ab6f5a2..c39a923f 100644 --- a/YooKassaPayments/Private/Modules/Sberbank/View/SberbankViewController.swift +++ b/YooKassaPayments/Private/Modules/Sberbank/View/SberbankViewController.swift @@ -76,7 +76,6 @@ final class SberbankViewController: UIViewController, PlaceholderProvider { submitButton.translatesAutoresizingMaskIntoConstraints = false view.addSubview(submitButton) let defaultHeight = submitButton.heightAnchor.constraint(equalToConstant: Space.triple * 2) - defaultHeight.priority = .defaultLow + 1 NSLayoutConstraint.activate([ submitButton.leadingAnchor.constraint(equalTo: view.leadingAnchor), submitButton.topAnchor.constraint(equalTo: view.topAnchor), @@ -90,6 +89,7 @@ final class SberbankViewController: UIViewController, PlaceholderProvider { private let termsOfServiceLinkedTextView: LinkedTextView = { let view = LinkedTextView() + view.setContentCompressionResistancePriority(.required, for: .vertical) view.tintColor = CustomizationStorage.shared.mainScheme view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) return view @@ -97,6 +97,7 @@ final class SberbankViewController: UIViewController, PlaceholderProvider { private let safeDealLinkedTextView: LinkedTextView = { let view = LinkedTextView() + view.setContentCompressionResistancePriority(.required, for: .vertical) view.tintColor = CustomizationStorage.shared.mainScheme view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) return view @@ -126,7 +127,7 @@ final class SberbankViewController: UIViewController, PlaceholderProvider { private lazy var scrollViewHeightConstraint: NSLayoutConstraint = { let constraint = scrollView.heightAnchor.constraint(equalToConstant: 0) - constraint.priority = .defaultLow + constraint.priority = .defaultHigh + 1 return constraint }() @@ -244,7 +245,7 @@ final class SberbankViewController: UIViewController, PlaceholderProvider { } private func updateContentHeight() { - scrollViewHeightConstraint.constant = contentStackView.bounds.height + scrollViewHeightConstraint.constant = ceil(scrollView.contentSize.height) + Space.triple * 2 } } @@ -285,6 +286,8 @@ extension SberbankViewController: SberbankViewInput { termsOfServiceLinkedTextView.textAlignment = .center safeDealLinkedTextView.textAlignment = .center + viewModel.paymentOptionTitle.map { navigationItem.title = $0 } + if let section = viewModel.recurrencyAndDataSavingSection { contentStackView.addArrangedSubview(section) } diff --git a/YooKassaPayments/Private/Modules/Sberbank/View/ViewModel/SberbankViewModel.swift b/YooKassaPayments/Private/Modules/Sberbank/View/ViewModel/SberbankViewModel.swift index 7de8d137..00e7ae70 100644 --- a/YooKassaPayments/Private/Modules/Sberbank/View/ViewModel/SberbankViewModel.swift +++ b/YooKassaPayments/Private/Modules/Sberbank/View/ViewModel/SberbankViewModel.swift @@ -8,4 +8,5 @@ struct SberbankViewModel { let termsOfService: NSAttributedString let safeDealText: NSAttributedString? let recurrencyAndDataSavingSection: UIView? + let paymentOptionTitle: String? } diff --git a/YooKassaPayments/Private/Modules/Sberpay/Assembly/SberpayAssembly.swift b/YooKassaPayments/Private/Modules/Sberpay/Assembly/SberpayAssembly.swift index 23ac3b2a..de07b2fc 100644 --- a/YooKassaPayments/Private/Modules/Sberpay/Assembly/SberpayAssembly.swift +++ b/YooKassaPayments/Private/Modules/Sberpay/Assembly/SberpayAssembly.swift @@ -16,23 +16,25 @@ enum SberpayAssembly { isBackBarButtonHidden: inputData.isBackBarButtonHidden, isSafeDeal: inputData.isSafeDeal, clientSavePaymentMethod: inputData.clientSavePaymentMethod, - isSavePaymentMethodAllowed: inputData.paymentOption.savePaymentMethod == .allowed + isSavePaymentMethodAllowed: inputData.paymentOption.savePaymentMethod == .allowed, + config: inputData.config ) 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 analyticsService = AnalyticsTrackingAssembly.make(isLoggingEnabled: inputData.isLoggingEnabled) let threatMetrixService = ThreatMetrixServiceFactory.makeService() + let authorizationService = AuthorizationServiceAssembly.makeService( + isLoggingEnabled: inputData.isLoggingEnabled, + testModeSettings: inputData.testModeSettings, + moneyAuthClientId: nil + ) + let interactor = SberpayInteractor( + authService: authorizationService, paymentService: paymentService, - analyticsProvider: analyticsProvider, analyticsService: analyticsService, threatMetrixService: threatMetrixService, clientApplicationKey: inputData.clientApplicationKey, diff --git a/YooKassaPayments/Private/Modules/Sberpay/Interactor/SberpayInteractor.swift b/YooKassaPayments/Private/Modules/Sberpay/Interactor/SberpayInteractor.swift index f97014da..c75caa6b 100644 --- a/YooKassaPayments/Private/Modules/Sberpay/Interactor/SberpayInteractor.swift +++ b/YooKassaPayments/Private/Modules/Sberpay/Interactor/SberpayInteractor.swift @@ -8,9 +8,9 @@ final class SberpayInteractor { // MARK: - Init + private let authService: AuthorizationService private let paymentService: PaymentService - private let analyticsProvider: AnalyticsProvider - private let analyticsService: AnalyticsService + private let analyticsService: AnalyticsTracking private let threatMetrixService: ThreatMetrixService private let clientApplicationKey: String private let amount: MonetaryAmount @@ -18,17 +18,17 @@ final class SberpayInteractor { private let customerId: String? init( + authService: AuthorizationService, paymentService: PaymentService, - analyticsProvider: AnalyticsProvider, - analyticsService: AnalyticsService, + analyticsService: AnalyticsTracking, threatMetrixService: ThreatMetrixService, clientApplicationKey: String, amount: MonetaryAmount, returnUrl: String, customerId: String? ) { + self.authService = authService self.paymentService = paymentService - self.analyticsProvider = analyticsProvider self.analyticsService = analyticsService self.threatMetrixService = threatMetrixService self.clientApplicationKey = clientApplicationKey @@ -43,8 +43,7 @@ final class SberpayInteractor { extension SberpayInteractor: SberpayInteractorInput { func tokenizeSberpay(savePaymentMethod: Bool) { threatMetrixService.profileApp { [weak self] result in - guard let self = self, - let output = self.output else { return } + guard let self = self, let output = self.output else { return } switch result { case let .success(tmxSessionId): @@ -76,15 +75,12 @@ extension SberpayInteractor: SberpayInteractorInput { } } - func makeTypeAnalyticsParameters() -> ( - authType: AnalyticsEvent.AuthType, - tokenType: AnalyticsEvent.AuthTokenType? - ) { - analyticsProvider.makeTypeAnalyticsParameters() + func track(event: AnalyticsEvent) { + analyticsService.track(event: event) } - func trackEvent(_ event: AnalyticsEvent) { - analyticsService.trackEvent(event) + func analyticsAuthType() -> AnalyticsEvent.AuthType { + authService.analyticsAuthType() } } diff --git a/YooKassaPayments/Private/Modules/Sberpay/Presenter/SberpayPresenter.swift b/YooKassaPayments/Private/Modules/Sberpay/Presenter/SberpayPresenter.swift index 255f86aa..0fea98f1 100644 --- a/YooKassaPayments/Private/Modules/Sberpay/Presenter/SberpayPresenter.swift +++ b/YooKassaPayments/Private/Modules/Sberpay/Presenter/SberpayPresenter.swift @@ -15,24 +15,26 @@ final class SberpayPresenter { private let purchaseDescription: String private let priceViewModel: PriceViewModel private let feeViewModel: PriceViewModel? - private let termsOfService: TermsOfService + private let termsOfService: NSAttributedString private let isBackBarButtonHidden: Bool private let isSafeDeal: Bool private let clientSavePaymentMethod: SavePaymentMethod private var recurrencySectionSwitchValue: Bool? private let isSavePaymentMethodAllowed: Bool + private let config: Config init( shopName: String, purchaseDescription: String, priceViewModel: PriceViewModel, feeViewModel: PriceViewModel?, - termsOfService: TermsOfService, + termsOfService: NSAttributedString, isBackBarButtonHidden: Bool, isSafeDeal: Bool, clientSavePaymentMethod: SavePaymentMethod, - isSavePaymentMethodAllowed: Bool + isSavePaymentMethodAllowed: Bool, + config: Config ) { self.shopName = shopName self.purchaseDescription = purchaseDescription @@ -43,6 +45,7 @@ final class SberpayPresenter { self.isSafeDeal = isSafeDeal self.clientSavePaymentMethod = clientSavePaymentMethod self.isSavePaymentMethodAllowed = isSavePaymentMethodAllowed + self.config = config } } @@ -58,20 +61,22 @@ extension SberpayPresenter: SberpayViewOutput { feeValue = "\(CommonLocalized.Contract.fee) " + makePrice(feeViewModel) } - let termsOfServiceValue = makeTermsOfService( - termsOfService, - font: UIFont.dynamicCaption2, - foregroundColor: UIColor.AdaptiveColors.secondary - ) - var section: PaymentRecurrencyAndDataSavingSection? if isSavePaymentMethodAllowed { switch clientSavePaymentMethod { case .userSelects: - section = PaymentRecurrencyAndDataSavingSectionFactory.make(mode: .allowRecurring, output: self) + section = PaymentRecurrencyAndDataSavingSectionFactory.make( + mode: .allowRecurring, + texts: config.savePaymentMethodOptionTexts, + output: self + ) recurrencySectionSwitchValue = section?.switchValue case .on: - section = PaymentRecurrencyAndDataSavingSectionFactory.make(mode: .requiredRecurring, output: self) + section = PaymentRecurrencyAndDataSavingSectionFactory.make( + mode: .requiredRecurring, + texts: config.savePaymentMethodOptionTexts, + output: self + ) recurrencySectionSwitchValue = true case .off: section = nil @@ -83,24 +88,23 @@ extension SberpayPresenter: SberpayViewOutput { description: purchaseDescription, priceValue: priceValue, feeValue: feeValue, - termsOfService: termsOfServiceValue, + termsOfService: termsOfService, safeDealText: isSafeDeal ? PaymentMethodResources.Localized.safeDealInfoLink : nil, - recurrencyAndDataSavingSection: section + recurrencyAndDataSavingSection: section, + paymentOptionTitle: config.paymentMethods.first { $0.kind == .sberbank }?.title ) 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 + guard let self = self else { return } + self.interactor.track(event: + .screenPaymentContract( + scheme: .sberpay, + currentAuthType: self.interactor.analyticsAuthType() + ) ) - interactor.trackEvent(event) } } @@ -128,22 +132,14 @@ extension SberpayPresenter: SberpayViewOutput { // 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 + func didTokenize(_ data: Tokens) { + interactor.track(event: + .actionTokenize( + scheme: .sberpay, + currentAuthType: interactor.analyticsAuthType() + ) ) + moduleOutput?.sberpayModule(self, didTokenize: data, paymentMethodType: .sberbank) DispatchQueue.main.async { [weak self] in guard let view = self?.view else { return } @@ -151,16 +147,10 @@ extension SberpayPresenter: SberpayInteractorOutput { } } - func didFailTokenize( - _ error: Error - ) { - let parameters = interactor.makeTypeAnalyticsParameters() - let event: AnalyticsEvent = .screenError( - authType: parameters.authType, - scheme: .smsSbol, - sdkVersion: Bundle.frameworkVersion + func didFailTokenize(_ error: Error) { + interactor.track( + event: .screenErrorContract(scheme: .sberpay, currentAuthType: interactor.analyticsAuthType()) ) - interactor.trackEvent(event) let message = makeMessage(error) @@ -183,6 +173,9 @@ extension SberpayPresenter: ActionTitleTextDialogDelegate { view.showActivity() DispatchQueue.global().async { [weak self] in guard let self = self, let interactor = self.interactor else { return } + interactor.track( + event: .actionTryTokenize(scheme: .sberpay, currentAuthType: interactor.analyticsAuthType()) + ) interactor.tokenizeSberpay(savePaymentMethod: self.recurrencySectionSwitchValue ?? false) } } @@ -198,23 +191,38 @@ extension SberpayPresenter: PaymentRecurrencyAndDataSavingSectionOutput { switch mode { case .allowRecurring, .requiredRecurring: router.presentSafeDealInfo( - title: CommonLocalized.CardSettingsDetails.autopayInfoTitle, - body: CommonLocalized.CardSettingsDetails.autopayInfoDetails + title: htmlOut(source: config.savePaymentMethodOptionTexts.screenRecurrentOnBindOffTitle), + body: htmlOut(source: config.savePaymentMethodOptionTexts.screenRecurrentOnBindOffText) ) case .savePaymentData, .requiredSaveData: router.presentSafeDealInfo( - title: CommonLocalized.RecurrencyAndSavePaymentData.saveDataInfoTitle, - body: CommonLocalized.RecurrencyAndSavePaymentData.saveDataInfoMessage + title: htmlOut(source: config.savePaymentMethodOptionTexts.screenRecurrentOffBindOnTitle), + body: htmlOut(source: config.savePaymentMethodOptionTexts.screenRecurrentOffBindOnText) ) case .allowRecurringAndSaveData, .requiredRecurringAndSaveData: router.presentSafeDealInfo( - title: CommonLocalized.RecurrencyAndSavePaymentData.saveDataAndAutopaymentsInfoTitle, - body: CommonLocalized.RecurrencyAndSavePaymentData.saveDataAndAutopaymentsInfoMessage + title: htmlOut(source: config.savePaymentMethodOptionTexts.screenRecurrentOnBindOnTitle), + body: htmlOut(source: config.savePaymentMethodOptionTexts.screenRecurrentOnBindOnText) ) default: break } } + + /// Convert
-> \n and other html text formatting to native `String` + private func htmlOut(source: String) -> String { + guard let data = source.data(using: .utf16) else { return source } + do { + let html = try NSAttributedString( + data: data, + options: [.documentType: NSAttributedString.DocumentType.html], + documentAttributes: nil + ) + return html.string + } catch { + return source + } + } } // MARK: - SberpayModuleInput @@ -233,33 +241,6 @@ private extension SberpayPresenter { + 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 { diff --git a/YooKassaPayments/Private/Modules/Sberpay/Router/SberpayRouter.swift b/YooKassaPayments/Private/Modules/Sberpay/Router/SberpayRouter.swift index fc9ed361..c7d775e0 100644 --- a/YooKassaPayments/Private/Modules/Sberpay/Router/SberpayRouter.swift +++ b/YooKassaPayments/Private/Modules/Sberpay/Router/SberpayRouter.swift @@ -9,6 +9,7 @@ final class SberpayRouter { extension SberpayRouter: SberpayRouterInput { func presentTermsOfServiceModule(_ url: URL) { + guard url.scheme == "http" || url.scheme == "https" else { return } let viewController = SFSafariViewController(url: url) viewController.modalPresentationStyle = .overFullScreen transitionHandler?.present( diff --git a/YooKassaPayments/Private/Modules/Sberpay/SberpayInteractorIO.swift b/YooKassaPayments/Private/Modules/Sberpay/SberpayInteractorIO.swift index 79c3ba3a..d6ae4735 100644 --- a/YooKassaPayments/Private/Modules/Sberpay/SberpayInteractorIO.swift +++ b/YooKassaPayments/Private/Modules/Sberpay/SberpayInteractorIO.swift @@ -1,17 +1,11 @@ -protocol SberpayInteractorInput: AnalyticsTrack { +protocol SberpayInteractorInput { func tokenizeSberpay(savePaymentMethod: Bool) - func makeTypeAnalyticsParameters() -> ( - authType: AnalyticsEvent.AuthType, - tokenType: AnalyticsEvent.AuthTokenType? - ) + func track(event: AnalyticsEvent) + func analyticsAuthType() -> AnalyticsEvent.AuthType } protocol SberpayInteractorOutput: AnyObject { - func didTokenize( - _ data: Tokens - ) - func didFailTokenize( - _ error: Error - ) + func didTokenize(_ data: Tokens) + func didFailTokenize(_ error: Error) } diff --git a/YooKassaPayments/Private/Modules/Sberpay/SberpayModuleIO.swift b/YooKassaPayments/Private/Modules/Sberpay/SberpayModuleIO.swift index 1d7b28c8..c5c6a869 100644 --- a/YooKassaPayments/Private/Modules/Sberpay/SberpayModuleIO.swift +++ b/YooKassaPayments/Private/Modules/Sberpay/SberpayModuleIO.swift @@ -12,11 +12,12 @@ struct SberpayModuleInputData { let purchaseDescription: String let priceViewModel: PriceViewModel let feeViewModel: PriceViewModel? - let termsOfService: TermsOfService + let termsOfService: NSAttributedString let returnUrl: String let isBackBarButtonHidden: Bool let customerId: String? let isSafeDeal: Bool + let config: Config } protocol SberpayModuleOutput: AnyObject { diff --git a/YooKassaPayments/Private/Modules/Sberpay/View/SberpayViewController.swift b/YooKassaPayments/Private/Modules/Sberpay/View/SberpayViewController.swift index 13351366..efd9f131 100644 --- a/YooKassaPayments/Private/Modules/Sberpay/View/SberpayViewController.swift +++ b/YooKassaPayments/Private/Modules/Sberpay/View/SberpayViewController.swift @@ -15,12 +15,6 @@ final class SberpayViewController: UIViewController, PlaceholderProvider { 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 @@ -29,11 +23,13 @@ final class SberpayViewController: UIViewController, PlaceholderProvider { }(UIStackView()) private lazy var orderView: OrderView = { + $0.translatesAutoresizingMaskIntoConstraints = false $0.setStyles(UIView.Styles.grayBackground) return $0 }(OrderView()) private lazy var sberpayMethodView: LargeIconView = { + $0.translatesAutoresizingMaskIntoConstraints = false $0.setStyles( UIView.Styles.grayBackground ) @@ -52,6 +48,7 @@ final class SberpayViewController: UIViewController, PlaceholderProvider { private lazy var submitButton: Button = { $0.tintColor = CustomizationStorage.shared.mainScheme + $0.translatesAutoresizingMaskIntoConstraints = false $0.setStyles( UIButton.DynamicStyle.primary, UIView.Styles.heightAsContent @@ -71,7 +68,6 @@ final class SberpayViewController: UIViewController, PlaceholderProvider { submitButton.translatesAutoresizingMaskIntoConstraints = false view.addSubview(submitButton) let defaultHeight = submitButton.heightAnchor.constraint(equalToConstant: Space.triple * 2) - defaultHeight.priority = .defaultLow + 1 NSLayoutConstraint.activate([ submitButton.leadingAnchor.constraint(equalTo: view.leadingAnchor), submitButton.topAnchor.constraint(equalTo: view.topAnchor), @@ -85,6 +81,8 @@ final class SberpayViewController: UIViewController, PlaceholderProvider { private let termsOfServiceLinkedTextView: LinkedTextView = { let view = LinkedTextView() + view.translatesAutoresizingMaskIntoConstraints = false + view.setContentCompressionResistancePriority(.required, for: .vertical) view.tintColor = CustomizationStorage.shared.mainScheme view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) return view @@ -92,6 +90,8 @@ final class SberpayViewController: UIViewController, PlaceholderProvider { private let safeDealLinkedTextView: LinkedTextView = { let view = LinkedTextView() + view.translatesAutoresizingMaskIntoConstraints = false + view.setContentCompressionResistancePriority(.required, for: .vertical) view.tintColor = CustomizationStorage.shared.mainScheme view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) return view @@ -150,11 +150,7 @@ final class SberpayViewController: UIViewController, PlaceholderProvider { actionButtonStackView, ].forEach(view.addSubview) - scrollView.addSubview(contentView) - - [ - contentStackView, - ].forEach(contentView.addSubview) + scrollView.addSubview(contentStackView) [ orderView, @@ -169,12 +165,13 @@ final class SberpayViewController: UIViewController, PlaceholderProvider { } private func setupConstraints() { + scrollViewHeightConstraint.priority = .defaultHigh + 1 let bottomConstraint: NSLayoutConstraint let topConstraint: NSLayoutConstraint if #available(iOS 11.0, *) { - bottomConstraint = actionButtonStackView.bottomAnchor.constraint( - equalTo: view.safeAreaLayoutGuide.bottomAnchor, - constant: -Space.double + bottomConstraint = view.safeAreaLayoutGuide.bottomAnchor.constraint( + equalTo: actionButtonStackView.bottomAnchor, + constant: Space.double ) topConstraint = scrollView.topAnchor.constraint( equalTo: view.safeAreaLayoutGuide.topAnchor @@ -193,33 +190,19 @@ final class SberpayViewController: UIViewController, PlaceholderProvider { scrollViewHeightConstraint, topConstraint, + bottomConstraint, scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - scrollView.bottomAnchor.constraint( - equalTo: actionButtonStackView.topAnchor, - constant: -Space.double - ), + actionButtonStackView.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: Space.double), - actionButtonStackView.leadingAnchor.constraint( - equalTo: view.leadingAnchor, - constant: Space.double - ), - actionButtonStackView.trailingAnchor.constraint( - equalTo: view.trailingAnchor, - constant: -Space.double - ), - bottomConstraint, + actionButtonStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Space.double), + actionButtonStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Space.double), - 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), + contentStackView.topAnchor.constraint(equalTo: scrollView.topAnchor), + contentStackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + contentStackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + contentStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + contentStackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), ] NSLayoutConstraint.activate(constraints) } @@ -234,7 +217,7 @@ final class SberpayViewController: UIViewController, PlaceholderProvider { } private func fixTableViewHeight() { - scrollViewHeightConstraint.constant = contentStackView.bounds.height + scrollViewHeightConstraint.constant = ceil(scrollView.contentSize.height) + Space.triple * 2 } // MARK: - Action @@ -261,6 +244,8 @@ extension SberpayViewController: SberpayViewInput { termsOfServiceLinkedTextView.textAlignment = .center safeDealLinkedTextView.textAlignment = .center + viewModel.paymentOptionTitle.map { navigationItem.title = $0 } + if let section = viewModel.recurrencyAndDataSavingSection { contentStackView.addArrangedSubview(section) } diff --git a/YooKassaPayments/Private/Modules/Sberpay/View/ViewModel/SberpayViewModel.swift b/YooKassaPayments/Private/Modules/Sberpay/View/ViewModel/SberpayViewModel.swift index 8bb6d53d..76de9ba6 100644 --- a/YooKassaPayments/Private/Modules/Sberpay/View/ViewModel/SberpayViewModel.swift +++ b/YooKassaPayments/Private/Modules/Sberpay/View/ViewModel/SberpayViewModel.swift @@ -8,4 +8,5 @@ struct SberpayViewModel { let termsOfService: NSAttributedString let safeDealText: NSAttributedString? let recurrencyAndDataSavingSection: UIView? + let paymentOptionTitle: String? } diff --git a/YooKassaPayments/Private/Modules/YooMoney/Assembly/YooMoneyAssembly.swift b/YooKassaPayments/Private/Modules/YooMoney/Assembly/YooMoneyAssembly.swift index 9a337157..3bf95a9b 100644 --- a/YooKassaPayments/Private/Modules/YooMoney/Assembly/YooMoneyAssembly.swift +++ b/YooKassaPayments/Private/Modules/YooMoney/Assembly/YooMoneyAssembly.swift @@ -24,7 +24,8 @@ enum YooMoneyAssembly { tmxSessionId: inputData.tmxSessionId, initialSavePaymentMethod: inputData.initialSavePaymentMethod, isBackBarButtonHidden: inputData.isBackBarButtonHidden, - isSafeDeal: inputData.isSafeDeal + isSafeDeal: inputData.isSafeDeal, + paymentOptionTitle: inputData.paymentOptionTitle ) let authorizationService = AuthorizationServiceAssembly.makeService( @@ -32,12 +33,10 @@ enum YooMoneyAssembly { testModeSettings: inputData.testModeSettings, moneyAuthClientId: inputData.moneyAuthClientId ) - let analyticsService = AnalyticsServiceAssembly.makeService( + let analyticsService = AnalyticsTrackingAssembly.make( isLoggingEnabled: inputData.isLoggingEnabled ) - let analyticsProvider = AnalyticsProviderAssembly.makeProvider( - testModeSettings: inputData.testModeSettings - ) + let paymentService = PaymentServiceAssembly.makeService( tokenizationSettings: inputData.tokenizationSettings, testModeSettings: inputData.testModeSettings, @@ -48,7 +47,6 @@ enum YooMoneyAssembly { let interactor = YooMoneyInteractor( authorizationService: authorizationService, analyticsService: analyticsService, - analyticsProvider: analyticsProvider, paymentService: paymentService, imageDownloadService: imageDownloadService, threatMetrixService: threatMetrixService, diff --git a/YooKassaPayments/Private/Modules/YooMoney/Interactor/YooMoneyInteractor.swift b/YooKassaPayments/Private/Modules/YooMoney/Interactor/YooMoneyInteractor.swift index ee8b26e5..b4c3fffc 100644 --- a/YooKassaPayments/Private/Modules/YooMoney/Interactor/YooMoneyInteractor.swift +++ b/YooKassaPayments/Private/Modules/YooMoney/Interactor/YooMoneyInteractor.swift @@ -10,9 +10,8 @@ final class YooMoneyInteractor { // MARK: - Init data private let authorizationService: AuthorizationService - private let analyticsService: AnalyticsService + private let analyticsService: AnalyticsTracking private let paymentService: PaymentService - private let analyticsProvider: AnalyticsProvider private let imageDownloadService: ImageDownloadService private let threatMetrixService: ThreatMetrixService @@ -23,8 +22,7 @@ final class YooMoneyInteractor { init( authorizationService: AuthorizationService, - analyticsService: AnalyticsService, - analyticsProvider: AnalyticsProvider, + analyticsService: AnalyticsTracking, paymentService: PaymentService, imageDownloadService: ImageDownloadService, threatMetrixService: ThreatMetrixService, @@ -33,7 +31,6 @@ final class YooMoneyInteractor { ) { self.authorizationService = authorizationService self.analyticsService = analyticsService - self.analyticsProvider = analyticsProvider self.paymentService = paymentService self.imageDownloadService = imageDownloadService self.threatMetrixService = threatMetrixService @@ -128,15 +125,12 @@ extension YooMoneyInteractor: YooMoneyInteractorInput { return authorizationService.hasReusableWalletToken() } - func trackEvent(_ event: AnalyticsEvent) { - analyticsService.trackEvent(event) + func track(event: AnalyticsEvent) { + analyticsService.track(event: event) } - func makeTypeAnalyticsParameters() -> ( - authType: AnalyticsEvent.AuthType, - tokenType: AnalyticsEvent.AuthTokenType? - ) { - return analyticsProvider.makeTypeAnalyticsParameters() + func analyticsAuthType() -> AnalyticsEvent.AuthType { + authorizationService.analyticsAuthType() } func getWalletDisplayName() -> String? { diff --git a/YooKassaPayments/Private/Modules/YooMoney/Presenter/YooMoneyPresenter.swift b/YooKassaPayments/Private/Modules/YooMoney/Presenter/YooMoneyPresenter.swift index 7e355f25..2f217087 100644 --- a/YooKassaPayments/Private/Modules/YooMoney/Presenter/YooMoneyPresenter.swift +++ b/YooKassaPayments/Private/Modules/YooMoney/Presenter/YooMoneyPresenter.swift @@ -23,13 +23,14 @@ final class YooMoneyPresenter { private let fee: PriceViewModel? private let paymentMethod: PaymentMethodViewModel private let paymentOption: PaymentInstrumentYooMoneyWallet - private let termsOfService: TermsOfService + private let termsOfService: NSAttributedString private let returnUrl: String? private let savePaymentMethodViewModel: SavePaymentMethodViewModel? private let tmxSessionId: String? private var initialSavePaymentMethod: Bool private let isBackBarButtonHidden: Bool private let isSafeDeal: Bool + private let paymentOptionTitle: String? // MARK: - Init @@ -44,13 +45,14 @@ final class YooMoneyPresenter { fee: PriceViewModel?, paymentMethod: PaymentMethodViewModel, paymentOption: PaymentInstrumentYooMoneyWallet, - termsOfService: TermsOfService, + termsOfService: NSAttributedString, returnUrl: String?, savePaymentMethodViewModel: SavePaymentMethodViewModel?, tmxSessionId: String?, initialSavePaymentMethod: Bool, isBackBarButtonHidden: Bool, - isSafeDeal: Bool + isSafeDeal: Bool, + paymentOptionTitle: String? ) { self.clientApplicationKey = clientApplicationKey self.testModeSettings = testModeSettings @@ -70,6 +72,7 @@ final class YooMoneyPresenter { self.initialSavePaymentMethod = initialSavePaymentMethod self.isBackBarButtonHidden = isBackBarButtonHidden self.isSafeDeal = isSafeDeal + self.paymentOptionTitle = paymentOptionTitle } // MARK: - Properties @@ -90,7 +93,8 @@ extension YooMoneyPresenter: YooMoneyViewOutput { fee: fee, paymentMethod: paymentMethod, terms: termsOfService, - safeDealText: isSafeDeal ? PaymentMethodResources.Localized.safeDealInfoLink : nil + safeDealText: isSafeDeal ? PaymentMethodResources.Localized.safeDealInfoLink : nil, + paymentOptionTitle: paymentOptionTitle ) view.setupViewModel(viewModel) @@ -107,17 +111,15 @@ extension YooMoneyPresenter: YooMoneyViewOutput { view.setBackBarButtonHidden(isBackBarButtonHidden) DispatchQueue.global().async { [weak self] in - guard let self = self, - let interactor = self.interactor else { return } + guard let self = self, let interactor = self.interactor else { return } interactor.loadAvatar() - let (authType, _) = interactor.makeTypeAnalyticsParameters() - let event: AnalyticsEvent = .screenPaymentContract( - authType: authType, - scheme: .wallet, - sdkVersion: Bundle.frameworkVersion + interactor.track(event: + .screenPaymentContract( + scheme: .wallet, + currentAuthType: self.interactor.analyticsAuthType() + ) ) - interactor.trackEvent(event) } } @@ -169,15 +171,11 @@ extension YooMoneyPresenter: YooMoneyViewOutput { ) } - func didChangeSavePaymentMethodState( - _ state: Bool - ) { + func didChangeSavePaymentMethodState(_ state: Bool) { initialSavePaymentMethod = state } - func didChangeSaveAuthInAppState( - _ state: Bool - ) { + func didChangeSaveAuthInAppState(_ state: Bool) { isReusableToken = state } } @@ -185,9 +183,7 @@ extension YooMoneyPresenter: YooMoneyViewOutput { // MARK: - ActionTitleTextDialogDelegate extension YooMoneyPresenter: ActionTitleTextDialogDelegate { - func didPressButton( - in actionTitleTextDialog: ActionTitleTextDialog - ) { + func didPressButton(in actionTitleTextDialog: ActionTitleTextDialog) { guard let view = view else { return } view.hidePlaceholder() view.showActivity() @@ -202,12 +198,11 @@ extension YooMoneyPresenter: ActionTitleTextDialogDelegate { // MARK: - YooMoneyInteractorOutput extension YooMoneyPresenter: YooMoneyInteractorOutput { - func didLoginInWallet( - _ response: WalletLoginResponse - ) { + func didLoginInWallet(_ response: WalletLoginResponse) { switch response { case .authorized: tokenize() + interactor.track(event: .actionAuthFinished) case let .notAuthorized( authTypeState: authTypeState, processId: processId, @@ -235,34 +230,30 @@ extension YooMoneyPresenter: YooMoneyInteractorOutput { } } - func failLoginInWallet( - _ error: Error - ) { + func failLoginInWallet(_ error: Error) { DispatchQueue.main.async { [weak self] in - guard let view = self?.view else { return } - view.hideActivity() + guard let self = self else { return } + self.view?.hideActivity() let message = makeMessage(error) - view.presentError(with: message) + self.view?.presentError(with: message) DispatchQueue.global().async { [weak self] in - guard let self = self, let interactor = self.interactor else { return } - let (authType, _) = interactor.makeTypeAnalyticsParameters() - let event: AnalyticsEvent = .screenError( - authType: authType, - scheme: .wallet, - sdkVersion: Bundle.frameworkVersion + guard let self = self else { return } + self.interactor.track(event: + .screenErrorContract( + scheme: .wallet, + currentAuthType: self.interactor.analyticsAuthType() + ) ) - interactor.trackEvent(event) } } } func didTokenizeData(_ token: Tokens) { DispatchQueue.main.async { [weak self] in - guard let self = self, - let view = self.view else { return } - view.hideActivity() + guard let self = self else { return } + self.view?.hideActivity() self.moduleOutput?.tokenizationModule( self, didTokenize: token, @@ -270,15 +261,14 @@ extension YooMoneyPresenter: YooMoneyInteractorOutput { ) DispatchQueue.global().async { [weak self] in - guard let self = self, let interactor = self.interactor else { return } - let type = interactor.makeTypeAnalyticsParameters() - let event: AnalyticsEvent = .actionTokenize( - scheme: .wallet, - authType: type.authType, - tokenType: type.tokenType, - sdkVersion: Bundle.frameworkVersion + guard let self = self else { return } + + self.interactor.track(event: + .actionTokenize( + scheme: .wallet, + currentAuthType: self.interactor.analyticsAuthType() + ) ) - interactor.trackEvent(event) } } } @@ -286,6 +276,9 @@ extension YooMoneyPresenter: YooMoneyInteractorOutput { func failTokenizeData(_ error: Error) { let message = makeMessage(error) + interactor.track( + event: .screenErrorContract(scheme: .wallet, currentAuthType: interactor.analyticsAuthType()) + ) DispatchQueue.main.async { [weak self] in guard let view = self?.view else { return } view.hideActivity() @@ -293,19 +286,16 @@ extension YooMoneyPresenter: YooMoneyInteractorOutput { } } - func didLoadAvatar( - _ avatar: UIImage - ) { + func didLoadAvatar(_ avatar: UIImage) { DispatchQueue.main.async { [weak self] in self?.view?.setupAvatar(avatar) } } - func didFailLoadAvatar( - _ error: Error - ) {} + func didFailLoadAvatar(_ error: Error) {} private func tokenize() { + interactor.track(event: .actionTryTokenize(scheme: .wallet, currentAuthType: interactor.analyticsAuthType())) interactor.tokenize( confirmation: Confirmation(type: .redirect, returnUrl: returnUrl), savePaymentMethod: initialSavePaymentMethod, @@ -319,10 +309,7 @@ extension YooMoneyPresenter: YooMoneyInteractorOutput { // MARK: - PaymentAuthorizationModuleOutput extension YooMoneyPresenter: PaymentAuthorizationModuleOutput { - func didCheckUserAnswer( - _ module: PaymentAuthorizationModuleInput, - response: WalletLoginResponse - ) { + func didCheckUserAnswer(_ module: PaymentAuthorizationModuleInput, response: WalletLoginResponse) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.router.closePaymentAuthorization() @@ -337,13 +324,9 @@ extension YooMoneyPresenter: PaymentAuthorizationModuleOutput { extension YooMoneyPresenter: LogoutConfirmationModuleOutput { func logoutDidConfirm(on module: LogoutConfirmationModuleInput) { DispatchQueue.global().async { [weak self] in - guard let self = self, - let interactor = self.interactor else { return } - interactor.logout() - let event: AnalyticsEvent = .actionLogout( - sdkVersion: Bundle.frameworkVersion - ) - interactor.trackEvent(event) + guard let self = self else { return } + self.interactor.logout() + self.interactor.track(event: .actionLogout) self.moduleOutput?.didLogout(self) } } diff --git a/YooKassaPayments/Private/Modules/YooMoney/Router/YooMoneyRouter.swift b/YooKassaPayments/Private/Modules/YooMoney/Router/YooMoneyRouter.swift index 9cdb1285..69550580 100644 --- a/YooKassaPayments/Private/Modules/YooMoney/Router/YooMoneyRouter.swift +++ b/YooKassaPayments/Private/Modules/YooMoney/Router/YooMoneyRouter.swift @@ -8,6 +8,7 @@ final class YooMoneyRouter { extension YooMoneyRouter: YooMoneyRouterInput { func presentTermsOfServiceModule(_ url: URL) { + guard url.scheme == "http" || url.scheme == "https" else { return } let viewController = SFSafariViewController(url: url) viewController.modalPresentationStyle = .overFullScreen transitionHandler?.present(viewController, animated: true, completion: nil) diff --git a/YooKassaPayments/Private/Modules/YooMoney/View/YooMoneyViewController.swift b/YooKassaPayments/Private/Modules/YooMoney/View/YooMoneyViewController.swift index 0983d28c..fac62b3c 100644 --- a/YooKassaPayments/Private/Modules/YooMoney/View/YooMoneyViewController.swift +++ b/YooKassaPayments/Private/Modules/YooMoney/View/YooMoneyViewController.swift @@ -80,7 +80,6 @@ final class YooMoneyViewController: UIViewController, PlaceholderProvider { submitButton.translatesAutoresizingMaskIntoConstraints = false view.addSubview(submitButton) let defaultHeight = submitButton.heightAnchor.constraint(equalToConstant: Space.triple * 2) - defaultHeight.priority = .defaultLow + 1 NSLayoutConstraint.activate([ submitButton.leadingAnchor.constraint(equalTo: view.leadingAnchor), submitButton.topAnchor.constraint(equalTo: view.topAnchor), @@ -94,6 +93,7 @@ final class YooMoneyViewController: UIViewController, PlaceholderProvider { private let termsOfServiceLinkedTextView: LinkedTextView = { let view = LinkedTextView() + view.setContentCompressionResistancePriority(.required, for: .vertical) view.tintColor = CustomizationStorage.shared.mainScheme view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) return view @@ -101,6 +101,7 @@ final class YooMoneyViewController: UIViewController, PlaceholderProvider { private let safeDealLinkedTextView: LinkedTextView = { let view = LinkedTextView() + view.setContentCompressionResistancePriority(.required, for: .vertical) view.tintColor = CustomizationStorage.shared.mainScheme view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) return view @@ -225,7 +226,7 @@ final class YooMoneyViewController: UIViewController, PlaceholderProvider { private lazy var scrollViewHeightConstraint: NSLayoutConstraint = { let constraint = scrollView.heightAnchor.constraint(equalToConstant: 0) - constraint.priority = .defaultLow + constraint.priority = .defaultHigh + 1 return constraint }() @@ -350,7 +351,7 @@ final class YooMoneyViewController: UIViewController, PlaceholderProvider { } private func fixTableViewHeight() { - scrollViewHeightConstraint.constant = contentStackView.bounds.height + scrollViewHeightConstraint.constant = ceil(scrollView.contentSize.height) + Space.triple * 2 } // MARK: - Action @@ -384,6 +385,8 @@ extension YooMoneyViewController: YooMoneyViewInput { orderView.subvalue = nil } + viewModel.paymentOptionTitle.map { navigationItem.title = $0 } + paymentMethodView.title = viewModel.paymentMethod.title paymentMethodView.subtitle = viewModel.paymentMethod.subtitle ?? "" paymentMethodView.image = UIImage.avatar @@ -391,11 +394,7 @@ extension YooMoneyViewController: YooMoneyViewInput { paymentMethodView.rightButtonTitle = Localized.logout paymentMethodView.output = self - termsOfServiceLinkedTextView.attributedText = makeTermsOfService( - viewModel.terms, - font: UIFont.dynamicCaption2, - foregroundColor: UIColor.AdaptiveColors.secondary - ) + termsOfServiceLinkedTextView.attributedText = viewModel.terms safeDealLinkedTextView.attributedText = viewModel.safeDealText safeDealLinkedTextView.isHidden = viewModel.safeDealText?.string.isEmpty ?? true termsOfServiceLinkedTextView.textAlignment = .center @@ -468,33 +467,6 @@ extension YooMoneyViewController: YooMoneyViewInput { + price.currency } - private 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 - } - private func makeSavePaymentMethodAttributedString( text: String, hyperText: String, diff --git a/YooKassaPayments/Private/Modules/YooMoney/YooMoneyInteractorIO.swift b/YooKassaPayments/Private/Modules/YooMoney/YooMoneyInteractorIO.swift index 1921f831..80b1420e 100644 --- a/YooKassaPayments/Private/Modules/YooMoney/YooMoneyInteractorIO.swift +++ b/YooKassaPayments/Private/Modules/YooMoney/YooMoneyInteractorIO.swift @@ -1,7 +1,7 @@ import class YooKassaPaymentsApi.PaymentOption import UIKit -protocol YooMoneyInteractorInput: AnyObject, AnalyticsTrack { +protocol YooMoneyInteractorInput: AnyObject { func loginInWallet( amount: MonetaryAmount, reusableToken: Bool, @@ -20,10 +20,8 @@ protocol YooMoneyInteractorInput: AnyObject, AnalyticsTrack { func hasReusableWalletToken() -> Bool - func makeTypeAnalyticsParameters() -> ( - authType: AnalyticsEvent.AuthType, - tokenType: AnalyticsEvent.AuthTokenType? - ) + func track(event: AnalyticsEvent) + func analyticsAuthType() -> AnalyticsEvent.AuthType func getWalletDisplayName() -> String? func logout() diff --git a/YooKassaPayments/Private/Modules/YooMoney/YooMoneyModuleIO.swift b/YooKassaPayments/Private/Modules/YooMoney/YooMoneyModuleIO.swift index 5562a507..b24162ff 100644 --- a/YooKassaPayments/Private/Modules/YooMoney/YooMoneyModuleIO.swift +++ b/YooKassaPayments/Private/Modules/YooMoney/YooMoneyModuleIO.swift @@ -13,7 +13,7 @@ struct YooMoneyModuleInputData { let fee: PriceViewModel? let paymentMethod: PaymentMethodViewModel let paymentOption: PaymentInstrumentYooMoneyWallet - let termsOfService: TermsOfService + let termsOfService: NSAttributedString let returnUrl: String let savePaymentMethodViewModel: SavePaymentMethodViewModel? let tmxSessionId: String? @@ -21,6 +21,7 @@ struct YooMoneyModuleInputData { let isBackBarButtonHidden: Bool let customerId: String? let isSafeDeal: Bool + let paymentOptionTitle: String? } protocol YooMoneyModuleInput: AnyObject {} diff --git a/YooKassaPayments/Private/Modules/YooMoney/YooMoneyViewIO.swift b/YooKassaPayments/Private/Modules/YooMoney/YooMoneyViewIO.swift index b2b98870..a79c4657 100644 --- a/YooKassaPayments/Private/Modules/YooMoney/YooMoneyViewIO.swift +++ b/YooKassaPayments/Private/Modules/YooMoney/YooMoneyViewIO.swift @@ -1,4 +1,4 @@ -import UIKit.UIImage +import UIKit struct YooMoneyViewModel { let shopName: String @@ -6,8 +6,9 @@ struct YooMoneyViewModel { let price: PriceViewModel let fee: PriceViewModel? let paymentMethod: PaymentMethodViewModel - let terms: TermsOfService + let terms: NSAttributedString let safeDealText: NSAttributedString? + let paymentOptionTitle: String? } protocol YooMoneyViewInput: ActivityIndicatorFullViewPresenting, PlaceholderPresenting, NotificationPresenting { diff --git a/YooKassaPayments/Private/Services/Analytics/AnalyticsEvent.swift b/YooKassaPayments/Private/Services/Analytics/AnalyticsEvent.swift index 8c8c33f0..cc6bcbc5 100644 --- a/YooKassaPayments/Private/Services/Analytics/AnalyticsEvent.swift +++ b/YooKassaPayments/Private/Services/Analytics/AnalyticsEvent.swift @@ -1,82 +1,133 @@ enum AnalyticsEvent { - - // MARK: - Screen viewing events. - - /// Open the payment method selection screen. - case screenPaymentOptions(authType: AuthType, sdkVersion: String) - - /// The opening screen of the contract. - case screenPaymentContract(authType: AuthType, scheme: TokenizeScheme, sdkVersion: String) - - /// Open the Linked Bank card for data entry screen. - case screenLinkedCardForm(sdkVersion: String) - - /// Open the Bank card screen for entering Data. - case screenBankCardForm(authType: AuthType, sdkVersion: String) - - /// The opening screen of the error. - case screenError(authType: AuthType, scheme: TokenizeScheme?, sdkVersion: String) - - /// The opening pages 3DS. - case screen3ds(sdkVersion: String) - - /// Open Bank Card screen with screen recurring - case screenRecurringCardForm(sdkVersion: String) - - case screenDetailsUnbindWalletCard(sdkVersion: String) + case actionSDKInitialised + case screenPaymentOptions(currentAuthType: AuthType) + case screenPaymentContract(scheme: TokenizeScheme, currentAuthType: AuthType) + case screenErrorContract(scheme: TokenizeScheme, currentAuthType: AuthType) + case screenError(scheme: TokenizeScheme?, currentAuthType: AuthType) + case screen3ds + case screen3dsClose(success: Bool) + case screenDetailsUnbindWalletCard case screenUnbindCard(cardType: LinkedCardType) + case actionTryTokenize(scheme: TokenizeScheme, currentAuthType: AuthType) + case actionTokenize(scheme: TokenizeScheme, currentAuthType: AuthType) + case actionPaymentAuthorization(success: Bool) + case actionLogout + case actionAuthWithoutWallet + case actionBankCardForm(action: BankCardFormAction) + case userStartAuthorization + case userCancelAuthorization + case actionMoneyAuthLogin(scheme: MoneyAuthLoginScheme, status: MoneyAuthLoginStatus) + case actionSberPayConfirmation(success: Bool) + case actionUnbindBankCard(success: Bool) + case actionAuthFinished + + var name: String { + switch self { + case .actionSDKInitialised: return "actionSDKInitialised" + case .screenPaymentOptions: return "screenPaymentOptions" + case .screenPaymentContract: return "screenPaymentContract" + case .screenError: return "screenError" + case .screen3ds: return "screen3ds" + case .screen3dsClose: return "screen3dsClose" + case .screenDetailsUnbindWalletCard: return "screenDetailsUnbindWalletCard" + case .screenUnbindCard: return "screenUnbindCard" + case .actionTryTokenize: return "actionTryTokenize" + case .actionTokenize: return "actionTokenize" + case .actionPaymentAuthorization: return "actionPaymentAuthorization" + case .actionLogout: return "actionLogout" + case .actionAuthWithoutWallet: return "actionAuthWithoutWallet" + case .actionBankCardForm: return "actionBankCardForm" + case .userStartAuthorization: return "userStartAuthorization" + case .userCancelAuthorization: return "userCancelAuthorization" + case .actionMoneyAuthLogin: return "actionMoneyAuthLogin" + case .actionSberPayConfirmation: return "actionSberPayConfirmation" + case .actionUnbindBankCard: return "actionUnbindBankCard" + case .actionAuthFinished: return "actionAuthFinished" + case .screenErrorContract: return "screenErrorContract" + } + } + // swiftlint:disable:next cyclomatic_complexity + func parameters(context: AnalyticsEventContext?) -> [String: String] { + var result: [String: String] = [:] + if let context = context { + result["msdkVersion"] = context.sdkVersion + + var attribution = context.isCustomerIdPresent ? "customerId;": "" + attribution += context.isWalletAuthPresent ? "yoomoney;" : "" + if !attribution.isEmpty { + result["userAttiributionOnInit"] = attribution + } else { + result["userAttiributionOnInit"] = "none" + } + } - // MARK: - Actions - - /// Create a payment token with the payment method selected. - case actionTokenize(scheme: TokenizeScheme, authType: AuthType, tokenType: AuthTokenType?, sdkVersion: String) - - /// Payment authorization. - case actionPaymentAuthorization(authPaymentStatus: AuthPaymentStatus, sdkVersion: String) - - /// The user is logged out. - case actionLogout(sdkVersion: String) - - /// Authorization without wallet. - case actionAuthWithoutWallet(sdkVersion: String) - - /// BankCard form interactions. - case actionBankCardForm(action: BankCardFormAction, sdkVersion: String) - - case userStartAuthorization(sdkVersion: String) - case userCancelAuthorization(sdkVersion: String) - - case actionMoneyAuthLogin( - scheme: MoneyAuthLoginScheme, - status: MoneyAuthLoginStatus, - sdkVersion: String - ) + switch self { + case .screen3ds, .screenDetailsUnbindWalletCard, .actionLogout, + .actionAuthWithoutWallet, .userStartAuthorization, .userCancelAuthorization, .actionAuthFinished: + break + + case .actionSDKInitialised, .screenPaymentOptions: + if let context = context { + result["authType"] = context.initialAuthType.rawValue + result["customColor"] = String(context.usingCustomColor) + result["yookassaIcon"] = String(context.yookassaIconShown) + result["savePaymentMethod"] = context.savePaymentMethod.description + } + case .screenPaymentContract(let scheme, let currentAuthType), + .actionTokenize(let scheme, let currentAuthType), .actionTryTokenize(let scheme, let currentAuthType), + .screenErrorContract(let scheme, let currentAuthType): + result[TokenizeScheme.key] = scheme.rawValue + result["authType"] = currentAuthType.rawValue + + if let context = context { + result["customColor"] = String(context.usingCustomColor) + result["yookassaIcon"] = String(context.yookassaIconShown) + result["savePaymentMethod"] = context.savePaymentMethod.description + } + case .screenError(let scheme, let currentAuthType): + result["authType"] = currentAuthType.rawValue + if let scheme = scheme { + result[TokenizeScheme.key] = scheme.rawValue + } - /// SberPay confirmation - case actionSberPayConfirmation(sberPayConfirmationStatus: SberPayConfirmationStatus, sdkVersion: String) + if let context = context { + result["savePaymentMethod"] = context.savePaymentMethod.description + result["customColor"] = String(context.usingCustomColor) + result["yookassaIcon"] = String(context.yookassaIconShown) + } - case actionUnbindBankCard(actionUnbindCardStatus: ActionUnbindCardStatus) + case .screen3dsClose(let success): + result["Success"] = String(success) - // MARK: - Analytic parameters. + case .screenUnbindCard(let cardType): + result[LinkedCardType.key] = cardType.rawValue - /// Current status of user authorization. - enum AuthType: String { + case .actionPaymentAuthorization(let success): + result["authPaymentStatus"] = success ? "Success" : "Fail" - /// The user is not authorized. - case withoutAuth + case .actionBankCardForm(let action): + result[BankCardFormAction.key] = action.rawValue - /// Successfully completed authorization in Money center authorization - case moneyAuth + case .actionMoneyAuthLogin(let scheme, let status): + result[MoneyAuthLoginScheme.key] = scheme.rawValue + result[MoneyAuthLoginStatus.key] = status.description + if case .fail(let error) = status, !error.localizedDescription.isEmpty { + result["error"] = error.localizedDescription + } - /// Successful payment authorization in the wallet. - case paymentAuth + case .actionSberPayConfirmation(let success): + result["actionSberPayConfirmation"] = success ? "Success" : "Fail" - var key: String { - return Key.authType.rawValue + case .actionUnbindBankCard(let success): + result["actionUnbindCardStatus"] = success ? "Success" : "Fail" } + + return result } +} - /// Creating a payment token. +// MARK: - Parameters +extension AnalyticsEvent { enum TokenizeScheme: String { case wallet case linkedCard = "linked-card" @@ -88,131 +139,68 @@ enum AnalyticsEvent { case customerIdLinkedCard = "customer-id-linked-card" case customerIdLinkedCardCvc = "customer-id-linked-card-cvc" - var key: String { - return Key.tokenizeScheme.rawValue - } + static let key = "tokenizeScheme" } + enum MoneyAuthLoginScheme: String { + case moneyAuthSdk + case yoomoneyApp - /// Token type. - enum AuthTokenType: String { - - /// one-time authorization token. - case single - - /// reusable authorization token. - case multiple + static let key = "moneyAuthLoginScheme" + } - var key: String { - return Key.authTokenType.rawValue - } + enum AuthType: String { + case withoutAuth + case moneyAuth + case paymentAuth } - /// Payment authorization status. - enum AuthPaymentStatus: String { - case success = "Success" - case fail = "Fail" + enum AuthTokenType: String { + case single + case multiple - var key: String { - return Key.authPaymentStatus.rawValue - } + static let key = "authTokenType" } - private enum Key: String { - case tokenizeScheme - case authType - case authTokenType - case authPaymentStatus - case action - case moneyAuthLoginScheme - case moneyAuthLoginStatus - case sberPayConfirmationStatus - case linkedCardType - case actionUnbindCardStatus + enum Form: String { + case bankCard + case recurring + case linkedCard + static let key = "Form" } - // MARK: - BankCardForm actions - enum BankCardFormAction: String { - /// The user clicked on the scan button; case scanBankCardAction - /// The user entered the wrong card number; case cardNumberInputError - /// The user entered the wrong card expiration date; case cardExpiryInputError - /// The user entered an incorrect CVC; case cardCvcInputError - /// The user erased the card number (tap on the cross); case cardNumberClearAction - /// Successful entry of bank card information; case cardNumberInputSuccess - /// The user clicked on the arrow and moved to the next field; case cardNumberContinueAction - /// Returns to the card number input field. case cardNumberReturnToEdit - var key: String { - Key.action.rawValue - } + static let key = "bankCardFormAction" } - enum MoneyAuthLoginScheme: String { - case moneyAuthSdk - case yoomoneyApp + enum LinkedCardType: String { + case wallet = "Wallet" + case bankCard = "BankCard" - var key: String { - Key.moneyAuthLoginScheme.rawValue - } + static let key = "linkedCardType" } - enum MoneyAuthLoginStatus { + enum MoneyAuthLoginStatus: CustomStringConvertible { case success - case fail(String) - case canceled + case fail(Error) + case cancelled - var rawValue: String { + var description: String { switch self { - case .success: - return "Success" - case .fail: - return "Fail" - case .canceled: - return "Canceled" + case .success: return "Success" + case .fail: return "Fail" + case .cancelled: return "Cancelled" } } - var key: String { - Key.moneyAuthLoginStatus.rawValue - } - } - - // MARK: - SberPayConfirmationStatus - - enum SberPayConfirmationStatus: String { - case success = "Success" - - var key: String { - return Key.sberPayConfirmationStatus.rawValue - } - } - - enum LinkedCardType: String { - case wallet = "Wallet" - case bankCard = "BankCard" - var key: String { Key.linkedCardType.rawValue } - } - - enum ActionUnbindCardStatus: String { - case fail = "Fail" - case success = "Success" - var key: String { Key.actionUnbindCardStatus.rawValue } - } -} - -// MARK: - Primitive type keys - -extension AnalyticsEvent { - enum Keys: String { - case error - case msdkVersion + static let key = "moneyAuthLoginStatus" } } diff --git a/YooKassaPayments/Private/Services/Analytics/AnalyticsEventContext.swift b/YooKassaPayments/Private/Services/Analytics/AnalyticsEventContext.swift new file mode 100644 index 00000000..e277500f --- /dev/null +++ b/YooKassaPayments/Private/Services/Analytics/AnalyticsEventContext.swift @@ -0,0 +1,11 @@ +import Foundation + +struct AnalyticsEventContext { + let sdkVersion: String + let initialAuthType: AnalyticsEvent.AuthType + let isCustomerIdPresent: Bool + let isWalletAuthPresent: Bool + let usingCustomColor: Bool + let yookassaIconShown: Bool + let savePaymentMethod: SavePaymentMethod +} diff --git a/YooKassaPayments/Private/Services/Analytics/AnalyticsService.swift b/YooKassaPayments/Private/Services/Analytics/AnalyticsService.swift deleted file mode 100644 index bee72a37..00000000 --- a/YooKassaPayments/Private/Services/Analytics/AnalyticsService.swift +++ /dev/null @@ -1,5 +0,0 @@ -protocol AnalyticsService { - func start() - func stop() - func trackEvent(_ event: AnalyticsEvent) -} diff --git a/YooKassaPayments/Private/Services/Analytics/AnalyticsServiceAssembly.swift b/YooKassaPayments/Private/Services/Analytics/AnalyticsServiceAssembly.swift deleted file mode 100644 index 3236cc66..00000000 --- a/YooKassaPayments/Private/Services/Analytics/AnalyticsServiceAssembly.swift +++ /dev/null @@ -1,9 +0,0 @@ -enum AnalyticsServiceAssembly { - static func makeService( - isLoggingEnabled: Bool - ) -> AnalyticsService { - return AnalyticsServiceImpl( - isLoggingEnabled: isLoggingEnabled - ) - } -} diff --git a/YooKassaPayments/Private/Services/Analytics/AnalyticsServiceImpl.swift b/YooKassaPayments/Private/Services/Analytics/AnalyticsServiceImpl.swift deleted file mode 100644 index 91eadcc4..00000000 --- a/YooKassaPayments/Private/Services/Analytics/AnalyticsServiceImpl.swift +++ /dev/null @@ -1,287 +0,0 @@ -import YandexMobileMetrica - -final class AnalyticsServiceImpl { - - // MARK: - Init data - - private let isLoggingEnabled: Bool - - // MARK: - Init - - init(isLoggingEnabled: Bool) { - self.isLoggingEnabled = isLoggingEnabled - } - - // MARK: - Properties - - #if DEBUG - private let yandexMetricaKey = "fdeb958c-8bfd-4dab-98df-f9be4bdb6646" - #else - private let yandexMetricaKey = "b1ddbdc0-dca6-489c-a205-f71e0158bfcb" - #endif - - private lazy var reporter = YMMYandexMetrica.reporter(forApiKey: yandexMetricaKey) -} - -// MARK: - AnalyticsService - -extension AnalyticsServiceImpl: AnalyticsService { - func start() { - reporter?.resumeSession() - } - - func stop() { - reporter?.pauseSession() - } - - func trackEvent(_ event: AnalyticsEvent) { - let eventName = makeAnalyticsEventName(event) - let parameters = makeAnalyticsParameters(event) - trackEventNamed(eventName, parameters: parameters) - } - - private func trackEventNamed( - _ name: String, - parameters: [String: String]? - ) { - logAnalyticsEventNamed( - name, - parameters: parameters, - isLoggingEnabled: isLoggingEnabled - ) - reporter?.reportEvent( - name, - parameters: parameters - ) - } - - // swiftlint:disable cyclomatic_complexity - private func makeAnalyticsEventName( - _ event: AnalyticsEvent - ) -> String { - let eventName: String - - switch event { - case .screenPaymentOptions: - eventName = EventKey.screenPaymentOptions.rawValue - - case .screenPaymentContract: - eventName = EventKey.screenPaymentContract.rawValue - - case .screenLinkedCardForm: - eventName = EventKey.screenLinkedCardForm.rawValue - - case .screenBankCardForm: - eventName = EventKey.screenBankCardForm.rawValue - - case .screenError: - eventName = EventKey.screenError.rawValue - - case .screen3ds: - eventName = EventKey.screen3ds.rawValue - - case .screenRecurringCardForm: - eventName = EventKey.screenRecurringCardForm.rawValue - - case .screenDetailsUnbindWalletCard: - eventName = EventKey.screenDetailsUnbindWalletCard.rawValue - - case .screenUnbindCard: - eventName = EventKey.screenUnbindCard.rawValue - - case .actionTokenize: - eventName = EventKey.actionTokenize.rawValue - - case .actionPaymentAuthorization: - eventName = EventKey.actionPaymentAuthorization.rawValue - - case .actionLogout: - eventName = EventKey.actionLogout.rawValue - - case .actionAuthWithoutWallet: - eventName = EventKey.actionAuthWithoutWallet.rawValue - - case .userStartAuthorization: - eventName = EventKey.userStartAuthorization.rawValue - - case .userCancelAuthorization: - eventName = EventKey.userCancelAuthorization.rawValue - - case .actionBankCardForm: - eventName = EventKey.actionBankCardForm.rawValue - - case .actionMoneyAuthLogin: - eventName = EventKey.actionMoneyAuthLogin.rawValue - - case .actionSberPayConfirmation: - eventName = EventKey.actionSberPayConfirmation.rawValue - - case .actionUnbindBankCard: - eventName = EventKey.actionUnbindBankCard.rawValue - } - return eventName - } - // swiftlint:enable cyclomatic_complexity - - // swiftlint:disable cyclomatic_complexity - private func makeAnalyticsParameters( - _ event: AnalyticsEvent - ) -> [String: String]? { - var parameters: [String: String]? - - switch event { - case let .screenPaymentOptions(authType, sdkVersion): - parameters = [ - authType.key: authType.rawValue, - AnalyticsEvent.Keys.msdkVersion.rawValue: sdkVersion, - ] - - case let .screenPaymentContract(authType, scheme, sdkVersion): - parameters = [ - authType.key: authType.rawValue, - scheme.key: scheme.rawValue, - AnalyticsEvent.Keys.msdkVersion.rawValue: sdkVersion, - ] - - case let .screenLinkedCardForm(sdkVersion): - parameters = [ - AnalyticsEvent.Keys.msdkVersion.rawValue: sdkVersion, - ] - - case let .screenBankCardForm(authType, sdkVersion): - parameters = [ - authType.key: authType.rawValue, - AnalyticsEvent.Keys.msdkVersion.rawValue: sdkVersion, - ] - - case let .screenError(authType, scheme, sdkVersion): - parameters = [ - authType.key: authType.rawValue, - AnalyticsEvent.Keys.msdkVersion.rawValue: sdkVersion, - ] - if let scheme = scheme { - parameters?[scheme.key] = scheme.rawValue - } - - case let .screen3ds(sdkVersion): - parameters = [ - AnalyticsEvent.Keys.msdkVersion.rawValue: sdkVersion, - ] - - case let .screenRecurringCardForm(sdkVersion): - parameters = [ - AnalyticsEvent.Keys.msdkVersion.rawValue: sdkVersion, - ] - - case let .actionTokenize(scheme, authType, tokenType, sdkVersion): - parameters = [ - scheme.key: scheme.rawValue, - authType.key: authType.rawValue, - AnalyticsEvent.Keys.msdkVersion.rawValue: sdkVersion, - ] - if let tokenType = tokenType { - parameters?[tokenType.key] = tokenType.rawValue - } - - case let .actionPaymentAuthorization(authPaymentStatus, sdkVersion): - parameters = [ - authPaymentStatus.key: authPaymentStatus.rawValue, - AnalyticsEvent.Keys.msdkVersion.rawValue: sdkVersion, - ] - - case let .actionLogout(sdkVersion): - parameters = [ - AnalyticsEvent.Keys.msdkVersion.rawValue: sdkVersion, - ] - - case let .actionAuthWithoutWallet(sdkVersion): - parameters = [ - AnalyticsEvent.Keys.msdkVersion.rawValue: sdkVersion, - ] - - case let .userStartAuthorization(sdkVersion): - parameters = [ - AnalyticsEvent.Keys.msdkVersion.rawValue: sdkVersion, - ] - - case let .userCancelAuthorization(sdkVersion): - parameters = [ - AnalyticsEvent.Keys.msdkVersion.rawValue: sdkVersion, - ] - - case let .actionBankCardForm(action, sdkVersion): - parameters = [ - action.key: action.rawValue, - AnalyticsEvent.Keys.msdkVersion.rawValue: sdkVersion, - ] - - case let .actionMoneyAuthLogin(scheme, status, sdkVersion): - parameters = [ - scheme.key: scheme.rawValue, - status.key: status.rawValue, - AnalyticsEvent.Keys.msdkVersion.rawValue: sdkVersion, - ] - - if case .fail(let errorLocalizedDescription) = status { - parameters?[AnalyticsEvent.Keys.error.rawValue] = errorLocalizedDescription - } - - case let .actionSberPayConfirmation(sberPayConfirmationStatus, sdkVersion): - parameters = [ - sberPayConfirmationStatus.key: sberPayConfirmationStatus.rawValue, - AnalyticsEvent.Keys.msdkVersion.rawValue: sdkVersion, - ] - case .screenDetailsUnbindWalletCard(let sdkVersion): - return [AnalyticsEvent.Keys.msdkVersion.rawValue: sdkVersion] - - case .screenUnbindCard(let cardType): - return [cardType.key: cardType.rawValue] - - case .actionUnbindBankCard(let actionUnbindCardStatus): - return [actionUnbindCardStatus.key: actionUnbindCardStatus.rawValue] - } - - return parameters - } - // swiftlint:enable cyclomatic_complexity - - private enum EventKey: String { - case screenPaymentOptions - case screenPaymentContract - case screenLinkedCardForm - case screenBankCardForm - case screenError - case screen3ds - case screenRecurringCardForm - case screenUnbindCard - case screenDetailsUnbindWalletCard - case actionTokenize - case actionPaymentAuthorization - case actionLogout - case actionAuthWithoutWallet - case actionBankCardForm - case actionMoneyAuthLogin - case actionSberPayConfirmation - case actionUnbindBankCard - - // MARK: - Authorization - - case userStartAuthorization - case userCancelAuthorization - } -} - -private func logAnalyticsEventNamed( - _ name: String, - parameters: [String: String]?, - isLoggingEnabled: Bool -) { - guard isLoggingEnabled == true else { return } - #if DEBUG - guard let parameters = parameters else { - print("YandexMetrica event name: \(name)") - return - } - print("YandexMetrica event name: \(name), parameters: \(parameters)") - #endif -} diff --git a/YooKassaPayments/Private/Services/Analytics/AnalyticsTrack.swift b/YooKassaPayments/Private/Services/Analytics/AnalyticsTrack.swift deleted file mode 100644 index ef6a6475..00000000 --- a/YooKassaPayments/Private/Services/Analytics/AnalyticsTrack.swift +++ /dev/null @@ -1,3 +0,0 @@ -protocol AnalyticsTrack { - func trackEvent(_ event: AnalyticsEvent) -} diff --git a/YooKassaPayments/Private/Services/Analytics/AnalyticsTracking.swift b/YooKassaPayments/Private/Services/Analytics/AnalyticsTracking.swift new file mode 100644 index 00000000..6bf42983 --- /dev/null +++ b/YooKassaPayments/Private/Services/Analytics/AnalyticsTracking.swift @@ -0,0 +1,11 @@ +protocol AnalyticsTracking { + func track(name: String, parameters: [String: String]?) + func resume() + func pause() +} + +extension AnalyticsTracking { + func track(event: AnalyticsEvent) { + track(name: event.name, parameters: event.parameters(context: YKSdk.shared.analyticsContext)) + } +} diff --git a/YooKassaPayments/Private/Services/Analytics/AnalyticsTrackingAssembly.swift b/YooKassaPayments/Private/Services/Analytics/AnalyticsTrackingAssembly.swift new file mode 100644 index 00000000..ff83d944 --- /dev/null +++ b/YooKassaPayments/Private/Services/Analytics/AnalyticsTrackingAssembly.swift @@ -0,0 +1,5 @@ +enum AnalyticsTrackingAssembly { + static func make(isLoggingEnabled: Bool) -> AnalyticsTracking { + CommonTracker(isLoggingEnabled: isLoggingEnabled) + } +} diff --git a/YooKassaPayments/Private/Services/Analytics/CommonTracker.swift b/YooKassaPayments/Private/Services/Analytics/CommonTracker.swift new file mode 100644 index 00000000..3e29ea47 --- /dev/null +++ b/YooKassaPayments/Private/Services/Analytics/CommonTracker.swift @@ -0,0 +1,41 @@ +import YandexMobileMetrica +class CommonTracker: AnalyticsTracking { + // MARK: - Properties + + #if DEBUG + private let yandexMetricaKey = "fdeb958c-8bfd-4dab-98df-f9be4bdb6646" + #else + private let yandexMetricaKey = "b1ddbdc0-dca6-489c-a205-f71e0158bfcb" + #endif + + private lazy var yandexMetrica = YMMYandexMetrica.reporter(forApiKey: yandexMetricaKey) + + private let isLoggingEnabled: Bool + + init(isLoggingEnabled: Bool) { + self.isLoggingEnabled = isLoggingEnabled + } + + func track(name: String, parameters: [String: String]?) { + yandexMetrica?.reportEvent(name, parameters: parameters) + + if isLoggingEnabled { + #if DEBUG + let paraString = parameters.map { ", parameters: \($0)" } ?? "" + print("!YMMYandexMetrica report event. name: \(name)" + paraString) + #endif + } + } + + func track(event: AnalyticsEvent) { + track(name: event.name, parameters: event.parameters(context: YKSdk.shared.analyticsContext)) + } + + func resume() { + yandexMetrica?.resumeSession() + } + + func pause() { + yandexMetrica?.pauseSession() + } +} diff --git a/YooKassaPayments/Private/Services/AnalyticsProvider/AnalyticsProvider.swift b/YooKassaPayments/Private/Services/AnalyticsProvider/AnalyticsProvider.swift deleted file mode 100644 index f7368aa7..00000000 --- a/YooKassaPayments/Private/Services/AnalyticsProvider/AnalyticsProvider.swift +++ /dev/null @@ -1,4 +0,0 @@ -protocol AnalyticsProvider { - func makeTypeAnalyticsParameters() -> (authType: AnalyticsEvent.AuthType, - tokenType: AnalyticsEvent.AuthTokenType?) -} diff --git a/YooKassaPayments/Private/Services/AnalyticsProvider/AnalyticsProviderAssembly.swift b/YooKassaPayments/Private/Services/AnalyticsProvider/AnalyticsProviderAssembly.swift deleted file mode 100644 index 7b78c547..00000000 --- a/YooKassaPayments/Private/Services/AnalyticsProvider/AnalyticsProviderAssembly.swift +++ /dev/null @@ -1,21 +0,0 @@ -enum AnalyticsProviderAssembly { - static func makeProvider( - testModeSettings: TestModeSettings? - ) -> AnalyticsProvider { - let keyValueStoring: KeyValueStoring - - switch testModeSettings { - case .some(let testModeSettings): - keyValueStoring = KeyValueStoringAssembly.makeKeychainStorageMock( - testModeSettings: testModeSettings - ) - - case .none: - keyValueStoring = KeyValueStoringAssembly.makeKeychainStorage() - } - - return AnalyticsProviderImpl( - keyValueStoring: keyValueStoring - ) - } -} diff --git a/YooKassaPayments/Private/Services/AnalyticsProvider/AnalyticsProviderImpl.swift b/YooKassaPayments/Private/Services/AnalyticsProvider/AnalyticsProviderImpl.swift deleted file mode 100644 index 6502e4b9..00000000 --- a/YooKassaPayments/Private/Services/AnalyticsProvider/AnalyticsProviderImpl.swift +++ /dev/null @@ -1,46 +0,0 @@ -struct AnalyticsProviderImpl { - - // MARK: - Init data - - private let keyValueStoring: KeyValueStoring - - // MARK: - Init - - init( - keyValueStoring: KeyValueStoring - ) { - self.keyValueStoring = keyValueStoring - } -} - -// MARK: - AnalyticsProvider - -extension AnalyticsProviderImpl: AnalyticsProvider { - func makeTypeAnalyticsParameters() -> (authType: AnalyticsEvent.AuthType, - tokenType: AnalyticsEvent.AuthTokenType?) { - - let authType: AnalyticsEvent.AuthType - let tokenType: AnalyticsEvent.AuthTokenType? - - let hasReusableWalletToken = keyValueStoring.getString( - for: KeyValueStoringKeys.walletToken - ) != nil - && keyValueStoring.getBool( - for: KeyValueStoringKeys.isReusableWalletToken - ) == true - - if hasReusableWalletToken { - authType = .paymentAuth - tokenType = .multiple - } else if keyValueStoring.getString( - for: KeyValueStoringKeys.moneyCenterAuthToken - ) != nil { - authType = .moneyAuth - tokenType = .single - } else { - authType = .withoutAuth - tokenType = nil - } - return (authType, tokenType) - } -} diff --git a/YooKassaPayments/Private/Services/ApiLogger.swift b/YooKassaPayments/Private/Services/ApiLogger.swift index 55c8f0d2..cfec46cc 100644 --- a/YooKassaPayments/Private/Services/ApiLogger.swift +++ b/YooKassaPayments/Private/Services/ApiLogger.swift @@ -4,7 +4,7 @@ class ApiLogger {} // MARK: - Logger -extension ApiLogger: Logger { +extension ApiLogger: YooMoneyCoreApi.Logger { func log(message: String) { print(message) } diff --git a/YooKassaPayments/Private/Services/Authorization/AuthorizationService.swift b/YooKassaPayments/Private/Services/Authorization/AuthorizationService.swift index a048046e..b2c7fe54 100644 --- a/YooKassaPayments/Private/Services/Authorization/AuthorizationService.swift +++ b/YooKassaPayments/Private/Services/Authorization/AuthorizationService.swift @@ -1,9 +1,7 @@ protocol AuthorizationService { func getMoneyCenterAuthToken() -> String? - func setMoneyCenterAuthToken( - _ token: String - ) + func setMoneyCenterAuthToken(_ token: String) func getWalletToken() -> String? @@ -11,24 +9,20 @@ protocol AuthorizationService { func logout() - func setWalletDisplayName( - _ walletDisplayName: String? - ) + func setWalletDisplayName(_ walletDisplayName: String?) func getWalletDisplayName() -> String? - func setWalletPhoneTitle( - _ walletPhoneTitle: String? - ) + func setWalletPhoneTitle(_ walletPhoneTitle: String?) func getWalletPhoneTitle() -> String? - func setWalletAvatarURL( - _ walletAvatarURL: String? - ) + func setWalletAvatarURL(_ walletAvatarURL: String?) func getWalletAvatarURL() -> String? + func analyticsAuthType() -> AnalyticsEvent.AuthType + // MARK: - Wallet 2FA func loginInWallet( diff --git a/YooKassaPayments/Private/Services/Authorization/AuthorizationServiceImpl.swift b/YooKassaPayments/Private/Services/Authorization/AuthorizationServiceImpl.swift index ff016032..7224322b 100644 --- a/YooKassaPayments/Private/Services/Authorization/AuthorizationServiceImpl.swift +++ b/YooKassaPayments/Private/Services/Authorization/AuthorizationServiceImpl.swift @@ -35,6 +35,19 @@ final class AuthorizationServiceImpl { // MARK: - AuthorizationService extension AuthorizationServiceImpl: AuthorizationService { + func analyticsAuthType() -> AnalyticsEvent.AuthType { + let walletTokenPresent = tokenStorage.getString(for: KeyValueStoringKeys.walletToken) != nil + let isReusableWalletTokenPresent = tokenStorage.getBool(for: KeyValueStoringKeys.isReusableWalletToken) == true + + if walletTokenPresent && isReusableWalletTokenPresent { + return .paymentAuth + } else if tokenStorage.getString(for: KeyValueStoringKeys.moneyCenterAuthToken) != nil { + return .moneyAuth + } else { + return .withoutAuth + } + } + func getMoneyCenterAuthToken() -> String? { return tokenStorage.getString( for: KeyValueStoringKeys.moneyCenterAuthToken diff --git a/YooKassaPayments/Private/Services/Config/Config.swift b/YooKassaPayments/Private/Services/Config/Config.swift new file mode 100644 index 00000000..4374d70f --- /dev/null +++ b/YooKassaPayments/Private/Services/Config/Config.swift @@ -0,0 +1,142 @@ +import YooKassaPaymentsApi +import YooMoneyCoreApi + +struct Config: Codable { + struct PaymentMethod: Codable { + enum Kind: String { + case bankCard = "bank_card" + case applePay = "apple_pay" + case yoomoney = "yoo_money" + case sberbank = "sberbank" + case unknown + } + let kind: Kind + let title: String? + let iconUrl: URL + + enum CodingKeys: String, CodingKey { + case method, title, iconUrl + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + title = try container.decodeIfPresent(String.self, forKey: .title) + let rawKind = try container.decode(String.self, forKey: .method) + kind = Kind(rawValue: rawKind) ?? .unknown + iconUrl = try container.decode(URL.self, forKey: .iconUrl) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(title, forKey: .title) + try container.encode(kind.rawValue, forKey: .method) + try container.encode(iconUrl, forKey: .iconUrl) + } + } + struct SavePaymentMethodOptionTexts: Codable { + /// Заголовок для переключателя, связанного с реккурентными платежами + let switchRecurrentOnBindOnTitle: String + + /// Заголовок для переключателя, связанного с реккурентными платежами + let switchRecurrentOnBindOnSubtitle: String + + /// Заголовок для переключателя, связанного с реккурентными платежами + let switchRecurrentOnBindOffTitle: String + + /// Заголовок для переключателя, связанного с реккурентными платежами + let switchRecurrentOnBindOffSubtitle: String + + /// Заголовок для переключателя, связанного с реккурентными платежами + let switchRecurrentOffBindOnTitle: String + + /// Заголовок для переключателя, связанного с реккурентными платежами + let switchRecurrentOffBindOnSubtitle: String + + // Сообщение, отображаемое при изменении состояния, связанного с переключателем реккурентного платежа + let messageRecurrentOnBindOnTitle: String + + /// Сообщение, отображаемое при изменении состояния, связанного с переключателем реккурентного платежа + let messageRecurrentOnBindOnSubtitle: String + + /// Сообщение, отображаемое при изменении состояния, связанного с переключателем реккурентного платежа + let messageRecurrentOnBindOffTitle: String + + /// Сообщение, отображаемое при изменении состояния, связанного с переключателем реккурентного платежа + let messageRecurrentOnBindOffSubtitle: String + + /// Сообщение, отображаемое при изменении состояния, связанного с переключателем реккурентного платежа + let messageRecurrentOffBindOnTitle: String + + /// Сообщение, отображаемое при изменении состояния, связанного с переключателем реккурентного платежа + let messageRecurrentOffBindOnSubtitle: String + + /// Сообщение, отображаемое на экране, связанном с реккурентными платежами + let screenRecurrentOnBindOnTitle: String + + /// Сообщение, отображаемое на экране, связанном с реккурентными платежами + let screenRecurrentOnBindOnText: String + + /// Сообщение, отображаемое на экране, связанном с реккурентными платежами + let screenRecurrentOnBindOffTitle: String + + /// Сообщение, отображаемое на экране, связанном с реккурентными платежами + let screenRecurrentOnBindOffText: String + + /// Сообщение, отображаемое на экране, связанном с реккурентными платежами + let screenRecurrentOffBindOnTitle: String + + /// Сообщение, отображаемое на экране, связанном с реккурентными платежами + let screenRecurrentOffBindOnText: String + + /// Заголовок, связанный с реккурентными платежами SberPay + let screenRecurrentOnSberpayTitle: String + + /// Текст, связанный с реккурентыми платежами SberPay + let screenRecurrentOnSberpayText: String + } + + let yooMoneyLogoUrlLight: String + let yooMoneyLogoUrlDark: String + let paymentMethods: [PaymentMethod] + let savePaymentMethodOptionTexts: SavePaymentMethodOptionTexts + let userAgreementUrl: String + let yooMoneyApiEndpoint: URL + let yooMoneyPaymentAuthorizationApiEndpoint: URL + let yooMoneyAuthApiEndpoint: String? +} + +struct ConfigResponse: Codable, PaymentsApiResponse, JsonApiResponse { + let config: Config + + struct Method: Codable { + let oauthToken: String + + func encode(to encoder: Encoder) throws { + _ = encoder.unkeyedContainer() + } + } +} + +extension ConfigResponse.Method: ApiMethod { + public typealias Response = ConfigResponse + + public var hostProviderKey: String { GlobalConstants.Hosts.config } + public var httpMethod: HTTPMethod { .get } + public var parametersEncoding: ParametersEncoding { QueryParametersEncoder() } + + public var headers: Headers { + let headers = Headers( + [ + "Authorization": "Basic" + " " + oauthToken, + ] + ) + return headers + } + + public func urlInfo(from hostProvider: YooMoneyCoreApi.HostProvider) throws -> URLInfo { + .components( + host: try hostProvider.host(for: hostProviderKey), + path: "/api/merchant-profile/v1/remote-config/msdk" + ) + } +} diff --git a/YooKassaPayments/Private/Services/Config/ConfigMediator.swift b/YooKassaPayments/Private/Services/Config/ConfigMediator.swift new file mode 100644 index 00000000..9bcd24d9 --- /dev/null +++ b/YooKassaPayments/Private/Services/Config/ConfigMediator.swift @@ -0,0 +1,16 @@ +import UIKit + +protocol ConfigMediator { + func storedConfig() -> Config + func getConfig(token: String, completion: @escaping (Config) -> Void) + func asset(for key: ConfigurableAssetKey) -> UIImage + func asset(for key: ConfigurableAssetKey, completion: @escaping (UIImage?) -> Void) +} + +enum ConfigurableAssetKey: String { + case bankCard = "bank_card" + case yoomoney = "yoo_money" + case sberbank = "sberbank" + case applePay = "apple_pay" + case logo = "logo" +} diff --git a/YooKassaPayments/Private/Services/Config/ConfigMediatorAssembly.swift b/YooKassaPayments/Private/Services/Config/ConfigMediatorAssembly.swift new file mode 100644 index 00000000..27c7df59 --- /dev/null +++ b/YooKassaPayments/Private/Services/Config/ConfigMediatorAssembly.swift @@ -0,0 +1,10 @@ +import Foundation + +enum ConfigMediatorAssembly { + static func make(isLoggingEnabled: Bool) -> ConfigMediator { + return ConfigMediatorImpl( + service: ConfigServiceAssembly.make(isLoggingEnabled: isLoggingEnabled), + storage: KeyValueStoringAssembly.makeSettingsStorage() + ) + } +} diff --git a/YooKassaPayments/Private/Services/Config/ConfigMediatorImpl.swift b/YooKassaPayments/Private/Services/Config/ConfigMediatorImpl.swift new file mode 100644 index 00000000..9c85cf00 --- /dev/null +++ b/YooKassaPayments/Private/Services/Config/ConfigMediatorImpl.swift @@ -0,0 +1,256 @@ +import Foundation +import UIKit + +enum StorageKeys { + static var configKey: String { "config".appendingLanguageCode() } + static var assetsKey: String { "assets".appendingLanguageCode() } +} + +class ConfigMediatorImpl: ConfigMediator { + enum UpdateError: Error { + case canceled + } + + private let configService: ConfigService + private let storage: KeyValueStoring + + init(service: ConfigService, storage: KeyValueStoring) { + self.configService = service + self.storage = storage + } + + static var defaultConfig: Config { + let name = "defaultConfig".appendingLanguageCode() + guard + let url = Bundle.framework.url(forResource: name, withExtension: "json") + else { fatalError("URL for \(name).json not found ") } + + do { + let data = try Data(contentsOf: url) + return try JSONDecoder().decode(Config.self, from: data) + } catch { + fatalError("Could not load default config \(url); \nError: \(error)") + } + } + + private func write(config: Config) { + storage.write(value: config, for: StorageKeys.configKey) { [weak self] result in + guard let self = self else { return } + switch result { + case .success: + PrintLogger.trace("Config written successfully") + case .failure(let error): + PrintLogger.debugWarn( + "Failed to write to storage", + info: ["error": error.localizedDescription] + ) + PrintLogger.trace("Recovery attempt") + self.storage.write(value: Config?.none, for: StorageKeys.configKey) { [weak self] _ in + self?.storage.write(value: config, for: StorageKeys.configKey) { recoveryResult in + switch recoveryResult { + case .success: + PrintLogger.trace("Config written successfully") + case .failure(let error): + PrintLogger.debugWarn( + "Failed to write to storage", + info: ["error": error.localizedDescription] + ) + } + } + } + } + } + } + + private func write(assets: [URL: Data]) { + storage.write(value: assets, for: StorageKeys.assetsKey) { result in + switch result { + case .success: + PrintLogger.trace("Assets written successfully") + case .failure(let error): + PrintLogger.debugWarn( + "Failed to write to storage", + info: ["error": error.localizedDescription] + ) + } + } + } + + private func update(token: String, completion: @escaping (Result) -> Void) { + configService.getConfig(token: token) { [weak self] result in + guard let self = self else { return completion(.failure(UpdateError.canceled)) } + switch result { + case .success(let config): + self.write(config: config) + self.loadAssets(config: config) { + completion(.success(config)) + } + case .failure(let error): + completion(.failure(error)) + } + } + } + + func getConfig(token: String, completion: @escaping (Config) -> Void) { + update(token: token) { [weak self] result in + guard let self = self else { return } + switch result { + case .failure(let error): + PrintLogger.debugWarn("Update config failed", info: ["error": error.localizedDescription]) + self.readStoredConfig(completion: completion) + case .success(let config): + completion(config) + } + } + } + + private func readStoredConfig(completion: @escaping (Config) -> Void) { + storage.readValue(for: StorageKeys.configKey) { [weak self] (result: Result) in + guard let self = self else { return } + switch result { + case .failure(let error): + PrintLogger.debugWarn("Read storage failed", info: ["error": error.localizedDescription]) + let config = ConfigMediatorImpl.defaultConfig + self.storage.write(value: config, for: StorageKeys.configKey, completion: nil) + completion(config) + case .success(.none): + PrintLogger.trace("Init default storage") + let config = ConfigMediatorImpl.defaultConfig + self.storage.write(value: config, for: StorageKeys.configKey, completion: nil) + completion(config) + case .success(let config?): + completion(config) + } + } + } + + func storedConfig() -> Config { + do { + if let config: Config = try storage.readValue(for: StorageKeys.configKey) { + return config + } + PrintLogger.trace("stored config was nil") + return ConfigMediatorImpl.defaultConfig + } catch { + PrintLogger.trace("read stored config failed", info: ["error": error.localizedDescription]) + return ConfigMediatorImpl.defaultConfig + } + } + + // MARK: - Asset managment + + func asset(for key: ConfigurableAssetKey) -> UIImage { + let value: [String: Data]? = try? storage.readValue(for: StorageKeys.assetsKey) + PrintLogger.trace(String(describing: value)) + let image = value?[key.localizedKey].flatMap { UIImage(data: $0) } + return (image ?? defaultAsset(for: key)) + } + + func asset(for key: ConfigurableAssetKey, completion: @escaping (UIImage?) -> Void) { + let value: [String: Data]? = try? storage.readValue(for: StorageKeys.assetsKey) + PrintLogger.trace(String(describing: value)) + storage.readValue(for: StorageKeys.assetsKey) { (result: Result<[String: Data]?, Error>) in + guard + case .success(let assets) = result, + let data = assets?[key.localizedKey], + let image = UIImage(data: data) + else { return completion(nil) } + completion(image) + } + } + + private func defaultAsset(for key: ConfigurableAssetKey) -> UIImage { + switch key { + case .bankCard: return PaymentMethodResources.Image.unknown + case .yoomoney: return PaymentMethodResources.Image.yooMoney + case .sberbank: return PaymentMethodResources.Image.sberpay + case .applePay: return PaymentMethodResources.Image.applePay + case .logo: return UIImage.localizedImage("image.logo") + } + } + + private var loader: DataLoader? + + private func loadAssets(config: Config, completion: @escaping () -> Void) { + guard loader == nil else { + PrintLogger.debugWarn( + "Attempt to start new assets loading while previous loading didn't finish", + info: ["function": #function] ) + return + } + + var urls: [String: URL] = [:] + config.paymentMethods.forEach { + if let key = ConfigurableAssetKey(rawValue: $0.kind.rawValue) { + urls[key.localizedKey] = $0.iconUrl + } + } + if let dark = URL(string: config.yooMoneyLogoUrlDark) { + let key = (ConfigurableAssetKey.logo.rawValue + "_dark").appendingLanguageCode() + urls[key] = dark + } + if let light = URL(string: config.yooMoneyLogoUrlLight) { + let key = (ConfigurableAssetKey.logo.rawValue + "_light").appendingLanguageCode() + urls[key] = light + } + + let newLoader = DataLoader(urls: [URL](urls.values)) + newLoader.load { [weak self] result in + guard let self = self else { return } + self.loader = nil + PrintLogger.trace("Assets loading finished", info: ["result": result.description]) + var toStore: [String: Data] = [:] + result.forEach { (url: URL, value: Result) in + let keysToUpdate = urls.keys.filter { urls[$0] == url } + PrintLogger.trace( + "storing", + info: [ + "keysToUpdate": keysToUpdate.debugDescription, + "url": url.debugDescription, + ] + ) + if case .success(let data) = value { + keysToUpdate.forEach { + toStore[$0] = data + } + } + } + self.storage.write(value: toStore, for: StorageKeys.assetsKey) { _ in } + completion() + } + loader = newLoader + } +} + +private extension ConfigurableAssetKey { + var localizedKey: String { + switch self { + case .bankCard, .yoomoney, .sberbank, .applePay: + return rawValue.appendingLanguageCode() + case .logo: + let style: String + if #available(iOS 13.0, *) { + switch UIScreen.main.traitCollection.userInterfaceStyle { + case .dark: style = "dark" + case .light: style = "light" + default: style = "light" + } + } else { + style = "light" + } + + return [rawValue, style].joined(separator: "_").appendingLanguageCode() + } + } +} + +private extension String { + func appendingLanguageCode() -> String { + guard + let code = Locale.autoupdatingCurrent.languageCode, + ["ru", "en"].contains(code) + else { return self + "_" + "ru" } + + return self + "_" + code + } +} diff --git a/YooKassaPayments/Private/Services/Config/ConfigService.swift b/YooKassaPayments/Private/Services/Config/ConfigService.swift new file mode 100644 index 00000000..323fd5e1 --- /dev/null +++ b/YooKassaPayments/Private/Services/Config/ConfigService.swift @@ -0,0 +1,6 @@ +import Foundation +import YooMoneyCoreApi + +protocol ConfigService { + func getConfig(token: String, completion: @escaping (Result) -> Void) +} diff --git a/YooKassaPayments/Private/Services/Config/ConfigServiceAssembly.swift b/YooKassaPayments/Private/Services/Config/ConfigServiceAssembly.swift new file mode 100644 index 00000000..6c2d2f32 --- /dev/null +++ b/YooKassaPayments/Private/Services/Config/ConfigServiceAssembly.swift @@ -0,0 +1,10 @@ +import Foundation + +enum ConfigServiceAssembly { + static func make(isLoggingEnabled: Bool) -> ConfigService { + ConfigServiceImpl( + session: ApiSessionAssembly.makeApiSession(isLoggingEnabled: isLoggingEnabled), + loginEnabled: isLoggingEnabled + ) + } +} diff --git a/YooKassaPayments/Private/Services/Config/ConfigServiceImpl.swift b/YooKassaPayments/Private/Services/Config/ConfigServiceImpl.swift new file mode 100644 index 00000000..713b5d9b --- /dev/null +++ b/YooKassaPayments/Private/Services/Config/ConfigServiceImpl.swift @@ -0,0 +1,24 @@ +import Foundation +import YooMoneyCoreApi + +class ConfigServiceImpl: ConfigService { + private let session: ApiSession + + let isLoggingEnabled: Bool + + init(session: ApiSession, loginEnabled: Bool) { + self.session = session + self.isLoggingEnabled = loginEnabled + } + + func getConfig(token: String, completion: @escaping (Result) -> Void) { + session.perform(apiMethod: ConfigResponse.Method(oauthToken: token)).responseApi { response in + switch response { + case .left(let error): + completion(.failure(error)) + case .right(let response): + completion(.success(response.config)) + } + } + } +} diff --git a/YooKassaPayments/Private/Services/HostProvider.swift b/YooKassaPayments/Private/Services/HostProvider.swift index 9da82910..18a55a5f 100644 --- a/YooKassaPayments/Private/Services/HostProvider.swift +++ b/YooKassaPayments/Private/Services/HostProvider.swift @@ -1,3 +1,4 @@ +import Foundation import YooKassaPaymentsApi import YooKassaWalletApi import YooMoneyCoreApi @@ -6,38 +7,55 @@ final class HostProvider { // MARK: - Init data - let settingsStorage: KeyValueStoring + private let settingsStorage: KeyValueStoring + private let configStorage: KeyValueStoring + private let defaultConfig: Config // MARK: - Init - init(settingStorage: KeyValueStoring) { + init(settingStorage: KeyValueStoring, configStorage: KeyValueStoring, defaultConfig: Config) { self.settingsStorage = settingStorage + self.configStorage = configStorage + self.defaultConfig = defaultConfig } } // MARK: - YooMoneyCoreApi.HostProvider extension HostProvider: YooMoneyCoreApi.HostProvider { - func host( - for key: String - ) throws -> String { + func host(for key: String) throws -> String { let isDevHost = settingsStorage.getBool(for: Settings.Keys.devHost) ?? false let host: String + let mediator = ConfigMediatorAssembly.make(isLoggingEnabled: false) if isDevHost, let devHost = try makeDevHost(key: key) { host = devHost } else { + let config: Config = mediator.storedConfig() + let string: String switch key { case YooKassaPaymentsApi.Constants.paymentsApiMethodsKey: - host = "//sdk.yookassa.ru" + string = config.yooMoneyApiEndpoint.absoluteString case YooKassaWalletApi.Constants.walletApiMethodsKey: - host = "//yoomoney.ru" + string = config.yooMoneyPaymentAuthorizationApiEndpoint.absoluteString case GlobalConstants.Hosts.moneyAuth: - host = "//yoomoney.ru" + if let auth = config.yooMoneyAuthApiEndpoint, !auth.isEmpty { + string = auth + } else { + string = "https://yoomoney.ru" + } + case GlobalConstants.Hosts.config: + string = "https://yookassa.ru" default: throw HostProviderError.unknownKey(key) } + + guard var components = URLComponents(string: string) else { throw HostProviderError.unknownKey(key) } + components.path = "" + + guard let url = components.url else { throw HostProviderError.unknownKey(key) } + host = url.absoluteString } return host @@ -50,28 +68,43 @@ extension HostProvider: YooMoneyCoreApi.HostProvider { return nil } + let config = ConfigMediatorAssembly.make(isLoggingEnabled: false).storedConfig() + let host: String switch key { case YooKassaWalletApi.Constants.walletApiMethodsKey: - host = devHosts.wallet + host = config.yooMoneyPaymentAuthorizationApiEndpoint.absoluteString case YooKassaPaymentsApi.Constants.paymentsApiMethodsKey: - host = devHosts.payments + host = config.yooMoneyApiEndpoint.absoluteString case GlobalConstants.Hosts.moneyAuth: - host = devHosts.moneyAuth + if let auth = config.yooMoneyAuthApiEndpoint, !auth.isEmpty { + host = auth + } else { + host = devHosts.moneyAuth + } + case GlobalConstants.Hosts.config: + host = devHosts.config default: throw HostProviderError.unknownKey(key) } - return host + guard var components = URLComponents(string: host) else { throw HostProviderError.unknownKey(key) } + components.path = "" + + guard let url = components.url else { throw HostProviderError.unknownKey(key) } + return url.absoluteString } private static var hosts: HostsConfig? = { - guard let url = Bundle.framework.url(forResource: "Hosts", withExtension: "plist"), - let hosts = NSDictionary(contentsOf: url) as? [String: Any], - let walletHost = hosts[Keys.wallet.rawValue] as? String, - let paymentsHost = hosts[Keys.payments.rawValue] as? String, - let moneyAuthHost = hosts[Keys.moneyAuth.rawValue] as? String else { + guard + let url = Bundle.framework.url(forResource: "Hosts", withExtension: "plist"), + let hosts = NSDictionary(contentsOf: url) as? [String: Any], + let walletHost = hosts[Keys.wallet.rawValue] as? String, + let paymentsHost = hosts[Keys.payments.rawValue] as? String, + let moneyAuthHost = hosts[Keys.moneyAuth.rawValue] as? String, + let config = hosts[Keys.config.rawValue] as? String + else { assertionFailure("Couldn't load Hosts.plist from framework bundle") return nil } @@ -79,7 +112,8 @@ extension HostProvider: YooMoneyCoreApi.HostProvider { return HostsConfig( wallet: walletHost, payments: paymentsHost, - moneyAuth: moneyAuthHost + moneyAuth: moneyAuthHost, + config: config ) }() @@ -87,5 +121,6 @@ extension HostProvider: YooMoneyCoreApi.HostProvider { case wallet case payments case moneyAuth + case config } } diff --git a/YooKassaPayments/Private/Services/KeyValueStoring/KeyValueStoring.swift b/YooKassaPayments/Private/Services/KeyValueStoring/KeyValueStoring.swift index 12af20c1..a579e0fd 100644 --- a/YooKassaPayments/Private/Services/KeyValueStoring/KeyValueStoring.swift +++ b/YooKassaPayments/Private/Services/KeyValueStoring/KeyValueStoring.swift @@ -1,7 +1,58 @@ -protocol KeyValueStoring: AnyObject { - func getString(for key: String) -> String? - func set(string: String?, for key: String) +/// `Key - Value` persistent storage interface +protocol KeyValueStoring { + /// Set a value for a given key. If value is `nil` & `key` exists, then this `key - value` pair is removed + /// from storage. Otherwise `key - value` pair is added if key is not present, or existing value is replaced + /// with `value`. + /// + /// Throws error if writing fails. + func write(value: T?, for key: String) throws - func getBool(for key: String) -> Bool? - func set(bool: Bool?, for key: String) + /// Read value of type `T` for the given `key`. Returns nil if key is not present in the storage. + func readValue(for key: String) throws -> T? +} + +extension KeyValueStoring { + func write( + value: T?, + for key: String, + completion: ((Result) -> Void)? + ) { + DispatchQueue.global().async { + do { + try write(value: value, for: key) + completion?(.success(())) + } catch { + completion?(.failure(error)) + } + } + } + func readValue( + for key: String, + queue: DispatchQueue = DispatchQueue.global(), + completion: @escaping (Result) -> Void + ) { + queue.async { + do { + completion(.success(try readValue(for: key))) + } catch { + completion(.failure(error)) + } + + } + } + + // TODO: - Remove legacy + func getString(for key: String) -> String? { + try? readValue(for: key) + } + func set(string: String?, for key: String) { + try? write(value: string, for: key) + } + + func getBool(for key: String) -> Bool? { + try? readValue(for: key) + } + func set(bool: Bool?, for key: String) { + try? write(value: bool, for: key) + } } diff --git a/YooKassaPayments/Private/Services/KeyValueStoring/KeyValueStoringAssembly.swift b/YooKassaPayments/Private/Services/KeyValueStoring/KeyValueStoringAssembly.swift index b8e7af5a..145f7a81 100644 --- a/YooKassaPayments/Private/Services/KeyValueStoring/KeyValueStoringAssembly.swift +++ b/YooKassaPayments/Private/Services/KeyValueStoring/KeyValueStoringAssembly.swift @@ -1,18 +1,40 @@ enum KeyValueStoringAssembly { - static weak var shared: KeyValueStoring? + static var shared: KeyValueStoring? static func makeKeychainStorage() -> KeyValueStoring { return KeychainStorage(service: Constants.Keys.serviceId) } + static func makeUserDefaultsStorage() -> KeyValueStoring { + UserDefaultsStorage(userDefaults: UserDefaults.standard) + } + static func makeSettingsStorage() -> KeyValueStoring { - return UserDefaultsStorage(userDefaults: .standard) + let manager = FileManager.default + guard + let url = manager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first + else { + fatalError("failed to initialize settings storage") + } + + let directory = url.appendingPathComponent("yoo.msdk", isDirectory: true) + if !manager.fileExists(atPath: directory.path, isDirectory: nil) { + do { + try manager.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil) + } catch { + PrintLogger.error( + "failed to create yoo.msdk directory", + info: ["error": error.localizedDescription] + ) + } + } + + let file = url.appendingPathComponent("settings", isDirectory: false) + return PlistStorage(storageUrl: file) } - static func makeKeychainStorageMock( - testModeSettings: TestModeSettings - ) -> KeyValueStoring { + static func makeKeychainStorageMock(testModeSettings: TestModeSettings) -> KeyValueStoring { let storage: KeyValueStoring if let shared = KeyValueStoringAssembly.shared { storage = shared diff --git a/YooKassaPayments/Private/Services/Logger.swift b/YooKassaPayments/Private/Services/Logger.swift new file mode 100644 index 00000000..0ff4484e --- /dev/null +++ b/YooKassaPayments/Private/Services/Logger.swift @@ -0,0 +1,169 @@ +import Foundation + +import Foundation + +/// Possible log level enumeration +enum LogLevel { + /// Lowest level. Most fine-grain info. One can expect the trace level to be very verbose. You can use it + /// for example to annotate each step in the algorithm or each individual query with parameters in your code. + case trace + + /// Less granular compared to the TRACE level, but it is more than you will need in everyday use. + /// The DEBUG log level should be used for information that may be needed for diagnosing issues and + /// troubleshooting or when running application in the test environment for the purpose of + /// making sure everything is running correctly + case debug + + /// The standard log level indicating that something happened, the application entered a certain state, etc. + /// For example, a controller of your authorization API may include an INFO log level with information on which + /// user requested authorization if the authorization was successful or not. The information logged using the + /// `info` log level should be purely informative and not looking into them on a regular basis shouldn’t + /// result in missing any important information. + case info + + /// The log level that indicates that something unexpected happened in the application, a problem, or a situation + /// that might disturb one of the processes. But that doesn’t mean that the application failed. The `warn` level + /// should be used in situations that are unexpected, but the code can continue the work. + /// For example, a parsing error occurred that resulted in a certain document not being processed. + case warn + + /// The log level that should be used when the application hits an issue preventing one or more functionalities + /// from properly functioning. The `error` log level can be used when one of the payment systems is not available, + /// but there is still the option to check out the basket in the e-commerce application or when your social media + /// logging option is not working for some reason. + case error + + /// The log level that tells that the application encountered an event or entered a state in which one of the + /// crucial business functionality is no longer working. A FATAL log level may be used when the application is + /// not able to connect to a crucial data store like a database or all the payment systems are not available + /// and users can’t checkout their baskets in your e-commerce. + case fatal +} + +extension LogLevel { + /// Default tag + var tag: String { + switch self { + case .trace: return "🕵🏻‍♂️" + case .debug: return "🇩🇪🐛" + case .info: return "💬" + case .warn: return "⚠️" + case .error: return "⛑" + case .fatal: return "🔥☠️🔥☠️🔥" + } + } +} + +extension LogLevel: CustomStringConvertible { + public var description: String { + switch self { + case .trace: return "TRACE" + case .debug: return "DEBUG" + case .info: return "INFO" + case .warn: return "WARN" + case .error: return "ERROR" + case .fatal: return "FATAL" + } + } +} + +extension LogLevel: Comparable {} + +/// Logger interface +protocol Logger { + /// Log message with level. Also add optional tag and/or additional info. + static func log(_ message: String, level: LogLevel, tag: String?, info: [String: String]?) +} + +extension Logger { + /// if `DEBUG` logs message and info, if any, to console with `.trace` log level using that level tag + static func trace(_ message: String, info: [String: String]? = nil) { + #if DEBUG + let level = LogLevel.trace + log(message, level: level, tag: level.tag, info: info) + #endif + } + + /// if `DEBUG` logs message and info, if any, to console with `.debug` log level using that level tag + static func debug(_ message: String, info: [String: String]? = nil) { + #if DEBUG + let level = LogLevel.debug + log(message, level: level, tag: level.tag, info: info) + #endif + } + + /// Log message and info, if any, to console with `.info` log level using that level tag + static func info(_ message: String, info: [String: String]? = nil) { + let level = LogLevel.info + log(message, level: level, tag: level.tag, info: info) + } + + /// Log message and info, if any, to console with `.warn` log level using that level tag + static func warn(_ message: String, info: [String: String]? = nil) { + let level = LogLevel.warn + log(message, level: level, tag: level.tag, info: info) + } + + /// Log message and info, if any, to console with `.warn` log level using that level tag + static func debugWarn(_ message: String, info: [String: String]? = nil) { + #if DEBUG + let level = LogLevel.warn + log(message, level: level, tag: level.tag, info: info) + #endif + } + + /// Log message and info, if any, to console with `.error` log level using that level tag + static func error(_ message: String, info: [String: String]? = nil) { + let level = LogLevel.error + log(message, level: level, tag: level.tag, info: info) + } + + /// Log message and info, if any, to console with `.fatal` log level using that level tag + static func fatal(_ message: String, info: [String: String]? = nil) { + let level = LogLevel.fatal + log(message, level: level, tag: level.tag, info: info) + } +} + +/// Log using `print()` +struct PrintLogger: Logger { + private static let title = "PrintLogger" + + /// Logging messages >= than `level`. Global filter + static var level: LogLevel = .info + static var forceSilence = false + + private enum PrintLoggerError: Error { + case prettyFailed + } + + private static func pretty(_ some: Any) throws -> String { + let data = try JSONSerialization.data(withJSONObject: some, options: .prettyPrinted) + guard let string = String(data: data, encoding: .utf8) else { + throw PrintLoggerError.prettyFailed + } + return string + } + + static func log(_ message: String, level: LogLevel, tag: String?, info: [String: String]?) { + guard level >= PrintLogger.level, !PrintLogger.forceSilence else { return } + let tagText = tag ?? "" + print(tagText + "=====" + " \(title)" + "=====") + + var log: [Any] = [ + "LOG LEVEL: \(level)", + "MESSAGE: \(message)", + ] + info + .map { unwrapped -> String in + if let pretty = try? pretty(unwrapped) { + return pretty + } + return unwrapped.description + } + .map { log.append("INFO: \($0)") } + + log.forEach { print($0) } + print(tagText + "=====") + } +} diff --git a/YooKassaPayments/Private/Services/Payment/TooManyRequestsError.swift b/YooKassaPayments/Private/Services/Payment/TooManyRequestsError.swift new file mode 100644 index 00000000..4c2a3219 --- /dev/null +++ b/YooKassaPayments/Private/Services/Payment/TooManyRequestsError.swift @@ -0,0 +1,29 @@ +import YooKassaPaymentsApi + +struct TooManyRequestsError: PresentableError { + var title: String? { + return nil + } + + var message: String { Localized.message } + + var style: PresentableNotificationStyle { + return .alert + } + var actions: [PresentableNotificationAction] { + return [] + } +} + +// MARK: - Localized + +private extension TooManyRequestsError { + enum Localized { + static let message = NSLocalizedString( + "Error.paymentOptions.tooManyRequests.429", + bundle: Bundle.framework, + value: "Превышен лимит запросов", + comment: "HTTP 429 Ошибка `Превышен лимит запросов` на экране выбора способа оплаты" + ) + } +} diff --git a/YooKassaPayments/Private/Services/Storage/KeychainStorage.swift b/YooKassaPayments/Private/Services/Storage/KeychainStorage.swift index eab86c1e..e0133627 100644 --- a/YooKassaPayments/Private/Services/Storage/KeychainStorage.swift +++ b/YooKassaPayments/Private/Services/Storage/KeychainStorage.swift @@ -1,7 +1,6 @@ import Security final class KeychainStorage { - // MARK: - Init data private let service: String @@ -127,29 +126,35 @@ extension KeychainStorage { // MARK: - KeyValueStoring extension KeychainStorage: KeyValueStoring { - - func getString(for key: String) -> String? { - return getValue(for: key) - } - - func set(string: String?, for key: String) { - setValue(string, for: key) - } - - func getBool(for key: String) -> Bool? { - guard let value = getValue(for: key) else { return nil } - return ["YES", "1"].contains(value) + func write(value: T?, for key: String) throws where T: Encodable { + switch value { + case let string as String?: + return setValue(string, for: key) + case let bool as Bool?: + switch bool { + case true?: + setValue("YES", for: key) + case false?: + setValue("NO", for: key) + case nil: + removeValue(for: key) + } + default: + PrintLogger.debugWarn("Attempt to write unsuported value") + } } - func set(bool: Bool?, for key: String) { - switch bool { - case true?: - setValue("YES", for: key) - case false?: - setValue("NO", for: key) - case nil: - removeValue(for: key) + func readValue(for key: String) throws -> T? where T: Decodable { + if T.self == String.self { + let value = getValue(for: key) + let casted = value as? T + return casted + } else if T.self == Bool.self { + let value = getValue(for: key).map { ["YES", "1"].contains($0) } + return value as? T } + PrintLogger.debugWarn("Attempt to read unsuported value type") + return nil } } diff --git a/YooKassaPayments/Private/Services/Storage/KeychainStorageMock.swift b/YooKassaPayments/Private/Services/Storage/KeychainStorageMock.swift index d910d3dc..daa14eb4 100644 --- a/YooKassaPayments/Private/Services/Storage/KeychainStorageMock.swift +++ b/YooKassaPayments/Private/Services/Storage/KeychainStorageMock.swift @@ -1,29 +1,16 @@ final class KeychainStorageMock { + let writeQueue = DispatchQueue(label: "com.msdk.KeychainStorageMock.writeQueue") private var data: [String: Any] = [:] } // MARK: - KeyValueStoring extension KeychainStorageMock: KeyValueStoring { - func getString(for key: String) -> String? { - guard let value = data[key] as? String else { - return nil - } - return value + func readValue(for key: String) throws -> T? where T: Decodable { + data[key] as? T } - func set(string: String?, for key: String) { - data[key] = string - } - - func getBool(for key: String) -> Bool? { - guard let value = data[key] as? Bool else { - return nil - } - return value - } - - func set(bool: Bool?, for key: String) { - data[key] = bool + func write(value: T?, for key: String) throws where T: Encodable { + data[key] = value } } diff --git a/YooKassaPayments/Private/Services/Storage/PlistStorage.swift b/YooKassaPayments/Private/Services/Storage/PlistStorage.swift new file mode 100644 index 00000000..731c103d --- /dev/null +++ b/YooKassaPayments/Private/Services/Storage/PlistStorage.swift @@ -0,0 +1,88 @@ +import Foundation + +/// A simple storage not ment for storing large amounts of data +/// If you need to store significant amount (>10MB) use sqlite or other +class PlistStorage: KeyValueStoring { + private let decoder = PropertyListDecoder() + private let encoder = PropertyListEncoder() + private let storageUrl: URL + init(storageUrl: URL) { + writeQueue = DispatchQueue(label: "com.writeQueue.PlistStorage", attributes: .concurrent) + self.storageUrl = storageUrl + setup() + } + + private func setup() { + do { + _ = try Data(contentsOf: storageUrl) + } catch { + PrintLogger.trace("Read storage failed", info: ["error": error.localizedDescription]) + setupEmpty() + } + } + + private func setupEmpty() { + do { + PrintLogger.trace("Attempting to setup empty storage") + let empty = try PropertyListEncoder().encode([String: String]()) + try empty.write(to: storageUrl, options: .atomic) + } catch { + PrintLogger.error( + "Setup empty storage failed", + info: [ + "error": error.localizedDescription, + "storageUrl": storageUrl.debugDescription, + ] + ) + } + } + + // MARK: - KeyValueStoring implementation + let writeQueue: DispatchQueue + func write(value: T?, for key: String) throws where T: Encodable { + try writeQueue.sync(flags: .barrier) { + do { + PrintLogger.trace( + "Plist storage writing", + info: ["key": key] + ) + let storage = try Data(contentsOf: storageUrl) + var target = try decoder.decode(Dictionary.self, from: storage) + target[key] = try encoder.encode(value) + let update = try encoder.encode(target) + try update.write(to: storageUrl, options: .atomicWrite) + } catch { + PrintLogger.trace( + "failed write storage", + info: [ + "error": error.localizedDescription, + "key": key, + "url": storageUrl.absoluteString, + ] + ) + throw error + } + } + } + func readValue(for key: String) throws -> T? where T: Decodable { + do { + let data = try Data(contentsOf: storageUrl) + let decoded = try decoder.decode(Dictionary.self, from: data) + let value = decoded[key] + return try value.map { valueData -> T in + do { + let value = try decoder.decode(T.self, from: valueData) + let description = (value as? CustomStringConvertible)?.description ?? "no description" + PrintLogger.trace("decoded \(type(of: value))", info: ["value": description]) + return value + } catch { + PrintLogger.trace("failed decoding", info: ["error": error.localizedDescription, "key": key]) + throw error + } + } + } catch { + PrintLogger.error(error.localizedDescription) + throw error + } + } +} diff --git a/YooKassaPayments/Private/Services/Storage/UserDefaultsStorage.swift b/YooKassaPayments/Private/Services/Storage/UserDefaultsStorage.swift index 2cbcfbf0..0b93ebdb 100644 --- a/YooKassaPayments/Private/Services/Storage/UserDefaultsStorage.swift +++ b/YooKassaPayments/Private/Services/Storage/UserDefaultsStorage.swift @@ -1,5 +1,4 @@ final class UserDefaultsStorage { - // MARK: - Init data let userDefaults: UserDefaults @@ -40,19 +39,11 @@ extension UserDefaultsStorage { // MARK: - KeyValueStoring extension UserDefaultsStorage: KeyValueStoring { - func getString(for key: String) -> String? { - return userDefaults.string(forKey: key) - } - - func set(string: String?, for key: String) { - userDefaults.set(string, forKey: key) - } - - func getBool(for key: String) -> Bool? { - return userDefaults.bool(forKey: key) + func write(value: T?, for key: String) throws where T: Encodable { + userDefaults.setValue(value, forKey: key) } - func set(bool: Bool?, for key: String) { - userDefaults.set(bool, forKey: key) + func readValue(for key: String) throws -> T? where T: Decodable { + userDefaults.value(forKey: key) as? T } } diff --git a/YooKassaPayments/Private/SheetView/SheetContentViewController.swift b/YooKassaPayments/Private/SheetView/SheetContentViewController.swift index dc1fd41a..254e173a 100644 --- a/YooKassaPayments/Private/SheetView/SheetContentViewController.swift +++ b/YooKassaPayments/Private/SheetView/SheetContentViewController.swift @@ -10,10 +10,12 @@ final class SheetContentViewController: UIViewController { lazy var contentView: UIView = { $0.translatesAutoresizingMaskIntoConstraints = false + $0.accessibilityIdentifier = "contentView" return $0 }(UIView()) private lazy var contentWrapperView: UIView = { + $0.accessibilityIdentifier = "contentWrapperView" $0.translatesAutoresizingMaskIntoConstraints = false $0.layer.masksToBounds = true if #available(iOS 11.0, *) { @@ -40,13 +42,17 @@ final class SheetContentViewController: UIViewController { return $0 }(UIView()) + private lazy var backdropView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.setStyles(UIView.Styles.defaultBackground) + return view + }() + private lazy var childContainerView: UIView = { + $0.accessibilityIdentifier = "childContainerView" $0.translatesAutoresizingMaskIntoConstraints = false - if #available(iOS 13.0, *) { - $0.backgroundColor = UIColor.systemBackground - } else { - $0.backgroundColor = UIColor.white - } + $0.setStyles(UIView.Styles.defaultBackground) $0.layer.masksToBounds = true if #available(iOS 11.0, *) { $0.layer.maskedCorners = [ @@ -60,7 +66,14 @@ final class SheetContentViewController: UIViewController { // MARK: - NSLayoutConstraint private lazy var contentTopConstraint: NSLayoutConstraint = { - return contentView.topAnchor.constraint(equalTo: view.topAnchor) + let constraint: NSLayoutConstraint + if #available(iOS 11.0, *) { + constraint = contentView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor) + } else { + // Fallback on earlier versions + constraint = contentView.topAnchor.constraint(equalTo: view.topAnchor) + } + return constraint }() private lazy var contentBottomConstraint: NSLayoutConstraint = { @@ -157,6 +170,8 @@ final class SheetContentViewController: UIViewController { private func setupContentView() { view.addSubview(contentView) + view.addSubview(backdropView) + view.sendSubviewToBack(backdropView) contentView.addSubview(contentWrapperView) contentView.addSubview(overflowView) @@ -171,6 +186,11 @@ final class SheetContentViewController: UIViewController { contentWrapperView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), contentWrapperView.topAnchor.constraint(equalTo: contentView.topAnchor), + backdropView.leftAnchor.constraint(equalTo: contentView.leftAnchor), + backdropView.rightAnchor.constraint(equalTo: contentView.rightAnchor), + backdropView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + backdropView.topAnchor.constraint(equalTo: contentWrapperView.topAnchor, constant: Space.fivefold), + overflowView.leftAnchor.constraint(equalTo: contentView.leftAnchor), overflowView.rightAnchor.constraint(equalTo: contentView.rightAnchor), overflowView.heightAnchor.constraint(equalToConstant: 200), @@ -184,11 +204,12 @@ final class SheetContentViewController: UIViewController { private func setupChildContainerView() { contentWrapperView.addSubview(childContainerView) + let top = childContainerView.topAnchor.constraint( + equalTo: contentWrapperView.topAnchor, + constant: options.pullBarHeight + ) NSLayoutConstraint.activate([ - childContainerView.topAnchor.constraint( - equalTo: contentWrapperView.topAnchor, - constant: options.pullBarHeight - ), + top, childContainerView.leftAnchor.constraint(equalTo: contentWrapperView.leftAnchor), childContainerView.rightAnchor.constraint(equalTo: contentWrapperView.rightAnchor), childContainerView.bottomAnchor.constraint(equalTo: contentWrapperView.bottomAnchor), diff --git a/YooKassaPayments/Private/SheetView/SheetViewController.swift b/YooKassaPayments/Private/SheetView/SheetViewController.swift index bc693862..d785ef16 100644 --- a/YooKassaPayments/Private/SheetView/SheetViewController.swift +++ b/YooKassaPayments/Private/SheetView/SheetViewController.swift @@ -183,6 +183,7 @@ final class SheetViewController: UIViewController { contentViewHeightConstraint = contentViewController.view.heightAnchor.constraint( equalToConstant: height(for: .intrinsic) ) + contentViewHeightConstraint.priority = .highest let leftLowPriorityConstraint = contentViewController.view.leadingAnchor.constraint( equalTo: view.leadingAnchor ) @@ -194,7 +195,6 @@ final class SheetViewController: UIViewController { let topConstraint = contentViewController.view.topAnchor.constraint( greaterThanOrEqualTo: view.topAnchor ) - topConstraint.priority = .highest var constraints = [ leftLowPriorityConstraint, leftGreaterThanOrEqualToConstraint, diff --git a/YooKassaPayments/Public/Models/Misc/PaymentMethodType.swift b/YooKassaPayments/Public/Models/Misc/PaymentMethodType.swift index bd1868da..f8c1f8e7 100644 --- a/YooKassaPayments/Public/Models/Misc/PaymentMethodType.swift +++ b/YooKassaPayments/Public/Models/Misc/PaymentMethodType.swift @@ -41,6 +41,8 @@ extension PaymentMethodType { self = .applePay case .yooMoney: self = .yooMoney + @unknown default: + fatalError("unsupported paymentMethodType") } } diff --git a/YooKassaPayments/Public/Models/Misc/SavePaymentMethod.swift b/YooKassaPayments/Public/Models/Misc/SavePaymentMethod.swift index 31c5d540..40d5f281 100644 --- a/YooKassaPayments/Public/Models/Misc/SavePaymentMethod.swift +++ b/YooKassaPayments/Public/Models/Misc/SavePaymentMethod.swift @@ -19,3 +19,13 @@ public enum SavePaymentMethod { /// if payment method can be saved). case userSelects } + +extension SavePaymentMethod: CustomStringConvertible { + public var description: String { + switch self { + case .on: return "on" + case .off: return "off" + case .userSelects: return "userSelects" + } + } +} diff --git a/YooKassaPayments/Public/Resources/defaultConfig_en.json b/YooKassaPayments/Public/Resources/defaultConfig_en.json new file mode 100644 index 00000000..a9ddde42 --- /dev/null +++ b/YooKassaPayments/Public/Resources/defaultConfig_en.json @@ -0,0 +1,47 @@ +{ + "yooMoneyLogoUrlLight": "https://static.yoomoney.ru/mobile-app-content-front/msdk/payment-options/v1/iokassa-light-eng.png", + "yooMoneyLogoUrlDark": "https://static.yoomoney.ru/mobile-app-content-front/msdk/payment-options/v1/iokassa-dark-eng.png", + "paymentMethods": [{ + "method": "bank_card", + "title": "Bank card", + "iconUrl": "https://static.yoomoney.ru/mobile-app-content-front/msdk/payment-options/v1/ym_ic_add_card.png" + }, { + "method": "yoo_money", + "title": "YooMoney", + "iconUrl": "https://static.yoomoney.ru/mobile-app-content-front/msdk/payment-options/v1/ym_ic_yoomoney.png" + }, { + "method": "sberbank", + "title": "SberPay", + "iconUrl": "https://static.yoomoney.ru/mobile-app-content-front/msdk/payment-options/v1/ym_ic_sberbank.png" + }, { + "method": "apple_pay", + "title": "Apple Pay", + "iconUrl": "https://static.yoomoney.ru/mobile-app-content-front/msdk/payment-options/v1/other_cardSystemApplePay.png" + }], + "savePaymentMethodOptionTexts": { + "switchRecurrentOnBindOnTitle": "Allow autopayments and save payment details", + "switchRecurrentOnBindOnSubtitle": "After the payment, the store will save your bank card details and it'll be able to debit money without your participation", + "switchRecurrentOnBindOffTitle": "Allow autopayments", + "switchRecurrentOnBindOffSubtitle": "After the payment, this card will be saved: the store will be able to debit money without your participation", + "switchRecurrentOffBindOnTitle": "Save payment details", + "switchRecurrentOffBindOnSubtitle": "The store will save your bank card details: next time, you won't need to enter them", + "messageRecurrentOnBindOnTitle": "Allowing autopayments and saving payment details", + "messageRecurrentOnBindOnSubtitle": "By making this payment, you allow saving your bank card details and debiting money without your participation", + "messageRecurrentOnBindOffTitle": "Allowing autopayments", + "messageRecurrentOnBindOffSubtitle": "By making this payment, you allow saving this card and debiting money without your participation", + "messageRecurrentOffBindOnTitle": "Saving payment details", + "messageRecurrentOffBindOnSubtitle": "By making this payment, you allow the store to save your bank card details: next time, you won't need to enter them
", + "screenRecurrentOnBindOnTitle": "Autopayments
and saving payment details", + "screenRecurrentOnBindOnText": "If you allowed it, the store will save your bank card details: card number, cardholder's name, and expiration date (everything except the CVC). Next time, you won't need to enter them for payments in this store.

Besides that, we'll link the card (including if it's been used via Apple Pay) to the store. After that, the store will be able to send requests for debiting money automatically: then the payment is made without an additional confirmation from you.

Autopayments will continue even after you reissue the card, even if your bank can update the data automatically. You can cancel them and unlink the card at any moment via store's support service.", + "screenRecurrentOnBindOffTitle": "How autopayments work", + "screenRecurrentOnBindOffText": "If you allow autopayments, we'll link the bank card (including if it's been used via Apple Pay) to the store. After that, the store will be able to send requests for debiting money automatically, then the payment is made without an additional confirmation from you.

Autopayments will continue even after you reissue the card, even if your bank can update the data automatically. You can cancel them and unlink the card at any moment via store's support service.", + "screenRecurrentOffBindOnTitle": "Saving payment details", + "screenRecurrentOffBindOnText": "If you allowed it, your bank card details will be saved for this store and its partners: card number, cardholder's name, and expiration date (everything except the CVC). Next time, you won't need to enter them for payments in this store.

You can delete the bank card details during the payment process (tap on the three dots next to the card and select \"Remove the card\") or by contacting the support service.", + "screenRecurrentOnSberpayTitle": "How autopayments work", + "screenRecurrentOnSberpayText": "If you allow autopayments, we'll link the bank card (including if it's been used via Apple Pay) to the store. After that, the store will be able to send requests for debiting money automatically, then the payment is made without an additional confirmation from you.

Autopayments will continue even after you reissue the card, even if your bank can update the data automatically. You can cancel them and unlink the card at any moment via store's support service." + }, + "userAgreementUrl": "By clicking this button, you accept
the terms and conditions of the service", + "yooMoneyApiEndpoint": "https://sdk.yookassa.ru/api/frontend/v3", + "yooMoneyPaymentAuthorizationApiEndpoint": "https://yoomoney.ru/api/wallet-auth/v1", + "yooMoneyAuthApiEndpoint": "" +} diff --git a/YooKassaPayments/Public/Resources/defaultConfig_ru.json b/YooKassaPayments/Public/Resources/defaultConfig_ru.json new file mode 100644 index 00000000..67e5fbf7 --- /dev/null +++ b/YooKassaPayments/Public/Resources/defaultConfig_ru.json @@ -0,0 +1,52 @@ +{ + "yooMoneyLogoUrlLight": "https://static.yoomoney.ru/mobile-app-content-front/msdk/payment-options/v1/iokassa-light-rus.png", + "yooMoneyLogoUrlDark": "https://static.yoomoney.ru/mobile-app-content-front/msdk/payment-options/v1/iokassa-dark-rus.png", + "paymentMethods": [ + { + "method": "bank_card", + "title": "Банковская карта", + "iconUrl": "https://static.yoomoney.ru/mobile-app-content-front/msdk/payment-options/v1/ym_ic_add_card.png" + }, + { + "method": "yoo_money", + "title": "ЮMoney", + "iconUrl": "https://static.yoomoney.ru/mobile-app-content-front/msdk/payment-options/v1/ym_ic_yoomoney.png" + }, + { + "method": "sberbank", + "title": "SberPay", + "iconUrl": "https://static.yoomoney.ru/mobile-app-content-front/msdk/payment-options/v1/ym_ic_sberbank.png" + }, + { + "method": "apple_pay", + "title": "Apple Pay", + "iconUrl": "https://static.yoomoney.ru/mobile-app-content-front/msdk/payment-options/v1/other_cardSystemApplePay.png" + } + ], + "savePaymentMethodOptionTexts": { + "switchRecurrentOnBindOnTitle": "Разрешить автосписания и сохранить платёжные данные", + "switchRecurrentOnBindOnSubtitle": "После оплаты магазин сохранит данные карты и сможет списывать деньги без вашего участия", + "switchRecurrentOnBindOffTitle": "Разрешить автосписания", + "switchRecurrentOnBindOffSubtitle": "После оплаты запомним эту карту: магазин сможет списывать деньги без вашего участия", + "switchRecurrentOffBindOnTitle": "Сохранить платёжные данные", + "switchRecurrentOffBindOnSubtitle": "Магазин сохранит данные вашей карты —
в следующий раз можно будет их не вводить", + "messageRecurrentOnBindOnTitle": "Разрешим автосписания и сохраним платёжные данные", + "messageRecurrentOnBindOnSubtitle": "Заплатив здесь, вы соглашаетесь
сохранить данные карты и списывать деньги без вашего участия", + "messageRecurrentOnBindOffTitle": "Разрешим автосписания", + "messageRecurrentOnBindOffSubtitle": "Заплатив здесь, вы разрешаете запомнить эту карту и списывать деньги без вашего участия", + "messageRecurrentOffBindOnTitle": "Сохраним платёжные данные", + "messageRecurrentOffBindOnSubtitle": "Заплатив здесь, вы разрешаете магазину сохранить данные вашей карты — в следующий раз можно их не вводить
", + "screenRecurrentOnBindOnTitle": "Автосписания
и сохранение платёжных данных", + "screenRecurrentOnBindOnText": "Если вы это разрешили, мы сохраним для этого магазина и его партнёров данные вашей банковской карты — номер, имя владельца, срок действия (всё, кроме кода CVC). В следующий раз не нужно будет их вводить, чтобы заплатить в этом магазине.

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

Автосписания продолжатся даже при перевыпуске карты, если ваш банк умеет автоматически обновлять данные. Отключить их и отвязать карту можно в любой момент — через службу поддержки магазина.", + "screenRecurrentOnBindOffTitle": "Как работают автоматические списания", + "screenRecurrentOnBindOffText": "Если вы согласитесь на автосписания, мы привяжем банковскую карту (в том числе использованную через Apple Pay) к магазину. После этого магазин сможет присылать запросы на автоматические списания денег — тогда платёж выполняется без дополнительного подтверждения с вашей стороны.

Автосписания продолжатся даже при перевыпуске карты, если ваш банк умеет автоматически обновлять данные. Отключить их и отвязать карту можно в любой момент — через службу поддержки магазина.", + "screenRecurrentOffBindOnTitle": "Сохранение платёжных данных", + "screenRecurrentOffBindOnText": "Если вы это разрешили, мы сохраним для этого магазина и его партнёров данные вашей банковской карты — номер, имя владельца и срок действия (всё, кроме кода CVC). В следующий раз не нужно будет вводить их, чтобы заплатить в этом магазине.

Удалить данные карты можно в процессе оплаты (нажмите на три точки напротив карты и выберите «Удалить карту») или через службу поддержки.", + "screenRecurrentOnSberpayTitle": "Как работают автоматические списания", + "screenRecurrentOnSberpayText": "Если вы согласитесь на автосписания, мы привяжем банковскую карту (в том числе использованную через Apple Pay) к магазину. После этого магазин сможет присылать запросы на автоматические списания денег — тогда платёж выполняется без дополнительного подтверждения с вашей стороны.

Автосписания продолжатся даже при перевыпуске карты, если ваш банк умеет автоматически обновлять данные. Отключить их и отвязать карту можно в любой момент — через службу поддержки магазина." + }, + "userAgreementUrl": "Нажимая кнопку, вы принимаете
условия сервиса", + "yooMoneyApiEndpoint": "https://sdk.yookassa.ru/api/frontend/v3", + "yooMoneyPaymentAuthorizationApiEndpoint": "https://yoomoney.ru/api/wallet-auth/v1", + "yooMoneyAuthApiEndpoint": "" +} diff --git a/YooKassaPayments/Public/Resources/ru.lproj/Localizable.strings b/YooKassaPayments/Public/Resources/ru.lproj/Localizable.strings index 0a5e649d..035c64b9 100644 --- a/YooKassaPayments/Public/Resources/ru.lproj/Localizable.strings +++ b/YooKassaPayments/Public/Resources/ru.lproj/Localizable.strings @@ -113,6 +113,9 @@ /* После авторизации в кошельке при запросе доступных методов кошелёк отсутствует */ "Error.noWalletTitle" = "Оплата кошельком ЮMoney недоступна"; +/* HTTP 429 Ошибка `Превышен лимит запросов` на экране выбора способа оплаты */ +"Error.paymentOptions.tooManyRequests.429" = "Превышен лимит запросов"; + /* Пользователь ввел верный код, но возникла ошибка. Создаем новую сессию на авторизацию */ "Error.resendAuthCodeAndStartOver" = "Не получилось. Попробуйте начать сначала"; diff --git a/YooKassaPayments/Public/TokenizationAssembly.swift b/YooKassaPayments/Public/TokenizationAssembly.swift index 5e0bf198..41932b80 100644 --- a/YooKassaPayments/Public/TokenizationAssembly.swift +++ b/YooKassaPayments/Public/TokenizationAssembly.swift @@ -1,5 +1,7 @@ import UIKit +private var configPreloader: ConfigMediator? + /// Tokenization module builder. public enum TokenizationAssembly { @@ -10,7 +12,6 @@ public enum TokenizationAssembly { inputData: TokenizationFlow, moduleOutput: TokenizationModuleOutput ) -> UIViewController & TokenizationModuleInput { - switch inputData { case .tokenization(let tokenizationModuleInputData): CustomizationStorage.shared.mainScheme @@ -49,59 +50,94 @@ 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, - shopName: inputData.shopName, - purchaseDescription: inputData.purchaseDescription, - amount: inputData.amount, - tokenizationSettings: inputData.tokenizationSettings, - testModeSettings: inputData.testModeSettings, - isLoggingEnabled: inputData.isLoggingEnabled, - getSavePaymentMethod: inputData.boolFromSavePaymentMethod, - moneyAuthClientId: inputData.moneyAuthClientId, - returnUrl: inputData.returnUrl, - savePaymentMethod: inputData.savePaymentMethod, - userPhoneNumber: inputData.userPhoneNumber, - cardScanning: inputData.cardScanning, - customerId: inputData.customerId - ) + PrintLogger.forceSilence = !inputData.isLoggingEnabled + + func paymentMethodsModule(config: Config) -> (view: UIViewController, moduleInput: PaymentMethodsModuleInput) { + let paymentMethodsModuleInputData = PaymentMethodsModuleInputData( + applicationScheme: inputData.applicationScheme, + clientApplicationKey: inputData.clientApplicationKey, + applePayMerchantIdentifier: inputData.applePayMerchantIdentifier, + gatewayId: inputData.gatewayId, + shopName: inputData.shopName, + purchaseDescription: inputData.purchaseDescription, + amount: inputData.amount, + tokenizationSettings: inputData.tokenizationSettings, + testModeSettings: inputData.testModeSettings, + isLoggingEnabled: inputData.isLoggingEnabled, + getSavePaymentMethod: inputData.boolFromSavePaymentMethod, + moneyAuthClientId: inputData.moneyAuthClientId, + returnUrl: inputData.returnUrl, + savePaymentMethod: inputData.savePaymentMethod, + userPhoneNumber: inputData.userPhoneNumber, + cardScanning: inputData.cardScanning, + customerId: inputData.customerId, + config: config + ) - let (viewController, moduleInput) = PaymentMethodsAssembly.makeModule( - inputData: paymentMethodsModuleInputData, - tokenizationModuleOutput: moduleOutput - ) + return PaymentMethodsAssembly.makeModule( + inputData: paymentMethodsModuleInputData, + tokenizationModuleOutput: moduleOutput + ) + } - let navigationController = NavigationController( - rootViewController: viewController - ) + let loading = LoadingViewController() + let navigationController = NavigationController(rootViewController: loading) + loading.showActivity() let viewControllerToReturn: UIViewController & TokenizationModuleInput + var resultingNavigationController: NavigationController? + var resultingSheetViewController: SheetViewController? switch UIScreen.main.traitCollection.userInterfaceIdiom { case .pad: - navigationController.moduleOutput = moduleInput navigationController.modalPresentationStyle = .formSheet viewControllerToReturn = navigationController + resultingNavigationController = navigationController default: let sheetViewController = SheetViewController( contentViewController: navigationController ) - sheetViewController.moduleOutput = moduleInput viewControllerToReturn = sheetViewController - + resultingSheetViewController = sheetViewController } + let authService = AuthorizationServiceAssembly.makeService( + isLoggingEnabled: inputData.isLoggingEnabled, + testModeSettings: inputData.testModeSettings, + moneyAuthClientId: inputData.moneyAuthClientId + ) + YKSdk.shared.moduleOutput = moduleOutput YKSdk.shared.applicationScheme = inputData.applicationScheme - YKSdk.shared.paymentMethodsModuleInput = moduleInput + + let preloader = ConfigMediatorAssembly.make(isLoggingEnabled: inputData.isLoggingEnabled) + configPreloader = preloader + preloader.getConfig(token: inputData.clientApplicationKey) { config in + DispatchQueue.main.async { + let (viewController, moduleInput) = paymentMethodsModule(config: config) + resultingNavigationController?.moduleOutput = moduleInput + resultingSheetViewController?.moduleOutput = moduleInput + YKSdk.shared.paymentMethodsModuleInput = moduleInput + + loading.hideActivity() + navigationController.setViewControllers([viewController], animated: true) + } + + configPreloader = nil + } + + YKSdk.shared.analyticsTracking = AnalyticsTrackingAssembly.make(isLoggingEnabled: inputData.isLoggingEnabled) + YKSdk.shared.analyticsContext = AnalyticsEventContext( + sdkVersion: Bundle.frameworkVersion, + initialAuthType: authService.analyticsAuthType(), + isCustomerIdPresent: inputData.customerId != nil, + isWalletAuthPresent: authService.getWalletToken() != nil, + usingCustomColor: inputData.customizationSettings.mainScheme != CustomizationColors.blueRibbon, + yookassaIconShown: inputData.tokenizationSettings.showYooKassaLogo, + savePaymentMethod: inputData.savePaymentMethod + ) + + YKSdk.shared.analyticsTracking.track(event: .actionSDKInitialised) return viewControllerToReturn } diff --git a/YooKassaPayments/Public/TokenizationModuleIO.swift b/YooKassaPayments/Public/TokenizationModuleIO.swift index 2ba5c086..897fc30b 100644 --- a/YooKassaPayments/Public/TokenizationModuleIO.swift +++ b/YooKassaPayments/Public/TokenizationModuleIO.swift @@ -2,14 +2,6 @@ /// /// In the process of running mSDK, allows you to run processes using the `TokenizationModuleInput` protocol methods. public protocol TokenizationModuleInput: AnyObject { - - /// Start 3-D Secure process. - /// - /// - Parameters: - /// - requestUrl: URL string for request website. - @available(*, deprecated, message: "Use startConfirmationProcess(confirmationUrl:paymentMethodType:) instead") - func start3dsProcess(requestUrl: String) - /// Start confirmation process /// /// - Parameters: @@ -36,24 +28,11 @@ public protocol TokenizationModuleOutput: AnyObject { with error: YooKassaPaymentsError? ) - /// Will be called when the 3-D Secure process successfully passes. - /// - /// - Parameters: - /// - 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 - ) + 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 index 74537f3d..c7d9a9e3 100644 --- a/YooKassaPayments/Public/YKSdkService.swift +++ b/YooKassaPayments/Public/YKSdkService.swift @@ -10,7 +10,8 @@ public final class YKSdk { /// Application scheme for returning after opening a deeplink. var applicationScheme: String? - var analyticsService: AnalyticsService? + var analyticsTracking: AnalyticsTracking! + var analyticsContext: AnalyticsEventContext! private init() {} @@ -31,11 +32,7 @@ public final class YKSdk { switch deeplink { case .invoicingSberpay: - let event: AnalyticsEvent = .actionSberPayConfirmation( - sberPayConfirmationStatus: .success, - sdkVersion: Bundle.frameworkVersion - ) - analyticsService?.trackEvent(event) + analyticsTracking.track(event: .actionSberPayConfirmation(success: true)) moduleOutput?.didSuccessfullyConfirmation(paymentMethodType: .sberbank) case .yooMoneyExchange(let cryptogram): diff --git a/YooKassaPaymentsDemoApp.xcodeproj/project.pbxproj b/YooKassaPaymentsDemoApp.xcodeproj/project.pbxproj index e5ff9b2f..d254b0b3 100644 --- a/YooKassaPaymentsDemoApp.xcodeproj/project.pbxproj +++ b/YooKassaPaymentsDemoApp.xcodeproj/project.pbxproj @@ -125,6 +125,7 @@ 309BD427265CE9FC00D4804D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 309BD430265CEBAA00D4804D /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/LaunchScreen.strings; sourceTree = ""; }; 309BD432265CECA000D4804D /* YooKassaPaymentsDemoApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = YooKassaPaymentsDemoApp.entitlements; sourceTree = ""; }; + 309BD433265CED7700D4804D /* RootViewController+CardScanning+Carthage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "RootViewController+CardScanning+Carthage.swift"; sourceTree = ""; }; 309BD435265CED7700D4804D /* RootViewController+TokenizationModuleOutput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "RootViewController+TokenizationModuleOutput.swift"; sourceTree = ""; }; 309BD43B265CED9200D4804D /* TestSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestSettings.swift; sourceTree = ""; }; 309BD43C265CED9200D4804D /* ProcessConfirmation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProcessConfirmation.swift; sourceTree = ""; }; @@ -168,6 +169,7 @@ 309BD472265CED9300D4804D /* TextFieldView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldView.swift; sourceTree = ""; }; 309BD473265CED9300D4804D /* ContainerTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContainerTableViewCell.swift; sourceTree = ""; }; 309BD474265CED9300D4804D /* TitledSwitchView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TitledSwitchView.swift; sourceTree = ""; }; + 309BD475265CED9300D4804D /* Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Bridging-Header.h"; sourceTree = ""; }; 309BD477265CED9300D4804D /* DevHostService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DevHostService.swift; sourceTree = ""; }; 309BD479265CED9300D4804D /* UserDefaultsStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsStorage.swift; sourceTree = ""; }; 309BD47A265CED9300D4804D /* KeyValueStoring.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyValueStoring.swift; sourceTree = ""; }; @@ -256,6 +258,7 @@ isa = PBXGroup; children = ( 309BD419265CE9FA00D4804D /* AppDelegate.swift */, + 309BD433265CED7700D4804D /* RootViewController+CardScanning+Carthage.swift */, 309BD435265CED7700D4804D /* RootViewController+TokenizationModuleOutput.swift */, 309BD431265CEC2600D4804D /* Resources */, 309BD439265CED9200D4804D /* Source */, @@ -280,6 +283,7 @@ 309BD439265CED9200D4804D /* Source */ = { isa = PBXGroup; children = ( + 309BD475265CED9300D4804D /* Bridging-Header.h */, 309BD4D8265CF2CB00D4804D /* Identifier.swift */, 309BD4DC265CF31700D4804D /* ActionTextDialog */, 309BD457265CED9300D4804D /* AtomicDesign */, @@ -616,7 +620,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1250; - LastUpgradeCheck = 1250; + LastUpgradeCheck = 1310; TargetAttributes = { 309BD415265CE9FA00D4804D = { CreatedOnToolsVersion = 12.5; @@ -852,6 +856,7 @@ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_CODE_COVERAGE = NO; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; @@ -895,7 +900,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -914,6 +919,7 @@ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_CODE_COVERAGE = NO; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; @@ -951,7 +957,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -971,7 +977,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 2752592HU5; INFOPLIST_FILE = YooKassaPaymentsDemoApp/Resources/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -995,7 +1001,7 @@ CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = 2752592HU5; INFOPLIST_FILE = YooKassaPaymentsDemoApp/Resources/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1019,6 +1025,7 @@ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_CODE_COVERAGE = NO; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; @@ -1056,7 +1063,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -1076,7 +1083,7 @@ CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = YooKassaPaymentsDemoApp/Resources/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/YooKassaPaymentsDemoApp.xcodeproj/xcshareddata/xcschemes/YooKassaPaymentsDemoApp.xcscheme b/YooKassaPaymentsDemoApp.xcodeproj/xcshareddata/xcschemes/YooKassaPaymentsDemoApp.xcscheme index 2028ef9c..f03faea8 100644 --- a/YooKassaPaymentsDemoApp.xcodeproj/xcshareddata/xcschemes/YooKassaPaymentsDemoApp.xcscheme +++ b/YooKassaPaymentsDemoApp.xcodeproj/xcshareddata/xcschemes/YooKassaPaymentsDemoApp.xcscheme @@ -1,6 +1,6 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 6.2.5 + 6.4.0 CFBundleURLTypes diff --git a/YooKassaPaymentsDemoApp/Source/UserStories/Cards/AttachedCardCountViewController.swift b/YooKassaPaymentsDemoApp/Source/UserStories/Cards/AttachedCardCountViewController.swift index af18065b..028b94e7 100644 --- a/YooKassaPaymentsDemoApp/Source/UserStories/Cards/AttachedCardCountViewController.swift +++ b/YooKassaPaymentsDemoApp/Source/UserStories/Cards/AttachedCardCountViewController.swift @@ -63,10 +63,10 @@ final class AttachedCardCountViewController: UIViewController { 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), + countPickerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + view.layoutMarginsGuide.topAnchor.constraint(equalTo: countPickerView.topAnchor), + countPickerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + countPickerView.heightAnchor.constraint(equalToConstant: 154), ] NSLayoutConstraint.activate(constraints) diff --git a/YooKassaPaymentsDemoApp/Source/UserStories/Process/ProcessViewController.swift b/YooKassaPaymentsDemoApp/Source/UserStories/Process/ProcessViewController.swift index 41cf59c3..1fb970b4 100644 --- a/YooKassaPaymentsDemoApp/Source/UserStories/Process/ProcessViewController.swift +++ b/YooKassaPaymentsDemoApp/Source/UserStories/Process/ProcessViewController.swift @@ -72,10 +72,10 @@ final class ProcessViewController: UIViewController { 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), + countPickerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + view.layoutMarginsGuide.topAnchor.constraint(equalTo: countPickerView.topAnchor), + countPickerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + countPickerView.heightAnchor.constraint(equalToConstant: 154), ] NSLayoutConstraint.activate(constraints) diff --git a/YooKassaPaymentsDemoApp/Source/UserStories/Root/RootViewController.swift b/YooKassaPaymentsDemoApp/Source/UserStories/Root/RootViewController.swift index f8052714..89c9e416 100644 --- a/YooKassaPaymentsDemoApp/Source/UserStories/Root/RootViewController.swift +++ b/YooKassaPaymentsDemoApp/Source/UserStories/Root/RootViewController.swift @@ -61,8 +61,6 @@ final class RootViewController: UIViewController { fileprivate lazy var nameLabel: UILabel = { if #available(iOS 11.0, *) { $0.setStyles(UILabel.DynamicStyle.title1) - } else if #available(iOS 9.0, *) { - $0.setStyles(UILabel.DynamicStyle.title2) } else { $0.setStyles(UILabel.DynamicStyle.headline1) } @@ -96,7 +94,7 @@ final class RootViewController: UIViewController { fileprivate lazy var ratingImageView = UIImageView(image: #imageLiteral(resourceName: "Root.Rating")) fileprivate lazy var payButtonBottomConstraint: NSLayoutConstraint = - self.bottomLayoutGuide.top.constraint(equalTo: payButton.bottom) + self.view.layoutMarginsGuide.bottomAnchor.constraint(equalTo: payButton.bottomAnchor) fileprivate lazy var nameLabelTopConstraint: NSLayoutConstraint = nameLabel.top.constraint(equalTo: imageView.bottom) @@ -347,7 +345,7 @@ final class RootViewController: UIViewController { oauthToken = "live_MTkzODU2VY5GiyQq2GMPsCQ0PW7f_RSLtJYOT-mp_CA" } - let inputData: TokenizationFlow = .tokenization(TokenizationModuleInputData( + let data = TokenizationModuleInputData( clientApplicationKey: oauthToken, shopName: translate(Localized.name), purchaseDescription: translate(Localized.description), @@ -363,7 +361,8 @@ final class RootViewController: UIViewController { moneyAuthClientId: "hitm6hg51j1d3g1u3ln040bajiol903b", applicationScheme: "yookassapaymentsexample://", customerId: "app.example.demo.payments.yookassa" - )) + ) + let inputData = TokenizationFlow.tokenization(data) // let inputData: TokenizationFlow = .bankCardRepeat(BankCardRepeatModuleInputData( // clientApplicationKey: oauthToken,