diff --git a/.version b/.version index 03825cde..e7e42a4b 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -6.2.5 \ No newline at end of file +6.3.0 \ No newline at end of file diff --git a/.version_code b/.version_code index f1ef71b4..7eb5383c 100644 --- a/.version_code +++ b/.version_code @@ -1 +1 @@ -6020500 \ No newline at end of file +6030000 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index be36cded..7e2ce247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ ### NEXT_VERSION_DESCRIPTION_BEGIN ### NEXT_VERSION_DESCRIPTION_END +## [6.3.0] (06-09-2021) + +* Добавлена функциональность сохранения платёжного метода + +## [6.2.6] (12-08-2021) + +* исправления для CI + ## [6.2.5] (06-08-2021) * Исправление вёрстки для корнер кейса с неполностью сконфигурированным навбаром презентующего контроллера. diff --git a/Cartfile b/Cartfile index b5568d1d..d3e19ae9 100644 --- a/Cartfile +++ b/Cartfile @@ -1,7 +1,7 @@ github "yoomoney/functional-swift" ~> 1.6.7 github "yoomoney/core-api-swift" ~> 1.11.4 github "yoomoney/yookassa-wallet-api-swift" ~> 2.3.1 -github "yoomoney/yookassa-payments-api-swift" ~> 2.7.2 +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/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 diff --git a/Cartfile.resolved b/Cartfile.resolved index 8cf1c314..82bd4945 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,7 +1,9 @@ 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/yookassa-threat-metrix-adapter-ios/main/ThreatMetrixAdapter.json" "3.3.0" +github "AliSoftware/OHHTTPStubs" "8.0.0" github "yoomoney/core-api-swift" "1.11.4" github "yoomoney/functional-swift" "1.6.7" -github "yoomoney/yookassa-payments-api-swift" "2.7.2" +github "yoomoney/test-instruments-api-swift" "2.2.4" +github "yoomoney/yookassa-payments-api-swift" "2.11.0" github "yoomoney/yookassa-wallet-api-swift" "2.3.1" diff --git a/Podfile b/Podfile index 41b28135..fada4dc8 100644 --- a/Podfile +++ b/Podfile @@ -1,5 +1,5 @@ source 'https://github.com/CocoaPods/Specs.git' -source 'git@github.com:yoomoney-tech/cocoa-pod-specs.git' +source 'git@github.com:yoomoney/cocoa-pod-specs.git' platform :ios, '10.0' use_frameworks! @@ -13,6 +13,7 @@ target 'YooKassaPaymentsDemoApp' do pod 'Reveal-SDK', :configurations => ['Debug'] pod 'YooKassaPayments', :path => './' + pod 'YooKassaPaymentsApi', '~> 2.11.0' end post_install do |installer| diff --git a/Podfile.lock b/Podfile.lock index 14741c99..a5a0f8ff 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -14,14 +14,14 @@ PODS: - YandexMobileMetrica/Dynamic/Core (3.17.0) - YandexMobileMetrica/Dynamic/Crashes (3.17.0): - YandexMobileMetrica/Dynamic/Core - - YooKassaPayments (6.2.5): + - YooKassaPayments (6.3.0): - MoneyAuth (~> 2.34.1) - ThreatMetrixAdapter (~> 3.3.0) - YandexMobileMetrica/Dynamic (~> 3.0) - - YooKassaPaymentsApi (~> 2.7.2) + - YooKassaPaymentsApi (~> 2.11.0) - YooKassaWalletApi (~> 2.3.1) - YooMoneyCoreApi (~> 1.11.4) - - YooKassaPaymentsApi (2.7.2): + - YooKassaPaymentsApi (2.11.0): - FunctionalSwift (~> 1.6.7) - YooMoneyCoreApi (~> 1.11.4) - YooKassaWalletApi (2.3.1): @@ -35,6 +35,7 @@ DEPENDENCIES: - Reveal-SDK - SwiftLint - YooKassaPayments (from `./`) + - YooKassaPaymentsApi (~> 2.11.0) SPEC REPOS: "git@github.com:yoomoney-tech/cocoa-pod-specs.git": @@ -62,11 +63,11 @@ SPEC CHECKSUMS: SwiftLint: 99f82d07b837b942dd563c668de129a03fc3fb52 ThreatMetrixAdapter: 309bc3d0afff4706c882844203df6d3f89a6bf0c YandexMobileMetrica: 9e713c16bb6aca0ba63b84c8d7b8b86d32f4ecc4 - YooKassaPayments: 6305b342c6cd599b787604e5291b5d9d8ace3b5f - YooKassaPaymentsApi: 50d9ff8a3e76f3aaf01efe5ff7dcd1b813635044 + YooKassaPayments: 70de07ab77f1cd0ddb99bc3ba409f3c04bd65eb1 + YooKassaPaymentsApi: e967fc3ffbb797e6412e077f0f896a1562c9bdc3 YooKassaWalletApi: c1916b692ad842ae04917a10ce66d6d2f971c653 YooMoneyCoreApi: d228ca30d6936bc81642988c93d26eba271261f8 -PODFILE CHECKSUM: 244e8940e0dad39285e45c19cac783789255a4e4 +PODFILE CHECKSUM: e3d8979584d168459cceff74a485647132ad28ae COCOAPODS: 1.10.1 diff --git a/README.md b/README.md index 34263c1c..88f19ec1 100644 --- a/README.md +++ b/README.md @@ -531,6 +531,7 @@ let moduleData = TokenizationModuleInputData( | customizationSettings | CustomizationSettings | The blueRibbon color is used by default. Color of the main elements, button, switches, and input fields. | | moneyAuthClientId | String | By default: `nil`. ID for the center of authorizationin the YooMoney system | | applicationScheme | String | By default: `nil`. Scheme for returning to the app after a successful payment via `Sberpay` in the Sberbank Online app or after a successful sign-in to `YooMoney` via the mobile app. | +| customerId | String | By default: `nil`. Unique customer id for your system, ex: email or phone number. 200 symbols max. Used by library to save user payment method and display saved methods. It is your responsibility to make sure that a particular customerId identifies the user, which is willing to make a purchase. For example use two-factor authentication. Using wrong id will let the user to use payment methods that don't belong to this user.| ### BankCardRepeatModuleInputData >Required parameters: diff --git a/README_RU.md b/README_RU.md index 931ecd99..084c90b3 100644 --- a/README_RU.md +++ b/README_RU.md @@ -531,6 +531,7 @@ let moduleData = TokenizationModuleInputData( | customizationSettings | CustomizationSettings | По умолчанию используется цвет blueRibbon. Цвет основных элементов, кнопки, переключатели, поля ввода. | | moneyAuthClientId | String | По умолчанию `nil`. Идентификатор для центра авторизации в системе YooMoney. | | applicationScheme | String | По умолчанию `nil`. Схема для возврата в приложение после успешной оплаты с помощью `Sberpay` в приложении СберБанк Онлайн или после успешной авторизации в `YooMoney` через мобильное приложение. | +| customerId | String | По умолчанию `nil`. Уникальный идентификатор покупателя в вашей системе, например электронная почта или номер телефона. Не более 200 символов. Используется, если вы хотите запомнить банковскую карту и отобразить ее при повторном платеже в mSdk. Убедитесь, что customerId относится к пользователю, который хочет совершить покупку. Например, используйте двухфакторную аутентификацию. Если передать неверный идентификатор, пользователь сможет выбрать для оплаты чужие банковские карты.| ### BankCardRepeatModuleInputData >Обязательные: diff --git a/YooKassaPayments.podspec b/YooKassaPayments.podspec index f2edeee1..9aa692f6 100644 --- a/YooKassaPayments.podspec +++ b/YooKassaPayments.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'YooKassaPayments' - s.version = '6.2.5' + s.version = '6.3.0' s.homepage = 'https://github.com/yoomoney/yookassa-payments-swift' s.license = { :type => "MIT", @@ -30,9 +30,8 @@ Pod::Spec.new do |s| s.ios.library = 'z' s.ios.dependency 'YooMoneyCoreApi', '~> 1.11.4' - s.ios.dependency 'YooKassaPaymentsApi', '~> 2.7.2' + 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' diff --git a/YooKassaPayments.xcodeproj/project.pbxproj b/YooKassaPayments.xcodeproj/project.pbxproj index 6814ba59..8bb9e488 100644 --- a/YooKassaPayments.xcodeproj/project.pbxproj +++ b/YooKassaPayments.xcodeproj/project.pbxproj @@ -18,6 +18,19 @@ 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 */; }; + 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 */; }; + 25347BE426BADBE800FDD1DA /* CardSettingsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25347BD926BADBE800FDD1DA /* CardSettingsInteractor.swift */; }; + 25347BE526BADBE800FDD1DA /* CardSettingsAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25347BDB26BADBE800FDD1DA /* CardSettingsAssembly.swift */; }; + 25347BE626BADBE800FDD1DA /* CardSettingsViewIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25347BDD26BADBE800FDD1DA /* CardSettingsViewIO.swift */; }; + 25347BE726BADBE800FDD1DA /* CardSettingsModuleIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25347BDE26BADBE800FDD1DA /* CardSettingsModuleIO.swift */; }; + 25347BE826BADBE800FDD1DA /* CardSettingsRouterIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25347BDF26BADBE800FDD1DA /* CardSettingsRouterIO.swift */; }; + 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 */; }; + 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 */; }; 3089EF4B23846F6C00CB7319 /* StrictSavePaymentMethodViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3089EF4A23846F6C00CB7319 /* StrictSavePaymentMethodViewModel.swift */; }; 3089EF4D23846F7400CB7319 /* SavePaymentMethodViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3089EF4C23846F7400CB7319 /* SavePaymentMethodViewModel.swift */; }; @@ -400,7 +413,20 @@ 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; }; + 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 = ""; }; + 25347BD926BADBE800FDD1DA /* CardSettingsInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardSettingsInteractor.swift; sourceTree = ""; }; + 25347BDB26BADBE800FDD1DA /* CardSettingsAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardSettingsAssembly.swift; sourceTree = ""; }; + 25347BDD26BADBE800FDD1DA /* CardSettingsViewIO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardSettingsViewIO.swift; sourceTree = ""; }; + 25347BDE26BADBE800FDD1DA /* CardSettingsModuleIO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardSettingsModuleIO.swift; sourceTree = ""; }; + 25347BDF26BADBE800FDD1DA /* CardSettingsRouterIO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardSettingsRouterIO.swift; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; 3089EF4823846F6300CB7319 /* SwitcherSavePaymentMethodViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitcherSavePaymentMethodViewModel.swift; sourceTree = ""; }; 3089EF4A23846F6C00CB7319 /* StrictSavePaymentMethodViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrictSavePaymentMethodViewModel.swift; sourceTree = ""; }; 3089EF4C23846F7400CB7319 /* SavePaymentMethodViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavePaymentMethodViewModel.swift; sourceTree = ""; }; @@ -835,6 +861,46 @@ ); sourceTree = ""; }; + 25347BD426BADBE800FDD1DA /* CardSettings */ = { + isa = PBXGroup; + children = ( + 25347BD526BADBE800FDD1DA /* Viper bundle */, + 25347BDA26BADBE800FDD1DA /* Assembly */, + 25347BDC26BADBE800FDD1DA /* IO */, + ); + path = CardSettings; + sourceTree = ""; + }; + 25347BD526BADBE800FDD1DA /* Viper bundle */ = { + isa = PBXGroup; + children = ( + 25347BD626BADBE800FDD1DA /* CardSettingsRouter.swift */, + 25347BD726BADBE800FDD1DA /* CardSettingsPresenter.swift */, + 25347BD826BADBE800FDD1DA /* CardSettingsViewController.swift */, + 25347BD926BADBE800FDD1DA /* CardSettingsInteractor.swift */, + ); + path = "Viper bundle"; + sourceTree = ""; + }; + 25347BDA26BADBE800FDD1DA /* Assembly */ = { + isa = PBXGroup; + children = ( + 25347BDB26BADBE800FDD1DA /* CardSettingsAssembly.swift */, + ); + path = Assembly; + sourceTree = ""; + }; + 25347BDC26BADBE800FDD1DA /* IO */ = { + isa = PBXGroup; + children = ( + 25347BDD26BADBE800FDD1DA /* CardSettingsViewIO.swift */, + 25347BDE26BADBE800FDD1DA /* CardSettingsModuleIO.swift */, + 25347BDF26BADBE800FDD1DA /* CardSettingsRouterIO.swift */, + 25347BE026BADBE800FDD1DA /* CardSettingsInteractorIO.swift */, + ); + path = IO; + sourceTree = ""; + }; 3089EF4723846F5700CB7319 /* ViewModels */ = { isa = PBXGroup; children = ( @@ -1043,6 +1109,7 @@ 402EB152B4911AB058FF53B1 /* Views */ = { isa = PBXGroup; children = ( + 25347BEC26BADC6800FDD1DA /* ActionTemplate.swift */, 402EB302A726A82FE26D8B76 /* LinkedTextView.swift */, 402EB3A8ED8334D338C7DAEC /* UnderlinedTextField */, 402EB09BDE187E570A3EECBC /* BankCardDataInput */, @@ -1209,6 +1276,7 @@ 781CDD6F25D4053400C34912 /* ApplePayContract */, 402EB8DC7B8A023C9B3961A7 /* BankCard */, 402EB03AF715BD6339B32F52 /* BankCardRepeat */, + 25347BD426BADBE800FDD1DA /* CardSettings */, 7860293525D166E200B9E961 /* LinkedCard */, 402EB84797D7E864A73001A0 /* LogoutConfirmation */, 786028F125C981CF00B9E961 /* PaymentAuthorization */, @@ -1370,6 +1438,7 @@ 402EB634BAF3BC404B10B86D /* Molecules */ = { isa = PBXGroup; children = ( + 25347BEA26BADC2E00FDD1DA /* LargeActionInformer.swift */, 7811067B25C3FCB8004DB71D /* OrderView.swift */, 402EBB761608F1C750B0D45F /* PriceView.swift */, 7860291325CC1F3B00B9E961 /* SectionHeaderView.swift */, @@ -1641,6 +1710,8 @@ children = ( 402EB68E3BE45D5A884DDA52 /* BankCardViewController.swift */, 402EB1CD1CD348852BD88F76 /* ViewModel */, + 25FD881226CD4AD70032B5FD /* PaymentRecurrencyAndDataSavingSection.swift */, + 25FD881426CD4B820032B5FD /* PaymentRecurrencyAndDataSavingSectionFactory.swift */, ); path = View; sourceTree = ""; @@ -2656,6 +2727,7 @@ 402EB91BEC54829B7D570406 /* PhoneNumberInputModuleOutput.swift in Sources */, 402EB5EFF251B3E8A8C0C35B /* PriceView.swift in Sources */, 7860291D25CC56DC00B9E961 /* LargeIconButtonItemView+Style.swift in Sources */, + 25347BEB26BADC2E00FDD1DA /* LargeActionInformer.swift in Sources */, 7860293425D1231F00B9E961 /* ActionTitleTextDialog+Style.swift in Sources */, 784A1AE225B9CC3B00637CB5 /* BankCard.swift in Sources */, 3089EF4F2384700800CB7319 /* SavePaymentMethod.swift in Sources */, @@ -2692,6 +2764,7 @@ 402EBB0834948CFD4278DF96 /* UILabel+Style.swift in Sources */, 402EB52D1B69D9D12BA05F02 /* UIFont+Style.swift in Sources */, 78C1BD1F25EFA11B0058080F /* SberpayViewIO.swift in Sources */, + 25347BE926BADBE800FDD1DA /* CardSettingsInteractorIO.swift in Sources */, 784A1AE625B9CE8A00637CB5 /* PaymentMethod.swift in Sources */, 402EBC73F5045B2B33EED401 /* UIImage+Style.swift in Sources */, 402EB2F13EA4EBCE01956D61 /* UIImageView+Style.swift in Sources */, @@ -2725,6 +2798,8 @@ 7860290825C982E600B9E961 /* PaymentAuthorizationViewController.swift in Sources */, 78997A2425D562160093CAE2 /* LargeIconView.swift in Sources */, 7811067425C2D212004DB71D /* YooMoneyInteractorIO.swift in Sources */, + 25347BE726BADBE800FDD1DA /* CardSettingsModuleIO.swift in Sources */, + 25347BE126BADBE800FDD1DA /* CardSettingsRouter.swift in Sources */, 402EBBB3276C2F6BC3AC5827 /* UIButton+Stylable.swift in Sources */, 402EB6F495D788A1A3FE0194 /* PlaceholderProvider.swift in Sources */, 402EBC8C95C263C57EDD975F /* PlaceholderPresenting.swift in Sources */, @@ -2831,6 +2906,7 @@ 402EBC41089B09F83AC6D4CA /* ApplePayContractModuleIO.swift in Sources */, 784A1AE425B9CCEA00637CB5 /* Confirmation.swift in Sources */, 308E41882385829700B44490 /* SavePaymentMethodInfoViewModel.swift in Sources */, + 25347BE626BADBE800FDD1DA /* CardSettingsViewIO.swift in Sources */, 784A1AD025B9C57B00637CB5 /* MonetaryAmount.swift in Sources */, 784A1A9625B974CD00637CB5 /* WebLoggerService.swift in Sources */, 789C373F25AF260F00BA94D1 /* PaymentMethodHandlerServiceAssembly.swift in Sources */, @@ -2853,6 +2929,7 @@ 402EBBE91DE40AA08E3AB8B5 /* ApplePayAssembly.swift in Sources */, 402EB7C74860DFA65827A00C /* ApplePayModuleIO.swift in Sources */, 30B20E2B2535FC1C00941574 /* KeyValueStoringKeys.swift in Sources */, + 25FD881326CD4AD70032B5FD /* PaymentRecurrencyAndDataSavingSection.swift in Sources */, 402EBE309FF80B279847B66D /* BankCardRepeatPresenter.swift in Sources */, 402EB8D43DDD23DC7A645AA1 /* BankCardRepeatInteractor.swift in Sources */, 402EB1F0EAA3B2E1422FD95D /* BankCardRepeatInteractorIO.swift in Sources */, @@ -2876,6 +2953,7 @@ 402EBD63F90CE9E26E3ABF36 /* KeychainStorageMock.swift in Sources */, 402EB3397345E69677F29347 /* KeychainStorage.swift in Sources */, 402EB52D11504C419695D49F /* UserDefaultsStorage.swift in Sources */, + 25347BE526BADBE800FDD1DA /* CardSettingsAssembly.swift in Sources */, 402EBFE2285B7B73BF2290ED /* WalletLoginServiceImpl.swift in Sources */, 782E75E1260CCF7500CF2BFD /* YKSdkService.swift in Sources */, 402EBDA16A62F97489BF95AC /* WalletLoginService.swift in Sources */, @@ -2893,6 +2971,7 @@ 402EB0D571A0BE68455B56D8 /* AnalyticsEvent.swift in Sources */, 402EBB302AB46E50088A5B40 /* AnalyticsServiceAssembly.swift in Sources */, 402EB52500117CC0F9F7337E /* AnalyticsService.swift in Sources */, + 25347BED26BADC6800FDD1DA /* ActionTemplate.swift in Sources */, 402EB5609877054CD489DF14 /* AnalyticsTrack.swift in Sources */, 784A1AD925B9C57B00637CB5 /* PaymentUsageLimit.swift in Sources */, 402EBB04E607A7D25433D1A8 /* AnalyticsProvider.swift in Sources */, @@ -2901,6 +2980,7 @@ 402EB10E2B1F8A4E8BD2DEB8 /* BankSettingsService.swift in Sources */, 402EB324EC0352C1CF0A8A59 /* BankSettingsServiceImpl.swift in Sources */, 7860292C25CD406500B9E961 /* ImageCacheImpl.swift in Sources */, + 25347BE426BADBE800FDD1DA /* CardSettingsInteractor.swift in Sources */, 78C1BD1B25EFA1090058080F /* SberpayModuleIO.swift in Sources */, 402EBF6F0AF3AB32E9C844AC /* TokenizationAssembly.swift in Sources */, 402EB337CF8A33565554E71D /* CardScanning.swift in Sources */, @@ -2937,6 +3017,7 @@ 402EB4040D6DB2DC51F53B51 /* SheetOptions.swift in Sources */, 402EBE4E6840A072C11BF813 /* SheetTransition.swift in Sources */, 402EB6F018F924AD0C929C0A /* UIColorExtension.swift in Sources */, + 25347BE226BADBE800FDD1DA /* CardSettingsPresenter.swift in Sources */, 402EB74BD7755B9D8CA0202E /* SheetViewController.swift in Sources */, 402EB449E96F93EC94C40745 /* SheetContentViewDelegate.swift in Sources */, 402EB2E49C9F809B26838BF2 /* SheetContentViewController.swift in Sources */, @@ -2956,6 +3037,7 @@ 402EB3FDECF234568CC75455 /* UnderlinedTextField.swift in Sources */, 402EB4FEA877102C24D0AEE2 /* UnderlinedTextField+InputView.swift in Sources */, 402EB30D837F20F5E69E196C /* UnderlinedTextField+Styles.swift in Sources */, + 25347BE326BADBE800FDD1DA /* CardSettingsViewController.swift in Sources */, 402EBF9EAC29349F6CD640C2 /* BankCardInteractorIO.swift in Sources */, 402EBCE0D0434D717711E283 /* BankCardViewIO.swift in Sources */, 402EBAAF81518608501DBE6D /* BankCardAssembly.swift in Sources */, @@ -2981,8 +3063,10 @@ 402EBC42507159E3CD4859EC /* BankCardDataInputRouterIO.swift in Sources */, 402EB3AB073F3C17EAE8919C /* BankCardDataInputViewModel.swift in Sources */, 402EBBDF51F6662CEE54298A /* Bundle+Tools.swift in Sources */, + 25FD881526CD4B820032B5FD /* PaymentRecurrencyAndDataSavingSectionFactory.swift in Sources */, 78C1BD2325EFA1330058080F /* SberpayRouter.swift in Sources */, 402EB7392A73C3DE20A9581B /* PlaceholderView.swift in Sources */, + 25347BE826BADBE800FDD1DA /* CardSettingsRouterIO.swift in Sources */, 402EBD76466A7E8153264B72 /* Identifier.swift in Sources */, 402EB4E93D7A00A973BB93FA /* UITableViewHeaderFooterView+Identifier.swift in Sources */, 402EBF5E02044CFB30A2A40A /* UITableViewCell+Identifier.swift in Sources */, diff --git a/YooKassaPayments/Info.plist b/YooKassaPayments/Info.plist index 8bb69f1d..43bf9a13 100644 --- a/YooKassaPayments/Info.plist +++ b/YooKassaPayments/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 6.1.0 + 6.3.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass diff --git a/YooKassaPayments/Private/Atomic Design/Molecules/ItemViews/LargeIconButtonItemView/LargeIconButtonItemViewCell.swift b/YooKassaPayments/Private/Atomic Design/Molecules/ItemViews/LargeIconButtonItemView/LargeIconButtonItemViewCell.swift index 55231786..b85080c6 100644 --- a/YooKassaPayments/Private/Atomic Design/Molecules/ItemViews/LargeIconButtonItemView/LargeIconButtonItemViewCell.swift +++ b/YooKassaPayments/Private/Atomic Design/Molecules/ItemViews/LargeIconButtonItemView/LargeIconButtonItemViewCell.swift @@ -40,6 +40,10 @@ final class LargeIconButtonItemViewCell: UITableViewCell { } } + var rightButton: UIButton { + return itemView.rightButton + } + var rightButtonPressHandler: (() -> Void)? // MARK: - Creating a View Object diff --git a/YooKassaPayments/Private/Atomic Design/Molecules/LargeActionInformer.swift b/YooKassaPayments/Private/Atomic Design/Molecules/LargeActionInformer.swift new file mode 100644 index 00000000..982fd5a7 --- /dev/null +++ b/YooKassaPayments/Private/Atomic Design/Molecules/LargeActionInformer.swift @@ -0,0 +1,223 @@ +import UIKit + +final class LargeActionInformer: UIView { + // MARK: - UI properties + + private(set) lazy var iconView: IconView = { + let iconView = IconView() + iconView.imageView.accessibilityIdentifier = "informer.icon.image.view" + return iconView + }() + + private(set) lazy var actionTemplate: ActionTemplate = { + let actionTemplate = ActionTemplate() + actionTemplate.translatesAutoresizingMaskIntoConstraints = false + actionTemplate.addTarget(self, action: #selector(actionTemplateDidPress), for: .touchUpInside) + actionTemplate.contentView = self.buttonLabel + return actionTemplate + }() + + private(set) lazy var buttonLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.setContentCompressionResistancePriority(.required, for: .vertical) + label.setContentCompressionResistancePriority(.required, for: .horizontal) + label.setContentHuggingPriority(.required, for: .vertical) + label.setContentHuggingPriority(.required, for: .horizontal) + label.accessibilityIdentifier = "informer.action.label" + return label + }() + + // Set values to display. Defaults `nil` for each param. + func set(icon: UIImage? = nil, message: String? = nil, actionTitle: String? = nil) { + iconView.image = icon ?? PaymentMethodResources.Image.unknown + messageLabel.styledText = message + buttonLabel.styledText = actionTitle + } + + private(set) lazy var messageLabel = UILabel() + + var actionHandler: (() -> Void)? + + // MARK: - Constraints + + private var activeConstraints: [NSLayoutConstraint] = [] + + // MARK: - Initializers + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupView() + } + + public override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + deinit { + unsubscribeFromNotifications() + } + + // MARK: - Setup view + + private func setupView() { + layer.cornerRadius = Space.single + layoutMargins = UIEdgeInsets(top: Space.double, left: Space.double, bottom: Space.double, right: Space.double) + Style.default(self) + setupSubviews() + setupConstraints() + subscribeOnNotifications() + } + + private func setupSubviews() { + let subviews: [UIView] = [ + iconView, + messageLabel, + actionTemplate, + ] + subviews.forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.setContentCompressionResistancePriority(.required, for: .vertical) + $0.setContentCompressionResistancePriority(.required, for: .horizontal) + $0.setContentHuggingPriority(.required, for: .vertical) + $0.setContentHuggingPriority(.required, for: .horizontal) + addSubview($0) + } + } + + private func setupConstraints() { + NSLayoutConstraint.deactivate(activeConstraints) + if UIApplication.shared.preferredContentSizeCategory.isAccessibilitySizeCategory { + activeConstraints = [ + iconView.top.constraint(equalTo: topMargin), + iconView.leading.constraint(equalTo: leadingMargin), + iconView.trailing.constraint(lessThanOrEqualTo: trailingMargin), + + messageLabel.leading.constraint(equalTo: leadingMargin), + messageLabel.trailing.constraint(equalTo: trailingMargin), + messageLabel.top.constraint(equalTo: iconView.bottom, constant: Space.double), + + actionTemplate.top.constraint(equalTo: messageLabel.bottom, constant: Space.double), + actionTemplate.trailing.constraint(equalTo: trailingMargin), + actionTemplate.leading.constraint(greaterThanOrEqualTo: leadingMargin), + actionTemplate.bottom.constraint(equalTo: bottomMargin), + ] + } else { + let buttonBottomConstraint = actionTemplate.bottom.constraint(equalTo: bottomMargin) + let iconViewBottomConstraint = iconView.bottom.constraint(equalTo: bottomMargin) + + let additionalConstraints: [NSLayoutConstraint] = [ + buttonBottomConstraint, + iconViewBottomConstraint, + ] + additionalConstraints.forEach { + $0.priority = .defaultHigh + } + activeConstraints = [ + iconView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor), + iconView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), + iconView.bottomAnchor.constraint(lessThanOrEqualTo: layoutMarginsGuide.bottomAnchor), + + messageLabel.topAnchor.constraint(greaterThanOrEqualTo: layoutMarginsGuide.topAnchor), + messageLabel.leading.constraint(equalTo: iconView.trailing, constant: Space.double), + messageLabel.trailingAnchor.constraint(lessThanOrEqualTo: layoutMarginsGuide.trailingAnchor), + + actionTemplate.topAnchor.constraint(equalTo: messageLabel.bottomAnchor, constant: Space.single), + actionTemplate.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), + actionTemplate.leadingAnchor.constraint(greaterThanOrEqualTo: layoutMarginsGuide.leadingAnchor), + actionTemplate.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor), + ] + activeConstraints += additionalConstraints + } + NSLayoutConstraint.activate(activeConstraints) + } + + // MARK: - Drawing and Updating the View + + public override func tintColorDidChange() { + super.tintColorDidChange() + buttonLabel.tintColor = tintColor + buttonLabel.applyStyles() + } + + // MARK: - Notifications + + private func subscribeOnNotifications() { + NotificationCenter.default.addObserver( + self, + selector: #selector(contentSizeCategoryDidChange), + name: UIContentSizeCategory.didChangeNotification, + object: nil + ) + } + + private func unsubscribeFromNotifications() { + NotificationCenter.default.removeObserver(self) + } + + @objc + private func contentSizeCategoryDidChange() { + iconView.applyStyles() + messageLabel.applyStyles() + buttonLabel.applyStyles() + setupConstraints() + } + + // MARK: - Actions + + @objc + private func actionTemplateDidPress() { + actionHandler?() + } +} + +// MARK: - Decorator + +extension LargeActionInformer { + struct Style { + private let target: LargeActionInformer + // Decorate target with default setup + @discardableResult + static func `default`(_ target: LargeActionInformer) -> Style { + Style(target: target) + } + + private init(target: LargeActionInformer) { + self.target = target + self.default() + } + + @discardableResult + private func `default`() -> Style { + target.iconView.setStyles(IconView.Styles.Tint.normal) + target.backgroundColor = .ghost + target.buttonLabel.setStyles(UILabel.DynamicStyle.bodySemibold, UILabel.ColorStyle.Link.normal) + target.messageLabel.setStyles( + UILabel.ColorStyle.secondary, + UILabel.DynamicStyle.body, + UILabel.Styles.multiline + ) + return self + } + + @discardableResult + func lamp() -> Style { + target.backgroundColor = .mousegrey + return self + } + + @discardableResult + func alert() -> Style { + target.iconView.image = UIImage.named("ic_attention_m").colorizedImage(color: .redOrange) + target.buttonLabel.setStyles(UILabel.ColorStyle.Link.disabled) + return self + } + + @discardableResult + func disabled() -> Style { + target.buttonLabel.appendStyle(UILabel.ColorStyle.Link.disabled) + return self + } + } +} diff --git a/YooKassaPayments/Private/Atomic Design/Molecules/MaskedCardView/MaskedCardView.swift b/YooKassaPayments/Private/Atomic Design/Molecules/MaskedCardView/MaskedCardView.swift index d4392168..c67bece9 100644 --- a/YooKassaPayments/Private/Atomic Design/Molecules/MaskedCardView/MaskedCardView.swift +++ b/YooKassaPayments/Private/Atomic Design/Molecules/MaskedCardView/MaskedCardView.swift @@ -6,14 +6,8 @@ protocol MaskedCardViewDelegate: AnyObject { shouldChangeCharactersIn range: NSRange, replacementString string: String ) -> Bool - - func textFieldDidBeginEditing( - _ textField: UITextField - ) - - func textFieldDidEndEditing( - _ textField: UITextField - ) + func textFieldDidBeginEditing(_ textField: UITextField) + func textFieldDidEndEditing(_ textField: UITextField) } final class MaskedCardView: UIView { @@ -21,6 +15,7 @@ final class MaskedCardView: UIView { enum CscState { case `default` case selected + case noCVC case error } @@ -32,6 +27,9 @@ final class MaskedCardView: UIView { var cscState: CscState = .default { didSet { + hintCardCodeLabel.isHidden = false + cardCodeTextView.isHidden = false + switch cscState { case .default: setStyles(UIView.Styles.grayBorder) @@ -50,6 +48,10 @@ final class MaskedCardView: UIView { hintCardCodeLabel.setStyles( UILabel.ColorStyle.alert ) + + case .noCVC: + hintCardCodeLabel.isHidden = true + cardCodeTextView.isHidden = true } } } diff --git a/YooKassaPayments/Private/Atomic Design/Molecules/UIKit+Styles/UIButton+Style.swift b/YooKassaPayments/Private/Atomic Design/Molecules/UIKit+Styles/UIButton+Style.swift index 5a541490..fea8fbb8 100644 --- a/YooKassaPayments/Private/Atomic Design/Molecules/UIKit+Styles/UIButton+Style.swift +++ b/YooKassaPayments/Private/Atomic Design/Molecules/UIKit+Styles/UIButton+Style.swift @@ -139,7 +139,7 @@ extension UIButton { // MARK: - Fileprivate /// Generate rounded image for button background. - fileprivate static func roundedBackground(color: UIColor, cornerRadius: CGFloat = 0) -> UIImage { + static func roundedBackground(color: UIColor, cornerRadius: CGFloat = 0) -> UIImage { let side = cornerRadius * 2 + 2 let size = CGSize(width: side, height: side) return UIImage.image(color: color) @@ -185,6 +185,90 @@ extension UIButton { self.setImage(colorizedImage, for: state) } + var style: Style { + Style(target: self) + } + + struct Style { + let target: UIButton + + @discardableResult + func submitHeight() -> Style { + NSLayoutConstraint.activate([target.heightAnchor.constraint(equalToConstant: 56)]) + return self + } + + @discardableResult + func submitText(color: UIColor) -> Style { + target.setTitleColor(color, for: .normal) + target.setTitleColor(.highlighted(from: color), for: .highlighted) + target.setTitleColor(disabledBackgroundColor, for: .disabled) + target.tintColor = color + target.titleLabel?.lineBreakMode = .byTruncatingTail + target.titleLabel?.font = UIFont.dynamicBodySemibold + target.contentEdgeInsets.left = Space.double + target.contentEdgeInsets.right = Space.double + return self + } + + @discardableResult + func submit(colored: UIColor = CustomizationStorage.shared.mainScheme, ghostTint: Bool = false) -> Style { + return submitText(color: colored).submitHeight().colored(target.tintColor, ghostTint: ghostTint) + } + + @discardableResult + func submitAlert(ghostTint: Bool) -> Style { + return submitText(color: .redOrange).colored(.redOrange, ghostTint: ghostTint).submitHeight() + } + + @discardableResult + func cancel() -> Style { submitHeight().submitText(color: UIColor.AdaptiveColors.primary) } + + private var disabledBackgroundColor: UIColor { + if #available(iOS 13.0, *) { + return .systemGray5 + } else { + return .mousegrey + } + } + + @discardableResult + func colored(_ color: UIColor, ghostTint: Bool = false) -> Style { + let hColor = UIColor.highlighted(from: color) + target.tintColor = color + + if ghostTint { + [ + (color, UIControl.State.normal), + (hColor, .highlighted), + (disabledBackgroundColor, .disabled), + ].forEach { + target.setTitleColor($0.0, for: $0.1) + target.setBackgroundImage( + Styles.roundedBackground(color: .ghostTint(from: $0.0), cornerRadius: Styles.cornerRadius), + for: $0.1) + + } + } else { + target.setTitleColor(.white, for: .normal) + target.setTitleColor(.white, for: .highlighted) + target.setTitleColor(disabledBackgroundColor, for: .disabled) + [ + (color, UIControl.State.normal), + (hColor, .highlighted), + (disabledBackgroundColor, .disabled), + ].forEach { + target.setBackgroundImage( + Styles.roundedBackground(color: $0.0, cornerRadius: Styles.cornerRadius), + for: $0.1 + ) + } + } + + return self + } + } + enum DynamicStyle { /// Style for primary button. diff --git a/YooKassaPayments/Private/Atomic Design/Molecules/UIKit+Styles/UIColor+Style.swift b/YooKassaPayments/Private/Atomic Design/Molecules/UIKit+Styles/UIColor+Style.swift index 323560a9..c42606d2 100644 --- a/YooKassaPayments/Private/Atomic Design/Molecules/UIKit+Styles/UIColor+Style.swift +++ b/YooKassaPayments/Private/Atomic Design/Molecules/UIKit+Styles/UIColor+Style.swift @@ -75,6 +75,12 @@ extension UIColor { static let black30 = UIColor(white: 0, alpha: 0.3) static let nobel = UIColor(white: 179 / 255, alpha: 1) + static var ghost: UIColor { + if #available(iOS 13, *) { + return .quaternaryLabel + } + return UIColor(white: 235 / 255, alpha: 1) + } static let blueRibbon50 = UIColor(red: 0 / 255, green: 112 / 255, blue: 240 / 255, alpha: 0.5) static let jordyBlue = UIColor(red: 135 / 255, green: 184 / 255, blue: 245 / 255, alpha: 1) @@ -105,6 +111,12 @@ extension UIColor { return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: alpha) } + static func ghostTint(from color: UIColor) -> UIColor { + var colors: (CGFloat, CGFloat, CGFloat, CGFloat) = (0.0, 0.0, 0.0, 0.0) + color.getRed(&colors.0, green: &colors.1, blue: &colors.2, alpha: &colors.3) + return UIColor(red: colors.0, green: colors.1, blue: colors.2, alpha: 0.15) + } + // MARK: - Adaptive colors enum AdaptiveColors { diff --git a/YooKassaPayments/Private/Atomic Design/Views/ActionTemplate.swift b/YooKassaPayments/Private/Atomic Design/Views/ActionTemplate.swift new file mode 100644 index 00000000..90ebcf97 --- /dev/null +++ b/YooKassaPayments/Private/Atomic Design/Views/ActionTemplate.swift @@ -0,0 +1,116 @@ +import UIKit + +/// Action template +class ActionTemplate: UIControl { + + // MARK: - Configuring the Control’s Attributes + + /// A Boolean value indicating whether the control is in the selected state. + override var isSelected: Bool { + didSet { + updateStyledState() + } + } + + /// A Boolean value indicating whether the control draws a highlight. + override var isHighlighted: Bool { + didSet { + updateStyledState() + } + } + + /// A Boolean value indicating whether the control is enabled. + override var isEnabled: Bool { + didSet { + updateStyledState() + } + } + + /// The main view to which you add your templates’s custom content. + var contentView: UIView? { + willSet { + contentView?.removeFromSuperview() + } + didSet { + guard let contentView = contentView else { return } + addSubview(contentView) + contentView.isUserInteractionEnabled = false + contentView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + contentView.left.constraint(equalTo: leftMargin), + contentView.right.constraint(equalTo: rightMargin), + contentView.top.constraint(equalTo: topMargin), + contentView.bottom.constraint(equalTo: bottomMargin), + ]) + updateContent(for: styledState) + accessibilityTraits = UIAccessibilityTraits.button + } + } + + // MARK: - Initializers + + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupView() + } + + // MARK: - Setup view + + private func setupView() { + layoutMargins = .zero + } + + override func tintColorDidChange() { + super.tintColorDidChange() + contentView?.applyStyles() + } + + // MARK: - Configuring ActionTemplate Presentation + + private(set) var styledState: UIControl.State = .normal { + didSet { + guard oldValue != styledState else { return } + updateContent(for: styledState) + } + } + + private var styles: [UInt: InternalStyle] = [:] + + /// Sets the style to use for the specified state. + /// + /// - Parameter style: The style to use for the specified state. + /// state: The state that uses the specified style. + func setStyle(_ style: InternalStyle, for state: UIControl.State) { + styles[state.rawValue] = style + if styledState == state { + updateContent(for: state) + } + } + + // MARK: - Managing the View + + private func updateStyledState() { + switch (isEnabled, isHighlighted, isSelected) { + case (true, true, false): + styledState = .highlighted + case (true, false, true): + styledState = .selected + case (false, _, false): + styledState = .disabled + default: + styledState = .normal + } + } + + private func updateContent(for state: UIControl.State) { + guard let style = styles[state.rawValue] else { return } + let view = contentView ?? self + _ = styles.mapValues(view.removeStyle) + view.appendStyle(style) + } +} diff --git a/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactory.swift b/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactory.swift index 5619ca8c..5dcd0609 100644 --- a/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactory.swift +++ b/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactory.swift @@ -3,25 +3,16 @@ import YooKassaPaymentsApi protocol PaymentMethodViewModelFactory { - // MARK: - Replace bullets - - func replaceBullets(_ pan: String) -> String - // MARK: - Transform ViewModel from PaymentOption - func makePaymentMethodViewModel( - paymentOption: PaymentOption, + func makePaymentMethodViewModels( + _ paymentOptions: [PaymentOption], walletDisplayName: String? - ) -> PaymentMethodViewModel - - func makePaymentMethodViewModel( - paymentOption: PaymentOption - ) -> PaymentMethodViewModel - - // MARK: - Making ViewModel from PaymentMethodType + ) -> (models: [PaymentMethodViewModel], indexMap: ([Int: Int])) func makePaymentMethodViewModel( - _ paymentMethodType: YooKassaPaymentsApi.PaymentMethodType + paymentOption: PaymentInstrumentYooMoneyWallet, + walletDisplayName: String? ) -> PaymentMethodViewModel // MARK: - Make Image @@ -31,10 +22,13 @@ protocol PaymentMethodViewModelFactory { ) -> UIImage func makeBankCardImage( - _ paymentMethodBankCard: PaymentMethodBankCard + first6Digits: String?, + bankCardType: BankCardType ) -> UIImage - func makePaymentMethodTypeImage( - _ paymentMethodType: YooKassaPaymentsApi.PaymentMethodType - ) -> UIImage + // MARK: - Replace bullets + + func replaceBullets(_ pan: String) -> String + + func makeMaskedPan(_ cardMask: String) -> String } diff --git a/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactoryImpl.swift b/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactoryImpl.swift index 4c74940a..c6ed286c 100644 --- a/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactoryImpl.swift +++ b/YooKassaPayments/Private/Factory/PaymentMethodViewModel/PaymentMethodViewModelFactoryImpl.swift @@ -22,135 +22,202 @@ final class PaymentMethodViewModelFactoryImpl { $0.numberStyle = .currency return $0 }(NumberFormatter()) + + func makeMaskedPan(_ cardMask: String) -> String { + let pan = String(cardMask.suffix(8)) + let replacedPan = replaceBullets(pan) + return replacedPan.chunks(of: 4).joined(separator: " ") + } } // MARK: - PaymentMethodViewModelFactory extension PaymentMethodViewModelFactoryImpl: PaymentMethodViewModelFactory { - // MARK: - Replace bullets + // MARK: - Transform ViewModel from PaymentOption - func replaceBullets(_ pan: String) -> String { - return pan.replacingOccurrences(of: "*", with: "•") + func makePaymentMethodViewModels( + _ paymentOptions: [PaymentOption], + walletDisplayName: String? + ) -> (models: [PaymentMethodViewModel], indexMap: ([Int: Int])) { + var map: [Int: Int] = [:] + let viewModels = paymentOptions + .map { element -> [PaymentMethodViewModel] in + return makePaymentMethodViewModel( + paymentOption: element, + walletDisplayName: walletDisplayName + ) + } + var index = 0 + viewModels.enumerated().forEach { enumerated in + enumerated.element.forEach { _ in + map[index] = enumerated.offset + index += 1 + } + } + return (viewModels.flatMap { $0 }, map) } - // MARK: - Transform ViewModel from PaymentOption - func makePaymentMethodViewModel( - paymentOption: PaymentOption, + paymentOption: PaymentInstrumentYooMoneyWallet, walletDisplayName: String? ) -> PaymentMethodViewModel { - let viewModel: PaymentMethodViewModel - switch paymentOption { - case let paymentOption as PaymentInstrumentYooMoneyWallet: - viewModel = makePaymentMethodViewModel( - paymentOption, - walletDisplayName: walletDisplayName + makePaymentMethodViewModel( + paymentOption, + walletDisplayName: walletDisplayName + ) + } + + // MARK: - Additional protocol methods + + func replaceBullets(_ pan: String) -> String { + return pan.replacingOccurrences(of: "*", with: "•") + } + + func makeBankCardImage( + _ paymentOption: PaymentInstrumentYooMoneyLinkedBankCard + ) -> UIImage { + if let bankSettings = bankSettingsService.bankSettings(paymentOption.cardMask) { + return UIImage.named(bankSettings.logoName) + } else { + return makeBankCardImage( + cardType: paymentOption.cardType ) - case let paymentOption as PaymentInstrumentYooMoneyLinkedBankCard: - viewModel = makePaymentMethodViewModel(paymentOption) - default: - viewModel = makePaymentMethodViewModel(paymentOption.paymentMethodType) } - return viewModel } + func makeBankCardImage( + first6Digits: String?, + bankCardType: BankCardType + ) -> UIImage { + let image: UIImage + if + let first6Digits = first6Digits, + let bankSettings = bankSettingsService.bankSettings(first6Digits) + { + image = UIImage.named(bankSettings.logoName) + } else if + let first6 = first6Digits, + let existing = BankCardImageFactoryAssembly.makeFactory().makeImage(first6) + { + image = existing + } else { + image = makeBankCardImage(cardType: bankCardType) + } + + return image + } +} + +// MARK: - Making ViewModel from PaymentInstrumentYooMoneyWallet + +private extension PaymentMethodViewModelFactoryImpl { func makePaymentMethodViewModel( - paymentOption: PaymentOption - ) -> PaymentMethodViewModel { - let viewModel: PaymentMethodViewModel + paymentOption: PaymentOption, + walletDisplayName: String? + ) -> [PaymentMethodViewModel] { switch paymentOption { case let paymentOption as PaymentInstrumentYooMoneyWallet: - viewModel = makePaymentMethodViewModel( - paymentOption - ) + return [makePaymentMethodViewModel(paymentOption, walletDisplayName: walletDisplayName)] case let paymentOption as PaymentInstrumentYooMoneyLinkedBankCard: - viewModel = makePaymentMethodViewModel(paymentOption) + return [makePaymentMethodViewModel(paymentOption)] default: - viewModel = makePaymentMethodViewModel(paymentOption.paymentMethodType) + switch paymentOption.paymentMethodType { + case .bankCard: + guard let option = paymentOption as? PaymentOptionBankCard else { fallthrough } + var cards = option.paymentInstruments?.map { card in + PaymentMethodViewModel( + id: card.paymentInstrumentId, + isShopLinkedCard: true, + image: makeBankCardImage( + first6Digits: card.first6, + bankCardType: card.cardType + ), + title: replaceBullets("**** \(card.last4)"), + subtitle: PaymentMethodResources.Localized.linkedCard, + hasActions: true + ) + } ?? [] + cards.append( + PaymentMethodViewModel( + id: nil, + isShopLinkedCard: false, + image: makePaymentMethodTypeImage(option.paymentMethodType), + title: makePaymentMethodTypeTitle(option.paymentMethodType), + subtitle: nil + ) + ) + return cards + default: + return [ + PaymentMethodViewModel( + id: nil, + isShopLinkedCard: false, + image: makePaymentMethodTypeImage(paymentOption.paymentMethodType), + title: makePaymentMethodTypeTitle(paymentOption.paymentMethodType), + subtitle: nil + ), + ] + } } - return viewModel } - // MARK: - Making ViewModel from PaymentInstrumentYooMoneyWallet - - private func makePaymentMethodViewModel( + func makePaymentMethodViewModel( _ paymentOption: PaymentInstrumentYooMoneyWallet, walletDisplayName: String? ) -> PaymentMethodViewModel { return PaymentMethodViewModel( + id: nil, + isShopLinkedCard: false, image: PaymentMethodResources.Image.yooMoney, title: walletDisplayName ?? paymentOption.accountId, subtitle: makeBalanceText(paymentOption.balance) ) } - private func makePaymentMethodViewModel( - _ paymentOption: PaymentInstrumentYooMoneyWallet - ) -> PaymentMethodViewModel { - return PaymentMethodViewModel( - image: PaymentMethodResources.Image.yooMoney, - title: PaymentMethodResources.Localized.wallet, - subtitle: makeBalanceText(paymentOption.balance) - ) - } - - private func makeBalanceText( + func makeBalanceText( _ balance: YooKassaPaymentsApi.MonetaryAmount ) -> String? { let amount = MonetaryAmountFactory.makeAmount(balance) balanceNumberFormatter.currencySymbol = amount.currency.symbol return balanceNumberFormatter.string(for: amount.value) } +} - // MARK: - Making ViewModel from PaymentInstrumentYooMoneyLinkedBankCard +// MARK: - Making ViewModel from PaymentInstrumentYooMoneyLinkedBankCard - private func makePaymentMethodViewModel( +private extension PaymentMethodViewModelFactoryImpl { + func makePaymentMethodViewModel( _ paymentOption: PaymentInstrumentYooMoneyLinkedBankCard ) -> PaymentMethodViewModel { return PaymentMethodViewModel( + id: nil, + isShopLinkedCard: false, image: makeBankCardImage(paymentOption), title: makeBankCardTitle(paymentOption), - subtitle: makeBankCardSubtitle(paymentOption) + subtitle: makeBankCardSubtitle(paymentOption), + hasActions: true ) } - private func makeBankCardTitle( + func makeBankCardTitle( _ paymentOption: PaymentInstrumentYooMoneyLinkedBankCard ) -> String { return paymentOption.cardName ?? makeMaskedPan(paymentOption.cardMask) } - private func makeBankCardSubtitle( + func makeBankCardSubtitle( _ paymentOption: PaymentInstrumentYooMoneyLinkedBankCard - ) -> String? { - guard paymentOption.cardName != nil else { - return nil - } - - return makeMaskedPan(paymentOption.cardMask) - } - - private func makeMaskedPan(_ cardMask: String) -> String { - let pan = String(cardMask.suffix(8)) - let replacedPan = replaceBullets(pan) - return replacedPan.chunks(of: 4).joined(separator: " ") + ) -> String { + return PaymentMethodResources.Localized.yooMoneyCard } +} - // MARK: - Making ViewModel from PaymentMethodType +// MARK: - Making ViewModel from PaymentMethodType - func makePaymentMethodViewModel( - _ paymentMethodType: YooKassaPaymentsApi.PaymentMethodType - ) -> PaymentMethodViewModel { - return PaymentMethodViewModel( - image: makePaymentMethodTypeImage(paymentMethodType), - title: makePaymentMethodTypeTitle(paymentMethodType), - subtitle: nil - ) - } - - private func makePaymentMethodTypeTitle( +private extension PaymentMethodViewModelFactoryImpl { + func makePaymentMethodTypeTitle( _ paymentMethodType: YooKassaPaymentsApi.PaymentMethodType ) -> String { let name: String @@ -169,32 +236,11 @@ extension PaymentMethodViewModelFactoryImpl: PaymentMethodViewModelFactory { } return name } +} - // MARK: - Make Image - - func makeBankCardImage( - _ paymentOption: PaymentInstrumentYooMoneyLinkedBankCard - ) -> UIImage { - if let bankSettings = bankSettingsService.bankSettings(paymentOption.cardMask) { - return UIImage.named(bankSettings.logoName) - } else { - return makeBankCardImage( - cardType: paymentOption.cardType - ) - } - } +// MARK: - Make Image - func makeBankCardImage( - _ paymentMethodBankCard: PaymentMethodBankCard - ) -> UIImage { - if let bankSettings = bankSettingsService.bankSettings(paymentMethodBankCard.first6) { - return UIImage.named(bankSettings.logoName) - } else { - return makeBankCardImage( - cardType: paymentMethodBankCard.cardType - ) - } - } +private extension PaymentMethodViewModelFactoryImpl { func makePaymentMethodTypeImage( _ paymentMethodType: YooKassaPaymentsApi.PaymentMethodType @@ -217,7 +263,7 @@ extension PaymentMethodViewModelFactoryImpl: PaymentMethodViewModelFactory { } // swiftlint:disable cyclomatic_complexity - private func makeBankCardImage( + func makeBankCardImage( cardType: BankCardType ) -> UIImage { let image: UIImage diff --git a/YooKassaPayments/Private/Helpers/Localization.swift b/YooKassaPayments/Private/Helpers/Localization.swift index 378ea0ee..e427e94c 100644 --- a/YooKassaPayments/Private/Helpers/Localization.swift +++ b/YooKassaPayments/Private/Helpers/Localization.swift @@ -161,5 +161,234 @@ enum CommonLocalized { comment: "Текст `SberPay` https://yadi.sk/i/T-XQGU9NaPMgKA" ) } + + enum CardSettingsDetails { + static let unbind = NSLocalizedString( + "card.details.unbind", + bundle: Bundle.framework, + value: "Отвязать карту", + comment: "Текст `Отвязать карту` https://disk.yandex.ru/i/QNJyBrfP52vQOw" + ) + static let autopaymentPersists = NSLocalizedString( + "card.details.autopaymentPersists", + bundle: Bundle.framework, + value: "После отвязки карты останутся автосписания. Отменить их можно через службу поддержки магазина.", + comment: "Текст, в информере, о сохранении автоплатежа https://disk.yandex.ru/i/QNJyBrfP52vQOw" + ) + static let moreInfo = NSLocalizedString( + "card.details.info.more", + bundle: Bundle.framework, + value: "Подробнее", + comment: "Текст кнопки, в информере, ведущей в подробности https://disk.yandex.ru/i/QNJyBrfP52vQOw" + ) + static let unwind = NSLocalizedString( + "card.details.unwind", + bundle: Bundle.framework, + value: "Вернуться", + comment: "Текст, ведущей назад, кнопки https://disk.yandex.ru/i/dcgivhF4QbURwA" + ) + static let yoocardUnbindDetails = NSLocalizedString( + "card.details.yoocardUnbindDetails", + bundle: Bundle.framework, + value: "Отвязать эту карту можно только в настройках кошелька", + comment: "Текст, в информере, для карты привязанной к кошельку https://disk.yandex.ru/i/dcgivhF4QbURwA" + ) + static let autopayInfoTitle = NSLocalizedString( + "card.details.info.autopay.title", + bundle: Bundle.framework, + value: "Как работают автоматические списания", + comment: "Заголовок информации о работе автосписания https://disk.yandex.ru/i/r9l5HObi2jZy6A" + ) + static let autopayInfoDetails = NSLocalizedString( + "card.details.info.autopay.details", + bundle: Bundle.framework, + value: "Если вы согласитесь на автосписания, мы привяжем банковскую карту к магазину. После этого магазин сможет присылать запросы на автоматические списания денег — тогда платёж выполняется без дополнительного подтверждения с вашей стороны.\nАвтосписания продолжатся даже при перевыпуске карты, если ваш банк умеет автоматически обновлять данные. Отменить их и отвязать карту можно в любой момент — через службу поддержки магазина.", + comment: "Текст информации о работе автосписания https://disk.yandex.ru/i/r9l5HObi2jZy6A" + ) + static let unbindInfoTitle = NSLocalizedString( + "card.details.info.unbind.title", + bundle: Bundle.framework, + value: "Как отвязать карту от кошелька", + comment: "Заголовок информации об отвязке карты https://disk.yandex.ru/i/59heYTl9Q4L2fA" + ) + static let unbindInfoDetails = NSLocalizedString( + "card.details.info.unbind.details", + bundle: Bundle.framework, + value: """ + Для этого зайдите в настройки кошелька на сайте или в приложении ЮMoney. + В приложении: нажмите на свою аватарку, выберите «Банковские карты», смахните нужную карту влево и нажмите «Удалить». + На сайте: перейдите в настройки кошелька, откройте вкладку «Привязанные карты», найдите нужную карту и нажмите «Отвязать». + """, + comment: "Текст информации об отвязке карты https://disk.yandex.ru/i/59heYTl9Q4L2fA" + ) + static let unbindSuccess = NSLocalizedString( + "card.details.unbind.success", + bundle: Bundle.framework, + value: "Карта %@ отвязана", + comment: "Текст нотификации об успешной отвязке карты. Параметр - маска карты https://disk.yandex.ru/i/JWC70LuzuJSeEw" + ) + static let unbindFail = NSLocalizedString( + "card.details.unbind.fail", + bundle: Bundle.framework, + value: "Не удалось отвязать карту %@", + comment: "Текст нотификации об ошибке отвязки карты. Параметр - маска карты https://disk.yandex.ru/i/QNJyBrfP52vQOw" + ) + } + + enum RecurrencyAndSavePaymentData { + static let saveDataInfoTitle = NSLocalizedString( + "RecurrencyAndSavePaymentData.info.saveData.title", + bundle: Bundle.framework, + value: "Сохранение платёжных данных", + comment: "Заголовок информации о сохранении данных карты https://disk.yandex.ru/i/yLD0tpyvO3zvLg" + ) + static let saveDataInfoMessage = NSLocalizedString( + "RecurrencyAndSavePaymentData.info.saveData.message", + bundle: Bundle.framework, + value: """ + Если вы это разрешили, мы сохраним для этого магазина и его партнёров данные вашей банковской карты — номер, имя владельца и срок действия (всё, кроме кода CVC). В следующий раз не нужно будет вводить их, чтобы заплатить в этом магазине. + + Удалить данные карты можно в процессе оплаты (нажмите на три точки напротив карты и выберите «Удалить карту») или через службу поддержки. + """, + comment: "Текст информации о сохранении данных карты https://disk.yandex.ru/i/yLD0tpyvO3zvLg" + ) + static let saveDataAndAutopaymentsInfoTitle = NSLocalizedString( + "RecurrencyAndSavePaymentData.info.saveDataAndAutopayments.title", + bundle: Bundle.framework, + value: "Автосписания и сохранение платёжных данных", + comment: "Заголовок информации о сохранении данных карты и автосписаниях https://disk.yandex.ru/i/yLD0tpyvO3zvLg" + ) + static let saveDataAndAutopaymentsInfoMessage = NSLocalizedString( + "RecurrencyAndSavePaymentData.info.saveDataAndAutopayments.message", + bundle: Bundle.framework, + value: """ + Если вы это разрешили, мы сохраним для этого магазина и его партнёров данные вашей банковской карты — номер, имя владельца, срок действия (всё, кроме кода CVC). В следующий раз не нужно будет их вводить, чтобы заплатить в этом магазине. + + Кроме того, мы привяжем карту (в том числе использованную через Google Pay)
к магазину. После этого магазин сможет присылать запросы на автоматические списания денег — тогда платёж выполняется без дополнительного подтверждения с вашей стороны. + + Автосписания продолжатся даже при перевыпуске карты, если ваш банк умеет автоматически обновлять данные. Отменить их и отвязать карту можно в любой момент — через службу поддержки магазина. + """, + comment: "Текст информации о сохранении данных карты и автосписаниях https://disk.yandex.ru/i/yLD0tpyvO3zvLg" + ) + + enum Header { + static let requiredSaveDataAndAutopaymentsHeader = NSLocalizedString( + "RecurrencyAndSavePaymentData.header.saveDataAndAutopayments.required", + bundle: Bundle.framework, + value: "Разрешим автосписания и сохраним платёжные данные", + comment: "Текст информера о неопциональном подключении автосписания и сохранении данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w" + ) + static let requiredAutopaymentsHeader = NSLocalizedString( + "RecurrencyAndSavePaymentData.header.autopayments.required", + bundle: Bundle.framework, + value: "Разрешим автосписания", + comment: "Текст информера о неопциональном подключении автосписания при платеже https://disk.yandex.ru/i/dcZY0utIfx634w" + ) + static let requiredSaveDataHeader = NSLocalizedString( + "RecurrencyAndSavePaymentData.header.saveData.required", + bundle: Bundle.framework, + value: "Сохраним платёжные данные", + comment: "Текст информера о неопциональном сохранении платёжных данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w" + ) + + static let optionalSaveDataAndAutopaymentsHeader = NSLocalizedString( + "RecurrencyAndSavePaymentData.header.saveDataAndAutopayments.optional", + bundle: Bundle.framework, + value: "Разрешить автосписания и сохранить платёжные данные", + comment: "Текст информера о опциональном подключении автосписания и сохранении данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w" + ) + static let optionalAutopaymentsHeader = NSLocalizedString( + "RecurrencyAndSavePaymentData.header.autopayments.optional", + bundle: Bundle.framework, + value: "Разрешить автосписания", + comment: "Текст информера о опциональном подключении автосписания при платеже https://disk.yandex.ru/i/dcZY0utIfx634w" + ) + static let optionalSaveDataHeader = NSLocalizedString( + "RecurrencyAndSavePaymentData.header.saveData.optional", + bundle: Bundle.framework, + value: "Сохранить платёжные данные", + comment: "Текст информера о опциональном сохранении платёжных данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w" + ) + } + + enum Link { + enum Optional { + static let saveDataLink = NSLocalizedString( + "RecurrencyAndSavePaymentData.link.saveData.optional", + bundle: Bundle.framework, + value: "Магазин сохранит данные вашей карты — в следующий раз можно будет их не вводить", + comment: "Текст со ссылкой информации об опциональном сохранении платёжных данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w" + ) + static let saveDataLinkInteractive = NSLocalizedString( + "RecurrencyAndSavePaymentData.link.interactive.saveData.optional", + bundle: Bundle.framework, + value: "сохранит данные вашей карты", + comment: "Интерактивная часть текста со ссылкой информации об опциональном сохранении платёжных данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w" + ) + static let autopaymentsLink = NSLocalizedString( + "RecurrencyAndSavePaymentData.link.autopayments.optional", + bundle: Bundle.framework, + value: "После оплаты запомним эту карту: магазин сможет списывать деньги без вашего участия", + comment: "Текст со ссылкой информации об опциональном подключении автоплатежа при платеже https://disk.yandex.ru/i/dcZY0utIfx634w" + ) + static let autopaymentsInteractive = NSLocalizedString( + "RecurrencyAndSavePaymentData.link.interactive.autopayments.optional", + bundle: Bundle.framework, + value: "списывать деньги без вашего участия", + comment: "Интерактивная часть текста со ссылкой информации об опциональном подключении автоплатежа при платеже https://disk.yandex.ru/i/dcZY0utIfx634w" + ) + static let autopaymentsAndSaveDataLink = NSLocalizedString( + "RecurrencyAndSavePaymentData.link.autopaymentsAndSaveData.optional", + bundle: Bundle.framework, + value: "После оплаты магазин сохранит данные карты и сможет списывать деньги без вашего участия", + comment: "Текст со ссылкой информации об опциональном подключении автоплатежа и сохранения данных карты при платеже https://disk.yandex.ru/i/dcZY0utIfx634w" + ) + static let autopaymentsAndSaveDataInteractive = NSLocalizedString( + "RecurrencyAndSavePaymentData.link.interactive.autopaymentsAndSaveData.optional", + bundle: Bundle.framework, + value: "сохранит данные карты и сможет списывать деньги без вашего участия", + comment: "Интерактивная часть текста со ссылкой информации об опциональном подключении автоплатежа и сохранения данных карты при платеже https://disk.yandex.ru/i/dcZY0utIfx634w" + ) + } + enum Required { + static let saveDataLink = NSLocalizedString( + "RecurrencyAndSavePaymentData.link.saveData.required", + bundle: Bundle.framework, + value: "Магазин сохранит данные вашей карты — в следующий раз можно будет их не вводить", + comment: "Текст со ссылкой информации об опциональном сохранении платёжных данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w" + ) + static let saveDataLinkInteractive = NSLocalizedString( + "RecurrencyAndSavePaymentData.link.interactive.saveData.required", + bundle: Bundle.framework, + value: "сохранит данные вашей карты", + comment: "Интерактивная часть текста со ссылкой информации об опциональном сохранении платёжных данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w" + ) + static let autopaymentsLink = NSLocalizedString( + "RecurrencyAndSavePaymentData.link.autopayments.required", + bundle: Bundle.framework, + value: "Заплатив здесь, вы разрешаете привязать карту и списывать деньги без вашего участия", + comment: "Текст со ссылкой информации об опциональном подключении автоплатежа при платеже https://disk.yandex.ru/i/dcZY0utIfx634w" + ) + static let autopaymentsInteractive = NSLocalizedString( + "RecurrencyAndSavePaymentData.link.interactive.autopayments.required", + bundle: Bundle.framework, + value: "списывать деньги без вашего участия", + comment: "Интерактивная часть текста со ссылкой информации об опциональном подключении автоплатежа при платеже https://disk.yandex.ru/i/dcZY0utIfx634w" + ) + static let autopaymentsAndSaveDataLink = NSLocalizedString( + "RecurrencyAndSavePaymentData.link.autopaymentsAndSaveData.required", + bundle: Bundle.framework, + value: "Заплатив здесь, вы соглашаетесь сохранить данные карты и списывать деньги без вашего участия", + comment: "Текст со ссылкой информации об опциональном подключении автоплатежа и сохранения данных карты при платеже https://disk.yandex.ru/i/dcZY0utIfx634w" + ) + static let autopaymentsAndSaveDataInteractive = NSLocalizedString( + "RecurrencyAndSavePaymentData.link.interactive.autopaymentsAndSaveData.required", + bundle: Bundle.framework, + value: "сохранить данные карты и списывать деньги без вашего участия", + comment: "Интерактивная часть текста со ссылкой информации об опциональном подключении автоплатежа и сохранения данных карты при платеже https://disk.yandex.ru/i/dcZY0utIfx634w" + ) + } + } + } // swiftlint:enable line_length } diff --git a/YooKassaPayments/Private/Helpers/UIViewController+NotificationPresenting.swift b/YooKassaPayments/Private/Helpers/UIViewController+NotificationPresenting.swift index 12fb6e50..4e9557c5 100644 --- a/YooKassaPayments/Private/Helpers/UIViewController+NotificationPresenting.swift +++ b/YooKassaPayments/Private/Helpers/UIViewController+NotificationPresenting.swift @@ -128,7 +128,7 @@ extension UIViewController: NotificationPresenting { } func presentError(with message: String) { - let notification = Notification( + let notification = ToastAlertNotification( title: nil, message: message, type: .error, @@ -147,9 +147,8 @@ extension UIViewController: NotificationPresenting { } } -@available(iOS 9.0, *) -private extension UIViewController { - struct Notification: PresentableNotification { +extension UIViewController { + struct ToastAlertNotification: PresentableNotification { private(set) var title: String? private(set) var message: String private(set) var type: PresentableNotificationType @@ -162,7 +161,6 @@ private extension UIViewController { } } -@available(iOS 9.0, *) private extension UIViewController { enum Constants { static let showAnimationDuration: TimeInterval = 0.25 diff --git a/YooKassaPayments/Private/Models/PaymentMethodResources.swift b/YooKassaPayments/Private/Models/PaymentMethodResources.swift index 6d636b0e..7839b5a7 100644 --- a/YooKassaPayments/Private/Models/PaymentMethodResources.swift +++ b/YooKassaPayments/Private/Models/PaymentMethodResources.swift @@ -2,6 +2,7 @@ import UIKit.UIImage enum PaymentMethodResources { enum Localized { + // swiftlint:disable line_length static let wallet = NSLocalizedString( "PaymentMethod.wallet", bundle: Bundle.framework, @@ -17,8 +18,8 @@ enum PaymentMethodResources { static let bankCard = NSLocalizedString( "PaymentMethod.bankCard", bundle: Bundle.framework, - value: "Банковская карта", - comment: "Способ оплаты - `Банковская карта` https://yadi.sk/i/smhhxBAxkP8Ebw" + value: "Ввести новую карту", + comment: "Способ оплаты - `Новая карта` https://disk.yandex.ru/d/wdNdER1Bis-YkA" ) static let sberpay = NSLocalizedString( "PaymentMethod.sberpay", @@ -26,6 +27,61 @@ enum PaymentMethodResources { value: "SberPay", comment: "Способ оплаты - `SberPay` https://yadi.sk/i/smhhxBAxkP8Ebw" ) + static let yooMoneyCard = NSLocalizedString( + "PaymentMethod.yooMoneyCard", + bundle: Bundle.framework, + value: "Карта Юмани", + comment: "Способ оплаты - `Карта Юмани` https://disk.yandex.ru/d/sFpmR3gLEc287Q" + ) + static let linkedCard = NSLocalizedString( + "PaymentMethod.linkedCard", + bundle: Bundle.framework, + value: "Привязанная карта", + comment: "Способ оплаты - `Привязанная карта` https://disk.yandex.ru/d/sFpmR3gLEc287Q" + ) + static let safeDealInfoTitle = NSLocalizedString( + "PaymentMethod.safeDealInfo.title", + bundle: Bundle.framework, + value: "Почему у платежа несколько получателей", + comment: "Тайтл информации о безопасной сделке https://disk.yandex.ru/i/zOrGowAKK4uz3A" + ) + static let safeDealInfoBody = NSLocalizedString( + "PaymentMethod.safeDealInfo.body", + bundle: Bundle.framework, + value: "Такое может быть, если вы платите на интернет-площадке, которая позволяет покупать одновременно у нескольких продавцов (например, на маркетплейсе).\n\nУточнить список получателей платежа можно на площадке, на которой вы совершаете платёж.", + comment: "Подробности о безопасной сделке https://disk.yandex.ru/i/zOrGowAKK4uz3A" + ) + private static let safeDealInfoLinkPartHighlighted = NSLocalizedString( + "PaymentMethod.safeDealInfo.link.highlighted", + bundle: Bundle.framework, + value: "несколько получателей", + comment: "текст-ссылка интерактивная часть https://disk.yandex.ru/i/UOEMl4Ig3Z_4UA" + ) + private static let safeDealInfoLinkPartBegining = NSLocalizedString( + "PaymentMethod.safeDealInfo.link.begining", + bundle: Bundle.framework, + value: "У платежа может быть ", + comment: "текст-ссылка https://disk.yandex.ru/i/UOEMl4Ig3Z_4UA" + ) + + static var safeDealInfoLink: NSAttributedString { + let result = NSMutableAttributedString( + attributedString: .init( + string: Self.safeDealInfoLinkPartBegining, + attributes: [.foregroundColor: UIColor.AdaptiveColors.secondary]) + ) + let link = NSAttributedString( + string: Self.safeDealInfoLinkPartHighlighted, + attributes: [.link: "yookassapayments://"] + ) + result.append(link) + result.addAttributes( + [.font: UIFont.dynamicCaption2], + range: NSRange(location: 0, length: (result.string as NSString).length) + ) + return result + } + // swiftlint:enable line_length } enum Image { @@ -48,5 +104,7 @@ enum PaymentMethodResources { static let visa = UIImage.named("PaymentMethod.Visa") static let yooMoney = UIImage.named("PaymentMethod.YooMoney") static let sberpay = UIImage.named("PaymentMethod.Sberpay") + static let more = UIImage.named("icon2_name_more_s").colorizedImage(color: UIColor.AdaptiveColors.secondary) + static let trash = UIImage.named("icon2_name_trash_m").colorizedImage(color: .white) } } diff --git a/YooKassaPayments/Private/Modules/ApplePayContract/ApplePayContractModuleIO.swift b/YooKassaPayments/Private/Modules/ApplePayContract/ApplePayContractModuleIO.swift index 69ba2ecc..76e5f548 100644 --- a/YooKassaPayments/Private/Modules/ApplePayContract/ApplePayContractModuleIO.swift +++ b/YooKassaPayments/Private/Modules/ApplePayContract/ApplePayContractModuleIO.swift @@ -16,6 +16,8 @@ struct ApplePayContractModuleInputData { let savePaymentMethodViewModel: SavePaymentMethodViewModel? let initialSavePaymentMethod: Bool let isBackBarButtonHidden: Bool + let customerId: String? + let isSafeDeal: Bool } protocol ApplePayContractModuleInput: AnyObject {} diff --git a/YooKassaPayments/Private/Modules/ApplePayContract/ApplePayContractRouterIO.swift b/YooKassaPayments/Private/Modules/ApplePayContract/ApplePayContractRouterIO.swift index 7761e5e7..a2370d59 100644 --- a/YooKassaPayments/Private/Modules/ApplePayContract/ApplePayContractRouterIO.swift +++ b/YooKassaPayments/Private/Modules/ApplePayContract/ApplePayContractRouterIO.swift @@ -1,16 +1,7 @@ protocol ApplePayContractRouterInput: AnyObject { func presentTermsOfServiceModule(_ url: URL) - - func presentSavePaymentMethodInfo( - inputData: SavePaymentMethodInfoModuleInputData - ) - - func presentApplePay( - inputData: ApplePayModuleInputData, - moduleOutput: ApplePayModuleOutput - ) - - func closeApplePay( - completion: (() -> Void)? - ) + func presentSafeDealInfo(title: String, body: String) + func presentSavePaymentMethodInfo(inputData: SavePaymentMethodInfoModuleInputData) + func presentApplePay(inputData: ApplePayModuleInputData, moduleOutput: ApplePayModuleOutput) + func closeApplePay(completion: (() -> Void)?) } diff --git a/YooKassaPayments/Private/Modules/ApplePayContract/ApplePayContractViewIO.swift b/YooKassaPayments/Private/Modules/ApplePayContract/ApplePayContractViewIO.swift index 01f31757..6a0fbfd7 100644 --- a/YooKassaPayments/Private/Modules/ApplePayContract/ApplePayContractViewIO.swift +++ b/YooKassaPayments/Private/Modules/ApplePayContract/ApplePayContractViewIO.swift @@ -14,8 +14,7 @@ protocol ApplePayContractViewOutput { func setupView() func didTapActionButton() func didTapTermsOfService(_ url: URL) + func didTapSafeDealInfo(_ url: URL) func didTapOnSavePaymentMethod() - func didChangeSavePaymentMethodState( - _ state: Bool - ) + func didChangeSavePaymentMethodState(_ state: Bool) } diff --git a/YooKassaPayments/Private/Modules/ApplePayContract/Assembly/ApplePayContractAssembly.swift b/YooKassaPayments/Private/Modules/ApplePayContract/Assembly/ApplePayContractAssembly.swift index edfa8595..a8534e8a 100644 --- a/YooKassaPayments/Private/Modules/ApplePayContract/Assembly/ApplePayContractAssembly.swift +++ b/YooKassaPayments/Private/Modules/ApplePayContract/Assembly/ApplePayContractAssembly.swift @@ -17,7 +17,8 @@ enum ApplePayContractAssembly { merchantIdentifier: inputData.merchantIdentifier, savePaymentMethodViewModel: inputData.savePaymentMethodViewModel, initialSavePaymentMethod: inputData.initialSavePaymentMethod, - isBackBarButtonHidden: inputData.isBackBarButtonHidden + isBackBarButtonHidden: inputData.isBackBarButtonHidden, + isSafeDeal: inputData.isSafeDeal ) let analyticsService = AnalyticsServiceAssembly.makeService( @@ -37,7 +38,8 @@ enum ApplePayContractAssembly { analyticsService: analyticsService, analyticsProvider: analyticsProvider, threatMetrixService: threatMetrixService, - clientApplicationKey: inputData.clientApplicationKey + clientApplicationKey: inputData.clientApplicationKey, + customerId: inputData.customerId ) let router = ApplePayContractRouter() diff --git a/YooKassaPayments/Private/Modules/ApplePayContract/Interactor/ApplePayContractInteractor.swift b/YooKassaPayments/Private/Modules/ApplePayContract/Interactor/ApplePayContractInteractor.swift index b51fd30b..feba372d 100644 --- a/YooKassaPayments/Private/Modules/ApplePayContract/Interactor/ApplePayContractInteractor.swift +++ b/YooKassaPayments/Private/Modules/ApplePayContract/Interactor/ApplePayContractInteractor.swift @@ -14,6 +14,7 @@ final class ApplePayContractInteractor { private let threatMetrixService: ThreatMetrixService private let clientApplicationKey: String + private let customerId: String? // MARK: - Init @@ -22,7 +23,8 @@ final class ApplePayContractInteractor { analyticsService: AnalyticsService, analyticsProvider: AnalyticsProvider, threatMetrixService: ThreatMetrixService, - clientApplicationKey: String + clientApplicationKey: String, + customerId: String? ) { self.paymentService = paymentService self.analyticsService = analyticsService @@ -30,6 +32,7 @@ final class ApplePayContractInteractor { self.threatMetrixService = threatMetrixService self.clientApplicationKey = clientApplicationKey + self.customerId = customerId } } @@ -94,6 +97,7 @@ extension ApplePayContractInteractor: ApplePayContractInteractorInput { savePaymentMethod: savePaymentMethod, amount: amount, tmxSessionId: tmxSessionId, + customerId: customerId, completion: completion ) } diff --git a/YooKassaPayments/Private/Modules/ApplePayContract/Presenter/ApplePayContractPresenter.swift b/YooKassaPayments/Private/Modules/ApplePayContract/Presenter/ApplePayContractPresenter.swift index 6e798057..220efaae 100644 --- a/YooKassaPayments/Private/Modules/ApplePayContract/Presenter/ApplePayContractPresenter.swift +++ b/YooKassaPayments/Private/Modules/ApplePayContract/Presenter/ApplePayContractPresenter.swift @@ -29,6 +29,7 @@ final class ApplePayContractPresenter: NSObject { private let savePaymentMethodViewModel: SavePaymentMethodViewModel? private var initialSavePaymentMethod: Bool private let isBackBarButtonHidden: Bool + private let isSafeDeal: Bool // MARK: - Init @@ -42,7 +43,8 @@ final class ApplePayContractPresenter: NSObject { merchantIdentifier: String?, savePaymentMethodViewModel: SavePaymentMethodViewModel?, initialSavePaymentMethod: Bool, - isBackBarButtonHidden: Bool + isBackBarButtonHidden: Bool, + isSafeDeal: Bool ) { self.shopName = shopName self.purchaseDescription = purchaseDescription @@ -54,6 +56,7 @@ final class ApplePayContractPresenter: NSObject { self.savePaymentMethodViewModel = savePaymentMethodViewModel self.initialSavePaymentMethod = initialSavePaymentMethod self.isBackBarButtonHidden = isBackBarButtonHidden + self.isSafeDeal = isSafeDeal } // MARK: - Stored properties @@ -73,7 +76,8 @@ extension ApplePayContractPresenter: ApplePayContractViewOutput { description: purchaseDescription, price: price, fee: fee, - terms: termsOfService + terms: termsOfService, + safeDealText: isSafeDeal ? PaymentMethodResources.Localized.safeDealInfoLink : nil ) view.setupViewModel(viewModel) @@ -106,6 +110,13 @@ extension ApplePayContractPresenter: ApplePayContractViewOutput { router.presentTermsOfServiceModule(url) } + func didTapSafeDealInfo(_ url: URL) { + router.presentSafeDealInfo( + title: PaymentMethodResources.Localized.safeDealInfoTitle, + body: PaymentMethodResources.Localized.safeDealInfoBody + ) + } + func didTapOnSavePaymentMethod() { let savePaymentMethodModuleinputData = SavePaymentMethodInfoModuleInputData( headerValue: SavePaymentMethodInfoLocalization.BankCard.header, @@ -116,15 +127,11 @@ extension ApplePayContractPresenter: ApplePayContractViewOutput { ) } - func didChangeSavePaymentMethodState( - _ state: Bool - ) { + func didChangeSavePaymentMethodState(_ state: Bool) { initialSavePaymentMethod = state } - private func trackScreenErrorAnalytics( - scheme: AnalyticsEvent.TokenizeScheme? - ) { + private func trackScreenErrorAnalytics(scheme: AnalyticsEvent.TokenizeScheme?) { DispatchQueue.global().async { [weak self] in guard let interactor = self?.interactor else { return } let (authType, _) = interactor.makeTypeAnalyticsParameters() diff --git a/YooKassaPayments/Private/Modules/ApplePayContract/Router/ApplePayContractRouter.swift b/YooKassaPayments/Private/Modules/ApplePayContract/Router/ApplePayContractRouter.swift index 7407c20c..b9f89e12 100644 --- a/YooKassaPayments/Private/Modules/ApplePayContract/Router/ApplePayContractRouter.swift +++ b/YooKassaPayments/Private/Modules/ApplePayContract/Router/ApplePayContractRouter.swift @@ -17,9 +17,11 @@ extension ApplePayContractRouter: ApplePayContractRouterInput { ) } - func presentSavePaymentMethodInfo( - inputData: SavePaymentMethodInfoModuleInputData - ) { + func presentSafeDealInfo(title: String, body: String) { + presentSavePaymentMethodInfo(inputData: .init(headerValue: title, bodyValue: body)) + } + + func presentSavePaymentMethodInfo(inputData: SavePaymentMethodInfoModuleInputData) { let viewController = SavePaymentMethodInfoAssembly.makeModule( inputData: inputData ) @@ -33,10 +35,7 @@ extension ApplePayContractRouter: ApplePayContractRouterInput { ) } - func presentApplePay( - inputData: ApplePayModuleInputData, - moduleOutput: ApplePayModuleOutput - ) { + func presentApplePay(inputData: ApplePayModuleInputData, moduleOutput: ApplePayModuleOutput) { if let viewController = ApplePayAssembly.makeModule( inputData: inputData, moduleOutput: moduleOutput @@ -52,9 +51,7 @@ extension ApplePayContractRouter: ApplePayContractRouterInput { } } - func closeApplePay( - completion: (() -> Void)? - ) { + func closeApplePay(completion: (() -> Void)?) { transitionHandler?.dismiss( animated: true, completion: completion diff --git a/YooKassaPayments/Private/Modules/ApplePayContract/View/ApplePayContractViewController.swift b/YooKassaPayments/Private/Modules/ApplePayContract/View/ApplePayContractViewController.swift index 12c0e04c..57468273 100644 --- a/YooKassaPayments/Private/Modules/ApplePayContract/View/ApplePayContractViewController.swift +++ b/YooKassaPayments/Private/Modules/ApplePayContract/View/ApplePayContractViewController.swift @@ -46,12 +46,13 @@ final class ApplePayContractViewController: UIViewController { $0.setStyles(UIView.Styles.grayBackground) $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical - $0.spacing = Space.double + $0.spacing = Space.single return $0 }(UIStackView()) private lazy var submitButton: Button = { $0.tintColor = CustomizationStorage.shared.mainScheme + $0.setStyles( UIButton.DynamicStyle.primary, UIView.Styles.heightAsContent @@ -65,15 +66,37 @@ final class ApplePayContractViewController: UIViewController { return $0 }(Button(type: .custom)) - private lazy var termsOfServiceLinkedTextView: LinkedTextView = { - $0.tintColor = CustomizationStorage.shared.mainScheme - $0.setStyles( - UIView.Styles.grayBackground, - UITextView.Styles.linked - ) - $0.delegate = self - return $0 - }(LinkedTextView()) + private lazy var submitButtonContainer: UIView = { + let view = UIView(frame: .zero) + view.translatesAutoresizingMaskIntoConstraints = false + 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), + view.trailingAnchor.constraint(equalTo: submitButton.trailingAnchor), + view.bottomAnchor.constraint(equalTo: submitButton.bottomAnchor, constant: Space.single), + defaultHeight, + ]) + + return view + }() + + private let termsOfServiceLinkedTextView: LinkedTextView = { + let view = LinkedTextView() + view.tintColor = CustomizationStorage.shared.mainScheme + view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) + return view + }() + + private let safeDealLinkedTextView: LinkedTextView = { + let view = LinkedTextView() + view.tintColor = CustomizationStorage.shared.mainScheme + view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) + return view + }() // MARK: - Switch save payment method UI Properties @@ -146,6 +169,10 @@ final class ApplePayContractViewController: UIViewController { view.setStyles(UIView.Styles.grayBackground) navigationItem.title = Localized.title + termsOfServiceLinkedTextView.delegate = self + safeDealLinkedTextView.delegate = self + safeDealLinkedTextView.isHidden = true + setupView() setupConstraints() } @@ -175,8 +202,9 @@ final class ApplePayContractViewController: UIViewController { ].forEach(contentStackView.addArrangedSubview) [ - submitButton, + submitButtonContainer, termsOfServiceLinkedTextView, + safeDealLinkedTextView, ].forEach(actionButtonStackView.addArrangedSubview) } @@ -262,9 +290,7 @@ final class ApplePayContractViewController: UIViewController { // MARK: - ApplePayContractViewInput extension ApplePayContractViewController: ApplePayContractViewInput { - func setupViewModel( - _ viewModel: ApplePayContractViewModel - ) { + func setupViewModel(_ viewModel: ApplePayContractViewModel) { orderView.title = viewModel.shopName orderView.subtitle = viewModel.description orderView.value = makePrice(viewModel.price) @@ -280,6 +306,12 @@ extension ApplePayContractViewController: ApplePayContractViewInput { foregroundColor: UIColor.AdaptiveColors.secondary ) termsOfServiceLinkedTextView.textAlignment = .center + + safeDealLinkedTextView.attributedText = viewModel.safeDealText + safeDealLinkedTextView.isHidden = viewModel.safeDealText?.string.isEmpty ?? true + + termsOfServiceLinkedTextView.textAlignment = .center + safeDealLinkedTextView.textAlignment = .center } func setSavePaymentMethodViewModel( @@ -391,6 +423,8 @@ extension ApplePayContractViewController: UITextViewDelegate { switch textView { case termsOfServiceLinkedTextView: output?.didTapTermsOfService(URL) + case safeDealLinkedTextView: + output?.didTapSafeDealInfo(URL) default: assertionFailure("Unsupported textView") } diff --git a/YooKassaPayments/Private/Modules/ApplePayContract/View/ViewModel/ApplePayContractViewModel.swift b/YooKassaPayments/Private/Modules/ApplePayContract/View/ViewModel/ApplePayContractViewModel.swift index 265a1f8f..95a70c68 100644 --- a/YooKassaPayments/Private/Modules/ApplePayContract/View/ViewModel/ApplePayContractViewModel.swift +++ b/YooKassaPayments/Private/Modules/ApplePayContract/View/ViewModel/ApplePayContractViewModel.swift @@ -4,4 +4,5 @@ struct ApplePayContractViewModel { let price: PriceViewModel let fee: PriceViewModel? let terms: TermsOfService + let safeDealText: NSAttributedString? } diff --git a/YooKassaPayments/Private/Modules/BankCard/Assembly/BankCardAssembly.swift b/YooKassaPayments/Private/Modules/BankCard/Assembly/BankCardAssembly.swift index 32dcb0bb..c3f82ca6 100644 --- a/YooKassaPayments/Private/Modules/BankCard/Assembly/BankCardAssembly.swift +++ b/YooKassaPayments/Private/Modules/BankCard/Assembly/BankCardAssembly.swift @@ -43,15 +43,20 @@ enum BankCardAssembly { inputData: BankCardModuleInputData ) -> BankCardPresenter { let presenter = BankCardPresenter( + cardService: CardService(), shopName: inputData.shopName, purchaseDescription: inputData.purchaseDescription, priceViewModel: inputData.priceViewModel, feeViewModel: inputData.feeViewModel, termsOfService: inputData.termsOfService, cardScanning: inputData.cardScanning, - savePaymentMethodViewModel: inputData.savePaymentMethodViewModel, - initialSavePaymentMethod: inputData.initialSavePaymentMethod, - isBackBarButtonHidden: inputData.isBackBarButtonHidden + isBackBarButtonHidden: inputData.isBackBarButtonHidden, + instrument: inputData.instrument, + canSaveInstrument: inputData.canSaveInstrument, + apiSavePaymentMethod: inputData.apiSavePaymentMethod, + clientSavePaymentMethod: inputData.savePaymentMethod, + paymentMethodViewModelFactory: PaymentMethodViewModelFactoryAssembly.makeFactory(), + isSafeDeal: inputData.isSafeDeal ) return presenter } @@ -71,6 +76,7 @@ enum BankCardAssembly { testModeSettings: inputData.testModeSettings ) let threatMetrixService = ThreatMetrixServiceFactory.makeService() + let interactor = BankCardInteractor( paymentService: paymentService, analyticsService: analyticsService, @@ -78,7 +84,8 @@ enum BankCardAssembly { threatMetrixService: threatMetrixService, clientApplicationKey: inputData.clientApplicationKey, amount: inputData.paymentOption.charge.plain, - returnUrl: inputData.returnUrl + returnUrl: inputData.returnUrl, + customerId: inputData.customerId ) return interactor } diff --git a/YooKassaPayments/Private/Modules/BankCard/BankCardInteractorIO.swift b/YooKassaPayments/Private/Modules/BankCard/BankCardInteractorIO.swift index fd6330db..fe8a4c5e 100644 --- a/YooKassaPayments/Private/Modules/BankCard/BankCardInteractorIO.swift +++ b/YooKassaPayments/Private/Modules/BankCard/BankCardInteractorIO.swift @@ -1,19 +1,10 @@ protocol BankCardInteractorInput: AnalyticsTrack { - func tokenizeBankCard( - cardData: CardData, - savePaymentMethod: Bool - ) - func makeTypeAnalyticsParameters() -> ( - authType: AnalyticsEvent.AuthType, - tokenType: AnalyticsEvent.AuthTokenType? - ) + func tokenizeInstrument(id: String, csc: String?, savePaymentMethod: Bool) + func tokenizeBankCard(cardData: CardData, savePaymentMethod: Bool, savePaymentInstrument: Bool?) + func makeTypeAnalyticsParameters() -> (authType: AnalyticsEvent.AuthType, tokenType: AnalyticsEvent.AuthTokenType?) } protocol BankCardInteractorOutput: AnyObject { - func didTokenize( - _ data: Tokens - ) - func didFailTokenize( - _ error: Error - ) + func didTokenize(_ data: Tokens) + func didFailTokenize(_ error: Error) } diff --git a/YooKassaPayments/Private/Modules/BankCard/BankCardModuleIO.swift b/YooKassaPayments/Private/Modules/BankCard/BankCardModuleIO.swift index 65daa992..f76225a1 100644 --- a/YooKassaPayments/Private/Modules/BankCard/BankCardModuleIO.swift +++ b/YooKassaPayments/Private/Modules/BankCard/BankCardModuleIO.swift @@ -5,7 +5,6 @@ struct BankCardModuleInputData { let testModeSettings: TestModeSettings? let isLoggingEnabled: Bool let tokenizationSettings: TokenizationSettings - let shopName: String let purchaseDescription: String let priceViewModel: PriceViewModel @@ -14,9 +13,13 @@ struct BankCardModuleInputData { let termsOfService: TermsOfService let cardScanning: CardScanning? let returnUrl: String - let savePaymentMethodViewModel: SavePaymentMethodViewModel? - let initialSavePaymentMethod: Bool + let savePaymentMethod: SavePaymentMethod + let canSaveInstrument: Bool + let apiSavePaymentMethod: YooKassaPaymentsApi.SavePaymentMethod let isBackBarButtonHidden: Bool + let customerId: String? + let instrument: PaymentInstrumentBankCard? + let isSafeDeal: Bool } protocol BankCardModuleOutput: AnyObject { diff --git a/YooKassaPayments/Private/Modules/BankCard/BankCardRouterIO.swift b/YooKassaPayments/Private/Modules/BankCard/BankCardRouterIO.swift index c00e6842..ca5e271b 100644 --- a/YooKassaPayments/Private/Modules/BankCard/BankCardRouterIO.swift +++ b/YooKassaPayments/Private/Modules/BankCard/BankCardRouterIO.swift @@ -1,8 +1,5 @@ protocol BankCardRouterInput: AnyObject { - func presentTermsOfServiceModule( - _ url: URL - ) - func presentSavePaymentMethodInfo( - inputData: SavePaymentMethodInfoModuleInputData - ) + func presentTermsOfServiceModule(_ url: URL) + func presentSafeDealInfo(title: String, body: String) + func presentSavePaymentMethodInfo(inputData: SavePaymentMethodInfoModuleInputData) } diff --git a/YooKassaPayments/Private/Modules/BankCard/BankCardViewIO.swift b/YooKassaPayments/Private/Modules/BankCard/BankCardViewIO.swift index e66179ab..f0f25b2d 100644 --- a/YooKassaPayments/Private/Modules/BankCard/BankCardViewIO.swift +++ b/YooKassaPayments/Private/Modules/BankCard/BankCardViewIO.swift @@ -1,31 +1,19 @@ import UIKit protocol BankCardViewInput: ActivityIndicatorPresenting, NotificationPresenting { - func setViewModel( - _ viewModel: BankCardViewModel - ) - func setSubmitButtonEnabled( - _ isEnabled: Bool - ) - func endEditing( - _ force: Bool - ) - func setSavePaymentMethodViewModel( - _ savePaymentMethodViewModel: SavePaymentMethodViewModel - ) - func setBackBarButtonHidden( - _ isHidden: Bool - ) + func setViewModel(_ viewModel: BankCardViewModel) + func setSubmitButtonEnabled(_ isEnabled: Bool) + func endEditing(_ force: Bool) + func setBackBarButtonHidden(_ isHidden: Bool) + func setCardState(_ state: MaskedCardView.CscState) } protocol BankCardViewOutput: AnyObject { func setupView() func didPressSubmitButton() - func didTapTermsOfService( - _ url: URL - ) + func didTapTermsOfService(_ url: URL) + func didTapSafeDealInfo(_ url: URL) func didTapOnSavePaymentMethod() - func didChangeSavePaymentMethodState( - _ state: Bool - ) + func didSetCsc(_ csc: String) + func endEditing() } diff --git a/YooKassaPayments/Private/Modules/BankCard/Interactor/BankCardInteractor.swift b/YooKassaPayments/Private/Modules/BankCard/Interactor/BankCardInteractor.swift index 4dbbfc77..558d6e99 100644 --- a/YooKassaPayments/Private/Modules/BankCard/Interactor/BankCardInteractor.swift +++ b/YooKassaPayments/Private/Modules/BankCard/Interactor/BankCardInteractor.swift @@ -15,6 +15,7 @@ final class BankCardInteractor { private let clientApplicationKey: String private let amount: MonetaryAmount private let returnUrl: String + private let customerId: String? init( paymentService: PaymentService, @@ -23,7 +24,8 @@ final class BankCardInteractor { threatMetrixService: ThreatMetrixService, clientApplicationKey: String, amount: MonetaryAmount, - returnUrl: String + returnUrl: String, + customerId: String? ) { self.paymentService = paymentService self.analyticsService = analyticsService @@ -32,19 +34,47 @@ final class BankCardInteractor { self.clientApplicationKey = clientApplicationKey self.amount = amount self.returnUrl = returnUrl + self.customerId = customerId } } // MARK: - BankCardInteractorInput extension BankCardInteractor: BankCardInteractorInput { - func tokenizeBankCard( - cardData: CardData, - savePaymentMethod: Bool - ) { + func tokenizeInstrument(id: String, csc: String?, savePaymentMethod: Bool) { + threatMetrixService.profileApp { [weak self] result in + guard let self = self, let output = self.output else { return } + switch result { + case .success(let tmxId): + self.paymentService.tokenizeCardInstrument( + clientApplicationKey: self.clientApplicationKey, + amount: self.amount, + tmxSessionId: tmxId.value, + confirmation: makeConfirmation(returnUrl: self.returnUrl), + savePaymentMethod: savePaymentMethod, + instrumentId: id, + csc: csc + ) { tokenizeResult in + switch tokenizeResult { + case .success(let tokens): + output.didTokenize(tokens) + case .failure(let error): + let mappedError = mapError(error) + output.didFailTokenize(mappedError) + } + } + case .failure(let error): + let mappedError = mapError(error) + output.didFailTokenize(mappedError) + } + } + } + func tokenizeBankCard(cardData: CardData, savePaymentMethod: Bool, savePaymentInstrument: 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): @@ -60,7 +90,9 @@ extension BankCardInteractor: BankCardInteractorInput { confirmation: confirmation, savePaymentMethod: savePaymentMethod, amount: self.amount, - tmxSessionId: tmxSessionId.value + tmxSessionId: tmxSessionId.value, + customerId: self.customerId, + savePaymentInstrument: savePaymentInstrument ) { result in switch result { case .success(let data): diff --git a/YooKassaPayments/Private/Modules/BankCard/Presenter/BankCardPresenter.swift b/YooKassaPayments/Private/Modules/BankCard/Presenter/BankCardPresenter.swift index 8b239e20..97906efe 100644 --- a/YooKassaPayments/Private/Modules/BankCard/Presenter/BankCardPresenter.swift +++ b/YooKassaPayments/Private/Modules/BankCard/Presenter/BankCardPresenter.swift @@ -1,5 +1,6 @@ import UIKit - +import YooKassaPaymentsApi +// swiftlint:disable cyclomatic_complexity final class BankCardPresenter { // MARK: - VIPER @@ -16,45 +17,57 @@ final class BankCardPresenter { // MARK: - Initialization + private let cardService: CardService private let shopName: String private let purchaseDescription: String private let priceViewModel: PriceViewModel private let feeViewModel: PriceViewModel? private let termsOfService: TermsOfService private let cardScanning: CardScanning? - private let savePaymentMethodViewModel: SavePaymentMethodViewModel? - private var initialSavePaymentMethod: Bool + private let clientSavePaymentMethod: SavePaymentMethod private let isBackBarButtonHidden: Bool + private let instrument: PaymentInstrumentBankCard? + private let canSaveInstrument: Bool + private let apiSavePaymentMethod: YooKassaPaymentsApi.SavePaymentMethod + private let paymentMethodViewModelFactory: PaymentMethodViewModelFactory + private let isSafeDeal: Bool init( + cardService: CardService, shopName: String, purchaseDescription: String, priceViewModel: PriceViewModel, feeViewModel: PriceViewModel?, termsOfService: TermsOfService, cardScanning: CardScanning?, - savePaymentMethodViewModel: SavePaymentMethodViewModel?, - initialSavePaymentMethod: Bool, - isBackBarButtonHidden: Bool + isBackBarButtonHidden: Bool, + instrument: PaymentInstrumentBankCard?, + canSaveInstrument: Bool, + apiSavePaymentMethod: YooKassaPaymentsApi.SavePaymentMethod, + clientSavePaymentMethod: SavePaymentMethod, + paymentMethodViewModelFactory: PaymentMethodViewModelFactory, + isSafeDeal: Bool ) { + self.cardService = cardService self.shopName = shopName self.purchaseDescription = purchaseDescription self.priceViewModel = priceViewModel self.feeViewModel = feeViewModel self.termsOfService = termsOfService self.cardScanning = cardScanning - self.savePaymentMethodViewModel = savePaymentMethodViewModel - self.initialSavePaymentMethod = initialSavePaymentMethod self.isBackBarButtonHidden = isBackBarButtonHidden + self.instrument = instrument + self.canSaveInstrument = canSaveInstrument + self.apiSavePaymentMethod = apiSavePaymentMethod + self.clientSavePaymentMethod = clientSavePaymentMethod + self.paymentMethodViewModelFactory = paymentMethodViewModelFactory + self.isSafeDeal = isSafeDeal } // MARK: - Stored properties - private var cardData = CardData( - pan: nil, - expiryDate: nil, - csc: nil - ) + private var cardData = CardData(pan: nil, expiryDate: nil, csc: nil) + private var saveInstrument: Bool? } // MARK: - BankCardViewOutput @@ -74,32 +87,94 @@ extension BankCardPresenter: BankCardViewOutput { font: UIFont.dynamicCaption2, foregroundColor: UIColor.AdaptiveColors.secondary ) + + let maskedNumber = instrument + .map { ($0.first6 ?? "") + "******" + $0.last4 } + .map(paymentMethodViewModelFactory.replaceBullets) + ?? paymentMethodViewModelFactory.replaceBullets("******") + + let logo: UIImage + let cscState: MaskedCardView.CscState + if let instrument = instrument, let first6 = instrument.first6 { + logo = paymentMethodViewModelFactory + .makeBankCardImage(first6Digits: first6, bankCardType: instrument.cardType) + + cscState = instrument.cscRequired ? .default : .noCVC + } else { + logo = PaymentMethodResources.Image.bankCard + cscState = .default + } + + let section: PaymentRecurrencyAndDataSavingSection? + if instrument != nil { + switch clientSavePaymentMethod { + case .on: + section = PaymentRecurrencyAndDataSavingSectionFactory.make( + mode: .requiredRecurring, + output: self + ) + case .userSelects: + section = PaymentRecurrencyAndDataSavingSectionFactory.make( + mode: .allowRecurring, + output: self + ) + case .off: + section = nil + } + } else { + section = PaymentRecurrencyAndDataSavingSectionFactory.make( + clientSavePaymentMethod: clientSavePaymentMethod, + apiSavePaymentMethod: apiSavePaymentMethod, + canSavePaymentInstrument: canSaveInstrument, + output: self + ) + } + let viewModel = BankCardViewModel( shopName: shopName, description: purchaseDescription, priceValue: priceValue, feeValue: feeValue, - termsOfService: termsOfServiceValue + termsOfService: termsOfServiceValue, + instrumentMode: instrument != nil, + maskedNumber: maskedNumber.splitEvery(4, separator: " "), + cardLogo: logo, + safeDealText: isSafeDeal ? PaymentMethodResources.Localized.safeDealInfoLink : nil, + recurrencyAndDataSavingSection: section ) - view.setViewModel(viewModel) - view.setSubmitButtonEnabled(false) - if let savePaymentMethodViewModel = savePaymentMethodViewModel { - view.setSavePaymentMethodViewModel( - savePaymentMethodViewModel - ) + if let section = section { + saveInstrument = section.switchValue + switch section.mode { + case .requiredSaveData, .requiredRecurringAndSaveData: + saveInstrument = true + case .requiredRecurring: + saveInstrument = false + default: + break + } } + view.setViewModel(viewModel) + view.setSubmitButtonEnabled(cscState == .noCVC) + view.setCardState(cscState) + view.setBackBarButtonHidden(isBackBarButtonHidden) DispatchQueue.global().async { [weak self] in guard let self = self else { return } let parameters = self.interactor.makeTypeAnalyticsParameters() - let event: AnalyticsEvent = .screenBankCardForm( + let form: AnalyticsEvent = .screenBankCardForm( authType: parameters.authType, sdkVersion: Bundle.frameworkVersion ) - self.interactor.trackEvent(event) + self.interactor.trackEvent(form) + let contract = AnalyticsEvent.screenPaymentContract( + authType: parameters.authType, + scheme: .bankCard, + sdkVersion: Bundle.frameworkVersion + ) + self.interactor.trackEvent(contract) } } @@ -108,22 +183,50 @@ extension BankCardPresenter: BankCardViewOutput { view.showActivity() view.endEditing(true) + let saveMethod: Bool + switch (clientSavePaymentMethod, apiSavePaymentMethod) { + case (.off, .allowed), (.off, .forbidden), (.on, .forbidden), (.userSelects, .forbidden): + saveMethod = false + case (.on, .allowed): + saveMethod = true + case (.userSelects, .allowed): + saveMethod = saveInstrument ?? false + case (_, .unknown(_)): + saveMethod = false + default: + saveMethod = false + } + let savePaymentInstrument = canSaveInstrument ? saveInstrument : false + DispatchQueue.global().async { [weak self] in - guard let self = self, - let interactor = self.interactor else { return } - interactor.tokenizeBankCard( - cardData: self.cardData, - savePaymentMethod: self.initialSavePaymentMethod - ) + guard let self = self, let interactor = self.interactor else { return } + if let instrument = self.instrument { + interactor.tokenizeInstrument( + id: instrument.paymentInstrumentId, + csc: self.cardData.csc, + savePaymentMethod: saveMethod + ) + } else { + interactor.tokenizeBankCard( + cardData: self.cardData, + savePaymentMethod: saveMethod, + savePaymentInstrument: savePaymentInstrument + ) + } } } - func didTapTermsOfService( - _ url: URL - ) { + func didTapTermsOfService(_ url: URL) { router.presentTermsOfServiceModule(url) } + func didTapSafeDealInfo(_ url: URL) { + router.presentSafeDealInfo( + title: PaymentMethodResources.Localized.safeDealInfoTitle, + body: PaymentMethodResources.Localized.safeDealInfoBody + ) + } + func didTapOnSavePaymentMethod() { let savePaymentMethodModuleInputData = SavePaymentMethodInfoModuleInputData( headerValue: SavePaymentMethodInfoLocalization.BankCard.header, @@ -134,10 +237,52 @@ extension BankCardPresenter: BankCardViewOutput { ) } - func didChangeSavePaymentMethodState( - _ state: Bool - ) { - initialSavePaymentMethod = state + func didSetCsc(_ csc: String) { + DispatchQueue.global(qos: .userInteractive).async { [weak self] in + guard let self = self else { return } + self.cardData.csc = csc + do { + try self.cardService.validate(csc: csc) + } catch { + if error is CardService.ValidationError { + DispatchQueue.main.async { [weak self] in + guard let view = self?.view else { return } + view.setSubmitButtonEnabled(false) + } + return + } + } + DispatchQueue.main.async { [weak self] in + guard let view = self?.view else { return } + view.setSubmitButtonEnabled(true) + } + } + } + + func endEditing() { + guard let csc = cardData.csc else { + view?.setCardState(.error) + return + } + + DispatchQueue.global(qos: .userInteractive).async { [weak self] in + guard let self = self else { return } + do { + try self.cardService.validate(csc: csc) + } catch { + if error is CardService.ValidationError { + DispatchQueue.main.async { [weak self] in + guard let view = self?.view else { return } + view.setCardState(.error) + } + return + } + } + DispatchQueue.main.async { [weak self] in + guard let view = self?.view else { return } + view.setCardState(.default) + } + } } } @@ -155,11 +300,18 @@ extension BankCardPresenter: BankCardInteractorOutput { paymentMethodType: .bankCard ) + let scheme: AnalyticsEvent.TokenizeScheme + if let instrument = self.instrument { + scheme = instrument.cscRequired ? .customerIdLinkedCardCvc : .customerIdLinkedCard + } else { + scheme = .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: .bankCard, + scheme: scheme, authType: type.authType, tokenType: type.tokenType, sdkVersion: Bundle.frameworkVersion @@ -225,6 +377,35 @@ extension BankCardPresenter: BankCardDataInputModuleOutput { } } +// MARK: - PaymentRecurrencyAndDataSavingSectionOutput + +extension BankCardPresenter: PaymentRecurrencyAndDataSavingSectionOutput { + func didChangeSwitchValue(newValue: Bool, mode: PaymentRecurrencyAndDataSavingSection.Mode) { + saveInstrument = newValue + } + func didTapInfoLink(mode: PaymentRecurrencyAndDataSavingSection.Mode) { + switch mode { + case .allowRecurring, .requiredRecurring: + router.presentSafeDealInfo( + title: CommonLocalized.CardSettingsDetails.autopayInfoTitle, + body: CommonLocalized.CardSettingsDetails.autopayInfoDetails + ) + case .savePaymentData, .requiredSaveData: + router.presentSafeDealInfo( + title: CommonLocalized.RecurrencyAndSavePaymentData.saveDataInfoTitle, + body: CommonLocalized.RecurrencyAndSavePaymentData.saveDataInfoMessage + ) + case .allowRecurringAndSaveData, .requiredRecurringAndSaveData: + router.presentSafeDealInfo( + title: CommonLocalized.RecurrencyAndSavePaymentData.saveDataAndAutopaymentsInfoTitle, + body: CommonLocalized.RecurrencyAndSavePaymentData.saveDataAndAutopaymentsInfoMessage + ) + default: + break + } + } +} + // MARK: - Private global helpers private func makeMessage( diff --git a/YooKassaPayments/Private/Modules/BankCard/Router/BankCardRouter.swift b/YooKassaPayments/Private/Modules/BankCard/Router/BankCardRouter.swift index d10e9360..6b99b54f 100644 --- a/YooKassaPayments/Private/Modules/BankCard/Router/BankCardRouter.swift +++ b/YooKassaPayments/Private/Modules/BankCard/Router/BankCardRouter.swift @@ -7,9 +7,7 @@ final class BankCardRouter { // MARK: - BankCardRouterInput extension BankCardRouter: BankCardRouterInput { - func presentTermsOfServiceModule( - _ url: URL - ) { + func presentTermsOfServiceModule(_ url: URL) { let viewController = SFSafariViewController(url: url) viewController.modalPresentationStyle = .overFullScreen transitionHandler?.present( @@ -19,9 +17,11 @@ extension BankCardRouter: BankCardRouterInput { ) } - func presentSavePaymentMethodInfo( - inputData: SavePaymentMethodInfoModuleInputData - ) { + func presentSafeDealInfo(title: String, body: String) { + presentSavePaymentMethodInfo(inputData: .init(headerValue: title, bodyValue: body)) + } + + func presentSavePaymentMethodInfo(inputData: SavePaymentMethodInfoModuleInputData) { let viewController = SavePaymentMethodInfoAssembly.makeModule( inputData: inputData ) diff --git a/YooKassaPayments/Private/Modules/BankCard/View/BankCardViewController.swift b/YooKassaPayments/Private/Modules/BankCard/View/BankCardViewController.swift index 4dc81e10..93b6d317 100644 --- a/YooKassaPayments/Private/Modules/BankCard/View/BankCardViewController.swift +++ b/YooKassaPayments/Private/Modules/BankCard/View/BankCardViewController.swift @@ -6,6 +6,15 @@ final class BankCardViewController: UIViewController { var output: BankCardViewOutput! + private var cachedCvc = "" + + private lazy var cvcTextInputPresenter: InputPresenter = { + let cvcTextStyle = CscInputPresenterStyle() + let cvcTextInputPresenter = InputPresenter(textInputStyle: cvcTextStyle) + cvcTextInputPresenter.output = maskedCardView.cardCodeTextView + return cvcTextInputPresenter + }() + // MARK: - Touches, Presses, and Gestures private lazy var viewTapGestureRecognizer: UITapGestureRecognizer = { @@ -20,6 +29,37 @@ final class BankCardViewController: UIViewController { var bankCardDataInputView: BankCardDataInputView! + private lazy var maskedCardView: MaskedCardView = { + let view = MaskedCardView(frame: .zero) + view.translatesAutoresizingMaskIntoConstraints = false + view.tintColor = CustomizationStorage.shared.mainScheme + view.setStyles(UIView.Styles.grayBackground, UIView.Styles.roundedShadow) + view.hintCardCode = CommonLocalized.BankCardView.inputCvcHint + view.hintCardNumber = CommonLocalized.BankCardView.inputPanHint + view.cardCodePlaceholder = CommonLocalized.BankCardView.inputCvcPlaceholder + view.delegate = self + return view + }() + + private lazy var errorCscView: UIView = { + let view = UIView(frame: .zero) + view.setStyles(UIView.Styles.grayBackground) + return view + }() + + private lazy var errorCscLabel: UILabel = { + let view = UILabel(frame: .zero) + view.isHidden = true + view.translatesAutoresizingMaskIntoConstraints = false + view.text = CommonLocalized.BankCardView.BottomHint.invalidCvc + view.setStyles( + UIView.Styles.grayBackground, + UILabel.DynamicStyle.caption1, + UILabel.ColorStyle.alert + ) + return view + }() + private lazy var scrollView: UIScrollView = { $0.setStyles(UIView.Styles.grayBackground) $0.translatesAutoresizingMaskIntoConstraints = false @@ -49,7 +89,7 @@ final class BankCardViewController: UIViewController { $0.setStyles(UIView.Styles.grayBackground) $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical - $0.spacing = Space.double + $0.spacing = Space.single return $0 }(UIStackView()) @@ -68,71 +108,37 @@ final class BankCardViewController: UIViewController { return $0 }(Button(type: .custom)) - private lazy var termsOfServiceLinkedTextView: LinkedTextView = { - $0.tintColor = CustomizationStorage.shared.mainScheme - $0.setStyles( - UIView.Styles.grayBackground, - UITextView.Styles.linked - ) - $0.delegate = self - return $0 - }(LinkedTextView()) - - // MARK: - Switch save payment method UI Properties - - private lazy var savePaymentMethodSwitchItemView: SwitchItemView = { - $0.tintColor = CustomizationStorage.shared.mainScheme - $0.layoutMargins = UIEdgeInsets( - top: Space.double, - left: Space.double, - bottom: Space.double, - right: Space.double - ) - $0.setStyles(SwitchItemView.Styles.primary) - $0.title = Localized.savePaymentMethodTitle - $0.delegate = self - return $0 - }(SwitchItemView()) - - private lazy var savePaymentMethodSwitchLinkedItemView: LinkedItemView = { - $0.tintColor = CustomizationStorage.shared.mainScheme - $0.layoutMargins = UIEdgeInsets( - top: Space.single / 2, - left: Space.double, - bottom: Space.double, - right: Space.double - ) - $0.setStyles(LinkedItemView.Styles.linked) - $0.delegate = self - return $0 - }(LinkedItemView()) - - // MARK: - Strict save payment method UI Properties + private lazy var submitButtonContainer: UIView = { + let view = UIView(frame: .zero) + view.translatesAutoresizingMaskIntoConstraints = false + 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), + view.trailingAnchor.constraint(equalTo: submitButton.trailingAnchor), + view.bottomAnchor.constraint(equalTo: submitButton.bottomAnchor, constant: Space.single), + defaultHeight, + ]) + + return view + }() - private lazy var savePaymentMethodStrictSectionHeaderView: SectionHeaderView = { - $0.layoutMargins = UIEdgeInsets( - top: Space.double, - left: Space.double, - bottom: 0, - right: Space.double - ) - $0.title = Localized.savePaymentMethodTitle - $0.setStyles(SectionHeaderView.Styles.primary) - return $0 - }(SectionHeaderView()) + private let termsOfServiceLinkedTextView: LinkedTextView = { + let view = LinkedTextView() + view.tintColor = CustomizationStorage.shared.mainScheme + view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) + return view + }() - private lazy var savePaymentMethodStrictLinkedItemView: LinkedItemView = { - $0.tintColor = CustomizationStorage.shared.mainScheme - $0.layoutMargins = UIEdgeInsets( - top: Space.single / 4, - left: Space.double, - bottom: Space.double, - right: Space.double - ) - $0.setStyles(LinkedItemView.Styles.linked) - $0.delegate = self - return $0 - }(LinkedItemView()) + private let safeDealLinkedTextView: LinkedTextView = { + let view = LinkedTextView() + view.tintColor = CustomizationStorage.shared.mainScheme + view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) + return view + }() // MARK: - Constraints @@ -151,6 +157,10 @@ final class BankCardViewController: UIViewController { navigationItem.title = Localized.title + termsOfServiceLinkedTextView.delegate = self + safeDealLinkedTextView.delegate = self + safeDealLinkedTextView.isHidden = true + setupView() setupConstraints() } @@ -163,26 +173,29 @@ final class BankCardViewController: UIViewController { // MARK: - SetupView private func setupView() { - [ - scrollView, - actionButtonStackView, - ].forEach(view.addSubview) + errorCscView.addSubview(errorCscLabel) + + [scrollView, actionButtonStackView].forEach(view.addSubview) scrollView.addSubview(contentView) + [contentStackView].forEach(contentView.addSubview) - [ - contentStackView, - ].forEach(contentView.addSubview) [ orderView, bankCardDataInputView, + maskedCardView, + errorCscView, ].forEach(contentStackView.addArrangedSubview) + if #available(iOS 11, *) { + contentStackView.setCustomSpacing(Space.double, after: maskedCardView) + } + [ - submitButton, + submitButtonContainer, termsOfServiceLinkedTextView, + safeDealLinkedTextView, ].forEach(actionButtonStackView.addArrangedSubview) - } private func setupConstraints() { @@ -238,9 +251,15 @@ final class BankCardViewController: UIViewController { contentStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), contentStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - bankCardDataInputView.heightAnchor.constraint( - lessThanOrEqualToConstant: 126 - ), + bankCardDataInputView.heightAnchor.constraint(lessThanOrEqualToConstant: 126), + + maskedCardView.leadingAnchor.constraint(equalTo: contentStackView.leadingAnchor, constant: Space.double), + contentStackView.trailingAnchor.constraint(equalTo: maskedCardView.trailingAnchor, constant: Space.double), + + errorCscLabel.topAnchor.constraint(equalTo: errorCscView.topAnchor), + errorCscLabel.bottomAnchor.constraint(equalTo: errorCscView.bottomAnchor), + errorCscLabel.leadingAnchor.constraint(equalTo: errorCscView.leadingAnchor, constant: Space.double), + errorCscView.trailingAnchor.constraint(equalTo: errorCscLabel.trailingAnchor, constant: Space.double), ] NSLayoutConstraint.activate(constraints) } @@ -262,15 +281,36 @@ final class BankCardViewController: UIViewController { // MARK: - BankCardViewInput extension BankCardViewController: BankCardViewInput { - func setViewModel( - _ viewModel: BankCardViewModel - ) { + func setViewModel(_ viewModel: BankCardViewModel) { orderView.title = viewModel.shopName orderView.subtitle = viewModel.description orderView.value = viewModel.priceValue orderView.subvalue = viewModel.feeValue termsOfServiceLinkedTextView.attributedText = viewModel.termsOfService + safeDealLinkedTextView.isHidden = viewModel.safeDealText?.string.isEmpty ?? true + safeDealLinkedTextView.attributedText = viewModel.safeDealText termsOfServiceLinkedTextView.textAlignment = .center + safeDealLinkedTextView.textAlignment = .center + + if viewModel.instrumentMode { + bankCardDataInputView.isHidden = true + maskedCardView.isHidden = false + errorCscView.isHidden = false + } else { + bankCardDataInputView.isHidden = false + maskedCardView.isHidden = true + errorCscView.isHidden = true + } + + maskedCardView.cardNumber = viewModel.maskedNumber + maskedCardView.cardLogo = viewModel.cardLogo + + if + let view = viewModel.recurrencyAndDataSavingSection, + let index = contentStackView.arrangedSubviews.firstIndex(of: maskedCardView) + { + contentStackView.insertArrangedSubview(view, at: contentStackView.arrangedSubviews.index(after: index)) + } } func setSubmitButtonEnabled( @@ -285,66 +325,9 @@ extension BankCardViewController: BankCardViewInput { view.endEditing(force) } - func setSavePaymentMethodViewModel( - _ savePaymentMethodViewModel: SavePaymentMethodViewModel - ) { - switch savePaymentMethodViewModel { - case .switcher(let viewModel): - savePaymentMethodSwitchItemView.state = viewModel.state - savePaymentMethodSwitchLinkedItemView.attributedString = makeSavePaymentMethodAttributedString( - text: viewModel.text, - hyperText: viewModel.hyperText, - font: UIFont.dynamicCaption1, - foregroundColor: UIColor.AdaptiveColors.secondary - ) - [ - savePaymentMethodSwitchItemView, - savePaymentMethodSwitchLinkedItemView, - ].forEach(contentStackView.addArrangedSubview) - - case .strict(let viewModel): - savePaymentMethodStrictLinkedItemView.attributedString = makeSavePaymentMethodAttributedString( - text: viewModel.text, - hyperText: viewModel.hyperText, - font: UIFont.dynamicCaption1, - foregroundColor: UIColor.AdaptiveColors.secondary - ) - [ - savePaymentMethodStrictSectionHeaderView, - savePaymentMethodStrictLinkedItemView, - ].forEach(contentStackView.addArrangedSubview) - } - } - - func setBackBarButtonHidden( - _ isHidden: Bool - ) { + func setBackBarButtonHidden(_ isHidden: Bool) { navigationItem.hidesBackButton = isHidden } - - private func makeSavePaymentMethodAttributedString( - text: String, - hyperText: String, - font: UIFont, - foregroundColor: UIColor - ) -> NSAttributedString { - let attributedText: NSMutableAttributedString - let attributes: [NSAttributedString.Key: Any] = [ - .font: font, - .foregroundColor: foregroundColor, - ] - attributedText = NSMutableAttributedString(string: "\(text) ", attributes: attributes) - - let linkAttributedText = NSMutableAttributedString(string: hyperText, attributes: attributes) - let linkRange = NSRange(location: 0, length: hyperText.count) - // swiftlint:disable force_unwrapping - let fakeLink = URL(string: "https://yookassa.ru")! - // swiftlint:enable force_unwrapping - linkAttributedText.addAttribute(.link, value: fakeLink, range: linkRange) - attributedText.append(linkAttributedText) - - return attributedText - } } // MARK: - ActivityIndicatorFullViewPresenting @@ -385,6 +368,8 @@ extension BankCardViewController: UITextViewDelegate { switch textView { case termsOfServiceLinkedTextView: output?.didTapTermsOfService(URL) + case safeDealLinkedTextView: + output?.didTapSafeDealInfo(URL) default: assertionFailure("Unsupported textView") } @@ -392,36 +377,6 @@ extension BankCardViewController: UITextViewDelegate { } } -// MARK: - LinkedItemViewOutput - -extension BankCardViewController: LinkedItemViewOutput { - func didTapOnLinkedView(on itemView: LinkedItemViewInput) { - switch itemView { - case _ where itemView === savePaymentMethodSwitchLinkedItemView, - _ where itemView === savePaymentMethodStrictLinkedItemView: - output?.didTapOnSavePaymentMethod() - default: - assertionFailure("Unsupported itemView") - } - } -} - -// MARK: - SwitchItemViewOutput - -extension BankCardViewController: SwitchItemViewOutput { - func switchItemView( - _ itemView: SwitchItemViewInput, - didChangeState state: Bool - ) { - switch itemView { - case _ where itemView === savePaymentMethodSwitchItemView: - output?.didChangeSavePaymentMethodState(state) - default: - assertionFailure("Unsupported itemView") - } - } -} - // MARK: - Actions @objc @@ -442,6 +397,46 @@ private extension BankCardViewController { } } +// MARK: - MaskedCardViewDelegate + +extension BankCardViewController: MaskedCardViewDelegate { + func textField( + _ textField: UITextField, + shouldChangeCharactersIn range: NSRange, + replacementString string: String + ) -> Bool { + let replacementText = cachedCvc.count < cvcTextInputPresenter.style.maximalLength + ? string + : "" + let cvc = (cachedCvc as NSString).replacingCharacters(in: range, with: replacementText) + cachedCvc = cvcTextInputPresenter.style.removedFormatting(from: cvc) + cvcTextInputPresenter.input( + changeCharactersIn: range, + replacementString: string, + currentString: textField.text ?? "" + ) + output.didSetCsc(cachedCvc) + return false + } + + func textFieldDidBeginEditing( + _ textField: UITextField + ) { + setCardState(.selected) + } + + func textFieldDidEndEditing( + _ textField: UITextField + ) { + output?.endEditing() + } + + func setCardState(_ state: MaskedCardView.CscState) { + maskedCardView.cscState = state + errorCscLabel.isHidden = state != .error + } +} + // MARK: - Localized private extension BankCardViewController { diff --git a/YooKassaPayments/Private/Modules/BankCard/View/PaymentRecurrencyAndDataSavingSection.swift b/YooKassaPayments/Private/Modules/BankCard/View/PaymentRecurrencyAndDataSavingSection.swift new file mode 100644 index 00000000..1ac71f02 --- /dev/null +++ b/YooKassaPayments/Private/Modules/BankCard/View/PaymentRecurrencyAndDataSavingSection.swift @@ -0,0 +1,145 @@ +import UIKit + +protocol PaymentRecurrencyAndDataSavingSectionOutput { + func didChangeSwitchValue(newValue: Bool, mode: PaymentRecurrencyAndDataSavingSection.Mode) + func didTapInfoLink(mode: PaymentRecurrencyAndDataSavingSection.Mode) +} + +class PaymentRecurrencyAndDataSavingSection: UIView, SwitchItemViewOutput, LinkedItemViewOutput { + enum Mode { + case empty + case savePaymentData + case allowRecurring + case allowRecurringAndSaveData + case requiredRecurringAndSaveData + case requiredRecurring + case requiredSaveData + } + + let mode: Mode + var output: PaymentRecurrencyAndDataSavingSectionOutput? + + private let switchSection = SwitchItemView() + private let headerSection = SectionHeaderView() + private let linkSection = LinkedItemView() + private let innerContainer = UIStackView() + + var switchValue: Bool { switchSection.state } + + init(mode: Mode, frame: CGRect = .zero) { + self.mode = mode + super.init(frame: frame) + + accessibilityIdentifier = "PaymentRecurrencyAndDataSavingSection" + + innerContainer.axis = .vertical + innerContainer.translatesAutoresizingMaskIntoConstraints = false + + addSubview(innerContainer) + + NSLayoutConstraint.activate([ + innerContainer.topAnchor.constraint(equalTo: topAnchor), + innerContainer.leadingAnchor.constraint(equalTo: leadingAnchor), + innerContainer.trailingAnchor.constraint(equalTo: trailingAnchor), + innerContainer.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + + switchSection.setStyles(SwitchItemView.Styles.primary) + switchSection.layoutMargins = .init( + top: Space.double, left: Space.double, bottom: Space.double, right: Space.double + ) + headerSection.setStyles(SectionHeaderView.Styles.primary) + headerSection.layoutMargins = .init( + top: Space.double, left: Space.double, bottom: 0, right: Space.double + ) + linkSection.setStyles(LinkedItemView.Styles.linked) + linkSection.layoutMargins = .init( + top: Space.single / 4, left: Space.double, bottom: Space.double, right: Space.double + ) + + [switchSection, headerSection, linkSection].forEach { view in + view.tintColor = CustomizationStorage.shared.mainScheme + innerContainer.addArrangedSubview(view) + } + + linkSection.delegate = self + switchSection.delegate = self + + update() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func switchItemView(_ itemView: SwitchItemViewInput, didChangeState state: Bool) { + output?.didChangeSwitchValue(newValue: state, mode: mode) + } + + func didTapOnLinkedView(on itemView: LinkedItemViewInput) { + output?.didTapInfoLink(mode: mode) + } + + private func update() { + typealias HeaderText = CommonLocalized.RecurrencyAndSavePaymentData.Header + typealias LinkText = CommonLocalized.RecurrencyAndSavePaymentData.Link + switch mode { + case .empty: + innerContainer.arrangedSubviews.forEach { $0.isHidden = true } + case .savePaymentData: + headerSection.isHidden = true + switchSection.title = HeaderText.optionalSaveDataHeader + switchSection.state = true + linkSection.attributedString = makeLink( + text: LinkText.Optional.saveDataLink, + interactive: LinkText.Optional.saveDataLinkInteractive + ) + case .allowRecurring: + headerSection.isHidden = true + switchSection.title = HeaderText.optionalAutopaymentsHeader + linkSection.attributedString = makeLink( + text: LinkText.Optional.autopaymentsLink, + interactive: LinkText.Optional.autopaymentsInteractive + ) + case .allowRecurringAndSaveData: + headerSection.isHidden = true + switchSection.title = HeaderText.optionalSaveDataAndAutopaymentsHeader + linkSection.attributedString = makeLink( + text: LinkText.Optional.autopaymentsAndSaveDataLink, + interactive: LinkText.Optional.autopaymentsAndSaveDataInteractive + ) + case .requiredRecurringAndSaveData: + switchSection.isHidden = true + headerSection.title = HeaderText.requiredSaveDataAndAutopaymentsHeader + linkSection.attributedString = makeLink( + text: LinkText.Required.autopaymentsAndSaveDataLink, + interactive: LinkText.Required.autopaymentsAndSaveDataInteractive + ) + case .requiredRecurring: + switchSection.isHidden = true + headerSection.title = HeaderText.requiredAutopaymentsHeader + linkSection.attributedString = makeLink( + text: LinkText.Required.autopaymentsLink, + interactive: LinkText.Required.autopaymentsInteractive + ) + case .requiredSaveData: + switchSection.isHidden = true + headerSection.title = HeaderText.requiredSaveDataHeader + linkSection.attributedString = makeLink( + text: LinkText.Required.saveDataLink, + interactive: LinkText.Required.saveDataLinkInteractive + ) + } + } + + 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 new file mode 100644 index 00000000..cc052dfb --- /dev/null +++ b/YooKassaPayments/Private/Modules/BankCard/View/PaymentRecurrencyAndDataSavingSectionFactory.swift @@ -0,0 +1,50 @@ +import Foundation +import enum YooKassaPaymentsApi.SavePaymentMethod + +enum PaymentRecurrencyAndDataSavingSectionFactory { + static func make( + clientSavePaymentMethod: SavePaymentMethod, + apiSavePaymentMethod: YooKassaPaymentsApi.SavePaymentMethod, + canSavePaymentInstrument: Bool, + output: PaymentRecurrencyAndDataSavingSectionOutput + ) -> PaymentRecurrencyAndDataSavingSection? { + let view: PaymentRecurrencyAndDataSavingSection? + switch (clientSavePaymentMethod, apiSavePaymentMethod, canSavePaymentInstrument) { + case (.on, .forbidden, false), (.off, .forbidden, false), (.userSelects, .forbidden, false), + (.off, .allowed, false): + view = nil + + case (.off, .forbidden, true), (.off, .allowed, true), (.userSelects, .forbidden, true): + view = PaymentRecurrencyAndDataSavingSection(mode: .savePaymentData) + + case (.userSelects, .allowed, false): + view = PaymentRecurrencyAndDataSavingSection(mode: .allowRecurring) + + case (.userSelects, .allowed, true): + view = PaymentRecurrencyAndDataSavingSection(mode: .allowRecurringAndSaveData) + + case (.on, .allowed, true): + view = PaymentRecurrencyAndDataSavingSection(mode: .requiredRecurringAndSaveData) + + case (.on, .allowed, false): + view = PaymentRecurrencyAndDataSavingSection(mode: .requiredRecurring) + + case (.on, .forbidden, true): + view = PaymentRecurrencyAndDataSavingSection(mode: .savePaymentData) + + default: + view = nil + } + view?.output = output + return view + } + + static func make( + mode: PaymentRecurrencyAndDataSavingSection.Mode, + output: PaymentRecurrencyAndDataSavingSectionOutput + ) -> PaymentRecurrencyAndDataSavingSection { + let view = PaymentRecurrencyAndDataSavingSection(mode: mode) + 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 97034992..f8af8749 100644 --- a/YooKassaPayments/Private/Modules/BankCard/View/ViewModel/BankCardViewModel.swift +++ b/YooKassaPayments/Private/Modules/BankCard/View/ViewModel/BankCardViewModel.swift @@ -1,7 +1,14 @@ +import UIKit + struct BankCardViewModel { let shopName: String let description: String? let priceValue: String let feeValue: String? let termsOfService: NSAttributedString + let instrumentMode: Bool + let maskedNumber: String + let cardLogo: UIImage + let safeDealText: NSAttributedString? + let recurrencyAndDataSavingSection: UIView? } diff --git a/YooKassaPayments/Private/Modules/BankCardRepeat/Assembly/BankCardRepeatAssembly.swift b/YooKassaPayments/Private/Modules/BankCardRepeat/Assembly/BankCardRepeatAssembly.swift index ecc326a1..9d54f850 100644 --- a/YooKassaPayments/Private/Modules/BankCardRepeat/Assembly/BankCardRepeatAssembly.swift +++ b/YooKassaPayments/Private/Modules/BankCardRepeat/Assembly/BankCardRepeatAssembly.swift @@ -30,7 +30,8 @@ enum BankCardRepeatAssembly { purchaseDescription: inputData.purchaseDescription, termsOfService: termsOfService, savePaymentMethodViewModel: savePaymentMethodViewModel, - initialSavePaymentMethod: initialSavePaymentMethod + initialSavePaymentMethod: initialSavePaymentMethod, + isSafeDeal: inputData.isSafeDeal ) let analyticsService = AnalyticsServiceAssembly.makeService( @@ -54,7 +55,8 @@ enum BankCardRepeatAssembly { amountNumberFormatter: amountNumberFormatter, clientApplicationKey: inputData.clientApplicationKey, gatewayId: inputData.gatewayId, - amount: inputData.amount + amount: inputData.amount, + customerId: inputData.customerId ) let router = BankCardRepeatRouter() diff --git a/YooKassaPayments/Private/Modules/BankCardRepeat/BankCardRepeatRouterIO.swift b/YooKassaPayments/Private/Modules/BankCardRepeat/BankCardRepeatRouterIO.swift index b76ff386..d2e55eb9 100644 --- a/YooKassaPayments/Private/Modules/BankCardRepeat/BankCardRepeatRouterIO.swift +++ b/YooKassaPayments/Private/Modules/BankCardRepeat/BankCardRepeatRouterIO.swift @@ -1,14 +1,7 @@ protocol BankCardRepeatRouterInput: AnyObject { func presentTermsOfServiceModule(_ url: URL) - - func presentSavePaymentMethodInfo( - inputData: SavePaymentMethodInfoModuleInputData - ) - - func present3dsModule( - inputData: CardSecModuleInputData, - moduleOutput: CardSecModuleOutput - ) - + func presentSafeDealInfo(title: String, body: String) + func presentSavePaymentMethodInfo(inputData: SavePaymentMethodInfoModuleInputData) + func present3dsModule(inputData: CardSecModuleInputData, moduleOutput: CardSecModuleOutput) func closeCardSecModule() } diff --git a/YooKassaPayments/Private/Modules/BankCardRepeat/BankCardRepeatViewIO.swift b/YooKassaPayments/Private/Modules/BankCardRepeat/BankCardRepeatViewIO.swift index 454ce9b4..084dca3d 100644 --- a/YooKassaPayments/Private/Modules/BankCardRepeat/BankCardRepeatViewIO.swift +++ b/YooKassaPayments/Private/Modules/BankCardRepeat/BankCardRepeatViewIO.swift @@ -15,12 +15,9 @@ protocol BankCardRepeatViewOutput: ActionTitleTextDialogDelegate { func setupView() func didTapActionButton() func didTapTermsOfService(_ url: URL) + func didTapSafeDealInfo(_ url: URL) func didTapOnSavePaymentMethod() - func didChangeSavePaymentMethodState( - _ state: Bool - ) - func didSetCsc( - _ csc: String - ) + func didChangeSavePaymentMethodState(_ state: Bool) + func didSetCsc(_ csc: String) func endEditing() } diff --git a/YooKassaPayments/Private/Modules/BankCardRepeat/Interactor/BankCardRepeatInteractor.swift b/YooKassaPayments/Private/Modules/BankCardRepeat/Interactor/BankCardRepeatInteractor.swift index 0eb3ce00..c0858894 100644 --- a/YooKassaPayments/Private/Modules/BankCardRepeat/Interactor/BankCardRepeatInteractor.swift +++ b/YooKassaPayments/Private/Modules/BankCardRepeat/Interactor/BankCardRepeatInteractor.swift @@ -17,6 +17,7 @@ final class BankCardRepeatInteractor { private let clientApplicationKey: String private let gatewayId: String? private let amount: Amount + private let customerId: String? // MARK: - Init @@ -28,7 +29,8 @@ final class BankCardRepeatInteractor { amountNumberFormatter: AmountNumberFormatter, clientApplicationKey: String, gatewayId: String?, - amount: Amount + amount: Amount, + customerId: String? ) { self.analyticsService = analyticsService self.analyticsProvider = analyticsProvider @@ -39,6 +41,7 @@ final class BankCardRepeatInteractor { self.clientApplicationKey = clientApplicationKey self.gatewayId = gatewayId self.amount = amount + self.customerId = customerId } } @@ -107,12 +110,13 @@ extension BankCardRepeatInteractor: BankCardRepeatInteractorInput { gatewayId: gatewayId, amount: amountNumberFormatter.string(from: amount.value), currency: amount.currency.rawValue, - getSavePaymentMethod: false + getSavePaymentMethod: false, + customerId: customerId ) { [weak self] result in guard let output = self?.output else { return } switch result { case let .success(data): - output.didFetchPaymentMethods(data) + output.didFetchPaymentMethods(data.options) case let .failure(error): output.didFetchPaymentMethods(error) } diff --git a/YooKassaPayments/Private/Modules/BankCardRepeat/Presenter/BankCardRepeatPresenter.swift b/YooKassaPayments/Private/Modules/BankCardRepeat/Presenter/BankCardRepeatPresenter.swift index ab6bbf19..3e0e115d 100644 --- a/YooKassaPayments/Private/Modules/BankCardRepeat/Presenter/BankCardRepeatPresenter.swift +++ b/YooKassaPayments/Private/Modules/BankCardRepeat/Presenter/BankCardRepeatPresenter.swift @@ -25,6 +25,7 @@ final class BankCardRepeatPresenter { private let termsOfService: TermsOfService private let savePaymentMethodViewModel: SavePaymentMethodViewModel? private var initialSavePaymentMethod: Bool + private let isSafeDeal: Bool // MARK: - Init @@ -39,7 +40,8 @@ final class BankCardRepeatPresenter { purchaseDescription: String, termsOfService: TermsOfService, savePaymentMethodViewModel: SavePaymentMethodViewModel?, - initialSavePaymentMethod: Bool + initialSavePaymentMethod: Bool, + isSafeDeal: Bool ) { self.cardService = cardService self.paymentMethodViewModelFactory = paymentMethodViewModelFactory @@ -54,6 +56,7 @@ final class BankCardRepeatPresenter { self.termsOfService = termsOfService self.savePaymentMethodViewModel = savePaymentMethodViewModel self.initialSavePaymentMethod = initialSavePaymentMethod + self.isSafeDeal = isSafeDeal } // MARK: - Stored Data @@ -99,6 +102,13 @@ extension BankCardRepeatPresenter: BankCardRepeatViewOutput { router.presentTermsOfServiceModule(url) } + func didTapSafeDealInfo(_ url: URL) { + router.presentSafeDealInfo( + title: PaymentMethodResources.Localized.safeDealInfoTitle, + body: PaymentMethodResources.Localized.safeDealInfoBody + ) + } + func didTapOnSavePaymentMethod() { let savePaymentMethodModuleinputData = SavePaymentMethodInfoModuleInputData( headerValue: SavePaymentMethodInfoLocalization.BankCard.header, @@ -193,7 +203,9 @@ extension BankCardRepeatPresenter: BankCardRepeatInteractorOutput { } let cardMask = card.first6 + "••••••" + card.last4 - let cardLogo = paymentMethodViewModelFactory.makeBankCardImage(card) + let cardLogo = paymentMethodViewModelFactory.makeBankCardImage( + first6Digits: card.first6, bankCardType: card.cardType + ) let priceViewModel = priceViewModelFactory.makeAmountPriceViewModel(paymentOption) let feeViewModel = priceViewModelFactory.makeFeePriceViewModel(paymentOption) @@ -204,7 +216,8 @@ extension BankCardRepeatPresenter: BankCardRepeatInteractorOutput { fee: feeViewModel, cardMask: formattingCardMask(cardMask), cardLogo: cardLogo, - terms: termsOfService + terms: termsOfService, + safeDealText: isSafeDeal ? PaymentMethodResources.Localized.safeDealInfoLink : nil ) DispatchQueue.main.async { [weak self] in diff --git a/YooKassaPayments/Private/Modules/BankCardRepeat/Router/BankCardRepeatRouter.swift b/YooKassaPayments/Private/Modules/BankCardRepeat/Router/BankCardRepeatRouter.swift index 66bf48b4..7f588250 100644 --- a/YooKassaPayments/Private/Modules/BankCardRepeat/Router/BankCardRepeatRouter.swift +++ b/YooKassaPayments/Private/Modules/BankCardRepeat/Router/BankCardRepeatRouter.swift @@ -17,9 +17,11 @@ extension BankCardRepeatRouter: BankCardRepeatRouterInput { ) } - func presentSavePaymentMethodInfo( - inputData: SavePaymentMethodInfoModuleInputData - ) { + func presentSafeDealInfo(title: String, body: String) { + presentSavePaymentMethodInfo(inputData: .init(headerValue: title, bodyValue: body)) + } + + func presentSavePaymentMethodInfo(inputData: SavePaymentMethodInfoModuleInputData) { let viewController = SavePaymentMethodInfoAssembly.makeModule( inputData: inputData ) @@ -33,10 +35,7 @@ extension BankCardRepeatRouter: BankCardRepeatRouterInput { ) } - func present3dsModule( - inputData: CardSecModuleInputData, - moduleOutput: CardSecModuleOutput - ) { + func present3dsModule(inputData: CardSecModuleInputData, moduleOutput: CardSecModuleOutput) { let viewController = CardSecAssembly.makeModule( inputData: inputData, moduleOutput: moduleOutput diff --git a/YooKassaPayments/Private/Modules/BankCardRepeat/View/BankCardRepeatViewController.swift b/YooKassaPayments/Private/Modules/BankCardRepeat/View/BankCardRepeatViewController.swift index 2337e9e0..61820ccc 100644 --- a/YooKassaPayments/Private/Modules/BankCardRepeat/View/BankCardRepeatViewController.swift +++ b/YooKassaPayments/Private/Modules/BankCardRepeat/View/BankCardRepeatViewController.swift @@ -87,7 +87,7 @@ final class BankCardRepeatViewController: UIViewController, PlaceholderProvider $0.setStyles(UIView.Styles.grayBackground) $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical - $0.spacing = Space.double + $0.spacing = Space.single return $0 }(UIStackView()) @@ -106,15 +106,37 @@ final class BankCardRepeatViewController: UIViewController, PlaceholderProvider return $0 }(Button(type: .custom)) - private lazy var termsOfServiceLinkedTextView: LinkedTextView = { - $0.tintColor = CustomizationStorage.shared.mainScheme - $0.setStyles( - UIView.Styles.grayBackground, - UITextView.Styles.linked - ) - $0.delegate = self - return $0 - }(LinkedTextView()) + private lazy var submitButtonContainer: UIView = { + let view = UIView(frame: .zero) + view.translatesAutoresizingMaskIntoConstraints = false + 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), + view.trailingAnchor.constraint(equalTo: submitButton.trailingAnchor), + view.bottomAnchor.constraint(equalTo: submitButton.bottomAnchor, constant: Space.single), + defaultHeight, + ]) + + return view + }() + + private let termsOfServiceLinkedTextView: LinkedTextView = { + let view = LinkedTextView() + view.tintColor = CustomizationStorage.shared.mainScheme + view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) + return view + }() + + private let safeDealLinkedTextView: LinkedTextView = { + let view = LinkedTextView() + view.tintColor = CustomizationStorage.shared.mainScheme + view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) + return view + }() private var activityIndicatorView: UIView? @@ -234,6 +256,9 @@ final class BankCardRepeatViewController: UIViewController, PlaceholderProvider view.addGestureRecognizer(viewTapGestureRecognizer) navigationItem.title = Localized.title + termsOfServiceLinkedTextView.delegate = self + safeDealLinkedTextView.delegate = self + safeDealLinkedTextView.isHidden = true setupView() setupConstraints() } @@ -272,8 +297,9 @@ final class BankCardRepeatViewController: UIViewController, PlaceholderProvider ].forEach(errorCscView.addSubview) [ - submitButton, + submitButtonContainer, termsOfServiceLinkedTextView, + safeDealLinkedTextView, ].forEach(actionButtonStackView.addArrangedSubview) } @@ -412,9 +438,7 @@ extension BankCardRepeatViewController: BankCardRepeatViewInput { view.endEditing(force) } - func setupViewModel( - _ viewModel: BankCardRepeatViewModel - ) { + func setupViewModel(_ viewModel: BankCardRepeatViewModel) { orderView.title = viewModel.shopName orderView.subtitle = viewModel.description orderView.value = makePrice(viewModel.price) @@ -432,7 +456,10 @@ extension BankCardRepeatViewController: BankCardRepeatViewInput { font: UIFont.dynamicCaption2, foregroundColor: UIColor.AdaptiveColors.secondary ) + safeDealLinkedTextView.attributedText = viewModel.safeDealText + safeDealLinkedTextView.isHidden = viewModel.safeDealText?.string.isEmpty ?? true termsOfServiceLinkedTextView.textAlignment = .center + safeDealLinkedTextView.textAlignment = .center } func setConfirmButtonEnabled( @@ -639,6 +666,8 @@ extension BankCardRepeatViewController: UITextViewDelegate { switch textView { case termsOfServiceLinkedTextView: output?.didTapTermsOfService(URL) + case safeDealLinkedTextView: + output?.didTapSafeDealInfo(URL) default: assertionFailure("Unsupported textView") } diff --git a/YooKassaPayments/Private/Modules/BankCardRepeat/View/ViewModel/BankCardRepeatViewModel.swift b/YooKassaPayments/Private/Modules/BankCardRepeat/View/ViewModel/BankCardRepeatViewModel.swift index a8893d74..d45ef6c3 100644 --- a/YooKassaPayments/Private/Modules/BankCardRepeat/View/ViewModel/BankCardRepeatViewModel.swift +++ b/YooKassaPayments/Private/Modules/BankCardRepeat/View/ViewModel/BankCardRepeatViewModel.swift @@ -8,4 +8,5 @@ struct BankCardRepeatViewModel { let cardMask: String let cardLogo: UIImage let terms: TermsOfService + let safeDealText: NSAttributedString? } diff --git a/YooKassaPayments/Private/Modules/CardSettings/Assembly/CardSettingsAssembly.swift b/YooKassaPayments/Private/Modules/CardSettings/Assembly/CardSettingsAssembly.swift new file mode 100644 index 00000000..0151a832 --- /dev/null +++ b/YooKassaPayments/Private/Modules/CardSettings/Assembly/CardSettingsAssembly.swift @@ -0,0 +1,30 @@ +import UIKit + +enum CardSettingsAssembly { + static func make(data: CardSettingsModuleInputData, output: CardSettingsModuleOutput? = nil) -> UIViewController { + let view = CardSettingsViewController(nibName: nil, bundle: nil) + let presenter = CardSettingsPresenter( + data: data, + paymentMethodViewModelFactory: PaymentMethodViewModelFactoryAssembly.makeFactory() + ) + let interactor = CardSettingsInteractor( + clientId: data.clientId, + paymentService: PaymentServiceAssembly.makeService( + tokenizationSettings: data.tokenizationSettings, + testModeSettings: data.testModeSettings, + isLoggingEnabled: data.isLoggingEnabled + ), + analyticsService: AnalyticsServiceAssembly.makeService(isLoggingEnabled: data.isLoggingEnabled) + ) + let router = CardSettingsRouter(transitionHandler: view) + + presenter.view = view + presenter.interactor = interactor + presenter.router = router + presenter.output = output + + view.output = presenter + interactor.output = presenter + return view + } +} diff --git a/YooKassaPayments/Private/Modules/CardSettings/IO/CardSettingsInteractorIO.swift b/YooKassaPayments/Private/Modules/CardSettings/IO/CardSettingsInteractorIO.swift new file mode 100644 index 00000000..a898d846 --- /dev/null +++ b/YooKassaPayments/Private/Modules/CardSettings/IO/CardSettingsInteractorIO.swift @@ -0,0 +1,11 @@ +import Foundation + +protocol CardSettingsInteractorInput: AnyObject { + func track(event: AnalyticsEvent) + func unbind(id: String) +} + +protocol CardSettingsInteractorOutput: AnyObject { + func didUnbind(id: String) + func didFailUnbind(error: Error, id: String) +} diff --git a/YooKassaPayments/Private/Modules/CardSettings/IO/CardSettingsModuleIO.swift b/YooKassaPayments/Private/Modules/CardSettings/IO/CardSettingsModuleIO.swift new file mode 100644 index 00000000..1fb1bf3f --- /dev/null +++ b/YooKassaPayments/Private/Modules/CardSettings/IO/CardSettingsModuleIO.swift @@ -0,0 +1,22 @@ +import UIKit + +struct CardSettingsModuleInputData { + enum Card { + case yoomoney(name: String?) + case card(name: String, id: String) + } + let cardLogo: UIImage + let cardMask: String + let infoText: String + let card: Card + + let testModeSettings: TestModeSettings? + let tokenizationSettings: TokenizationSettings + let isLoggingEnabled: Bool + let clientId: String +} + +protocol CardSettingsModuleOutput: AnyObject { + func cardSettingsModuleDidUnbindCard(mask: String) + func cardSettingsModuleDidCancel() +} diff --git a/YooKassaPayments/Private/Modules/CardSettings/IO/CardSettingsRouterIO.swift b/YooKassaPayments/Private/Modules/CardSettings/IO/CardSettingsRouterIO.swift new file mode 100644 index 00000000..05582822 --- /dev/null +++ b/YooKassaPayments/Private/Modules/CardSettings/IO/CardSettingsRouterIO.swift @@ -0,0 +1,5 @@ +import Foundation + +protocol CardSettingsRouterInput: AnyObject { + func openInfo(title: String, details: String) +} diff --git a/YooKassaPayments/Private/Modules/CardSettings/IO/CardSettingsViewIO.swift b/YooKassaPayments/Private/Modules/CardSettings/IO/CardSettingsViewIO.swift new file mode 100644 index 00000000..d0a9cdf5 --- /dev/null +++ b/YooKassaPayments/Private/Modules/CardSettings/IO/CardSettingsViewIO.swift @@ -0,0 +1,21 @@ +import UIKit + +protocol CardSettingsViewInput: NotificationPresenting, ActivityIndicatorFullViewPresenting { + func set( + title: String, + cardMaskHint: String, + cardLogo: UIImage, + cardMask: String, + cardTitle: String, + informerMessage: String, + canUnbind: Bool + ) + func hideSubmit(_ hide: Bool) + func disableSubmit() + func enableSubmit() +} +protocol CardSettingsViewOutput: AnyObject { + func setupView() + func didPressSubmit() + func didPressInformerMoreInfo() +} diff --git a/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsInteractor.swift b/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsInteractor.swift new file mode 100644 index 00000000..aff5361b --- /dev/null +++ b/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsInteractor.swift @@ -0,0 +1,31 @@ +import Foundation + +class CardSettingsInteractor: CardSettingsInteractorInput { + var output: CardSettingsInteractorOutput! + + private let analyticsService: AnalyticsService + private let paymentService: PaymentService + private let clientId: String + + init(clientId: String, paymentService: PaymentService, analyticsService: AnalyticsService) { + self.analyticsService = analyticsService + self.clientId = clientId + self.paymentService = paymentService + } + + func track(event: AnalyticsEvent) { + analyticsService.trackEvent(event) + } + + func unbind(id: String) { + paymentService.unbind(authToken: clientId, id: id) { [weak self] in + guard let self = self else { return } + switch $0 { + case .failure(let error): + self.output.didFailUnbind(error: error, id: id) + case .success: + self.output.didUnbind(id: id) + } + } + } +} diff --git a/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsPresenter.swift b/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsPresenter.swift new file mode 100644 index 00000000..108ee661 --- /dev/null +++ b/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsPresenter.swift @@ -0,0 +1,108 @@ +import UIKit + +final class CardSettingsPresenter: CardSettingsViewOutput, CardSettingsInteractorOutput { + weak var view: CardSettingsViewInput! + weak var output: CardSettingsModuleOutput! + + var interactor: CardSettingsInteractorInput! + var router: CardSettingsRouterInput! + let paymentMethodViewModelFactory: PaymentMethodViewModelFactory + + private let data: CardSettingsModuleInputData + init(data: CardSettingsModuleInputData, paymentMethodViewModelFactory: PaymentMethodViewModelFactory) { + self.data = data + self.paymentMethodViewModelFactory = paymentMethodViewModelFactory + } + + func setupView() { + typealias Text = CommonLocalized.CardSettingsDetails + let canUnbind: Bool + let displayName: String + let cardTitle: String + let cardMaskHint: String + switch data.card { + case .yoomoney(let name): + displayName = name ?? data.cardMask + cardTitle = name ?? PaymentMethodResources.Localized.yooMoneyCard + canUnbind = false + cardMaskHint = PaymentMethodResources.Localized.yooMoneyCard + view.hideSubmit(true) + interactor.track(event: AnalyticsEvent.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)) + } + + view.set( + title: displayName, + cardMaskHint: cardMaskHint, + cardLogo: data.cardLogo, + cardMask: paymentMethodViewModelFactory.replaceBullets(data.cardMask.splitEvery(4, separator: " ")), + cardTitle: cardTitle, + informerMessage: data.infoText, + canUnbind: canUnbind + ) + } + + func didPressSubmit() { + view.disableSubmit() + switch data.card { + case .yoomoney: + output.cardSettingsModuleDidCancel() + case .card(_, let id): + view.showActivity() + + DispatchQueue.global().async { [weak self] in + guard let self = self else { return } + self.interactor.unbind(id: id) + } + + } + } + func didPressCancel() { + output.cardSettingsModuleDidCancel() + } + func didPressInformerMoreInfo() { + switch data.card { + case .yoomoney: + router.openInfo( + title: CommonLocalized.CardSettingsDetails.unbindInfoTitle, + details: CommonLocalized.CardSettingsDetails.unbindInfoDetails + ) + interactor.track(event: .screenDetailsUnbindWalletCard(sdkVersion: Bundle.frameworkVersion)) + case .card: + router.openInfo( + title: CommonLocalized.CardSettingsDetails.autopayInfoTitle, + details: CommonLocalized.CardSettingsDetails.autopayInfoDetails + ) + } + } + + // MARK: - CardSettingsInteractorOutput + + func didFailUnbind(error: Error, id: String) { + interactor.track(event: .actionUnbindBankCard(actionUnbindCardStatus: .fail)) + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.view.hideActivity() + self.view.enableSubmit() + self.view.presentError( + with: String(format: CommonLocalized.CardSettingsDetails.unbindFail, self.data.cardMask) + ) + } + } + + func didUnbind(id: String) { + interactor.track(event: .actionUnbindBankCard(actionUnbindCardStatus: .success)) + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.view.enableSubmit() + self.view.hideActivity() + self.output.cardSettingsModuleDidUnbindCard(mask: self.data.cardMask) + } + } +} diff --git a/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsRouter.swift b/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsRouter.swift new file mode 100644 index 00000000..9ac17839 --- /dev/null +++ b/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsRouter.swift @@ -0,0 +1,21 @@ +import UIKit + +final class CardSettingsRouter: CardSettingsRouterInput { + let transitionHandler: TransitionHandler + init(transitionHandler: TransitionHandler) { + self.transitionHandler = transitionHandler + } + + func openInfo(title: String, details: String) { + let data = SavePaymentMethodInfoModuleInputData(headerValue: title, bodyValue: details) + let module = SavePaymentMethodInfoAssembly.makeModule(inputData: data) + let container = UINavigationController(rootViewController: module) + module.addCloseButtonIfNeeded(target: self, action: #selector(close)) + transitionHandler.present(container, animated: true, completion: nil) + } + + @objc + private func close() { + transitionHandler.dismiss(animated: true, completion: nil) + } +} diff --git a/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsViewController.swift b/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsViewController.swift new file mode 100644 index 00000000..f9f631ca --- /dev/null +++ b/YooKassaPayments/Private/Modules/CardSettings/Viper bundle/CardSettingsViewController.swift @@ -0,0 +1,143 @@ +import UIKit + +final class CardSettingsViewController: UIViewController, CardSettingsViewInput { + private let cardDetails = MaskedCardView(frame: .zero) + private let informer = LargeActionInformer(frame: .zero) + private let contentContainer = UIStackView(frame: .zero) + private let actionsContainer = UIStackView(frame: .zero) + + var output: CardSettingsViewOutput! + + private let submitButton: Button = { + let button = Button(type: .custom) + button.setTitle(CommonLocalized.Alert.cancel, for: .normal) + button.style.submit() + button.addTarget( + self, + action: #selector(didPressSubmit), + for: .touchUpInside + ) + return button + }() + + override func loadView() { + view = UIView(frame: .zero) + view.setStyles(UIView.Styles.defaultBackground) + + cardDetails.setStyles( + UIView.Styles.grayBackground, + UIView.Styles.roundedShadow + ) + cardDetails.hintCardNumberLabel.setStyles( + UILabel.DynamicStyle.caption1, + UILabel.Styles.singleLine, + UILabel.ColorStyle.primary + ) + LargeActionInformer.Style.default(informer).alert() + informer.buttonLabel.text = CommonLocalized.CardSettingsDetails.moreInfo + + contentContainer.axis = .vertical + contentContainer.spacing = Space.double + + actionsContainer.axis = .vertical + actionsContainer.spacing = Space.double + + [contentContainer, actionsContainer].forEach { view in + view.translatesAutoresizingMaskIntoConstraints = false + self.view.addSubview(view) + } + view.layoutMargins = UIEdgeInsets( + top: Space.double, + left: Space.double, + bottom: Space.double, + right: Space.double + ) + + LargeActionInformer.Style.default(informer).alert() + informer.actionHandler = { [weak self] in + self?.output.didPressInformerMoreInfo() + } + + contentContainer.addArrangedSubview(cardDetails) + contentContainer.addArrangedSubview(informer) + + actionsContainer.addArrangedSubview(submitButton) + setupConstraints() + } + + override func viewDidLoad() { + super.viewDidLoad() + output.setupView() + } + + private func setupConstraints() { + NSLayoutConstraint.activate([ + contentContainer.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), + contentContainer.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), + view.layoutMarginsGuide.trailingAnchor.constraint(equalTo: contentContainer.trailingAnchor), + + actionsContainer.topAnchor.constraint(equalTo: contentContainer.bottomAnchor, constant: Space.double), + actionsContainer.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), + view.layoutMarginsGuide.trailingAnchor.constraint(equalTo: actionsContainer.trailingAnchor), + view.layoutMarginsGuide.bottomAnchor.constraint(greaterThanOrEqualTo: actionsContainer.bottomAnchor), + ]) + } + + @objc + private func didPressSubmit() { + output.didPressSubmit() + } + + // MARK: CardSettingsViewInput + + func set( + title: String, + cardMaskHint: String, + cardLogo: UIImage, + cardMask: String, + cardTitle: String, + informerMessage: String, + canUnbind: Bool + ) { + self.title = title + cardDetails.hintCardNumber = cardMaskHint + cardDetails.cardLogo = cardLogo + cardDetails.cardNumber = cardMask + informer.messageLabel.styledText = informerMessage + + let title = canUnbind + ? CommonLocalized.CardSettingsDetails.unbind + : CommonLocalized.CardSettingsDetails.unwind + submitButton.setTitle(title, for: .normal) + + if canUnbind { + submitButton.style.submitAlert(ghostTint: true) + } else { + submitButton.style.submit(ghostTint: true) + } + } + + func disableSubmit() { + submitButton.isEnabled = false + } + + func enableSubmit() { + submitButton.isEnabled = true + } + + func hideSubmit(_ hide: Bool) { + submitButton.isHidden = hide + } +} + +// MARK: - ActivityIndicatorFullViewPresenting + +extension CardSettingsViewController: ActivityIndicatorFullViewPresenting { + func showActivity() { + showFullViewActivity(style: ActivityIndicatorView.Styles.cloudy) + } + + func hideActivity() { + hideFullViewActivity() + } +} diff --git a/YooKassaPayments/Private/Modules/LinkedCard/Assembly/LinkedCardAssembly.swift b/YooKassaPayments/Private/Modules/LinkedCard/Assembly/LinkedCardAssembly.swift index 6d387f03..82995d40 100644 --- a/YooKassaPayments/Private/Modules/LinkedCard/Assembly/LinkedCardAssembly.swift +++ b/YooKassaPayments/Private/Modules/LinkedCard/Assembly/LinkedCardAssembly.swift @@ -26,7 +26,8 @@ enum LinkedCardAssembly { returnUrl: inputData.returnUrl, tmxSessionId: inputData.tmxSessionId, initialSavePaymentMethod: inputData.initialSavePaymentMethod, - isBackBarButtonHidden: inputData.isBackBarButtonHidden + isBackBarButtonHidden: inputData.isBackBarButtonHidden, + isSafeDeal: inputData.isSafeDeal ) let authorizationService = AuthorizationServiceAssembly.makeService( @@ -52,7 +53,8 @@ enum LinkedCardAssembly { analyticsProvider: analyticsProvider, paymentService: paymentService, threatMetrixService: threatMetrixService, - clientApplicationKey: inputData.clientApplicationKey + clientApplicationKey: inputData.clientApplicationKey, + customerId: inputData.customerId ) let router = LinkedCardRouter() diff --git a/YooKassaPayments/Private/Modules/LinkedCard/Interactor/LinkedCardInteractor.swift b/YooKassaPayments/Private/Modules/LinkedCard/Interactor/LinkedCardInteractor.swift index eee15e05..f57b5cdf 100644 --- a/YooKassaPayments/Private/Modules/LinkedCard/Interactor/LinkedCardInteractor.swift +++ b/YooKassaPayments/Private/Modules/LinkedCard/Interactor/LinkedCardInteractor.swift @@ -15,6 +15,7 @@ final class LinkedCardInteractor { private let threatMetrixService: ThreatMetrixService private let clientApplicationKey: String + private let customerId: String? // MARK: - Init @@ -24,7 +25,8 @@ final class LinkedCardInteractor { analyticsProvider: AnalyticsProvider, paymentService: PaymentService, threatMetrixService: ThreatMetrixService, - clientApplicationKey: String + clientApplicationKey: String, + customerId: String? ) { self.authorizationService = authorizationService self.analyticsService = analyticsService @@ -33,6 +35,7 @@ final class LinkedCardInteractor { self.threatMetrixService = threatMetrixService self.clientApplicationKey = clientApplicationKey + self.customerId = customerId } } @@ -156,6 +159,7 @@ extension LinkedCardInteractor: LinkedCardInteractorInput { paymentMethodType: paymentMethodType, amount: amount, tmxSessionId: tmxSessionId, + customerId: customerId, completion: completion ) } diff --git a/YooKassaPayments/Private/Modules/LinkedCard/LinkedCardModuleIO.swift b/YooKassaPayments/Private/Modules/LinkedCard/LinkedCardModuleIO.swift index 313b416f..e38aa556 100644 --- a/YooKassaPayments/Private/Modules/LinkedCard/LinkedCardModuleIO.swift +++ b/YooKassaPayments/Private/Modules/LinkedCard/LinkedCardModuleIO.swift @@ -17,6 +17,8 @@ struct LinkedCardModuleInputData { let tmxSessionId: String? let initialSavePaymentMethod: Bool let isBackBarButtonHidden: Bool + let customerId: String? + let isSafeDeal: Bool } protocol LinkedCardModuleInput: AnyObject { diff --git a/YooKassaPayments/Private/Modules/LinkedCard/LinkedCardRouterIO.swift b/YooKassaPayments/Private/Modules/LinkedCard/LinkedCardRouterIO.swift index 033000f0..a8f0767a 100644 --- a/YooKassaPayments/Private/Modules/LinkedCard/LinkedCardRouterIO.swift +++ b/YooKassaPayments/Private/Modules/LinkedCard/LinkedCardRouterIO.swift @@ -1,10 +1,9 @@ protocol LinkedCardRouterInput: AnyObject { func presentTermsOfServiceModule(_ url: URL) - + func presentSafeDealInfo(title: String, body: String) func presentPaymentAuthorizationModule( inputData: PaymentAuthorizationModuleInputData, moduleOutput: PaymentAuthorizationModuleOutput? ) - func closePaymentAuthorization() } diff --git a/YooKassaPayments/Private/Modules/LinkedCard/LinkedCardViewIO.swift b/YooKassaPayments/Private/Modules/LinkedCard/LinkedCardViewIO.swift index 99c04194..e414e1ee 100644 --- a/YooKassaPayments/Private/Modules/LinkedCard/LinkedCardViewIO.swift +++ b/YooKassaPayments/Private/Modules/LinkedCard/LinkedCardViewIO.swift @@ -19,11 +19,8 @@ protocol LinkedCardViewOutput: ActionTitleTextDialogDelegate { func setupView() func didTapActionButton() func didTapTermsOfService(_ url: URL) - func didChangeSaveAuthInAppState( - _ state: Bool - ) - func didSetCsc( - _ csc: String - ) + func didTapSafeDealInfo(_ url: URL) + func didChangeSaveAuthInAppState(_ state: Bool) + func didSetCsc(_ csc: String) func endEditing() } diff --git a/YooKassaPayments/Private/Modules/LinkedCard/Presenter/LinkedCardPresenter.swift b/YooKassaPayments/Private/Modules/LinkedCard/Presenter/LinkedCardPresenter.swift index ee26f3a9..cc955774 100644 --- a/YooKassaPayments/Private/Modules/LinkedCard/Presenter/LinkedCardPresenter.swift +++ b/YooKassaPayments/Private/Modules/LinkedCard/Presenter/LinkedCardPresenter.swift @@ -30,6 +30,7 @@ final class LinkedCardPresenter { private let tmxSessionId: String? private var initialSavePaymentMethod: Bool private let isBackBarButtonHidden: Bool + private let isSafeDeal: Bool // MARK: - Init @@ -49,7 +50,8 @@ final class LinkedCardPresenter { returnUrl: String?, tmxSessionId: String?, initialSavePaymentMethod: Bool, - isBackBarButtonHidden: Bool + isBackBarButtonHidden: Bool, + isSafeDeal: Bool ) { self.cardService = cardService self.paymentMethodViewModelFactory = paymentMethodViewModelFactory @@ -69,6 +71,7 @@ final class LinkedCardPresenter { self.tmxSessionId = tmxSessionId self.initialSavePaymentMethod = initialSavePaymentMethod self.isBackBarButtonHidden = isBackBarButtonHidden + self.isSafeDeal = isSafeDeal } // MARK: - Stored Data @@ -97,7 +100,8 @@ extension LinkedCardPresenter: LinkedCardViewOutput { fee: fee, cardMask: formattingCardMask(cardMask), cardLogo: cardLogo, - terms: termsOfService + terms: termsOfService, + safeDealText: isSafeDeal ? PaymentMethodResources.Localized.safeDealInfoLink : nil ) view.setupViewModel(viewModel) @@ -110,10 +114,16 @@ extension LinkedCardPresenter: LinkedCardViewOutput { view.setBackBarButtonHidden(isBackBarButtonHidden) DispatchQueue.global().async { [weak self] in - let event: AnalyticsEvent = .screenLinkedCardForm( + 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.trackEvent(event) + self.interactor.trackEvent(contract) } } @@ -162,6 +172,13 @@ extension LinkedCardPresenter: LinkedCardViewOutput { router.presentTermsOfServiceModule(url) } + func didTapSafeDealInfo(_ url: URL) { + router.presentSafeDealInfo( + title: PaymentMethodResources.Localized.safeDealInfoTitle, + body: PaymentMethodResources.Localized.safeDealInfoBody + ) + } + func didChangeSaveAuthInAppState( _ state: Bool ) { diff --git a/YooKassaPayments/Private/Modules/LinkedCard/Router/LinkedCardRouter.swift b/YooKassaPayments/Private/Modules/LinkedCard/Router/LinkedCardRouter.swift index 7c728e4a..82c00046 100644 --- a/YooKassaPayments/Private/Modules/LinkedCard/Router/LinkedCardRouter.swift +++ b/YooKassaPayments/Private/Modules/LinkedCard/Router/LinkedCardRouter.swift @@ -17,6 +17,18 @@ extension LinkedCardRouter: LinkedCardRouterInput { ) } + func presentSafeDealInfo(title: String, body: String) { + let viewController = SavePaymentMethodInfoAssembly.makeModule( + inputData: .init(headerValue: title, bodyValue: body) + ) + let navigationController = UINavigationController(rootViewController: viewController) + transitionHandler?.present( + navigationController, + animated: true, + completion: nil + ) + } + func presentPaymentAuthorizationModule( inputData: PaymentAuthorizationModuleInputData, moduleOutput: PaymentAuthorizationModuleOutput? diff --git a/YooKassaPayments/Private/Modules/LinkedCard/View/LinkedCardViewController.swift b/YooKassaPayments/Private/Modules/LinkedCard/View/LinkedCardViewController.swift index 455bbed0..ffc4d215 100644 --- a/YooKassaPayments/Private/Modules/LinkedCard/View/LinkedCardViewController.swift +++ b/YooKassaPayments/Private/Modules/LinkedCard/View/LinkedCardViewController.swift @@ -87,7 +87,7 @@ final class LinkedCardViewController: UIViewController, PlaceholderProvider { $0.setStyles(UIView.Styles.grayBackground) $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical - $0.spacing = Space.double + $0.spacing = Space.single return $0 }(UIStackView()) @@ -106,15 +106,37 @@ final class LinkedCardViewController: UIViewController, PlaceholderProvider { return $0 }(Button(type: .custom)) - private lazy var termsOfServiceLinkedTextView: LinkedTextView = { - $0.tintColor = CustomizationStorage.shared.mainScheme - $0.setStyles( - UIView.Styles.grayBackground, - UITextView.Styles.linked - ) - $0.delegate = self - return $0 - }(LinkedTextView()) + private lazy var submitButtonContainer: UIView = { + let view = UIView(frame: .zero) + view.translatesAutoresizingMaskIntoConstraints = false + 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), + view.trailingAnchor.constraint(equalTo: submitButton.trailingAnchor), + view.bottomAnchor.constraint(equalTo: submitButton.bottomAnchor, constant: Space.single), + defaultHeight, + ]) + + return view + }() + + private let termsOfServiceLinkedTextView: LinkedTextView = { + let view = LinkedTextView() + view.tintColor = CustomizationStorage.shared.mainScheme + view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) + return view + }() + + private let safeDealLinkedTextView: LinkedTextView = { + let view = LinkedTextView() + view.tintColor = CustomizationStorage.shared.mainScheme + view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) + return view + }() // MARK: - PlaceholderProvider @@ -207,6 +229,10 @@ final class LinkedCardViewController: UIViewController, PlaceholderProvider { view.setStyles(UIView.Styles.grayBackground) view.addGestureRecognizer(viewTapGestureRecognizer) + termsOfServiceLinkedTextView.delegate = self + safeDealLinkedTextView.delegate = self + safeDealLinkedTextView.isHidden = true + setupView() setupConstraints() } @@ -245,8 +271,9 @@ final class LinkedCardViewController: UIViewController, PlaceholderProvider { ].forEach(errorCscView.addSubview) [ - submitButton, + submitButtonContainer, termsOfServiceLinkedTextView, + safeDealLinkedTextView, ].forEach(actionButtonStackView.addArrangedSubview) } @@ -382,9 +409,7 @@ extension LinkedCardViewController: LinkedCardViewInput { navigationItem.title = title ?? Localized.title } - func setupViewModel( - _ viewModel: LinkedCardViewModel - ) { + func setupViewModel(_ viewModel: LinkedCardViewModel) { orderView.title = viewModel.shopName orderView.subtitle = viewModel.description orderView.value = makePrice(viewModel.price) @@ -402,7 +427,10 @@ extension LinkedCardViewController: LinkedCardViewInput { font: UIFont.dynamicCaption2, foregroundColor: UIColor.AdaptiveColors.secondary ) + safeDealLinkedTextView.isHidden = viewModel.safeDealText?.string.isEmpty ?? true + safeDealLinkedTextView.attributedText = viewModel.safeDealText termsOfServiceLinkedTextView.textAlignment = .center + safeDealLinkedTextView.textAlignment = .center } func setSaveAuthInAppSwitchItemView() { @@ -425,9 +453,7 @@ extension LinkedCardViewController: LinkedCardViewInput { showPlaceholder() } - func setCardState( - _ state: MaskedCardView.CscState - ) { + func setCardState(_ state: MaskedCardView.CscState) { maskedCardView.cscState = state errorCscLabel.isHidden = state != .error } @@ -548,6 +574,8 @@ extension LinkedCardViewController: UITextViewDelegate { switch textView { case termsOfServiceLinkedTextView: output?.didTapTermsOfService(URL) + case safeDealLinkedTextView: + output?.didTapSafeDealInfo(URL) default: assertionFailure("Unsupported textView") } diff --git a/YooKassaPayments/Private/Modules/LinkedCard/View/ViewModel/LinkedCardViewModel.swift b/YooKassaPayments/Private/Modules/LinkedCard/View/ViewModel/LinkedCardViewModel.swift index da801720..e8b75da2 100644 --- a/YooKassaPayments/Private/Modules/LinkedCard/View/ViewModel/LinkedCardViewModel.swift +++ b/YooKassaPayments/Private/Modules/LinkedCard/View/ViewModel/LinkedCardViewModel.swift @@ -8,4 +8,5 @@ struct LinkedCardViewModel { let cardMask: String let cardLogo: UIImage let terms: TermsOfService + let safeDealText: NSAttributedString? } diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/Assembly/PaymentMethodsAssembly.swift b/YooKassaPayments/Private/Modules/PaymentMethods/Assembly/PaymentMethodsAssembly.swift index 5390336e..dfbdae49 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/Assembly/PaymentMethodsAssembly.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/Assembly/PaymentMethodsAssembly.swift @@ -39,7 +39,8 @@ enum PaymentMethodsAssembly { returnUrl: inputData.returnUrl, savePaymentMethod: inputData.savePaymentMethod, userPhoneNumber: inputData.userPhoneNumber, - cardScanning: inputData.cardScanning + cardScanning: inputData.cardScanning, + customerId: inputData.customerId ) let paymentService = PaymentServiceAssembly.makeService( @@ -79,7 +80,8 @@ enum PaymentMethodsAssembly { clientApplicationKey: inputData.clientApplicationKey, gatewayId: inputData.gatewayId, amount: inputData.amount, - getSavePaymentMethod: inputData.getSavePaymentMethod + getSavePaymentMethod: inputData.getSavePaymentMethod, + customerId: inputData.customerId ) let router = PaymentMethodsRouter() diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/Interactor/PaymentMethodsInteractor.swift b/YooKassaPayments/Private/Modules/PaymentMethods/Interactor/PaymentMethodsInteractor.swift index 125f9f27..23eed4dd 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/Interactor/PaymentMethodsInteractor.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/Interactor/PaymentMethodsInteractor.swift @@ -1,5 +1,6 @@ import MoneyAuth import ThreatMetrixAdapter +import YooKassaPaymentsApi class PaymentMethodsInteractor { @@ -22,6 +23,7 @@ class PaymentMethodsInteractor { private let gatewayId: String? private let amount: Amount private let getSavePaymentMethod: Bool? + private let customerId: String? // MARK: - Init @@ -37,7 +39,8 @@ class PaymentMethodsInteractor { clientApplicationKey: String, gatewayId: String?, amount: Amount, - getSavePaymentMethod: Bool? + getSavePaymentMethod: Bool?, + customerId: String? ) { self.paymentService = paymentService self.authorizationService = authorizationService @@ -52,10 +55,24 @@ class PaymentMethodsInteractor { self.gatewayId = gatewayId self.amount = amount self.getSavePaymentMethod = getSavePaymentMethod + self.customerId = customerId } } extension PaymentMethodsInteractor: PaymentMethodsInteractorInput { + func unbindCard(id: String) { + paymentService.unbind(authToken: clientApplicationKey, id: id) { [weak self] result in + guard let self = self else { return } + + switch result { + case .success: + self.output?.didUnbindCard(id: id) + case .failure(let error): + self.output?.didFailUnbindCard(id: id, error: mapError(error)) + } + } + } + func fetchPaymentMethods() { let authorizationToken = authorizationService.getMoneyCenterAuthToken() @@ -65,14 +82,15 @@ extension PaymentMethodsInteractor: PaymentMethodsInteractorInput { gatewayId: gatewayId, amount: amountNumberFormatter.string(from: amount.value), currency: amount.currency.rawValue, - getSavePaymentMethod: getSavePaymentMethod + getSavePaymentMethod: getSavePaymentMethod, + customerId: customerId ) { [weak self] result in guard let output = self?.output else { return } switch result { case let .success(data): - output.didFetchPaymentMethods(data) + output.didFetchShop(data) case let .failure(error): - output.didFetchPaymentMethods(error) + output.didFailFetchShop(error) } } } @@ -88,12 +106,16 @@ extension PaymentMethodsInteractor: PaymentMethodsInteractorInput { gatewayId: gatewayId, amount: amountNumberFormatter.string(from: amount.value), currency: amount.currency.rawValue, - getSavePaymentMethod: getSavePaymentMethod + getSavePaymentMethod: getSavePaymentMethod, + customerId: customerId ) { [weak self] result in guard let output = self?.output else { return } switch result { case let .success(data): - output.didFetchYooMoneyPaymentMethods(data.filter { $0.paymentMethodType == .yooMoney }) + output.didFetchYooMoneyPaymentMethods( + data.options.filter { $0.paymentMethodType == .yooMoney }, + shopProperties: data.properties + ) case let .failure(error): output.didFetchYooMoneyPaymentMethods(error) } @@ -208,9 +230,44 @@ extension PaymentMethodsInteractor { savePaymentMethod: savePaymentMethod, amount: amount, tmxSessionId: tmxSessionId, + customerId: customerId, completion: completion ) } + + func tokenizeInstrument( + instrument: PaymentInstrumentBankCard, + savePaymentMethod: Bool, + returnUrl: String?, + amount: MonetaryAmount + ) { + threatMetrixService.profileApp { [weak self] result in + guard let self = self, let output = self.output else { return } + switch result { + case .success(let tmxId): + self.paymentService.tokenizeCardInstrument( + clientApplicationKey: self.clientApplicationKey, + amount: amount, + tmxSessionId: tmxId.value, + confirmation: Confirmation(type: .redirect, returnUrl: returnUrl), + savePaymentMethod: savePaymentMethod, + instrumentId: instrument.paymentInstrumentId, + csc: nil + ) { tokenizeResult in + switch tokenizeResult { + case .success(let tokens): + output.didTokenizeInstrument(instrument: instrument, tokens: tokens) + case .failure(let error): + let mappedError = mapError(error) + output.didFailTokenizeInstrument(error: mappedError) + } + } + case .failure(let error): + let mappedError = mapError(error) + output.didFailTokenizeInstrument(error: mappedError) + } + } + } } private func mapError(_ error: Error) -> Error { diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsInteractorIO.swift b/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsInteractorIO.swift index d936f2f3..0b7a094e 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsInteractorIO.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsInteractorIO.swift @@ -3,69 +3,45 @@ import YooKassaPaymentsApi protocol PaymentMethodsInteractorInput: AnalyticsTrack, AnalyticsProvider { func fetchPaymentMethods() - - func fetchYooMoneyPaymentMethods( - moneyCenterAuthToken: String - ) - - func fetchAccount( - oauthToken: String - ) - - func decryptCryptogram( - _ cryptogram: String - ) - + func fetchYooMoneyPaymentMethods(moneyCenterAuthToken: String) + func fetchAccount(oauthToken: String) + func decryptCryptogram(_ cryptogram: String) func getWalletDisplayName() -> String? - func setAccount(_ account: UserAccount) - func startAnalyticsService() - func stopAnalyticsService() // MARK: - Apple Pay Tokenize - func tokenizeApplePay( - paymentData: String, + func tokenizeApplePay(paymentData: String, savePaymentMethod: Bool, amount: MonetaryAmount) + func tokenizeInstrument( + instrument: PaymentInstrumentBankCard, savePaymentMethod: Bool, + returnUrl: String?, amount: MonetaryAmount ) + func unbindCard(id: String) } protocol PaymentMethodsInteractorOutput: AnyObject { - func didFetchPaymentMethods( - _ paymentMethods: [PaymentOption] - ) - func didFetchPaymentMethods( - _ error: Error - ) + func didFetchShop(_: Shop) + func didFailFetchShop(_ error: Error) - func didFetchYooMoneyPaymentMethods( - _ paymentMethods: [PaymentOption] - ) - func didFetchYooMoneyPaymentMethods( - _ error: Error - ) + func didFetchYooMoneyPaymentMethods(_ paymentMethods: [PaymentOption], shopProperties: ShopProperties) + func didFetchYooMoneyPaymentMethods(_ error: Error) - func didFetchAccount( - _ account: UserAccount - ) - func didFailFetchAccount( - _ error: Error - ) + func didFetchAccount(_ account: UserAccount) + func didFailFetchAccount(_ error: Error) - func didDecryptCryptogram( - _ token: String - ) - func didFailDecryptCryptogram( - _ error: Error - ) + func didDecryptCryptogram(_ token: String) + func didFailDecryptCryptogram(_ error: Error) - func didTokenizeApplePay( - _ token: Tokens - ) - func failTokenizeApplePay( - _ error: Error - ) + func didTokenizeApplePay(_ token: Tokens) + func failTokenizeApplePay(_ error: Error) + + func didUnbindCard(id: String) + func didFailUnbindCard(id: String, error: Error) + + func didTokenizeInstrument(instrument: PaymentInstrumentBankCard, tokens: Tokens) + func didFailTokenizeInstrument(error: Error) } diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsModuleIO.swift b/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsModuleIO.swift index b6c22e56..7f230aae 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsModuleIO.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsModuleIO.swift @@ -17,6 +17,7 @@ struct PaymentMethodsModuleInputData { let savePaymentMethod: SavePaymentMethod let userPhoneNumber: String? let cardScanning: CardScanning? + let customerId: String? } protocol PaymentMethodsModuleInput: SheetViewModuleOutput { diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsRouterIO.swift b/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsRouterIO.swift index e59dec08..6eb16453 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsRouterIO.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsRouterIO.swift @@ -58,4 +58,8 @@ protocol PaymentMethodsRouterInput: AnyObject { ) func closeCardSecModule() + + func openCardSettingsModule(data: CardSettingsModuleInputData, output: CardSettingsModuleOutput) + func closeCardSettingsModule() + func showUnbindAlert(unbindHandler: @escaping (UIAlertAction) -> Void) } diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsViewIO.swift b/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsViewIO.swift index 03c25394..4786126b 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsViewIO.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/PaymentMethodsViewIO.swift @@ -15,4 +15,5 @@ protocol PaymentMethodsViewOutput: ActionTitleTextDialogDelegate { func numberOfRows() -> Int func viewModelForRow(at indexPath: IndexPath) -> PaymentMethodViewModel? func didSelect(at indexPath: IndexPath) + func didPressSettings(at indexPath: IndexPath) } diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/Presenter/PaymentMethodsPresenter.swift b/YooKassaPayments/Private/Modules/PaymentMethods/Presenter/PaymentMethodsPresenter.swift index b464c315..bbcf8295 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/Presenter/PaymentMethodsPresenter.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/Presenter/PaymentMethodsPresenter.swift @@ -48,6 +48,7 @@ final class PaymentMethodsPresenter: NSObject { private let savePaymentMethod: SavePaymentMethod private let userPhoneNumber: String? private let cardScanning: CardScanning? + private let customerId: String? // MARK: - Init @@ -69,7 +70,8 @@ final class PaymentMethodsPresenter: NSObject { returnUrl: String?, savePaymentMethod: SavePaymentMethod, userPhoneNumber: String?, - cardScanning: CardScanning? + cardScanning: CardScanning?, + customerId: String? ) { self.isLogoVisible = isLogoVisible self.paymentMethodViewModelFactory = paymentMethodViewModelFactory @@ -92,6 +94,7 @@ final class PaymentMethodsPresenter: NSObject { self.savePaymentMethod = savePaymentMethod self.userPhoneNumber = userPhoneNumber self.cardScanning = cardScanning + self.customerId = customerId } // MARK: - Stored properties @@ -99,8 +102,8 @@ final class PaymentMethodsPresenter: NSObject { private var moneyAuthCoordinator: MoneyAuth.AuthorizationCoordinator? private var yooMoneyTMXSessionId: String? - private var paymentMethods: [PaymentOption]? - private var viewModels: [PaymentMethodViewModel] = [] + private var shop: Shop? + private var viewModel: (models: [PaymentMethodViewModel], indexMap: ([Int: Int])) = ([], [:]) private lazy var termsOfService: TermsOfService = { TermsOfServiceFactory.makeTermsOfService() @@ -115,6 +118,8 @@ final class PaymentMethodsPresenter: NSObject { private var applePayCompletion: ((PKPaymentAuthorizationStatus) -> Void)? private var applePayState: ApplePayState = .idle private var applePayPaymentOption: PaymentOption? + + private var unbindCompletion: ((Bool) -> Void)? } // MARK: - PaymentMethodsViewOutput @@ -142,50 +147,117 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { func applicationDidBecomeActive() { if app2AppState == .idle, - paymentMethods?.count == 1, - paymentMethods?.first?.paymentMethodType == .yooMoney { + shop?.options.count == 1, + shop?.options.first?.paymentMethodType == .yooMoney { didFinish(module: self, error: nil) } } func numberOfRows() -> Int { - viewModels.count + viewModel.models.count } func viewModelForRow( at indexPath: IndexPath ) -> PaymentMethodViewModel? { - guard viewModels.indices.contains(indexPath.row) else { + guard viewModel.models.indices.contains(indexPath.row) else { assertionFailure("ViewModel at index \(indexPath.row) should be") return nil } - return viewModels[indexPath.row] + return viewModel.models[indexPath.row] } func didSelect(at indexPath: IndexPath) { - guard let paymentMethods = paymentMethods, - paymentMethods.indices.contains(indexPath.row) else { + guard + let shop = shop, + let optionIndex = viewModel.indexMap[indexPath.row] + else { + return assertionFailure("ViewModel at index \(indexPath.row) should be") + } + + let method = shop.options[optionIndex] + if + viewModel.models[indexPath.row].isShopLinkedCard, + let cardOption = method as? PaymentOptionBankCard + { + guard + let id = viewModel.models[indexPath.row].id, + let card = cardOption.paymentInstruments?.first(where: { $0.paymentInstrumentId == id }) + else { return assertionFailure("Couldn't match cardInstrument for indexPath: \(indexPath)") } + + openBankCardModule( + paymentOption: method, + instrument: card, + isSafeDeal: shop.isSafeDeal, + needReplace: false + ) + } else { + openPaymentMethod(method, isSafeDeal: shop.isSafeDeal, needReplace: false) + } + } + + func didPressSettings(at indexPath: IndexPath) { + guard + let optionIndex = viewModel.indexMap[indexPath.row], + let method = shop?.options[optionIndex] + else { assertionFailure("ViewModel at index \(indexPath.row) should be") return } - openPaymentMethod( - paymentMethods[indexPath.row], - needReplace: false - ) + let filtered = viewModel.indexMap.filter { $0.value == optionIndex }.values.sorted() + + switch method { + case let cardOption as PaymentOptionBankCard: + guard + let instrumentIndex = filtered.firstIndex(of: optionIndex), + let card = cardOption.paymentInstruments?[instrumentIndex] + else { return assertionFailure("Couldn't match cardInstrument for indexPath: \(indexPath)") } + + router?.openCardSettingsModule( + data: CardSettingsModuleInputData( + cardLogo: viewModel.models[indexPath.row].image, + cardMask: (card.first6 ?? "") + "••••••" + card.last4, + infoText: CommonLocalized.CardSettingsDetails.autopaymentPersists, + card: .card(name: viewModel.models[indexPath.row].title, id: card.paymentInstrumentId), + testModeSettings: testModeSettings, + tokenizationSettings: tokenizationSettings, + isLoggingEnabled: isLoggingEnabled, + clientId: clientApplicationKey + ), + output: self + ) + case let option as PaymentInstrumentYooMoneyLinkedBankCard: + router?.openCardSettingsModule( + data: CardSettingsModuleInputData( + cardLogo: viewModel.models[indexPath.row].image, + cardMask: option.cardMask, + infoText: CommonLocalized.CardSettingsDetails.yoocardUnbindDetails, + card: .yoomoney(name: viewModel.models[indexPath.row].title), + testModeSettings: testModeSettings, + tokenizationSettings: tokenizationSettings, + isLoggingEnabled: isLoggingEnabled, + clientId: clientApplicationKey + ), + output: self + ) + default: + assertionFailure("Only card and yoocard are supported \(indexPath.row)") + } } private func openPaymentMethod( _ paymentOption: PaymentOption, + isSafeDeal: Bool, needReplace: Bool ) { switch paymentOption { case let paymentOption as PaymentInstrumentYooMoneyLinkedBankCard: - openLinkedCard(paymentOption: paymentOption, needReplace: needReplace) + openLinkedCard(paymentOption: paymentOption, isSafeDeal: isSafeDeal, needReplace: needReplace) case let paymentOption as PaymentInstrumentYooMoneyWallet: - openYooMoneyWallet(paymentOption: paymentOption, needReplace: needReplace) + openYooMoneyWallet(paymentOption: paymentOption, isSafeDeal: isSafeDeal, needReplace: needReplace) case let paymentOption where paymentOption.paymentMethodType == .yooMoney: openYooMoneyAuthorization() @@ -193,22 +265,34 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { case let paymentOption where paymentOption.paymentMethodType == .sberbank: if shouldOpenSberpay(paymentOption), let returnUrl = makeSberpayReturnUrl() { - openSberpayModule(paymentOption: paymentOption, needReplace: needReplace, returnUrl: returnUrl) + openSberpayModule( + paymentOption: paymentOption, + isSafeDeal: isSafeDeal, + needReplace: needReplace, + returnUrl: returnUrl + ) } else { - openSberbankModule(paymentOption: paymentOption, needReplace: needReplace) + openSberbankModule(paymentOption: paymentOption, isSafeDeal: isSafeDeal, needReplace: needReplace) } case let paymentOption where paymentOption.paymentMethodType == .applePay: - openApplePay(paymentOption: paymentOption, needReplace: needReplace) + openApplePay(paymentOption: paymentOption, isSafeDeal: isSafeDeal, needReplace: needReplace) case let paymentOption where paymentOption.paymentMethodType == .bankCard: - openBankCardModule(paymentOption: paymentOption, needReplace: needReplace) + openBankCardModule(paymentOption: paymentOption, isSafeDeal: isSafeDeal, needReplace: needReplace) default: break } } + private func handleOpenPaymentMethodInstrument( + _ instrument: PaymentInstrumentBankCard + ) { + // TODO: Handle instrument payment MOC-2060 + print("\(#function) in \(self)") + } + private func openYooMoneyAuthorization() { if testModeSettings != nil { view?.showActivity() @@ -255,6 +339,7 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { private func openYooMoneyWallet( paymentOption: PaymentInstrumentYooMoneyWallet, + isSafeDeal: Bool, needReplace: Bool ) { let walletDisplayName = interactor.getWalletDisplayName() @@ -287,7 +372,9 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { savePaymentMethodViewModel: savePaymentMethodViewModel, tmxSessionId: yooMoneyTMXSessionId, initialSavePaymentMethod: initialSavePaymentMethod, - isBackBarButtonHidden: needReplace + isBackBarButtonHidden: needReplace, + customerId: customerId, + isSafeDeal: isSafeDeal ) router?.presentYooMoney( inputData: inputData, @@ -297,6 +384,7 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { private func openLinkedCard( paymentOption: PaymentInstrumentYooMoneyLinkedBankCard, + isSafeDeal: Bool, needReplace: Bool ) { let initialSavePaymentMethod = makeInitialSavePaymentMethod(savePaymentMethod) @@ -317,7 +405,9 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { returnUrl: returnUrl, tmxSessionId: yooMoneyTMXSessionId, initialSavePaymentMethod: initialSavePaymentMethod, - isBackBarButtonHidden: needReplace + isBackBarButtonHidden: needReplace, + customerId: customerId, + isSafeDeal: isSafeDeal ) router?.presentLinkedCard( inputData: inputData, @@ -327,6 +417,7 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { private func openApplePay( paymentOption: PaymentOption, + isSafeDeal: Bool, needReplace: Bool ) { let feeCondition = paymentOption.fee != nil @@ -335,7 +426,7 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { let savePaymentMethodCondition = paymentOption.savePaymentMethod == .allowed && inputSavePaymentMethodCondition - if feeCondition || savePaymentMethodCondition { + if feeCondition || savePaymentMethodCondition || isSafeDeal { let initialSavePaymentMethod = makeInitialSavePaymentMethod(savePaymentMethod) let savePaymentMethodViewModel = SavePaymentMethodViewModelFactory.makeSavePaymentMethodViewModel( paymentOption, @@ -358,7 +449,9 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { merchantIdentifier: applePayMerchantIdentifier, savePaymentMethodViewModel: savePaymentMethodViewModel, initialSavePaymentMethod: initialSavePaymentMethod, - isBackBarButtonHidden: needReplace + isBackBarButtonHidden: needReplace, + customerId: customerId, + isSafeDeal: isSafeDeal ) router.presentApplePayContractModule( inputData: inputData, @@ -384,6 +477,7 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { private func openSberbankModule( paymentOption: PaymentOption, + isSafeDeal: Bool, needReplace: Bool ) { let priceViewModel = priceViewModelFactory.makeAmountPriceViewModel(paymentOption) @@ -400,7 +494,9 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { feeViewModel: feeViewModel, termsOfService: termsOfService, userPhoneNumber: userPhoneNumber, - isBackBarButtonHidden: needReplace + isBackBarButtonHidden: needReplace, + customerId: customerId, + isSafeDeal: isSafeDeal ) router.openSberbankModule( inputData: inputData, @@ -410,6 +506,7 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { private func openSberpayModule( paymentOption: PaymentOption, + isSafeDeal: Bool, needReplace: Bool, returnUrl: String ) { @@ -427,7 +524,9 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { feeViewModel: feeViewModel, termsOfService: termsOfService, returnUrl: returnUrl, - isBackBarButtonHidden: needReplace + isBackBarButtonHidden: needReplace, + customerId: customerId, + isSafeDeal: isSafeDeal ) router.openSberpayModule( inputData: inputData, @@ -437,16 +536,35 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { private func openBankCardModule( paymentOption: PaymentOption, + instrument: PaymentInstrumentBankCard? = nil, + isSafeDeal: Bool, needReplace: Bool ) { let priceViewModel = priceViewModelFactory.makeAmountPriceViewModel(paymentOption) let feeViewModel = priceViewModelFactory.makeFeePriceViewModel(paymentOption) - let initialSavePaymentMethod = makeInitialSavePaymentMethod(savePaymentMethod) - let savePaymentMethodViewModel = SavePaymentMethodViewModelFactory.makeSavePaymentMethodViewModel( - paymentOption, - savePaymentMethod, - initialState: initialSavePaymentMethod - ) + + if let instrument = instrument, !isSafeDeal { + let hasService = paymentOption.fee?.service.map { $0.charge.value > 0.00 } ?? false + let hasCounterparty = paymentOption.fee?.counterparty.map { $0.charge.value > 0.00 } ?? false + let hasFee = hasService || hasCounterparty + + if savePaymentMethod == .off, !hasFee, !instrument.cscRequired { + DispatchQueue.main.async { + self.view?.showActivity() + } + + DispatchQueue.global().async { + self.interactor.tokenizeInstrument( + instrument: instrument, + savePaymentMethod: false, + returnUrl: self.returnUrl, + amount: paymentOption.charge.plain + ) + } + return + } + } + let inputData = BankCardModuleInputData( clientApplicationKey: clientApplicationKey, testModeSettings: testModeSettings, @@ -460,9 +578,13 @@ extension PaymentMethodsPresenter: PaymentMethodsViewOutput { termsOfService: termsOfService, cardScanning: cardScanning, returnUrl: returnUrl, - savePaymentMethodViewModel: savePaymentMethodViewModel, - initialSavePaymentMethod: initialSavePaymentMethod, - isBackBarButtonHidden: needReplace + savePaymentMethod: savePaymentMethod, + canSaveInstrument: paymentOption.savePaymentInstrument ?? false, + apiSavePaymentMethod: paymentOption.savePaymentMethod, + isBackBarButtonHidden: needReplace, + customerId: customerId, + instrument: instrument, + isSafeDeal: isSafeDeal ) router.openBankCardModule( inputData: inputData, @@ -602,7 +724,24 @@ extension PaymentMethodsPresenter: PaymentMethodsModuleInput { // MARK: - PaymentMethodsInteractorOutput extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { - func didFetchPaymentMethods(_ paymentMethods: [PaymentOption]) { + func didUnbindCard(id: String) { + interactor.fetchPaymentMethods() + DispatchQueue.main.async { + self.unbindCompletion?(true) + self.unbindCompletion = nil + } + } + + func didFailUnbindCard(id: String, error: Error) { + DispatchQueue.main.async { + self.view?.hideActivity() + self.view?.presentError(with: Localized.Error.unbindCardFailed) + self.unbindCompletion?(false) + self.unbindCompletion = nil + } + } + + func didFetchShop(_ shop: Shop) { let (authType, _) = interactor.makeTypeAnalyticsParameters() let event: AnalyticsEvent = .screenPaymentOptions( authType: authType, @@ -611,43 +750,55 @@ extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { interactor.trackEvent(event) DispatchQueue.main.async { [weak self] in - guard let self = self, - let view = self.view else { return } + guard let self = self, let view = self.view else { return } - self.paymentMethods = paymentMethods + self.shop = shop - if paymentMethods.count == 1, let paymentMethod = paymentMethods.first { - self.openPaymentMethod(paymentMethod, needReplace: true) - } else { + func showOptions() { let walletDisplayName = self.interactor.getWalletDisplayName() - self.viewModels = paymentMethods.map { - self.paymentMethodViewModelFactory.makePaymentMethodViewModel( - paymentOption: $0, - walletDisplayName: walletDisplayName - ) - } + self.viewModel = self.paymentMethodViewModelFactory.makePaymentMethodViewModels( + shop.options, + walletDisplayName: walletDisplayName + ) view.hideActivity() view.reloadData() } + + if shop.options.count == 1, let first = shop.options.first { + switch first { + case let paymentMethod as PaymentOptionBankCard: + if (paymentMethod.paymentInstruments?.count ?? 0) > 0 { + showOptions() + } else { + self.openPaymentMethod( + paymentMethod, + isSafeDeal: shop.isSafeDeal, + needReplace: true + ) + } + default: + self.openPaymentMethod(first, isSafeDeal: shop.isSafeDeal, needReplace: true) + } + } else { + showOptions() + } } } - func didFetchPaymentMethods(_ error: Error) { + func didFailFetchShop(_ error: Error) { presentError(error) } - func didFetchYooMoneyPaymentMethods( - _ paymentMethods: [PaymentOption] - ) { + func didFetchYooMoneyPaymentMethods(_ paymentMethods: [PaymentOption], shopProperties: ShopProperties) { let condition: (PaymentOption) -> Bool = { $0 is PaymentInstrumentYooMoneyWallet } - if let paymentOption = paymentMethods.first as? PaymentInstrumentYooMoneyWallet, - paymentMethods.count == 1 { - let needReplace = self.paymentMethods?.count == 1 + if let paymentOption = paymentMethods.first as? PaymentInstrumentYooMoneyWallet, paymentMethods.count == 1 { + let needReplace = self.shop?.options.count == 1 DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.openYooMoneyWallet( paymentOption: paymentOption, + isSafeDeal: shopProperties.isSafeDeal || shopProperties.isMarketplace, needReplace: needReplace ) self.shouldReloadOnViewDidAppear = true @@ -671,21 +822,15 @@ extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { presentError(error) } - func didFetchAccount( - _ account: UserAccount - ) { + func didFetchAccount(_ account: UserAccount) { guard let moneyCenterAuthToken = moneyCenterAuthToken else { return } interactor.setAccount(account) - interactor.fetchYooMoneyPaymentMethods( - moneyCenterAuthToken: moneyCenterAuthToken - ) + interactor.fetchYooMoneyPaymentMethods(moneyCenterAuthToken: moneyCenterAuthToken) } - func didFailFetchAccount( - _ error: Error - ) { + func didFailFetchAccount(_ error: Error) { guard let moneyCenterAuthToken = moneyCenterAuthToken else { return } @@ -694,9 +839,7 @@ extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { ) } - func didDecryptCryptogram( - _ token: String - ) { + func didDecryptCryptogram(_ token: String) { moneyCenterAuthToken = token DispatchQueue.global().async { [weak self] in guard let self = self else { return } @@ -710,9 +853,7 @@ extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { } } - func didFailDecryptCryptogram( - _ error: Error - ) { + func didFailDecryptCryptogram(_ error: Error) { let event: AnalyticsEvent = .actionMoneyAuthLogin( scheme: .yoomoneyApp, status: .fail(error.localizedDescription), @@ -726,9 +867,7 @@ extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { } } - func didTokenizeApplePay( - _ token: Tokens - ) { + func didTokenizeApplePay(_ token: Tokens) { guard applePayState == .success else { return } @@ -758,9 +897,7 @@ extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { } } - func failTokenizeApplePay( - _ error: Error - ) { + func failTokenizeApplePay(_ error: Error) { guard applePayState == .success else { return } @@ -776,7 +913,7 @@ extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { guard let self = self else { return } let message = CommonLocalized.ApplePay.failTokenizeData - if self.paymentMethods?.count == 1 { + if self.shop?.options.count == 1 { self.view?.hideActivity() self.view?.showPlaceholder(message: message) } else { @@ -805,9 +942,7 @@ extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { } } - private func trackScreenErrorAnalytics( - scheme: AnalyticsEvent.TokenizeScheme? - ) { + private func trackScreenErrorAnalytics(scheme: AnalyticsEvent.TokenizeScheme?) { DispatchQueue.global().async { [weak self] in guard let interactor = self?.interactor else { return } let (authType, _) = interactor.makeTypeAnalyticsParameters() @@ -820,9 +955,7 @@ extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { } } - 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() @@ -834,6 +967,55 @@ extension PaymentMethodsPresenter: PaymentMethodsInteractorOutput { interactor.trackEvent(event) } } + + func didTokenizeInstrument(instrument: PaymentInstrumentBankCard, tokens: Tokens) { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.view?.hideActivity() + + let scheme: AnalyticsEvent.TokenizeScheme = instrument.cscRequired + ? .customerIdLinkedCardCvc + : .customerIdLinkedCard + + 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 + ) + interactor.trackEvent(event) + } + } + } + + func didFailTokenizeInstrument(error: Error) { + let parameters = interactor.makeTypeAnalyticsParameters() + let event: AnalyticsEvent = .screenError( + authType: parameters.authType, + scheme: .bankCard, + sdkVersion: Bundle.frameworkVersion + ) + interactor.trackEvent(event) + + let message: String + switch error { + case let error as PresentableError: + message = error.message + default: + message = CommonLocalized.Error.unknown + } + + DispatchQueue.main.async { [weak self] in + guard let view = self?.view else { return } + view.hideActivity() + view.presentError(with: message) + } + } } // MARK: - ProcessCoordinatorDelegate @@ -878,9 +1060,7 @@ extension PaymentMethodsPresenter: AuthorizationCoordinatorDelegate { } } - func authorizationCoordinatorDidCancel( - _ coordinator: AuthorizationCoordinator - ) { + func authorizationCoordinatorDidCancel(_ coordinator: AuthorizationCoordinator) { self.moneyAuthCoordinator = nil let event = AnalyticsEvent.userCancelAuthorization( @@ -893,16 +1073,13 @@ extension PaymentMethodsPresenter: AuthorizationCoordinatorDelegate { if self.router.shouldDismissAuthorizationModule() { self.router.closeAuthorizationModule() } - if self.paymentMethods?.count == 1 { + if self.shop?.options.count == 1 { self.didFinish(module: self, error: nil) } } } - func authorizationCoordinator( - _ coordinator: AuthorizationCoordinator, - didFailureWith error: Error - ) { + func authorizationCoordinator(_ coordinator: AuthorizationCoordinator, didFailureWith error: Error) { self.moneyAuthCoordinator = nil let event: AnalyticsEvent = .actionMoneyAuthLogin( @@ -922,18 +1099,14 @@ extension PaymentMethodsPresenter: AuthorizationCoordinatorDelegate { } } - func authorizationCoordinatorDidPrepareProcess( - _ coordinator: AuthorizationCoordinator - ) {} + func authorizationCoordinatorDidPrepareProcess(_ coordinator: AuthorizationCoordinator) {} func authorizationCoordinator( _ coordinator: AuthorizationCoordinator, didFailPrepareProcessWithError error: Error ) {} - func authorizationCoordinatorDidRecoverPassword( - _ coordinator: AuthorizationCoordinator - ) {} + func authorizationCoordinatorDidRecoverPassword(_ coordinator: AuthorizationCoordinator) {} } // MARK: - YooMoneyModuleOutput @@ -947,11 +1120,11 @@ extension PaymentMethodsPresenter: YooMoneyModuleOutput { self.moneyCenterAuthToken = nil self.app2AppState = .idle let condition: (PaymentOption) -> Bool = { - $0 is PaymentInstrumentYooMoneyLinkedBankCard - || $0 is PaymentInstrumentYooMoneyWallet - || $0.paymentMethodType == .yooMoney + return $0 is PaymentInstrumentYooMoneyLinkedBankCard + || $0 is PaymentInstrumentYooMoneyWallet + || $0.paymentMethodType == .yooMoney } - if let paymentMethods = self.paymentMethods, + if let paymentMethods = self.shop?.options, paymentMethods.allSatisfy(condition) { self.didFinish(module: self, error: nil) } else { @@ -1012,7 +1185,7 @@ extension PaymentMethodsPresenter: ApplePayModuleOutput { let view = self.view else { return } let message = CommonLocalized.ApplePay.applePayUnavailableTitle - if self.paymentMethods?.count == 1 { + if self.shop?.options.count == 1 { view.hideActivity() view.showPlaceholder(message: message) } else { @@ -1064,7 +1237,7 @@ extension PaymentMethodsPresenter: ApplePayModuleOutput { router.closeApplePay(completion: nil) applePayState = .cancel - if paymentMethods?.count == 1 { + if shop?.options.count == 1 { didFinish(module: self, error: nil) } } @@ -1225,6 +1398,36 @@ extension PaymentMethodsPresenter: CardSecModuleOutput { } } +// MARK: - CardSecModuleOutput + +extension PaymentMethodsPresenter: CardSettingsModuleOutput { + func cardSettingsModuleDidCancel() { + DispatchQueue.main.async { + self.router.closeCardSettingsModule() + } + } + func cardSettingsModuleDidUnbindCard(mask: String) { + let notification = UIViewController.ToastAlertNotification( + title: nil, + message: String(format: CommonLocalized.CardSettingsDetails.unbindSuccess, mask), + type: .success, + style: .toast, + actions: [] + ) + + DispatchQueue.main.async { + self.view?.present(notification) + self.view?.showActivity() + self.view?.setLogoVisible(self.isLogoVisible) + self.router.closeCardSettingsModule() + } + + DispatchQueue.global().async { [weak self] in + self?.interactor.fetchPaymentMethods() + } + } +} + // MARK: - Private helpers private extension PaymentMethodsPresenter { @@ -1295,6 +1498,12 @@ private extension PaymentMethodsPresenter { value: "Оплата кошельком недоступна", comment: "После авторизации в кошельке при запросе доступных методов кошелёк отсутствует" ) + static let unbindCardFailed = NSLocalizedString( + "Error.unbindCardFailed", + bundle: Bundle.framework, + value: "Не удалось отвязать карту. Попробуйте ещё раз", + comment: "Текст ошибки при отвязке карты" + ) } } } diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/Router/PaymentMethodsRouter.swift b/YooKassaPayments/Private/Modules/PaymentMethods/Router/PaymentMethodsRouter.swift index 3d329987..3debf45f 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/Router/PaymentMethodsRouter.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/Router/PaymentMethodsRouter.swift @@ -179,4 +179,53 @@ extension PaymentMethodsRouter: PaymentMethodsRouterInput { completion: nil ) } + + func openCardSettingsModule(data: CardSettingsModuleInputData, output: CardSettingsModuleOutput) { + transitionHandler?.push(CardSettingsAssembly.make(data: data, output: output), animated: true) + } + + func closeCardSettingsModule() { + transitionHandler?.popTopViewController(animated: true) + } + + func showUnbindAlert(unbindHandler: @escaping (UIAlertAction) -> Void) { + let ctrl = UIAlertController( + title: nil, + message: CommonLocalized.CardSettingsDetails.autopaymentPersists, + preferredStyle: .alert + ) + let ok = UIAlertAction( + title: Localized.Alert.unbindCard, + style: .default, + handler: unbindHandler + ) + let cancel = UIAlertAction( + title: Localized.Alert.cancel, + style: .destructive, + handler: nil + ) + + ctrl.view.tintColor = CustomizationStorage.shared.mainScheme + ctrl.addAction(ok) + ctrl.addAction(cancel) + transitionHandler?.present(ctrl, animated: true, completion: nil) + } + + // MARK: - Localized + private enum Localized { + enum Alert { + static let unbindCard = NSLocalizedString( + "PaymentMethods.alert.unbindCard", + bundle: Bundle.framework, + value: "Отвязать", + comment: "Текст кнопки отвязать https://disk.yandex.ru/i/f9rYGyNbx2HJ0Q" + ) + static let cancel = NSLocalizedString( + "PaymentMethods.alert.cancel", + bundle: Bundle.framework, + value: "Отмена", + comment: "Текст кнопки отвязать https://disk.yandex.ru/i/f9rYGyNbx2HJ0Q" + ) + } + } } diff --git a/YooKassaPayments/Private/Modules/PaymentMethods/View/PaymentMethodsViewController.swift b/YooKassaPayments/Private/Modules/PaymentMethods/View/PaymentMethodsViewController.swift index d9ef2010..e44f297c 100644 --- a/YooKassaPayments/Private/Modules/PaymentMethods/View/PaymentMethodsViewController.swift +++ b/YooKassaPayments/Private/Modules/PaymentMethods/View/PaymentMethodsViewController.swift @@ -191,11 +191,23 @@ extension PaymentMethodsViewController: UITableViewDataSource { withType: LargeIconButtonItemViewCell.self, for: indexPath ) + largeCell.rightButton.setImage(nil, for: .normal) + largeCell.rightButtonPressHandler = nil largeCell.icon = viewModel.image largeCell.title = viewModel.title largeCell.subtitle = subtitle cell = largeCell + + if viewModel.hasActions { + largeCell.rightButton.setImage(PaymentMethodResources.Image.more, for: .normal) + largeCell.rightButton.contentEdgeInsets = UIEdgeInsets( + top: Space.double, left: Space.double, bottom: Space.double, right: Space.double + ) + largeCell.rightButtonPressHandler = { [weak self] in + self?.output.didPressSettings(at: indexPath) + } + } } else { let smallCell = tableView.dequeueReusableCell( withType: IconButtonItemTableViewCell.self, @@ -335,6 +347,12 @@ private extension PaymentMethodsViewController { value: "Способ оплаты", comment: "Title `Способ оплаты` на экране выбора способа оплаты https://yadi.sk/i/0dSpSggROTC0Jw" ) + static let unbindCard = NSLocalizedString( + "PaymentMethods.unbindCard", + bundle: Bundle.framework, + value: "Отвязать", + comment: "Текст кнопки отвязать " + ) } enum Resources { diff --git a/YooKassaPayments/Private/Modules/SavePaymentMethodInfo/View/SavePaymentMethodInfoViewController.swift b/YooKassaPayments/Private/Modules/SavePaymentMethodInfo/View/SavePaymentMethodInfoViewController.swift index 76f59ebd..ca497593 100644 --- a/YooKassaPayments/Private/Modules/SavePaymentMethodInfo/View/SavePaymentMethodInfoViewController.swift +++ b/YooKassaPayments/Private/Modules/SavePaymentMethodInfo/View/SavePaymentMethodInfoViewController.swift @@ -48,15 +48,12 @@ final class SavePaymentMethodInfoViewController: UIViewController { return view }() - private lazy var closeBarButtonItem: UIBarButtonItem = { - $0.tintColor = CustomizationStorage.shared.mainScheme - return $0 - }(UIBarButtonItem( + private lazy var closeBarButtonItem = UIBarButtonItem( image: UIImage.named("Common.close"), style: .plain, target: self, action: #selector(closeBarButtonItemDidPress) - )) + ) fileprivate lazy var actionButtonStackView: UIStackView = { $0.setStyles(UIView.Styles.grayBackground) @@ -66,19 +63,17 @@ final class SavePaymentMethodInfoViewController: UIViewController { }(UIStackView()) private lazy var gotItButton: Button = { - $0.tintColor = CustomizationStorage.shared.mainScheme - $0.setStyles( - UIButton.DynamicStyle.primary, - UIView.Styles.heightAsContent - ) - $0.setStyledTitle(Localized.buttonGotIt, for: .normal) - $0.addTarget( + let button = Button(type: .custom) + button.setTitle(Localized.buttonGotIt, for: .normal) + button.style.submit() + button.addTarget( self, action: #selector(closeBarButtonItemDidPress), for: .touchUpInside ) - return $0 - }(Button(type: .custom)) + + return button + }() // MARK: - Managing the View diff --git a/YooKassaPayments/Private/Modules/Sberbank/Assembly/SberbankAssembly.swift b/YooKassaPayments/Private/Modules/Sberbank/Assembly/SberbankAssembly.swift index 7bef7c8d..b39e584e 100644 --- a/YooKassaPayments/Private/Modules/Sberbank/Assembly/SberbankAssembly.swift +++ b/YooKassaPayments/Private/Modules/Sberbank/Assembly/SberbankAssembly.swift @@ -13,7 +13,8 @@ enum SberbankAssembly { feeViewModel: inputData.feeViewModel, termsOfService: inputData.termsOfService, userPhoneNumber: inputData.userPhoneNumber, - isBackBarButtonHidden: inputData.isBackBarButtonHidden + isBackBarButtonHidden: inputData.isBackBarButtonHidden, + isSafeDeal: inputData.isSafeDeal ) let paymentService = PaymentServiceAssembly.makeService( tokenizationSettings: inputData.tokenizationSettings, @@ -33,7 +34,8 @@ enum SberbankAssembly { analyticsService: analyticsService, threatMetrixService: threatMetrixService, clientApplicationKey: inputData.clientApplicationKey, - amount: inputData.paymentOption.charge.plain + amount: inputData.paymentOption.charge.plain, + customerId: inputData.customerId ) let router = SberbankRouter() diff --git a/YooKassaPayments/Private/Modules/Sberbank/Interactor/SberbankInteractor.swift b/YooKassaPayments/Private/Modules/Sberbank/Interactor/SberbankInteractor.swift index 4c54be81..20cb2d0c 100644 --- a/YooKassaPayments/Private/Modules/Sberbank/Interactor/SberbankInteractor.swift +++ b/YooKassaPayments/Private/Modules/Sberbank/Interactor/SberbankInteractor.swift @@ -14,6 +14,7 @@ final class SberbankInteractor { private let threatMetrixService: ThreatMetrixService private let clientApplicationKey: String private let amount: MonetaryAmount + private let customerId: String? init( paymentService: PaymentService, @@ -21,7 +22,8 @@ final class SberbankInteractor { analyticsService: AnalyticsService, threatMetrixService: ThreatMetrixService, clientApplicationKey: String, - amount: MonetaryAmount + amount: MonetaryAmount, + customerId: String? ) { self.paymentService = paymentService self.analyticsProvider = analyticsProvider @@ -29,6 +31,7 @@ final class SberbankInteractor { self.threatMetrixService = threatMetrixService self.clientApplicationKey = clientApplicationKey self.amount = amount + self.customerId = customerId } } @@ -39,8 +42,7 @@ extension SberbankInteractor: SberbankInteractorInput { phoneNumber: String ) { 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): @@ -54,7 +56,8 @@ extension SberbankInteractor: SberbankInteractorInput { confirmation: confirmation, savePaymentMethod: false, amount: self.amount, - tmxSessionId: tmxSessionId.value + tmxSessionId: tmxSessionId.value, + customerId: self.customerId ) { result in switch result { case .success(let data): diff --git a/YooKassaPayments/Private/Modules/Sberbank/Presenter/SberbankPresenter.swift b/YooKassaPayments/Private/Modules/Sberbank/Presenter/SberbankPresenter.swift index 01395de5..901fd5dc 100644 --- a/YooKassaPayments/Private/Modules/Sberbank/Presenter/SberbankPresenter.swift +++ b/YooKassaPayments/Private/Modules/Sberbank/Presenter/SberbankPresenter.swift @@ -22,6 +22,7 @@ final class SberbankPresenter { private let termsOfService: TermsOfService private let userPhoneNumber: String? private let isBackBarButtonHidden: Bool + private let isSafeDeal: Bool init( shopName: String, @@ -30,7 +31,8 @@ final class SberbankPresenter { feeViewModel: PriceViewModel?, termsOfService: TermsOfService, userPhoneNumber: String?, - isBackBarButtonHidden: Bool + isBackBarButtonHidden: Bool, + isSafeDeal: Bool ) { self.shopName = shopName self.purchaseDescription = purchaseDescription @@ -39,6 +41,7 @@ final class SberbankPresenter { self.termsOfService = termsOfService self.userPhoneNumber = userPhoneNumber self.isBackBarButtonHidden = isBackBarButtonHidden + self.isSafeDeal = isSafeDeal } // MARK: - Stored properties @@ -68,7 +71,8 @@ extension SberbankPresenter: SberbankViewOutput { description: purchaseDescription, priceValue: priceValue, feeValue: feeValue, - termsOfService: termsOfServiceValue + termsOfService: termsOfServiceValue, + safeDealText: isSafeDeal ? PaymentMethodResources.Localized.safeDealInfoLink : nil ) view.setViewModel(viewModel) @@ -107,11 +111,16 @@ extension SberbankPresenter: SberbankViewOutput { } } - func didPressTermsOfService( - _ url: URL - ) { + func didPressTermsOfService(_ url: URL) { router.presentTermsOfServiceModule(url) } + + func didTapSafeDealInfo(_ url: URL) { + router.presentSafeDealInfo( + title: PaymentMethodResources.Localized.safeDealInfoTitle, + body: PaymentMethodResources.Localized.safeDealInfoBody + ) + } } // MARK: - SberbankInteractorOutput diff --git a/YooKassaPayments/Private/Modules/Sberbank/Router/SberbankRouter.swift b/YooKassaPayments/Private/Modules/Sberbank/Router/SberbankRouter.swift index 692cc272..e7a950bf 100644 --- a/YooKassaPayments/Private/Modules/Sberbank/Router/SberbankRouter.swift +++ b/YooKassaPayments/Private/Modules/Sberbank/Router/SberbankRouter.swift @@ -8,9 +8,7 @@ final class SberbankRouter { // MARK: - SberbankRouterInput extension SberbankRouter: SberbankRouterInput { - func presentTermsOfServiceModule( - _ url: URL - ) { + func presentTermsOfServiceModule(_ url: URL) { let viewController = SFSafariViewController(url: url) viewController.modalPresentationStyle = .overFullScreen transitionHandler?.present( @@ -19,4 +17,16 @@ extension SberbankRouter: SberbankRouterInput { completion: nil ) } + + func presentSafeDealInfo(title: String, body: String) { + let viewController = SavePaymentMethodInfoAssembly.makeModule( + inputData: .init(headerValue: title, bodyValue: body) + ) + let navigationController = UINavigationController(rootViewController: viewController) + transitionHandler?.present( + navigationController, + animated: true, + completion: nil + ) + } } diff --git a/YooKassaPayments/Private/Modules/Sberbank/SberbankModuleIO.swift b/YooKassaPayments/Private/Modules/Sberbank/SberbankModuleIO.swift index 0e6695f8..84ffc328 100644 --- a/YooKassaPayments/Private/Modules/Sberbank/SberbankModuleIO.swift +++ b/YooKassaPayments/Private/Modules/Sberbank/SberbankModuleIO.swift @@ -14,6 +14,8 @@ struct SberbankModuleInputData { let termsOfService: TermsOfService let userPhoneNumber: String? let isBackBarButtonHidden: Bool + let customerId: String? + let isSafeDeal: Bool } protocol SberbankModuleOutput: AnyObject { diff --git a/YooKassaPayments/Private/Modules/Sberbank/SberbankRouterIO.swift b/YooKassaPayments/Private/Modules/Sberbank/SberbankRouterIO.swift index c54f4ede..dfee04e0 100644 --- a/YooKassaPayments/Private/Modules/Sberbank/SberbankRouterIO.swift +++ b/YooKassaPayments/Private/Modules/Sberbank/SberbankRouterIO.swift @@ -1,3 +1,4 @@ protocol SberbankRouterInput: AnyObject { func presentTermsOfServiceModule(_ url: URL) + func presentSafeDealInfo(title: String, body: String) } diff --git a/YooKassaPayments/Private/Modules/Sberbank/SberbankViewIO.swift b/YooKassaPayments/Private/Modules/Sberbank/SberbankViewIO.swift index 0e0f8484..f27244e4 100644 --- a/YooKassaPayments/Private/Modules/Sberbank/SberbankViewIO.swift +++ b/YooKassaPayments/Private/Modules/Sberbank/SberbankViewIO.swift @@ -2,9 +2,8 @@ protocol SberbankViewOutput: ActionTitleTextDialogDelegate, PhoneNumberInputModuleOutput { func setupView() func didPressSubmitButton() - func didPressTermsOfService( - _ url: URL - ) + func didPressTermsOfService(_ url: URL) + func didTapSafeDealInfo(_ url: URL) } protocol SberbankViewInput: ActivityIndicatorFullViewPresenting, diff --git a/YooKassaPayments/Private/Modules/Sberbank/View/SberbankViewController.swift b/YooKassaPayments/Private/Modules/Sberbank/View/SberbankViewController.swift index 62205662..0a25cfca 100644 --- a/YooKassaPayments/Private/Modules/Sberbank/View/SberbankViewController.swift +++ b/YooKassaPayments/Private/Modules/Sberbank/View/SberbankViewController.swift @@ -51,7 +51,7 @@ final class SberbankViewController: UIViewController, PlaceholderProvider { $0.setStyles(UIView.Styles.grayBackground) $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical - $0.spacing = Space.double + $0.spacing = Space.single return $0 }(UIStackView()) @@ -70,16 +70,37 @@ final class SberbankViewController: UIViewController, PlaceholderProvider { return $0 }(Button(type: .custom)) - private lazy var termsOfServiceLinkedTextView: LinkedTextView = { - $0.tintColor = CustomizationStorage.shared.mainScheme - $0.setStyles( - UIView.Styles.grayBackground, - UITextView.Styles.linked - ) - $0.textAlignment = .center - $0.delegate = self - return $0 - }(LinkedTextView()) + private lazy var submitButtonContainer: UIView = { + let view = UIView(frame: .zero) + view.translatesAutoresizingMaskIntoConstraints = false + 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), + view.trailingAnchor.constraint(equalTo: submitButton.trailingAnchor), + view.bottomAnchor.constraint(equalTo: submitButton.bottomAnchor, constant: Space.single), + defaultHeight, + ]) + + return view + }() + + private let termsOfServiceLinkedTextView: LinkedTextView = { + let view = LinkedTextView() + view.tintColor = CustomizationStorage.shared.mainScheme + view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) + return view + }() + + private let safeDealLinkedTextView: LinkedTextView = { + let view = LinkedTextView() + view.tintColor = CustomizationStorage.shared.mainScheme + view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) + return view + }() private var activityIndicatorView: UIView? @@ -117,6 +138,11 @@ final class SberbankViewController: UIViewController, PlaceholderProvider { view.addGestureRecognizer(viewTapGestureRecognizer) navigationItem.title = CommonLocalized.SberPay.title + + termsOfServiceLinkedTextView.delegate = self + safeDealLinkedTextView.delegate = self + safeDealLinkedTextView.isHidden = true + setupView() setupConstraints() } @@ -146,8 +172,9 @@ final class SberbankViewController: UIViewController, PlaceholderProvider { ].forEach(contentStackView.addArrangedSubview) [ - submitButton, + submitButtonContainer, termsOfServiceLinkedTextView, + safeDealLinkedTextView, ].forEach(actionButtonStackView.addArrangedSubview) } @@ -247,20 +274,19 @@ extension SberbankViewController: SberbankViewInput { view.endEditing(force) } - func setViewModel( - _ viewModel: SberbankViewModel - ) { + func setViewModel(_ viewModel: SberbankViewModel) { orderView.title = viewModel.shopName orderView.subtitle = viewModel.description orderView.value = viewModel.priceValue orderView.subvalue = viewModel.feeValue termsOfServiceLinkedTextView.attributedText = viewModel.termsOfService + safeDealLinkedTextView.attributedText = viewModel.safeDealText + safeDealLinkedTextView.isHidden = viewModel.safeDealText?.string.isEmpty ?? true termsOfServiceLinkedTextView.textAlignment = .center + safeDealLinkedTextView.textAlignment = .center } - func setSubmitButtonEnabled( - _ isEnabled: Bool - ) { + func setSubmitButtonEnabled(_ isEnabled: Bool) { submitButton.isEnabled = isEnabled } @@ -323,6 +349,8 @@ extension SberbankViewController: UITextViewDelegate { switch textView { case termsOfServiceLinkedTextView: output?.didPressTermsOfService(URL) + case safeDealLinkedTextView: + output?.didTapSafeDealInfo(URL) default: assertionFailure("Unsupported textView") } diff --git a/YooKassaPayments/Private/Modules/Sberbank/View/ViewModel/SberbankViewModel.swift b/YooKassaPayments/Private/Modules/Sberbank/View/ViewModel/SberbankViewModel.swift index af46b8a8..ded31e27 100644 --- a/YooKassaPayments/Private/Modules/Sberbank/View/ViewModel/SberbankViewModel.swift +++ b/YooKassaPayments/Private/Modules/Sberbank/View/ViewModel/SberbankViewModel.swift @@ -4,4 +4,5 @@ struct SberbankViewModel { let priceValue: String let feeValue: String? let termsOfService: NSAttributedString + let safeDealText: NSAttributedString? } diff --git a/YooKassaPayments/Private/Modules/Sberpay/Assembly/SberpayAssembly.swift b/YooKassaPayments/Private/Modules/Sberpay/Assembly/SberpayAssembly.swift index 5862a5ea..8c01055c 100644 --- a/YooKassaPayments/Private/Modules/Sberpay/Assembly/SberpayAssembly.swift +++ b/YooKassaPayments/Private/Modules/Sberpay/Assembly/SberpayAssembly.swift @@ -12,7 +12,8 @@ enum SberpayAssembly { priceViewModel: inputData.priceViewModel, feeViewModel: inputData.feeViewModel, termsOfService: inputData.termsOfService, - isBackBarButtonHidden: inputData.isBackBarButtonHidden + isBackBarButtonHidden: inputData.isBackBarButtonHidden, + isSafeDeal: inputData.isSafeDeal ) let paymentService = PaymentServiceAssembly.makeService( tokenizationSettings: inputData.tokenizationSettings, @@ -33,7 +34,8 @@ enum SberpayAssembly { threatMetrixService: threatMetrixService, clientApplicationKey: inputData.clientApplicationKey, amount: inputData.paymentOption.charge.plain, - returnUrl: inputData.returnUrl + returnUrl: inputData.returnUrl, + customerId: inputData.customerId ) let router = SberpayRouter() diff --git a/YooKassaPayments/Private/Modules/Sberpay/Interactor/SberpayInteractor.swift b/YooKassaPayments/Private/Modules/Sberpay/Interactor/SberpayInteractor.swift index 10300605..1a820756 100644 --- a/YooKassaPayments/Private/Modules/Sberpay/Interactor/SberpayInteractor.swift +++ b/YooKassaPayments/Private/Modules/Sberpay/Interactor/SberpayInteractor.swift @@ -15,6 +15,7 @@ final class SberpayInteractor { private let clientApplicationKey: String private let amount: MonetaryAmount private let returnUrl: String + private let customerId: String? init( paymentService: PaymentService, @@ -23,7 +24,8 @@ final class SberpayInteractor { threatMetrixService: ThreatMetrixService, clientApplicationKey: String, amount: MonetaryAmount, - returnUrl: String + returnUrl: String, + customerId: String? ) { self.paymentService = paymentService self.analyticsProvider = analyticsProvider @@ -32,6 +34,7 @@ final class SberpayInteractor { self.clientApplicationKey = clientApplicationKey self.amount = amount self.returnUrl = returnUrl + self.customerId = customerId } } @@ -54,7 +57,8 @@ extension SberpayInteractor: SberpayInteractorInput { confirmation: confirmation, savePaymentMethod: false, amount: self.amount, - tmxSessionId: tmxSessionId.value + tmxSessionId: tmxSessionId.value, + customerId: self.customerId ) { result in switch result { case .success(let data): diff --git a/YooKassaPayments/Private/Modules/Sberpay/Presenter/SberpayPresenter.swift b/YooKassaPayments/Private/Modules/Sberpay/Presenter/SberpayPresenter.swift index 333a4468..a98ae678 100644 --- a/YooKassaPayments/Private/Modules/Sberpay/Presenter/SberpayPresenter.swift +++ b/YooKassaPayments/Private/Modules/Sberpay/Presenter/SberpayPresenter.swift @@ -17,6 +17,7 @@ final class SberpayPresenter { private let feeViewModel: PriceViewModel? private let termsOfService: TermsOfService private let isBackBarButtonHidden: Bool + private let isSafeDeal: Bool init( shopName: String, @@ -24,7 +25,8 @@ final class SberpayPresenter { priceViewModel: PriceViewModel, feeViewModel: PriceViewModel?, termsOfService: TermsOfService, - isBackBarButtonHidden: Bool + isBackBarButtonHidden: Bool, + isSafeDeal: Bool ) { self.shopName = shopName self.purchaseDescription = purchaseDescription @@ -32,6 +34,7 @@ final class SberpayPresenter { self.feeViewModel = feeViewModel self.termsOfService = termsOfService self.isBackBarButtonHidden = isBackBarButtonHidden + self.isSafeDeal = isSafeDeal } } @@ -57,7 +60,8 @@ extension SberpayPresenter: SberpayViewOutput { description: purchaseDescription, priceValue: priceValue, feeValue: feeValue, - termsOfService: termsOfServiceValue + termsOfService: termsOfServiceValue, + safeDealText: isSafeDeal ? PaymentMethodResources.Localized.safeDealInfoLink : nil ) view.setupViewModel(viewModel) @@ -86,11 +90,16 @@ extension SberpayPresenter: SberpayViewOutput { } } - func didTapTermsOfService( - _ url: URL - ) { + func didTapTermsOfService(_ url: URL) { router.presentTermsOfServiceModule(url) } + + func didTapSafeDealInfo(_ url: URL) { + router.presentSafeDealInfo( + title: PaymentMethodResources.Localized.safeDealInfoTitle, + body: PaymentMethodResources.Localized.safeDealInfoBody + ) + } } // MARK: - SberpayInteractorOutput diff --git a/YooKassaPayments/Private/Modules/Sberpay/Router/SberpayRouter.swift b/YooKassaPayments/Private/Modules/Sberpay/Router/SberpayRouter.swift index 3c52651f..fc9ed361 100644 --- a/YooKassaPayments/Private/Modules/Sberpay/Router/SberpayRouter.swift +++ b/YooKassaPayments/Private/Modules/Sberpay/Router/SberpayRouter.swift @@ -8,9 +8,7 @@ final class SberpayRouter { // MARK: - SberpayRouterInput extension SberpayRouter: SberpayRouterInput { - func presentTermsOfServiceModule( - _ url: URL - ) { + func presentTermsOfServiceModule(_ url: URL) { let viewController = SFSafariViewController(url: url) viewController.modalPresentationStyle = .overFullScreen transitionHandler?.present( @@ -19,4 +17,16 @@ extension SberpayRouter: SberpayRouterInput { completion: nil ) } + + func presentSafeDealInfo(title: String, body: String) { + let viewController = SavePaymentMethodInfoAssembly.makeModule( + inputData: .init(headerValue: title, bodyValue: body) + ) + let navigationController = UINavigationController(rootViewController: viewController) + transitionHandler?.present( + navigationController, + animated: true, + completion: nil + ) + } } diff --git a/YooKassaPayments/Private/Modules/Sberpay/SberpayModuleIO.swift b/YooKassaPayments/Private/Modules/Sberpay/SberpayModuleIO.swift index cb962801..fcf58e5b 100644 --- a/YooKassaPayments/Private/Modules/Sberpay/SberpayModuleIO.swift +++ b/YooKassaPayments/Private/Modules/Sberpay/SberpayModuleIO.swift @@ -14,6 +14,8 @@ struct SberpayModuleInputData { let termsOfService: TermsOfService let returnUrl: String let isBackBarButtonHidden: Bool + let customerId: String? + let isSafeDeal: Bool } protocol SberpayModuleOutput: AnyObject { diff --git a/YooKassaPayments/Private/Modules/Sberpay/SberpayRouterIO.swift b/YooKassaPayments/Private/Modules/Sberpay/SberpayRouterIO.swift index 014ca140..b7b27bb7 100644 --- a/YooKassaPayments/Private/Modules/Sberpay/SberpayRouterIO.swift +++ b/YooKassaPayments/Private/Modules/Sberpay/SberpayRouterIO.swift @@ -1,3 +1,4 @@ protocol SberpayRouterInput: AnyObject { func presentTermsOfServiceModule(_ url: URL) + func presentSafeDealInfo(title: String, body: String) } diff --git a/YooKassaPayments/Private/Modules/Sberpay/SberpayViewIO.swift b/YooKassaPayments/Private/Modules/Sberpay/SberpayViewIO.swift index 266c50c6..6c878526 100644 --- a/YooKassaPayments/Private/Modules/Sberpay/SberpayViewIO.swift +++ b/YooKassaPayments/Private/Modules/Sberpay/SberpayViewIO.swift @@ -16,4 +16,5 @@ protocol SberpayViewOutput: ActionTitleTextDialogDelegate { func setupView() func didTapActionButton() func didTapTermsOfService(_ url: URL) + func didTapSafeDealInfo(_ url: URL) } diff --git a/YooKassaPayments/Private/Modules/Sberpay/View/SberpayViewController.swift b/YooKassaPayments/Private/Modules/Sberpay/View/SberpayViewController.swift index 9a3bc19c..e3fd6364 100644 --- a/YooKassaPayments/Private/Modules/Sberpay/View/SberpayViewController.swift +++ b/YooKassaPayments/Private/Modules/Sberpay/View/SberpayViewController.swift @@ -46,7 +46,7 @@ final class SberpayViewController: UIViewController, PlaceholderProvider { $0.setStyles(UIView.Styles.grayBackground) $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical - $0.spacing = Space.double + $0.spacing = Space.single return $0 }(UIStackView()) @@ -65,15 +65,37 @@ final class SberpayViewController: UIViewController, PlaceholderProvider { return $0 }(Button(type: .custom)) - private lazy var termsOfServiceLinkedTextView: LinkedTextView = { - $0.tintColor = CustomizationStorage.shared.mainScheme - $0.setStyles( - UIView.Styles.grayBackground, - UITextView.Styles.linked - ) - $0.delegate = self - return $0 - }(LinkedTextView()) + private lazy var submitButtonContainer: UIView = { + let view = UIView(frame: .zero) + view.translatesAutoresizingMaskIntoConstraints = false + 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), + view.trailingAnchor.constraint(equalTo: submitButton.trailingAnchor), + view.bottomAnchor.constraint(equalTo: submitButton.bottomAnchor, constant: Space.single), + defaultHeight, + ]) + + return view + }() + + private let termsOfServiceLinkedTextView: LinkedTextView = { + let view = LinkedTextView() + view.tintColor = CustomizationStorage.shared.mainScheme + view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) + return view + }() + + private let safeDealLinkedTextView: LinkedTextView = { + let view = LinkedTextView() + view.tintColor = CustomizationStorage.shared.mainScheme + view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) + return view + }() private var activityIndicatorView: UIView? @@ -107,6 +129,10 @@ final class SberpayViewController: UIViewController, PlaceholderProvider { view.setStyles(UIView.Styles.grayBackground) navigationItem.title = CommonLocalized.SberPay.title + termsOfServiceLinkedTextView.delegate = self + safeDealLinkedTextView.delegate = self + safeDealLinkedTextView.isHidden = true + setupView() setupConstraints() } @@ -136,8 +162,9 @@ final class SberpayViewController: UIViewController, PlaceholderProvider { ].forEach(contentStackView.addArrangedSubview) [ - submitButton, + submitButtonContainer, termsOfServiceLinkedTextView, + safeDealLinkedTextView, ].forEach(actionButtonStackView.addArrangedSubview) } @@ -223,20 +250,19 @@ final class SberpayViewController: UIViewController, PlaceholderProvider { // MARK: - SberpayViewInput extension SberpayViewController: SberpayViewInput { - func setupViewModel( - _ viewModel: SberpayViewModel - ) { + func setupViewModel(_ viewModel: SberpayViewModel) { orderView.title = viewModel.shopName orderView.subtitle = viewModel.description orderView.value = viewModel.priceValue orderView.subvalue = viewModel.feeValue termsOfServiceLinkedTextView.attributedText = viewModel.termsOfService + safeDealLinkedTextView.attributedText = viewModel.safeDealText + safeDealLinkedTextView.isHidden = viewModel.safeDealText?.string.isEmpty ?? true termsOfServiceLinkedTextView.textAlignment = .center + safeDealLinkedTextView.textAlignment = .center } - func setBackBarButtonHidden( - _ isHidden: Bool - ) { + func setBackBarButtonHidden(_ isHidden: Bool) { navigationItem.hidesBackButton = isHidden } @@ -293,6 +319,8 @@ extension SberpayViewController: UITextViewDelegate { switch textView { case termsOfServiceLinkedTextView: output?.didTapTermsOfService(URL) + case safeDealLinkedTextView: + output?.didTapSafeDealInfo(URL) default: assertionFailure("Unsupported textView") } diff --git a/YooKassaPayments/Private/Modules/Sberpay/View/ViewModel/SberpayViewModel.swift b/YooKassaPayments/Private/Modules/Sberpay/View/ViewModel/SberpayViewModel.swift index 40ff4e7e..46d34b8a 100644 --- a/YooKassaPayments/Private/Modules/Sberpay/View/ViewModel/SberpayViewModel.swift +++ b/YooKassaPayments/Private/Modules/Sberpay/View/ViewModel/SberpayViewModel.swift @@ -4,4 +4,5 @@ struct SberpayViewModel { let priceValue: String let feeValue: String? let termsOfService: NSAttributedString + let safeDealText: NSAttributedString? } diff --git a/YooKassaPayments/Private/Modules/YooMoney/Assembly/YooMoneyAssembly.swift b/YooKassaPayments/Private/Modules/YooMoney/Assembly/YooMoneyAssembly.swift index d6077dfb..9a337157 100644 --- a/YooKassaPayments/Private/Modules/YooMoney/Assembly/YooMoneyAssembly.swift +++ b/YooKassaPayments/Private/Modules/YooMoney/Assembly/YooMoneyAssembly.swift @@ -23,7 +23,8 @@ enum YooMoneyAssembly { savePaymentMethodViewModel: inputData.savePaymentMethodViewModel, tmxSessionId: inputData.tmxSessionId, initialSavePaymentMethod: inputData.initialSavePaymentMethod, - isBackBarButtonHidden: inputData.isBackBarButtonHidden + isBackBarButtonHidden: inputData.isBackBarButtonHidden, + isSafeDeal: inputData.isSafeDeal ) let authorizationService = AuthorizationServiceAssembly.makeService( @@ -51,7 +52,8 @@ enum YooMoneyAssembly { paymentService: paymentService, imageDownloadService: imageDownloadService, threatMetrixService: threatMetrixService, - clientApplicationKey: inputData.clientApplicationKey + clientApplicationKey: inputData.clientApplicationKey, + customerId: inputData.customerId ) let router = YooMoneyRouter() diff --git a/YooKassaPayments/Private/Modules/YooMoney/Interactor/YooMoneyInteractor.swift b/YooKassaPayments/Private/Modules/YooMoney/Interactor/YooMoneyInteractor.swift index e6b8f1fa..ee8b26e5 100644 --- a/YooKassaPayments/Private/Modules/YooMoney/Interactor/YooMoneyInteractor.swift +++ b/YooKassaPayments/Private/Modules/YooMoney/Interactor/YooMoneyInteractor.swift @@ -17,6 +17,7 @@ final class YooMoneyInteractor { private let threatMetrixService: ThreatMetrixService private let clientApplicationKey: String + private let customerId: String? // MARK: - Init @@ -27,7 +28,8 @@ final class YooMoneyInteractor { paymentService: PaymentService, imageDownloadService: ImageDownloadService, threatMetrixService: ThreatMetrixService, - clientApplicationKey: String + clientApplicationKey: String, + customerId: String? ) { self.authorizationService = authorizationService self.analyticsService = analyticsService @@ -36,6 +38,7 @@ final class YooMoneyInteractor { self.imageDownloadService = imageDownloadService self.threatMetrixService = threatMetrixService self.clientApplicationKey = clientApplicationKey + self.customerId = customerId } } @@ -176,6 +179,7 @@ extension YooMoneyInteractor: YooMoneyInteractorInput { paymentMethodType: paymentMethodType, amount: amount, tmxSessionId: tmxSessionId, + customerId: customerId, completion: completion ) } diff --git a/YooKassaPayments/Private/Modules/YooMoney/Presenter/YooMoneyPresenter.swift b/YooKassaPayments/Private/Modules/YooMoney/Presenter/YooMoneyPresenter.swift index 8d2edf83..7e355f25 100644 --- a/YooKassaPayments/Private/Modules/YooMoney/Presenter/YooMoneyPresenter.swift +++ b/YooKassaPayments/Private/Modules/YooMoney/Presenter/YooMoneyPresenter.swift @@ -29,6 +29,7 @@ final class YooMoneyPresenter { private let tmxSessionId: String? private var initialSavePaymentMethod: Bool private let isBackBarButtonHidden: Bool + private let isSafeDeal: Bool // MARK: - Init @@ -48,7 +49,8 @@ final class YooMoneyPresenter { savePaymentMethodViewModel: SavePaymentMethodViewModel?, tmxSessionId: String?, initialSavePaymentMethod: Bool, - isBackBarButtonHidden: Bool + isBackBarButtonHidden: Bool, + isSafeDeal: Bool ) { self.clientApplicationKey = clientApplicationKey self.testModeSettings = testModeSettings @@ -67,6 +69,7 @@ final class YooMoneyPresenter { self.tmxSessionId = tmxSessionId self.initialSavePaymentMethod = initialSavePaymentMethod self.isBackBarButtonHidden = isBackBarButtonHidden + self.isSafeDeal = isSafeDeal } // MARK: - Properties @@ -86,7 +89,8 @@ extension YooMoneyPresenter: YooMoneyViewOutput { price: price, fee: fee, paymentMethod: paymentMethod, - terms: termsOfService + terms: termsOfService, + safeDealText: isSafeDeal ? PaymentMethodResources.Localized.safeDealInfoLink : nil ) view.setupViewModel(viewModel) @@ -148,6 +152,13 @@ extension YooMoneyPresenter: YooMoneyViewOutput { router.presentTermsOfServiceModule(url) } + func didTapSafeDealInfo(_ url: URL) { + router.presentSafeDealInfo( + title: PaymentMethodResources.Localized.safeDealInfoTitle, + body: PaymentMethodResources.Localized.safeDealInfoBody + ) + } + func didTapOnSavePaymentMethod() { let savePaymentMethodModuleInputData = SavePaymentMethodInfoModuleInputData( headerValue: SavePaymentMethodInfoLocalization.Wallet.header, diff --git a/YooKassaPayments/Private/Modules/YooMoney/Router/YooMoneyRouter.swift b/YooKassaPayments/Private/Modules/YooMoney/Router/YooMoneyRouter.swift index 80d22e65..9cdb1285 100644 --- a/YooKassaPayments/Private/Modules/YooMoney/Router/YooMoneyRouter.swift +++ b/YooKassaPayments/Private/Modules/YooMoney/Router/YooMoneyRouter.swift @@ -13,6 +13,10 @@ extension YooMoneyRouter: YooMoneyRouterInput { transitionHandler?.present(viewController, animated: true, completion: nil) } + func presentSafeDealInfo(title: String, body: String) { + presentSavePaymentMethodInfo(inputData: .init(headerValue: title, bodyValue: body)) + } + func presentSavePaymentMethodInfo( inputData: SavePaymentMethodInfoModuleInputData ) { diff --git a/YooKassaPayments/Private/Modules/YooMoney/View/YooMoneyViewController.swift b/YooKassaPayments/Private/Modules/YooMoney/View/YooMoneyViewController.swift index 840b211a..0983d28c 100644 --- a/YooKassaPayments/Private/Modules/YooMoney/View/YooMoneyViewController.swift +++ b/YooKassaPayments/Private/Modules/YooMoney/View/YooMoneyViewController.swift @@ -55,7 +55,7 @@ final class YooMoneyViewController: UIViewController, PlaceholderProvider { $0.setStyles(UIView.Styles.grayBackground) $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical - $0.spacing = Space.double + $0.spacing = Space.single return $0 }(UIStackView()) @@ -74,15 +74,37 @@ final class YooMoneyViewController: UIViewController, PlaceholderProvider { return $0 }(Button(type: .custom)) - fileprivate lazy var termsOfServiceLinkedTextView: LinkedTextView = { - $0.tintColor = CustomizationStorage.shared.mainScheme - $0.setStyles( - UIView.Styles.grayBackground, - UITextView.Styles.linked - ) - $0.delegate = self - return $0 - }(LinkedTextView()) + private lazy var submitButtonContainer: UIView = { + let view = UIView(frame: .zero) + view.translatesAutoresizingMaskIntoConstraints = false + 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), + view.trailingAnchor.constraint(equalTo: submitButton.trailingAnchor), + view.bottomAnchor.constraint(equalTo: submitButton.bottomAnchor, constant: Space.single), + defaultHeight, + ]) + + return view + }() + + private let termsOfServiceLinkedTextView: LinkedTextView = { + let view = LinkedTextView() + view.tintColor = CustomizationStorage.shared.mainScheme + view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) + return view + }() + + private let safeDealLinkedTextView: LinkedTextView = { + let view = LinkedTextView() + view.tintColor = CustomizationStorage.shared.mainScheme + view.setStyles(UIView.Styles.grayBackground, UITextView.Styles.linked) + return view + }() // MARK: - PlaceholderProvider @@ -215,6 +237,9 @@ final class YooMoneyViewController: UIViewController, PlaceholderProvider { view.addGestureRecognizer(viewTapGestureRecognizer) navigationItem.title = Localized.title + termsOfServiceLinkedTextView.delegate = self + safeDealLinkedTextView.delegate = self + safeDealLinkedTextView.isHidden = true setupView() setupConstraints() } @@ -244,8 +269,9 @@ final class YooMoneyViewController: UIViewController, PlaceholderProvider { ].forEach(contentStackView.addArrangedSubview) [ - submitButton, + submitButtonContainer, termsOfServiceLinkedTextView, + safeDealLinkedTextView, ].forEach(actionButtonStackView.addArrangedSubview) [ @@ -348,9 +374,7 @@ final class YooMoneyViewController: UIViewController, PlaceholderProvider { // MARK: - YooMoneyViewInput extension YooMoneyViewController: YooMoneyViewInput { - func setupViewModel( - _ viewModel: YooMoneyViewModel - ) { + func setupViewModel(_ viewModel: YooMoneyViewModel) { orderView.title = viewModel.shopName orderView.subtitle = viewModel.description orderView.value = makePrice(viewModel.price) @@ -372,18 +396,17 @@ extension YooMoneyViewController: YooMoneyViewInput { font: UIFont.dynamicCaption2, foregroundColor: UIColor.AdaptiveColors.secondary ) + safeDealLinkedTextView.attributedText = viewModel.safeDealText + safeDealLinkedTextView.isHidden = viewModel.safeDealText?.string.isEmpty ?? true termsOfServiceLinkedTextView.textAlignment = .center + safeDealLinkedTextView.textAlignment = .center } - func setupAvatar( - _ avatar: UIImage - ) { + func setupAvatar(_ avatar: UIImage) { paymentMethodView.image = avatar.rounded(cornerRadius: Space.fivefold) } - func setSavePaymentMethodViewModel( - _ savePaymentMethodViewModel: SavePaymentMethodViewModel - ) { + func setSavePaymentMethodViewModel(_ savePaymentMethodViewModel: SavePaymentMethodViewModel) { if contentStackView.arrangedSubviews.contains(saveAuthInAppSwitchItemView) { contentStackView.addArrangedSubview(separatorView) } @@ -535,6 +558,8 @@ extension YooMoneyViewController: UITextViewDelegate { switch textView { case termsOfServiceLinkedTextView: output?.didTapTermsOfService(URL) + case safeDealLinkedTextView: + output?.didTapSafeDealInfo(URL) default: assertionFailure("Unsupported textView") } diff --git a/YooKassaPayments/Private/Modules/YooMoney/YooMoneyModuleIO.swift b/YooKassaPayments/Private/Modules/YooMoney/YooMoneyModuleIO.swift index eea5ca83..5562a507 100644 --- a/YooKassaPayments/Private/Modules/YooMoney/YooMoneyModuleIO.swift +++ b/YooKassaPayments/Private/Modules/YooMoney/YooMoneyModuleIO.swift @@ -19,6 +19,8 @@ struct YooMoneyModuleInputData { let tmxSessionId: String? let initialSavePaymentMethod: Bool let isBackBarButtonHidden: Bool + let customerId: String? + let isSafeDeal: Bool } protocol YooMoneyModuleInput: AnyObject {} diff --git a/YooKassaPayments/Private/Modules/YooMoney/YooMoneyRouterIO.swift b/YooKassaPayments/Private/Modules/YooMoney/YooMoneyRouterIO.swift index 27eb7324..5c16a0db 100644 --- a/YooKassaPayments/Private/Modules/YooMoney/YooMoneyRouterIO.swift +++ b/YooKassaPayments/Private/Modules/YooMoney/YooMoneyRouterIO.swift @@ -2,20 +2,15 @@ import MoneyAuth protocol YooMoneyRouterInput: AnyObject { func presentTermsOfServiceModule(_ url: URL) - - func presentSavePaymentMethodInfo( - inputData: SavePaymentMethodInfoModuleInputData - ) - + func presentSafeDealInfo(title: String, body: String) + func presentSavePaymentMethodInfo(inputData: SavePaymentMethodInfoModuleInputData) func presentLogoutConfirmation( inputData: LogoutConfirmationModuleInputData, moduleOutput: LogoutConfirmationModuleOutput ) - func presentPaymentAuthorizationModule( inputData: PaymentAuthorizationModuleInputData, moduleOutput: PaymentAuthorizationModuleOutput? ) - func closePaymentAuthorization() } diff --git a/YooKassaPayments/Private/Modules/YooMoney/YooMoneyViewIO.swift b/YooKassaPayments/Private/Modules/YooMoney/YooMoneyViewIO.swift index 3c8e01d9..b2b98870 100644 --- a/YooKassaPayments/Private/Modules/YooMoney/YooMoneyViewIO.swift +++ b/YooKassaPayments/Private/Modules/YooMoney/YooMoneyViewIO.swift @@ -7,6 +7,7 @@ struct YooMoneyViewModel { let fee: PriceViewModel? let paymentMethod: PaymentMethodViewModel let terms: TermsOfService + let safeDealText: NSAttributedString? } protocol YooMoneyViewInput: ActivityIndicatorFullViewPresenting, PlaceholderPresenting, NotificationPresenting { @@ -32,6 +33,7 @@ protocol YooMoneyViewOutput: ActionTitleTextDialogDelegate { func didTapActionButton() func didTapLogout() func didTapTermsOfService(_ url: URL) + func didTapSafeDealInfo(_ url: URL) func didTapOnSavePaymentMethod() func didChangeSavePaymentMethodState( _ state: Bool diff --git a/YooKassaPayments/Private/Services/Analytics/AnalyticsEvent.swift b/YooKassaPayments/Private/Services/Analytics/AnalyticsEvent.swift index 42631d86..8c8c33f0 100644 --- a/YooKassaPayments/Private/Services/Analytics/AnalyticsEvent.swift +++ b/YooKassaPayments/Private/Services/Analytics/AnalyticsEvent.swift @@ -23,6 +23,9 @@ enum AnalyticsEvent { /// Open Bank Card screen with screen recurring case screenRecurringCardForm(sdkVersion: String) + case screenDetailsUnbindWalletCard(sdkVersion: String) + case screenUnbindCard(cardType: LinkedCardType) + // MARK: - Actions /// Create a payment token with the payment method selected. @@ -52,6 +55,8 @@ enum AnalyticsEvent { /// SberPay confirmation case actionSberPayConfirmation(sberPayConfirmationStatus: SberPayConfirmationStatus, sdkVersion: String) + case actionUnbindBankCard(actionUnbindCardStatus: ActionUnbindCardStatus) + // MARK: - Analytic parameters. /// Current status of user authorization. @@ -80,6 +85,8 @@ enum AnalyticsEvent { case applePay = "apple-pay" case recurringCard = "recurring-card" case sberpay = "sber-pay" + case customerIdLinkedCard = "customer-id-linked-card" + case customerIdLinkedCardCvc = "customer-id-linked-card-cvc" var key: String { return Key.tokenizeScheme.rawValue @@ -119,6 +126,8 @@ enum AnalyticsEvent { case moneyAuthLoginScheme case moneyAuthLoginStatus case sberPayConfirmationStatus + case linkedCardType + case actionUnbindCardStatus } // MARK: - BankCardForm actions @@ -185,6 +194,18 @@ enum AnalyticsEvent { 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 diff --git a/YooKassaPayments/Private/Services/Analytics/AnalyticsServiceImpl.swift b/YooKassaPayments/Private/Services/Analytics/AnalyticsServiceImpl.swift index 01e1e476..91eadcc4 100644 --- a/YooKassaPayments/Private/Services/Analytics/AnalyticsServiceImpl.swift +++ b/YooKassaPayments/Private/Services/Analytics/AnalyticsServiceImpl.swift @@ -83,6 +83,12 @@ extension AnalyticsServiceImpl: AnalyticsService { case .screenRecurringCardForm: eventName = EventKey.screenRecurringCardForm.rawValue + case .screenDetailsUnbindWalletCard: + eventName = EventKey.screenDetailsUnbindWalletCard.rawValue + + case .screenUnbindCard: + eventName = EventKey.screenUnbindCard.rawValue + case .actionTokenize: eventName = EventKey.actionTokenize.rawValue @@ -109,6 +115,9 @@ extension AnalyticsServiceImpl: AnalyticsService { case .actionSberPayConfirmation: eventName = EventKey.actionSberPayConfirmation.rawValue + + case .actionUnbindBankCard: + eventName = EventKey.actionUnbindBankCard.rawValue } return eventName } @@ -222,6 +231,14 @@ extension AnalyticsServiceImpl: AnalyticsService { 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 @@ -236,6 +253,8 @@ extension AnalyticsServiceImpl: AnalyticsService { case screenError case screen3ds case screenRecurringCardForm + case screenUnbindCard + case screenDetailsUnbindWalletCard case actionTokenize case actionPaymentAuthorization case actionLogout @@ -243,6 +262,7 @@ extension AnalyticsServiceImpl: AnalyticsService { case actionBankCardForm case actionMoneyAuthLogin case actionSberPayConfirmation + case actionUnbindBankCard // MARK: - Authorization diff --git a/YooKassaPayments/Private/Services/Payment/PaymentService.swift b/YooKassaPayments/Private/Services/Payment/PaymentService.swift index 69f0c7d4..08ea3880 100644 --- a/YooKassaPayments/Private/Services/Payment/PaymentService.swift +++ b/YooKassaPayments/Private/Services/Payment/PaymentService.swift @@ -1,5 +1,13 @@ import YooKassaPaymentsApi +struct Shop { + let options: [PaymentOption] + let properties: ShopProperties + + var isSafeDeal: Bool { properties.isSafeDeal || properties.isMarketplace } + +} + protocol PaymentService { func fetchPaymentOptions( clientApplicationKey: String, @@ -8,7 +16,8 @@ protocol PaymentService { amount: String?, currency: String?, getSavePaymentMethod: Bool?, - completion: @escaping (Result<[PaymentOption], Error>) -> Void + customerId: String?, + completion: @escaping (Result) -> Void ) func fetchPaymentMethod( @@ -24,6 +33,8 @@ protocol PaymentService { savePaymentMethod: Bool, amount: MonetaryAmount?, tmxSessionId: String, + customerId: String?, + savePaymentInstrument: Bool?, completion: @escaping (Result) -> Void ) @@ -35,6 +46,7 @@ protocol PaymentService { paymentMethodType: PaymentMethodType, amount: MonetaryAmount?, tmxSessionId: String, + customerId: String?, completion: @escaping (Result) -> Void ) @@ -48,6 +60,7 @@ protocol PaymentService { paymentMethodType: PaymentMethodType, amount: MonetaryAmount?, tmxSessionId: String, + customerId: String?, completion: @escaping (Result) -> Void ) @@ -58,6 +71,7 @@ protocol PaymentService { savePaymentMethod: Bool, amount: MonetaryAmount?, tmxSessionId: String, + customerId: String?, completion: @escaping (Result) -> Void ) @@ -67,6 +81,7 @@ protocol PaymentService { savePaymentMethod: Bool, amount: MonetaryAmount?, tmxSessionId: String, + customerId: String?, completion: @escaping (Result) -> Void ) @@ -76,6 +91,7 @@ protocol PaymentService { savePaymentMethod: Bool, amount: MonetaryAmount?, tmxSessionId: String, + customerId: String?, completion: @escaping (Result) -> Void ) @@ -89,4 +105,17 @@ protocol PaymentService { csc: String, completion: @escaping (Result) -> Void ) + + func tokenizeCardInstrument( + clientApplicationKey: String, + amount: MonetaryAmount, + tmxSessionId: String, + confirmation: Confirmation, + savePaymentMethod: Bool, + instrumentId: String, + csc: String?, + completion: @escaping (Result) -> Void + ) + + func unbind(authToken: String, id: String, completion: @escaping (Result) -> Void) } diff --git a/YooKassaPayments/Private/Services/Payment/PaymentServiceImpl.swift b/YooKassaPayments/Private/Services/Payment/PaymentServiceImpl.swift index 3eb70405..54eb75ca 100644 --- a/YooKassaPayments/Private/Services/Payment/PaymentServiceImpl.swift +++ b/YooKassaPayments/Private/Services/Payment/PaymentServiceImpl.swift @@ -29,9 +29,9 @@ extension PaymentServiceImpl: PaymentService { amount: String?, currency: String?, getSavePaymentMethod: Bool?, - completion: @escaping (Result<[PaymentOption], Error>) -> Void + customerId: String?, + completion: @escaping (Result) -> Void ) { - let apiMethod = PaymentOptions.Method( oauthToken: clientApplicationKey, authorization: authorizationToken, @@ -39,7 +39,7 @@ extension PaymentServiceImpl: PaymentService { amount: amount, currency: currency, savePaymentMethod: getSavePaymentMethod, - merchantCustomerId: nil + merchantCustomerId: customerId ) session.perform(apiMethod: apiMethod).responseApi(queue: .global()) { [weak self] result in @@ -53,7 +53,7 @@ extension PaymentServiceImpl: PaymentService { if items.isEmpty { completion(.failure(PaymentProcessingError.emptyList)) } else { - completion(.success(items)) + completion(.success(Shop(options: items, properties: data.shopProperties))) } } } @@ -87,6 +87,8 @@ extension PaymentServiceImpl: PaymentService { savePaymentMethod: Bool, amount: MonetaryAmount?, tmxSessionId: String, + customerId: String?, + savePaymentInstrument: Bool?, completion: @escaping (Result) -> Void ) { let paymentMethodData = PaymentMethodDataBankCard(bankCard: bankCard.paymentsModel) @@ -96,8 +98,8 @@ extension PaymentServiceImpl: PaymentService { confirmation: confirmation.paymentsModel, savePaymentMethod: savePaymentMethod, paymentMethodData: paymentMethodData, - merchantCustomerId: nil, - savePaymentInstrument: nil + merchantCustomerId: customerId, + savePaymentInstrument: savePaymentInstrument ) let apiMethod = YooKassaPaymentsApi.Tokens.Method( oauthToken: clientApplicationKey, @@ -123,6 +125,7 @@ extension PaymentServiceImpl: PaymentService { paymentMethodType: PaymentMethodType, amount: MonetaryAmount?, tmxSessionId: String, + customerId: String?, completion: @escaping (Result) -> Void ) { let paymentMethodData = PaymentInstrumentDataYooMoneyWallet( @@ -136,7 +139,7 @@ extension PaymentServiceImpl: PaymentService { confirmation: confirmation.paymentsModel, savePaymentMethod: savePaymentMethod, paymentMethodData: paymentMethodData, - merchantCustomerId: nil, + merchantCustomerId: customerId, savePaymentInstrument: nil ) let apiMethod = YooKassaPaymentsApi.Tokens.Method( @@ -165,6 +168,7 @@ extension PaymentServiceImpl: PaymentService { paymentMethodType: PaymentMethodType, amount: MonetaryAmount?, tmxSessionId: String, + customerId: String?, completion: @escaping (Result) -> Void ) { let paymentMethodData = PaymentInstrumentDataYooMoneyLinkedBankCard( @@ -180,7 +184,7 @@ extension PaymentServiceImpl: PaymentService { confirmation: confirmation.paymentsModel, savePaymentMethod: savePaymentMethod, paymentMethodData: paymentMethodData, - merchantCustomerId: nil, + merchantCustomerId: customerId, savePaymentInstrument: nil ) let apiMethod = YooKassaPaymentsApi.Tokens.Method( @@ -205,6 +209,7 @@ extension PaymentServiceImpl: PaymentService { savePaymentMethod: Bool, amount: MonetaryAmount?, tmxSessionId: String, + customerId: String?, completion: @escaping (Result) -> Void ) { let paymentMethodData = PaymentMethodDataApplePay( @@ -216,7 +221,7 @@ extension PaymentServiceImpl: PaymentService { confirmation: nil, savePaymentMethod: savePaymentMethod, paymentMethodData: paymentMethodData, - merchantCustomerId: nil, + merchantCustomerId: customerId, savePaymentInstrument: nil ) let apiMethod = YooKassaPaymentsApi.Tokens.Method( @@ -242,6 +247,7 @@ extension PaymentServiceImpl: PaymentService { savePaymentMethod: Bool, amount: MonetaryAmount?, tmxSessionId: String, + customerId: String?, completion: @escaping (Result) -> Void ) { let paymentMethodData = PaymentMethodDataSberbank( @@ -253,7 +259,7 @@ extension PaymentServiceImpl: PaymentService { confirmation: confirmation.paymentsModel, savePaymentMethod: savePaymentMethod, paymentMethodData: paymentMethodData, - merchantCustomerId: nil, + merchantCustomerId: customerId, savePaymentInstrument: nil ) let apiMethod = YooKassaPaymentsApi.Tokens.Method( @@ -278,6 +284,7 @@ extension PaymentServiceImpl: PaymentService { savePaymentMethod: Bool, amount: MonetaryAmount?, tmxSessionId: String, + customerId: String?, completion: @escaping (Result) -> Void ) { let paymentMethodData = PaymentMethodDataSberbank( @@ -289,7 +296,7 @@ extension PaymentServiceImpl: PaymentService { confirmation: confirmation.paymentsModel, savePaymentMethod: savePaymentMethod, paymentMethodData: paymentMethodData, - merchantCustomerId: nil, + merchantCustomerId: customerId, savePaymentInstrument: nil ) let apiMethod = YooKassaPaymentsApi.Tokens.Method( @@ -341,6 +348,51 @@ extension PaymentServiceImpl: PaymentService { } } } + + func tokenizeCardInstrument( + clientApplicationKey: String, + amount: MonetaryAmount, + tmxSessionId: String, + confirmation: Confirmation, + savePaymentMethod: Bool, + instrumentId: String, + csc: String?, + completion: @escaping (Result) -> Void + ) { + let request = TokensRequestPaymentInstrumentId( + amount: .init(amount), + tmxSessionId: tmxSessionId, + confirmation: .init(confirmation), + savePaymentMethod: savePaymentMethod, + paymentInstrumentId: instrumentId, + csc: csc + ) + let apiMethod = YooKassaPaymentsApi.Tokens.Method( + oauthToken: clientApplicationKey, + tokensRequest: request + ) + session.perform(apiMethod: apiMethod).responseApi(queue: .global()) { result in + switch result { + case let .left(error): + let mappedError = mapError(error) + completion(.failure(mappedError)) + case let .right(data): + completion(.success(data.plain)) + } + } + } + + func unbind(authToken: String, id: String, completion: @escaping (Result) -> Void) { + session.perform(apiMethod: PaymentInstruments.Method(oauthToken: authToken, paymentInstrumentId: id)) + .responseApi(queue: .global()) { result in + switch result { + case .left(let error): + completion(.failure(mapError(error))) + case .right: + completion(.success(())) + } + } + } } private func mapError(_ error: Error) -> Error { diff --git a/YooKassaPayments/Private/Services/Payment/PaymentServiceMock.swift b/YooKassaPayments/Private/Services/Payment/PaymentServiceMock.swift index 6060826d..d57dbccc 100644 --- a/YooKassaPayments/Private/Services/Payment/PaymentServiceMock.swift +++ b/YooKassaPayments/Private/Services/Payment/PaymentServiceMock.swift @@ -24,6 +24,31 @@ final class PaymentServiceMock { // MARK: - PaymentService extension PaymentServiceMock: PaymentService { + func tokenizeCardInstrument( + clientApplicationKey: String, + amount: MonetaryAmount, + tmxSessionId: String, + confirmation: Confirmation, + savePaymentMethod: Bool, + instrumentId: String, + csc: String?, + completion: @escaping (Result) -> Void + ) { + DispatchQueue.global().asyncAfter(deadline: .now() + timeout) { + if Bool.random() { + completion(.failure(MockError.default)) + } else { + self.makeTokensPromise(completion: completion) + } + } + } + + func unbind(authToken: String, id: String, completion: @escaping (Result) -> Void) { + DispatchQueue.global().asyncAfter(deadline: .now() + timeout) { + if Bool.random() { completion(.failure(MockError.default)) } else { completion(.success(())) } + } + } + func fetchPaymentOptions( clientApplicationKey: String, authorizationToken: String?, @@ -31,7 +56,8 @@ extension PaymentServiceMock: PaymentService { amount: String?, currency: String?, getSavePaymentMethod: Bool?, - completion: @escaping (Result<[PaymentOption], Error>) -> Void + customerId: String?, + completion: @escaping (Result) -> Void ) { let authorized = keyValueStoring.getString( for: KeyValueStoringKeys.moneyCenterAuthToken @@ -41,12 +67,13 @@ extension PaymentServiceMock: PaymentService { handler: paymentMethodHandlerService, authorized: authorized ) + let properties = ShopProperties(isSafeDeal: true, isMarketplace: true) DispatchQueue.global().asyncAfter(deadline: .now() + timeout) { if items.isEmpty { completion(.failure(PaymentProcessingError.emptyList)) } else { - completion(.success(items)) + completion(.success(Shop(options: items, properties: properties))) } } } @@ -93,6 +120,8 @@ extension PaymentServiceMock: PaymentService { savePaymentMethod: Bool, amount: MonetaryAmount?, tmxSessionId: String, + customerId: String?, + savePaymentInstrument: Bool?, completion: @escaping (Result) -> Void ) { makeTokensPromise(completion: completion) @@ -106,6 +135,7 @@ extension PaymentServiceMock: PaymentService { paymentMethodType: PaymentMethodType, amount: MonetaryAmount?, tmxSessionId: String, + customerId: String?, completion: @escaping (Result) -> Void ) { makeTokensPromise(completion: completion) @@ -121,6 +151,7 @@ extension PaymentServiceMock: PaymentService { paymentMethodType: PaymentMethodType, amount: MonetaryAmount?, tmxSessionId: String, + customerId: String?, completion: @escaping (Result) -> Void ) { makeTokensPromise(completion: completion) @@ -133,6 +164,7 @@ extension PaymentServiceMock: PaymentService { savePaymentMethod: Bool, amount: MonetaryAmount?, tmxSessionId: String, + customerId: String?, completion: @escaping (Result) -> Void ) { makeTokensPromise(completion: completion) @@ -144,6 +176,7 @@ extension PaymentServiceMock: PaymentService { savePaymentMethod: Bool, amount: MonetaryAmount?, tmxSessionId: String, + customerId: String?, completion: @escaping (Result) -> Void ) { makeTokensPromise(completion: completion) @@ -155,6 +188,7 @@ extension PaymentServiceMock: PaymentService { savePaymentMethod: Bool, amount: MonetaryAmount?, tmxSessionId: String, + customerId: String?, completion: @escaping (Result) -> Void ) { makeTokensPromise(completion: completion) @@ -193,7 +227,7 @@ private let timeout: Double = 1 // MARK: - Data for Error -private struct MockError: Error { } +private struct MockError: Error { static let `default` = MockError() } private let mockError = MockError() @@ -218,8 +252,9 @@ private func makePaymentOptions( let paymentOptions = makeDefaultPaymentOptions( charge, fee: fee, - authorized: authorized - ) + linkedCards.map { $0 } + authorized: authorized, + yooMoneyLinkedCards: linkedCards + ) let filteredPaymentOptions = handler.filterPaymentMethods(paymentOptions) @@ -238,26 +273,32 @@ private func makeLinkedCards( charge: Amount, fee: Fee? ) -> [PaymentInstrumentYooMoneyLinkedBankCard] { - return (0.. PaymentInstrumentYooMoneyLinkedBankCard { + let cardName = named ? "Зарплатная карта" : nil return PaymentInstrumentYooMoneyLinkedBankCard( paymentMethodType: .yooMoney, confirmationTypes: nil, charge: MonetaryAmountFactory.makePaymentsMonetaryAmount(charge), instrumentType: .linkedBankCard, cardId: "123456789", - cardName: nil, + cardName: cardName, cardMask: makeRandomCardMask(), cardType: .masterCard, identificationRequirement: .simplified, fee: fee?.paymentsModel, savePaymentMethod: .allowed, - savePaymentInstrument: nil + savePaymentInstrument: true ) } @@ -270,47 +311,12 @@ private func makeRandomCardMask() -> String { private func makeDefaultPaymentOptions( _ charge: Amount, fee: Fee?, - authorized: Bool + authorized: Bool, + yooMoneyLinkedCards: [PaymentInstrumentYooMoneyLinkedBankCard] ) -> [PaymentOption] { - var response: [PaymentOption] = [] let charge = MonetaryAmountFactory.makePaymentsMonetaryAmount(charge) - if authorized { - - response += [ - PaymentInstrumentYooMoneyWallet( - paymentMethodType: .yooMoney, - confirmationTypes: [], - charge: charge, - instrumentType: .wallet, - accountId: "2736482364872", - balance: YooKassaPaymentsApi.MonetaryAmount( - value: 40_000, - currency: charge.currency - ), - identificationRequirement: .simplified, - fee: fee?.paymentsModel, - savePaymentMethod: .allowed, - savePaymentInstrument: nil - ), - ] - - } else { - - response += [ - PaymentOption( - paymentMethodType: .yooMoney, - confirmationTypes: [], - charge: charge, - identificationRequirement: nil, - fee: fee?.paymentsModel, - savePaymentMethod: .allowed, - savePaymentInstrument: nil - ), - ] - } - - response += [ + var response: [PaymentOption] = [ PaymentOption( paymentMethodType: .sberbank, confirmationTypes: [], @@ -318,27 +324,88 @@ private func makeDefaultPaymentOptions( identificationRequirement: nil, fee: fee?.paymentsModel, savePaymentMethod: .forbidden, - savePaymentInstrument: nil + savePaymentInstrument: true ), PaymentOption( - paymentMethodType: .bankCard, + paymentMethodType: .applePay, confirmationTypes: [], charge: charge, identificationRequirement: nil, fee: fee?.paymentsModel, - savePaymentMethod: .allowed, - savePaymentInstrument: nil + savePaymentMethod: .forbidden, + savePaymentInstrument: true ), - PaymentOption( - paymentMethodType: .applePay, + makeYooMoney( + authorized: authorized, + charge: charge.plain, + fee: fee + ), + ] + + response += yooMoneyLinkedCards + + response += [ + PaymentOptionBankCard( + paymentMethodType: .bankCard, confirmationTypes: [], charge: charge, identificationRequirement: nil, fee: fee?.paymentsModel, - savePaymentMethod: .forbidden, - savePaymentInstrument: nil + savePaymentMethod: .allowed, + savePaymentInstrument: true, + paymentInstruments: [ + .init( + paymentInstrumentId: "522188_id", + first6: "522188", + last4: "3352", + cscRequired: true, + cardType: .masterCard + ), + .init( + paymentInstrumentId: "547805_id", + first6: "547805", + last4: "1405", + cscRequired: false, + cardType: .masterCard + ), + ] ), ] return response } + +private func makeYooMoney( + authorized: Bool, + charge: MonetaryAmount, + fee: Fee? +) -> PaymentOption { + if authorized { + return PaymentInstrumentYooMoneyWallet( + paymentMethodType: .yooMoney, + confirmationTypes: [], + charge: charge.paymentsModel, + instrumentType: .wallet, + accountId: "2736482364872", + balance: YooKassaPaymentsApi.MonetaryAmount( + value: 40_000, + currency: charge.currency + ), + identificationRequirement: .simplified, + fee: fee?.paymentsModel, + savePaymentMethod: .allowed, + savePaymentInstrument: true + ) + + } else { + return PaymentOption( + paymentMethodType: .yooMoney, + confirmationTypes: [], + charge: charge.paymentsModel, + identificationRequirement: nil, + fee: fee?.paymentsModel, + savePaymentMethod: .allowed, + savePaymentInstrument: true + ) + } +} diff --git a/YooKassaPayments/Private/SheetView/SheetViewController.swift b/YooKassaPayments/Private/SheetView/SheetViewController.swift index 8da9bcc9..bc693862 100644 --- a/YooKassaPayments/Private/SheetView/SheetViewController.swift +++ b/YooKassaPayments/Private/SheetView/SheetViewController.swift @@ -416,6 +416,7 @@ private extension SheetViewController { initialSpringVelocity: sheetOptions.transitionVelocity, options: sheetOptions.animationOptions, animations: { + self.view.endEditing(true) self.contentViewController.view.transform = CGAffineTransform( translationX: 0, y: self.contentViewController.view.bounds.height diff --git a/YooKassaPayments/Private/ViewModels/PaymentMethodViewModel.swift b/YooKassaPayments/Private/ViewModels/PaymentMethodViewModel.swift index 476ccf03..2250eada 100644 --- a/YooKassaPayments/Private/ViewModels/PaymentMethodViewModel.swift +++ b/YooKassaPayments/Private/ViewModels/PaymentMethodViewModel.swift @@ -1,7 +1,26 @@ import UIKit.UIImage struct PaymentMethodViewModel { + let id: String? + let isShopLinkedCard: Bool let image: UIImage let title: String let subtitle: String? + let hasActions: Bool + + init( + id: String?, + isShopLinkedCard: Bool, + image: UIImage, + title: String, + subtitle: String?, + hasActions: Bool = false + ) { + self.id = id + self.isShopLinkedCard = isShopLinkedCard + self.image = image + self.title = title + self.subtitle = subtitle + self.hasActions = hasActions + } } diff --git a/YooKassaPayments/Public/InputData/BankCardRepeatModuleInputData.swift b/YooKassaPayments/Public/InputData/BankCardRepeatModuleInputData.swift index eb6cb1c3..9c7819e2 100644 --- a/YooKassaPayments/Public/InputData/BankCardRepeatModuleInputData.swift +++ b/YooKassaPayments/Public/InputData/BankCardRepeatModuleInputData.swift @@ -35,6 +35,13 @@ public struct BankCardRepeatModuleInputData { /// The cashier at the division of payment flows within a single account. let gatewayId: String? + /// Unique customer identifier by which you exclusively identify the custormer. + /// Can be represented by phone, email or any other id which uniquely identifies the customer. + let customerId: String? + + /// Is this shop a safe deal shop + let isSafeDeal: Bool + /// Creates instance of `BankCardRepeatModuleInputData`. /// /// - Parameters: @@ -63,7 +70,9 @@ public struct BankCardRepeatModuleInputData { isLoggingEnabled: Bool = false, customizationSettings: CustomizationSettings = CustomizationSettings(), savePaymentMethod: SavePaymentMethod, - gatewayId: String? = nil + gatewayId: String? = nil, + customerId: String?, + isSafeDeal: Bool ) { self.clientApplicationKey = (clientApplicationKey + ":").base64Encoded() self.shopName = shopName @@ -76,5 +85,7 @@ public struct BankCardRepeatModuleInputData { self.customizationSettings = customizationSettings self.savePaymentMethod = savePaymentMethod self.gatewayId = gatewayId + self.customerId = customerId + self.isSafeDeal = isSafeDeal } } diff --git a/YooKassaPayments/Public/InputData/TokenizationModuleInputData.swift b/YooKassaPayments/Public/InputData/TokenizationModuleInputData.swift index 2bbf4196..c89969f2 100644 --- a/YooKassaPayments/Public/InputData/TokenizationModuleInputData.swift +++ b/YooKassaPayments/Public/InputData/TokenizationModuleInputData.swift @@ -52,6 +52,10 @@ public struct TokenizationModuleInputData { /// Example: myapplication:// let applicationScheme: String? + /// Unique customer identifier by which you exclusively identify the custormer. + /// Can be represented by phone, email or any other id which uniquely identifies the customer. + let customerId: String? + /// Creates instance of `TokenizationModuleInputData`. /// /// - Parameters: @@ -91,7 +95,8 @@ public struct TokenizationModuleInputData { customizationSettings: CustomizationSettings = CustomizationSettings(), savePaymentMethod: SavePaymentMethod, moneyAuthClientId: String? = nil, - applicationScheme: String? = nil + applicationScheme: String? = nil, + customerId: String? = nil ) { self.clientApplicationKey = (clientApplicationKey + ":").base64Encoded() self.shopName = shopName @@ -109,5 +114,16 @@ public struct TokenizationModuleInputData { self.savePaymentMethod = savePaymentMethod self.moneyAuthClientId = moneyAuthClientId self.applicationScheme = applicationScheme + self.customerId = customerId + } +} + +extension TokenizationModuleInputData { + var boolFromSavePaymentMethod: Bool? { + switch savePaymentMethod { + case .on: return true + case .off: return false + case .userSelects: return nil + } } } diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/Common/icon2_name_more_s.imageset/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/Common/icon2_name_more_s.imageset/Contents.json new file mode 100644 index 00000000..74bbe388 --- /dev/null +++ b/YooKassaPayments/Public/Resources/Media.xcassets/Common/icon2_name_more_s.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icon2_name_more_s.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/Common/icon2_name_more_s.imageset/icon2_name_more_s.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/Common/icon2_name_more_s.imageset/icon2_name_more_s.pdf new file mode 100644 index 00000000..e8f6bea4 Binary files /dev/null and b/YooKassaPayments/Public/Resources/Media.xcassets/Common/icon2_name_more_s.imageset/icon2_name_more_s.pdf differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/Common/icon2_name_trash_m.imageset/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/Common/icon2_name_trash_m.imageset/Contents.json new file mode 100644 index 00000000..59797b03 --- /dev/null +++ b/YooKassaPayments/Public/Resources/Media.xcassets/Common/icon2_name_trash_m.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icon2_name_trash_m.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/Common/icon2_name_trash_m.imageset/icon2_name_trash_m.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/Common/icon2_name_trash_m.imageset/icon2_name_trash_m.pdf new file mode 100644 index 00000000..05fe6b9c Binary files /dev/null and b/YooKassaPayments/Public/Resources/Media.xcassets/Common/icon2_name_trash_m.imageset/icon2_name_trash_m.pdf differ diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/ic_attention_m.imageset/Contents.json b/YooKassaPayments/Public/Resources/Media.xcassets/ic_attention_m.imageset/Contents.json new file mode 100644 index 00000000..166b2fe7 --- /dev/null +++ b/YooKassaPayments/Public/Resources/Media.xcassets/ic_attention_m.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "ic_attention_m.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/YooKassaPayments/Public/Resources/Media.xcassets/ic_attention_m.imageset/ic_attention_m.pdf b/YooKassaPayments/Public/Resources/Media.xcassets/ic_attention_m.imageset/ic_attention_m.pdf new file mode 100644 index 00000000..eb9bbc8e Binary files /dev/null and b/YooKassaPayments/Public/Resources/Media.xcassets/ic_attention_m.imageset/ic_attention_m.pdf differ diff --git a/YooKassaPayments/Public/Resources/en.lproj/Localizable.strings b/YooKassaPayments/Public/Resources/en.lproj/Localizable.strings index 9c009f17..afec7a4b 100644 --- a/YooKassaPayments/Public/Resources/en.lproj/Localizable.strings +++ b/YooKassaPayments/Public/Resources/en.lproj/Localizable.strings @@ -1,4 +1,4 @@ -"ApplePayContract.SavePaymentMethod.Title" = "Allow to write off money"; +"ApplePayContract.SavePaymentMethod.Title" = "Allow autopayments"; "ApplePayContract.fee" = "Commission"; @@ -8,9 +8,9 @@ "ApplePayContract.title" = "Apple Pay"; /* По неизвестным нам причинам экран ApplePay не отобразился */ -"ApplePayUnavailable.title" = "Apple Pay is unavailable"; +"ApplePayUnavailable.title" = "Apple Pay is unavailable, try again"; -"BankCard.savePaymentMethod.title" = "Link a card"; +"BankCard.savePaymentMethod.title" = "Allow autopayments"; "BankCardDataInput.cvc" = "CVC"; @@ -27,9 +27,9 @@ "BankCardDataInputView.BottomHint.invalidPan" = "Check the card number"; /* Bank card repeat */ -"BankCardRepeat.savePaymentMethod.title" = "Link a card"; +"BankCardRepeat.savePaymentMethod.title" = "Allow autopayments"; -"BankCardRepeat.title" = "Saved card"; +"BankCardRepeat.title" = "Bank card"; "BankCardView.inputCvcHint" = "Code"; @@ -50,23 +50,23 @@ /* Отменить */ "Cancel" = "Cancel"; -"Common.Error.unknown" = "Something went wrong"; +"Common.Error.unknown" = "It didn't work. Try again"; /* Common */ "Common.PlaceholderView.buttonTitle" = "Try again"; -"Common.PlaceholderView.text" = "Please try again later."; +"Common.PlaceholderView.text" = "Please try again later"; "Common.button.cancel" = "Cancel"; "Common.button.ok" = "ОК"; /* Sberbank */ -"Contract.Sberbank.PhoneInput.BottomHint" = "For the text message from Sberbank with the payment code"; +"Contract.Sberbank.PhoneInput.BottomHint" = "For confirming the payment via Sberbank Online"; -"Contract.Sberbank.PhoneInput.Placeholder" = "+ 7 987 654 32 10"; +"Contract.Sberbank.PhoneInput.Placeholder" = "+ 7 900 000 00 00"; -"Contract.Sberbank.PhoneInput.Title" = "Phone number in Sberbank Online"; +"Contract.Sberbank.PhoneInput.Title" = "Your phone number in Sberbank Online"; "Contract.changePaymentMethod" = "Change"; @@ -88,87 +88,183 @@ "Contract.resendSms" = "Send again"; /* В процессе токенизации ApplePay произошла ошибка */ -"Error.ApplePayStrategy.failTokenizeData" = "An error occurred during ApplePay tokenization"; +"Error.ApplePayStrategy.failTokenizeData" = "It didn't work. Try again"; -"Error.emptyPaymentOptions" = "No available payment methods"; +"Error.emptyPaymentOptions" = "Couldn't load payment methods"; /* Пользователь потратил все попытки ввода. Создаем новую сессию на авторизацию */ -"Error.endedAttemptsToEnterStartOver" = "Too many attempts. Try again later"; +"Error.endedAttemptsToEnterStartOver" = "No attempts left, you need to get a new code"; -"Error.internet" = "Connection error. Try again once you're online."; +"Error.internet" = "Looks like a connection error. Check it and try again"; /* После авторизации в кошельке при запросе доступных методов кошелёк отсутствует */ -"Error.noWalletTitle" = "Wallet payment is not available"; +"Error.noWalletTitle" = "Payments via the YooMoney wallet are unavailable"; /* Пользователь ввел верный код, но возникла ошибка. Создаем новую сессию на авторизацию */ -"Error.resendAuthCodeAndStartOver" = "Error, try again"; +"Error.resendAuthCodeAndStartOver" = "It didn't work. Try starting over"; + +/* Текст ошибки при отвязке карты */ +"Error.unbindCardFailed" = "Couldn't remove the card. Please try again"; /* С сервера пришел неподдерживаемый способ прохождения платежной авторизации. */ "Error.unsupportedAuthType" = "This method of receiving passwords is not supported. You can change it in the wallet settings on the YooMoney website"; /* Linked card */ -"LinkedCard.title" = "Linked card"; +"LinkedCard.title" = "YooMoney сard"; "LogoutConfirmation.format.title" = "Are you sure you want to sign out of account '%@'?"; -"PaymentAuthorization.description.witPhone" = "We sent a verification code to %@"; +"PaymentAuthorization.description.witPhone" = "We sent a code to %@"; "PaymentAuthorization.description.withoutPhone" = "We sent a verification code"; "PaymentAuthorization.invalidAnswer" = "Invalid code. Double-check and try again"; -"PaymentAuthorization.invalidAnswer.sessionsLeft" = "Invalid code. Attempts left: %d"; +"PaymentAuthorization.invalidAnswer.sessionsLeft" = "Invalid code again. Attempts left: %d"; -"PaymentAuthorization.nextSessionTimeFormatter" = "d MMMM в HH:mm"; +"PaymentAuthorization.nextSessionTimeFormatter" = "d MMMM at HH:mm"; /* Payment authorization */ "PaymentAuthorization.remainingTime" = "Get a new code in %@"; -"PaymentAuthorization.verifyAttemptsExceeded" = "No attempts left"; +"PaymentAuthorization.verifyAttemptsExceeded" = "No attempts left, you need to get a new code"; -"PaymentAuthorization.verifyAttemptsExceeded.nextSession" = "No attempts left. You can try %@"; +"PaymentAuthorization.verifyAttemptsExceeded.nextSession" = "No attempts left. Try again in %@"; "PaymentMethod.applePay" = "Apple Pay"; "PaymentMethod.bankCard" = "Bank card"; +/* Способ оплаты - `Привязанная карта` https://disk.yandex.ru/d/sFpmR3gLEc287Q */ +"PaymentMethod.linkedCard" = "Bank card"; + +/* Подробности о безопасной сделке https://disk.yandex.ru/i/zOrGowAKK4uz3A */ +"PaymentMethod.safeDealInfo.body" = "It can happen when you pay on an online platform where it's possible to buy from multiple sellers at once (for example, on marketplaces).\n\nYou can find the list of recipients on the platform where you're making the payment."; + +/* текст-ссылка https://disk.yandex.ru/i/UOEMl4Ig3Z_4UA */ +"PaymentMethod.safeDealInfo.link.begining" = "The payment can have "; + +/* текст-ссылка интерактивная часть https://disk.yandex.ru/i/UOEMl4Ig3Z_4UA */ +"PaymentMethod.safeDealInfo.link.highlighted" = "multiple recipients"; + +/* Тайтл информации о безопасной сделке https://disk.yandex.ru/i/zOrGowAKK4uz3A */ +"PaymentMethod.safeDealInfo.title" = "Why the payment has multiple recipients"; + "PaymentMethod.sberpay" = "SberPay"; "PaymentMethod.wallet" = "YooMoney"; +/* Способ оплаты - `Карта Юмани` https://disk.yandex.ru/d/sFpmR3gLEc287Q */ +"PaymentMethod.yooMoneyCard" = "YooMoney сard"; + +/* Текст кнопки отвязать https://disk.yandex.ru/i/f9rYGyNbx2HJ0Q */ +"PaymentMethods.alert.cancel" = "Cancel"; + +/* Текст кнопки отвязать https://disk.yandex.ru/i/f9rYGyNbx2HJ0Q */ +"PaymentMethods.alert.unbindCard" = "Remove"; + /* Payment methods */ "PaymentMethods.paymentMethods" = "Payment method"; -"SavePaymentMethod.BankCard.Force.Text" = "After the payment, the card will be linked, so that"; +/* Текст кнопки отвязать */ +"PaymentMethods.unbindCard" = "Remove"; + +/* Текст информера о опциональном подключении автосписания при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.header.autopayments.optional" = "Allow autopayments"; + +/* Текст информера о неопциональном подключении автосписания при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.header.autopayments.required" = "Allowing autopayments"; + +/* Текст информера о опциональном сохранении платёжных данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.header.saveData.optional" = "Save payment details"; + +/* Текст информера о неопциональном сохранении платёжных данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.header.saveData.required" = "Saving payment details"; + +/* Текст информера о опциональном подключении автосписания и сохранении данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.header.saveDataAndAutopayments.optional" = "Allow autopayments and save payment details"; + +/* Текст информера о неопциональном подключении автосписания и сохранении данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.header.saveDataAndAutopayments.required" = "Allowing autopayments and saving payment details"; + +/* Текст информации о сохранении данных карты https://disk.yandex.ru/i/yLD0tpyvO3zvLg */ +"RecurrencyAndSavePaymentData.info.saveData.message" = "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.\n\nYou 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."; + +/* Заголовок информации о сохранении данных карты https://disk.yandex.ru/i/yLD0tpyvO3zvLg */ +"RecurrencyAndSavePaymentData.info.saveData.title" = "Saving payment details"; + +/* Текст информации о сохранении данных карты и автосписаниях https://disk.yandex.ru/i/yLD0tpyvO3zvLg */ +"RecurrencyAndSavePaymentData.info.saveDataAndAutopayments.message" = "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.\n\nBesides that, we'll link the card (including if it's been used via ApplePay) 
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.\n\nAutopayments 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."; + +/* Заголовок информации о сохранении данных карты и автосписаниях https://disk.yandex.ru/i/yLD0tpyvO3zvLg */ +"RecurrencyAndSavePaymentData.info.saveDataAndAutopayments.title" = "Autopayments and saving payment details"; + +/* Текст со ссылкой информации об опциональном подключении автоплатежа при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.autopayments.optional" = "After the payment, this card will be saved: the store will be able debiting money without your participation"; + +/* Текст со ссылкой информации об опциональном подключении автоплатежа при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.autopayments.required" = "By making this payment, you allow saving the card 
and debiting money without your participation"; + +/* Текст со ссылкой информации об опциональном подключении автоплатежа и сохранения данных карты при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.autopaymentsAndSaveData.optional" = "After the payment, the store will save your bank card details and will be able to debit money without your participation"; + +/* Текст со ссылкой информации об опциональном подключении автоплатежа и сохранения данных карты при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.autopaymentsAndSaveData.required" = "By making this payment, you allow saving your bank card details and debiting money without your participation"; + +/* Интерактивная часть текста со ссылкой информации об опциональном подключении автоплатежа при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.interactive.autopayments.optional" = "debiting money without your participation"; + +/* Интерактивная часть текста со ссылкой информации об опциональном подключении автоплатежа при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.interactive.autopayments.required" = "debiting money without your participation"; + +/* Интерактивная часть текста со ссылкой информации об опциональном подключении автоплатежа и сохранения данных карты при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.interactive.autopaymentsAndSaveData.optional" = "will save your bank card details and will be able to debit money without your participation"; -"SavePaymentMethod.BankCard.Force.hyperText" = "money could be debited upon store's request"; +/* Интерактивная часть текста со ссылкой информации об опциональном подключении автоплатежа и сохранения данных карты при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.interactive.autopaymentsAndSaveData.required" = "saving your bank card details and debiting money without your participation"; -"SavePaymentMethod.BankCard.UserPriority.Text" = "Link the card and"; +/* Интерактивная часть текста со ссылкой информации об опциональном сохранении платёжных данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.interactive.saveData.optional" = "will save your bank card details"; -"SavePaymentMethod.BankCard.UserPriority.hyperText" = "debit money upon store's request"; +/* Интерактивная часть текста со ссылкой информации об опциональном сохранении платёжных данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.interactive.saveData.required" = "will save your bank card details"; -"SavePaymentMethod.Wallet.Force.Text" = "After the payment, the wallet will be linked: the store will be able"; +/* Текст со ссылкой информации об опциональном сохранении платёжных данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.saveData.optional" = "The store will save your bank card details: next time, you won't need to enter them"; -"SavePaymentMethod.Wallet.Force.hyperText" = "to debit money without your participation"; +/* Текст со ссылкой информации об опциональном сохранении платёжных данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.saveData.required" = "The store will save your bank card details: next time, you won't need to enter them"; -"SavePaymentMethod.Wallet.UserPriority.Text" = "Allow the store"; +"SavePaymentMethod.BankCard.Force.Text" = "By making this payment, you allow saving the card 
and"; -"SavePaymentMethod.Wallet.UserPriority.hyperText" = "to debit money without my participation"; +"SavePaymentMethod.BankCard.Force.hyperText" = "debiting money without your participation"; -"SavePaymentMethodInfo.BankCard.Body" = "It means that you're allowing YooMoney to debit money upon store's request without an additional confirmation from you, from this card or a new one in case it's reissued (if your bank can update the details automatically).\n\nYou can cancel linking at any moment via store's support service."; +"SavePaymentMethod.BankCard.UserPriority.Text" = "After the payment, this card will be saved: the store will be able"; -"SavePaymentMethodInfo.BankCard.Header" = "Permission to debit money upon store's request"; +"SavePaymentMethod.BankCard.UserPriority.hyperText" = "to debit money without your participation"; + +"SavePaymentMethod.Wallet.Force.Text" = "By making this payment, you allow saving this wallet and"; + +"SavePaymentMethod.Wallet.Force.hyperText" = "debiting money without your participation"; + +"SavePaymentMethod.Wallet.UserPriority.Text" = "The store will be able"; + +"SavePaymentMethod.Wallet.UserPriority.hyperText" = "to debit money without your participation"; + +"SavePaymentMethodInfo.BankCard.Body" = "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. \n\nAutopayments 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."; + +"SavePaymentMethodInfo.BankCard.Header" = "How autopayments work"; "SavePaymentMethodInfo.Button.GotIt" = "Got it"; -"SavePaymentMethodInfo.Wallet.Body" = "It means that you're allowing YooMoney to debit money from the wallet upon store's request without an additional confirmation from you. You can cancel these autopayments at any moment in the wallet settings (on the YooMoney website)."; +"SavePaymentMethodInfo.Wallet.Body" = "It means that you're allowing YooMoney to debit money from the wallet upon store's request without an additional confirmation from you. You can disable these autopayments at any moment in the wallet settings
 (on the YooMoney website)."; /* Save payment method */ "SavePaymentMethodInfo.Wallet.Header" = "Permission to debit money without your participation"; "Sberpay.Contract.Title" = "SberPay"; -"Sberpay.paymentMethodTitle" = "Next, open the Sberbank Online application - confirm the payment"; +"Sberpay.paymentMethodTitle" = "Next, the SberBank Online application will be opened: confirm the payment"; "TermsOfService.Hyperlink" = "the terms and conditions of the service"; @@ -176,16 +272,16 @@ "TermsOfService.Text" = "By clicking this button, accept"; /* Текст условий сервиса с ссылкой на экране установки пароля для пользователя без установленной почты https://yadi.sk/i/DgL-5V4hQL15WQ */ -"Wallet.Authorization.addEmailTitle" = "For receipts and notifications"; +"Wallet.Authorization.addEmailTitle" = "Enter your email address"; /* Текст условий сервиса с ссылкой на экране установки пароля для пользователя c установленной почтой https://yadi.sk/i/DgL-5V4hQL15WQ */ -"Wallet.Authorization.emailCheckboxTitle" = "I'd like to receive news about the service, discounts, and surveys: not more than once a week"; +"Wallet.Authorization.emailCheckboxTitle" = "For receipts and notifications"; /* Текст свитча согласия на рассылку на экране ввода почты https://yadi.sk/i/8BSuo7q_6CJzbg */ -"Wallet.Authorization.hardMigrationScreenButtonSubtitle" = "It doesn't change any limits, commission rates, or other terms of use of the wallet in any way: learn more"; +"Wallet.Authorization.hardMigrationScreenButtonSubtitle" = "I'd like to get email notifications about promotions"; /* Текст под полем ввода почты на экране ввода почты https://yadi.sk/i/8BSuo7q_6CJzbg */ -"Wallet.Authorization.hardMigrationScreenSubtitle" = "You used to sign in to your wallet using your Yandex username and password, now you need a YooMoney account instead.\nLet us help you get it:\n\n— sign in using your Yandex username and password, \n— allow YooMoney to access your name and email address, \n— create a new password.\n\nYou'll get a YooMoney account with the same wallet that you've had in it.\nEmail address and password are used for signing in, and text message codes are used for confirming actions."; +"Wallet.Authorization.hardMigrationScreenSubtitle" = "For receipts and notifications"; /* Заголовок экрана про миграцию, который после нажатия на большой баннер на экране ввода почты/телефона при авторизации https://yadi.sk/i/_IMGLswOravIOw */ "Wallet.Authorization.hardMigrationScreenTitle" = "Time to switch to YooMoney"; @@ -197,10 +293,10 @@ "Wallet.Authorization.migrationBannerText" = "If you signed up before October 21, you need to switch to YooMoney"; /* Текст на экране про миграцию, который после нажатия на большой баннер на экране ввода почты/телефона при авторизации https://yadi.sk/i/_IMGLswOravIOw */ -"Wallet.Authorization.migrationScreenButtonSubtitle" = "It doesn't change any limits, commission rates, or other terms of use of the wallet in any way: learn more"; +"Wallet.Authorization.migrationScreenButtonSubtitle" = "You used to sign in to your wallet using your Yandex username and password, now you need a YooMoney account instead.\n\nLet us help you get it:\n\n— sign in using your Yandex username and password,\n— allow YooMoney to access your name and email address,\n— create a new password.\n\nYou'll get a YooMoney account with the same wallet that you've had in it.\nEmail address and password are used for signing in, and text message codes are used for confirming actions."; /* Текст с ссылкой под кнопкой на экране про миграцию, который после нажатия на большой баннер на экране ввода почты/телефона при авторизации https://yadi.sk/i/_IMGLswOravIOw */ -"Wallet.Authorization.migrationScreenSubtitle" = "Your wallet is now in YooMoney, separate from your Yandex account.\n\n— What remains as it was before: your wallet number, settings configuration, and terms of use.\n\n— What's changed: you'll use your email address or phone number instead of your username (like in Yandex). You can also update your password.\n\n— What you need to do now: sign in to your Yandex account, under which you have your wallet."; +"Wallet.Authorization.migrationScreenSubtitle" = "It doesn't change any limits, commission rates, or other terms of use of the wallet in any way: learn more"; /* Заголовок экрана про миграцию, который после нажатия на немигрированный аккаунт на экране выбора аккаунта https://yadi.sk/i/_IMGLswOravIOw */ "Wallet.Authorization.migrationScreenTitle" = "Why do I need to switch?"; @@ -212,11 +308,60 @@ "Wallet.Authorization.userWithEmailAgreementTitle" = "By clicking this button, I confirm that I'm aware of all the legal terms and conditions of the service"; /* Wallet */ -"Wallet.savePaymentMethod.title" = "Link a wallet"; +"Wallet.savePaymentMethod.title" = "Allow autopayments"; "YooMoney.title" = "YooMoney"; +/* Текст, в информере, о сохранении автоплатежа https://disk.yandex.ru/i/QNJyBrfP52vQOw */ +"card.details.autopaymentPersists" = "Autopayments will continue after you remove the card. To cancel autopayments, contact store's support service"; + +/* Текст информации о работе автосписания https://disk.yandex.ru/i/r9l5HObi2jZy6A */ +"card.details.info.autopay.details" = "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.\n\nAutopayments 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."; + +/* Заголовок информации о работе автосписания https://disk.yandex.ru/i/r9l5HObi2jZy6A */ +"card.details.info.autopay.title" = "How autopayments work"; + +/* Текст кнопки, в информере, ведущей в подробности https://disk.yandex.ru/i/QNJyBrfP52vQOw */ +"card.details.info.more" = "Learn more"; + +/* Текст информации об отвязке карты https://disk.yandex.ru/i/59heYTl9Q4L2fA */ +"card.details.info.unbind.details" = "To do that, go to the wallet settings on the YooMoney website or in the app.\n\nIn the app: tap on your profile picture, select \"Bank cards\", swipe the one you need to the left, and tap \"Remove\".\n\nOn the website: go to the wallet settings, open the \"Linked cards\" tab, find the one you need, and click \"Unlink\"."; + +/* Заголовок информации об отвязке карты https://disk.yandex.ru/i/59heYTl9Q4L2fA */ +"card.details.info.unbind.title" = "How to unlink a card from the wallet"; + +/* Текст `Отвязать карту` https://disk.yandex.ru/i/QNJyBrfP52vQOw */ +"card.details.unbind" = "Remove the card"; + +/* Текст нотификации об ошибке отвязки карты. Параметр - маска карты https://disk.yandex.ru/i/QNJyBrfP52vQOw */ +"card.details.unbind.fail" = "Couldn't remove card %@"; + +/* Текст нотификации об успешной отвязке карты. Параметр - маска карты https://disk.yandex.ru/i/JWC70LuzuJSeEw */ +"card.details.unbind.success" = "Card %@ removed"; + +/* Текст, ведущей назад, кнопки https://disk.yandex.ru/i/dcgivhF4QbURwA */ +"card.details.unwind" = "Back"; + +/* Текст, в информере, для карты привязанной к кошельку https://disk.yandex.ru/i/dcgivhF4QbURwA */ +"card.details.yoocardUnbindDetails" = "You can only unlink this card in the wallet settings"; + "image.logo" = "logo.kassa.en"; +"settings.payment_methods.apple_pay" = "Apple Pay"; + +"settings.payment_methods.bank_card" = "Bank card"; + +"settings.payment_methods.sberbank" = "SberBank Online"; + +"settings.payment_methods.title" = "Payment methods"; + +"settings.payment_methods.yoo_money" = "YooMoney"; + +"settings.test_mode.title" = "Test mode"; + +"settings.title" = "Settings"; + "settings.ui_customization.bank_card_scan_enabled" = "Bank card scanning"; +"settings.ui_customization.yoo_money_logo" = "YooMoney logo"; + diff --git a/YooKassaPayments/Public/Resources/ru.lproj/Localizable.strings b/YooKassaPayments/Public/Resources/ru.lproj/Localizable.strings index 46dc7463..0a5e649d 100644 --- a/YooKassaPayments/Public/Resources/ru.lproj/Localizable.strings +++ b/YooKassaPayments/Public/Resources/ru.lproj/Localizable.strings @@ -1,5 +1,5 @@ /* Текст на контракте Apple Pay `Разрешить списывать деньги` https://yadi.sk/i/FWhOeo-T3eCQzg */ -"ApplePayContract.SavePaymentMethod.Title" = "Разрешить списывать деньги"; +"ApplePayContract.SavePaymentMethod.Title" = "Разрешить автосписания"; /* `Комиссия` на экране Apple Pay https://yadi.sk/d/Vu310EJgWtvrAQ */ "ApplePayContract.fee" = "Комиссия"; @@ -11,10 +11,10 @@ "ApplePayContract.title" = "Apple Pay"; /* По неизвестным нам причинам экран ApplePay не отобразился */ -"ApplePayUnavailable.title" = "Apple Pay недоступен"; +"ApplePayUnavailable.title" = "Apple Pay недоступен, попробуйте ещё раз"; /* Текст `Привязать карту` на экране `Банковская карта` https://yadi.sk/i/Z2oi1Uun7nS-jA */ -"BankCard.savePaymentMethod.title" = "Привязать карту"; +"BankCard.savePaymentMethod.title" = "Разрешить автосписания"; /* Title `Банковская карта` на экране `Банковская карта` https://yadi.sk/i/Z2oi1Uun7nS-jA */ "BankCardDataInput.navigationBarTitle" = "Банковская карта"; @@ -29,10 +29,10 @@ "BankCardDataInputView.BottomHint.invalidPan" = "Проверьте номер карты"; /* Текст `Привязать карту` на экране `Сохраненная карта` https://yadi.sk/d/Cyocbh86zUr3cA */ -"BankCardRepeat.savePaymentMethod.title" = "Привязать карту"; +"BankCardRepeat.savePaymentMethod.title" = "Разрешить автосписания"; /* Title `Сохраненная карта` на экране `Сохраненная карта` https://yadi.sk/d/Cyocbh86zUr3cA */ -"BankCardRepeat.title" = "Сохраненная карта"; +"BankCardRepeat.title" = "Банковская карта"; /* Текст `Код` при вводе данных банковской карты https://yadi.sk/i/qhizzdr8cAATsw */ "BankCardView.inputCvcHint" = "Код"; @@ -55,17 +55,14 @@ /* Текст `Введите` при вводе данных банковской карты в случае если сканирование не доступно https://yadi.sk/i/fbrtpMi0d-k4xw */ "BankCardView.inputPanPlaceholderWithoutScan" = "Введите"; -/* Отменить */ -"Cancel" = "Отменить"; - /* Текст `Что то пошло не так` https://yadi.sk/i/JapUT2mTEVnTtw */ -"Common.Error.unknown" = "Что то пошло не так"; +"Common.Error.unknown" = "Не получилось. Попробуйте ещё раз"; /* Текст кнопки на Placeholder `Повторить` */ "Common.PlaceholderView.buttonTitle" = "Повторить"; /* Текст на Placeholder `Попробуйте повторить чуть позже.` */ -"Common.PlaceholderView.text" = "Попробуйте повторить чуть позже."; +"Common.PlaceholderView.text" = "Попробуйте повторить чуть позже"; /* Текст `Отменить` на Alert https://yadi.sk/i/68ImXb9rz31RkQ */ "Common.button.cancel" = "Отменить"; @@ -74,13 +71,13 @@ "Common.button.ok" = "ОК"; /* Текст `Для смс от Сбербанка с кодом для оплаты` https://yadi.sk/i/T-XQGU9NaPMgKA */ -"Contract.Sberbank.PhoneInput.BottomHint" = "Для смс от Сбербанка с кодом для оплаты"; +"Contract.Sberbank.PhoneInput.BottomHint" = "Для подтверждения оплаты в СберБанк Онлайн"; /* Текст `+ 7 987 654 32 10` https://yadi.sk/i/T-XQGU9NaPMgKA */ -"Contract.Sberbank.PhoneInput.Placeholder" = "+ 7 987 654 32 10"; +"Contract.Sberbank.PhoneInput.Placeholder" = "+ 7 900 000 00 00"; /* Текст `Номер в Сбербанк Онлайн` https://yadi.sk/i/T-XQGU9NaPMgKA */ -"Contract.Sberbank.PhoneInput.Title" = "Номер в Сбербанк Онлайн"; +"Contract.Sberbank.PhoneInput.Title" = "Ваш телефон в СберБанк Онлайн"; /* Текст на контракте `Включая комиссию` https://yadi.sk/i/Ri9RjHDtilycWw */ "Contract.fee" = "Включая комиссию"; @@ -102,44 +99,47 @@ "Contract.resendSms" = "Отправить снова"; /* В процессе токенизации ApplePay произошла ошибка https://yadi.sk/i/G9zC-PLLpmuQVw */ -"Error.ApplePayStrategy.failTokenizeData" = "В процессе токенизации ApplePay произошла ошибка"; +"Error.ApplePayStrategy.failTokenizeData" = "Не получилось. Попробуйте ещё раз"; /* Ошибка `Нет доступных способов оплаты` на экране выбора способа оплаты */ -"Error.emptyPaymentOptions" = "Нет доступных способов оплаты"; +"Error.emptyPaymentOptions" = "Способы оплаты не загрузились"; /* Пользователь потратил все попытки ввода. Создаем новую сессию на авторизацию */ -"Error.endedAttemptsToEnterStartOver" = "Слишком много попыток. Попробуйте позже"; +"Error.endedAttemptsToEnterStartOver" = "Попытки закончились, нужно получить новый код"; /* Ошибка `Проблема с интернетом` */ -"Error.internet" = "Проблема с интернетом. Попробуйте еще раз, когда будете онлайн"; +"Error.internet" = "Похоже, проблема с интернетом. Проверьте и попробуйте ещё раз"; /* После авторизации в кошельке при запросе доступных методов кошелёк отсутствует */ -"Error.noWalletTitle" = "Оплата кошельком недоступна"; +"Error.noWalletTitle" = "Оплата кошельком ЮMoney недоступна"; /* Пользователь ввел верный код, но возникла ошибка. Создаем новую сессию на авторизацию */ -"Error.resendAuthCodeAndStartOver" = "Не получилось, попробуйте заново"; +"Error.resendAuthCodeAndStartOver" = "Не получилось. Попробуйте начать сначала"; + +/* Текст ошибки при отвязке карты */ +"Error.unbindCardFailed" = "Не получилось удалить карту. Попробуйте ещё раз"; /* Title `Привязанная карта` на экране `Привязанная карта` https://yadi.sk/d/yLgHHmqAsklYng */ -"LinkedCard.title" = "Привязанная карта"; +"LinkedCard.title" = "Карта ЮMoney"; /* Текст в Alert при выходе из аккаунта ЮMoney https://yadi.sk/i/68ImXb9rz31RkQ */ "LogoutConfirmation.format.title" = "Уверены, что хотите выйти из аккаунта '%@'?"; -"PaymentAuthorization.description.witPhone" = "Отправили проверочный код на %@"; +"PaymentAuthorization.description.witPhone" = "Отправили код на %@"; "PaymentAuthorization.description.withoutPhone" = "Отправили проверочный код"; -"PaymentAuthorization.invalidAnswer" = "Это не тот код. Проверьте и введите ещё раз"; +"PaymentAuthorization.invalidAnswer" = "Это не тот код. Проверьте и попробуйте ещё раз"; -"PaymentAuthorization.invalidAnswer.sessionsLeft" = "Это не тот код. Осталось попыток: %d"; +"PaymentAuthorization.invalidAnswer.sessionsLeft" = "Снова не тот код. Осталось попыток: %d"; "PaymentAuthorization.nextSessionTimeFormatter" = "d MMMM в HH:mm"; "PaymentAuthorization.remainingTime" = "Получить новый код через %@"; -"PaymentAuthorization.verifyAttemptsExceeded" = "Попытки закончились"; +"PaymentAuthorization.verifyAttemptsExceeded" = "Попытки закончились, нужно получить новый код"; -"PaymentAuthorization.verifyAttemptsExceeded.nextSession" = "Попытки закончились. Попробовать можно %@"; +"PaymentAuthorization.verifyAttemptsExceeded.nextSession" = "Попытки закончились. Попробуйте снова через %@"; /* Способ оплаты - `Apple Pay` https://yadi.sk/i/smhhxBAxkP8Ebw */ "PaymentMethod.applePay" = "Apple Pay"; @@ -148,7 +148,19 @@ "PaymentMethod.bankCard" = "Банковская карта"; /* Способ оплаты - `Привязанная карта` https://disk.yandex.ru/d/sFpmR3gLEc287Q */ -"PaymentMethod.linkedCard" = "Привязанная карта"; +"PaymentMethod.linkedCard" = "Банковская карта"; + +/* Подробности о безопасной сделке https://disk.yandex.ru/i/zOrGowAKK4uz3A */ +"PaymentMethod.safeDealInfo.body" = "Такое может быть, если вы платите на интернет-площадке, которая позволяет покупать одновременно у нескольких продавцов (например, на маркетплейсе).\n\nУточнить список получателей платежа можно на площадке, на которой вы совершаете платёж."; + +/* текст-ссылка https://disk.yandex.ru/i/UOEMl4Ig3Z_4UA */ +"PaymentMethod.safeDealInfo.link.begining" = "У платежа может быть "; + +/* текст-ссылка интерактивная часть https://disk.yandex.ru/i/UOEMl4Ig3Z_4UA */ +"PaymentMethod.safeDealInfo.link.highlighted" = "несколько получателей"; + +/* Тайтл информации о безопасной сделке https://disk.yandex.ru/i/zOrGowAKK4uz3A */ +"PaymentMethod.safeDealInfo.title" = "Почему у платежа несколько получателей"; /* Способ оплаты - `SberPay` https://yadi.sk/i/smhhxBAxkP8Ebw */ "PaymentMethod.sberpay" = "SberPay"; @@ -157,46 +169,121 @@ "PaymentMethod.wallet" = "ЮMoney"; /* Способ оплаты - `Карта Юмани` https://disk.yandex.ru/d/sFpmR3gLEc287Q */ -"PaymentMethod.yooMoneyCard" = "Карта Юмани"; +"PaymentMethod.yooMoneyCard" = "Карта ЮMoney"; + +/* Текст кнопки отвязать https://disk.yandex.ru/i/f9rYGyNbx2HJ0Q */ +"PaymentMethods.alert.cancel" = "Отмена"; + +/* Текст кнопки отвязать https://disk.yandex.ru/i/f9rYGyNbx2HJ0Q */ +"PaymentMethods.alert.unbindCard" = "Удалить"; /* Title `Способ оплаты` на экране выбора способа оплаты https://yadi.sk/i/0dSpSggROTC0Jw */ "PaymentMethods.paymentMethods" = "Способ оплаты"; +/* Текст кнопки отвязать */ +"PaymentMethods.unbindCard" = "Удалить"; + +/* Текст информера о опциональном подключении автосписания при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.header.autopayments.optional" = "Разрешить автосписания"; + +/* Текст информера о неопциональном подключении автосписания при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.header.autopayments.required" = "Разрешим автосписания"; + +/* Текст информера о опциональном сохранении платёжных данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.header.saveData.optional" = "Сохранить платёжные данные"; + +/* Текст информера о неопциональном сохранении платёжных данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.header.saveData.required" = "Сохраним платёжные данные"; + +/* Текст информера о опциональном подключении автосписания и сохранении данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.header.saveDataAndAutopayments.optional" = "Разрешить автосписания и сохранить платёжные данные"; + +/* Текст информера о неопциональном подключении автосписания и сохранении данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.header.saveDataAndAutopayments.required" = "Разрешим автосписания и сохраним платёжные данные"; + +/* Текст информации о сохранении данных карты https://disk.yandex.ru/i/yLD0tpyvO3zvLg */ +"RecurrencyAndSavePaymentData.info.saveData.message" = "Если вы это разрешили, мы сохраним для этого магазина и его партнёров данные вашей банковской карты — номер, имя владельца и срок действия (всё, кроме кода CVC). В следующий раз не нужно будет вводить их, чтобы заплатить в этом магазине.\n\nУдалить данные карты можно в процессе оплаты (нажмите на три точки напротив карты и выберите «Удалить карту») или через службу поддержки."; + +/* Заголовок информации о сохранении данных карты https://disk.yandex.ru/i/yLD0tpyvO3zvLg */ +"RecurrencyAndSavePaymentData.info.saveData.title" = "Сохранение платёжных данных"; + +/* Текст информации о сохранении данных карты и автосписаниях https://disk.yandex.ru/i/yLD0tpyvO3zvLg */ +"RecurrencyAndSavePaymentData.info.saveDataAndAutopayments.message" = "Если вы это разрешили, мы сохраним для этого магазина и его партнёров данные вашей банковской карты — номер, имя владельца, срок действия (всё, кроме кода CVC). В следующий раз не нужно будет их вводить, чтобы заплатить в этом магазине.\n\nКроме того, мы привяжем карту (в том числе использованную через Apple Pay) 
к магазину. После этого магазин сможет присылать запросы на автоматические списания денег — тогда платёж выполняется без дополнительного подтверждения с вашей стороны.\n\nАвтосписания продолжатся даже при перевыпуске карты, если ваш банк умеет автоматически обновлять данные. Отменить их и отвязать карту можно в любой момент — через службу поддержки магазина."; + +/* Заголовок информации о сохранении данных карты и автосписаниях https://disk.yandex.ru/i/yLD0tpyvO3zvLg */ +"RecurrencyAndSavePaymentData.info.saveDataAndAutopayments.title" = "Автосписания и сохранение платёжных данных"; + +/* Текст со ссылкой информации об опциональном подключении автоплатежа при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.autopayments.optional" = "После оплаты запомним эту карту: магазин сможет списывать деньги без вашего участия"; + +/* Текст со ссылкой информации об опциональном подключении автоплатежа при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.autopayments.required" = "Заплатив здесь, вы разрешаете запомнить карту и списывать деньги без вашего участия"; + +/* Текст со ссылкой информации об опциональном подключении автоплатежа и сохранения данных карты при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.autopaymentsAndSaveData.optional" = "После оплаты магазин сохранит данные карты и сможет списывать деньги без вашего участия"; + +/* Текст со ссылкой информации об опциональном подключении автоплатежа и сохранения данных карты при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.autopaymentsAndSaveData.required" = "Заплатив здесь, вы соглашаетесь сохранить данные карты и списывать деньги без вашего участия"; + +/* Интерактивная часть текста со ссылкой информации об опциональном подключении автоплатежа при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.interactive.autopayments.optional" = "списывать деньги без вашего участия"; + +/* Интерактивная часть текста со ссылкой информации об опциональном подключении автоплатежа при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.interactive.autopayments.required" = "списывать деньги без вашего участия"; + +/* Интерактивная часть текста со ссылкой информации об опциональном подключении автоплатежа и сохранения данных карты при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.interactive.autopaymentsAndSaveData.optional" = "сохранит данные карты и сможет списывать деньги без вашего участия"; + +/* Интерактивная часть текста со ссылкой информации об опциональном подключении автоплатежа и сохранения данных карты при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.interactive.autopaymentsAndSaveData.required" = "сохранить данные карты и списывать деньги без вашего участия"; + +/* Интерактивная часть текста со ссылкой информации об опциональном сохранении платёжных данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.interactive.saveData.optional" = "сохранит данные вашей карты"; + +/* Интерактивная часть текста со ссылкой информации об опциональном сохранении платёжных данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.interactive.saveData.required" = "сохранит данные вашей карты"; + +/* Текст со ссылкой информации об опциональном сохранении платёжных данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.saveData.optional" = "Магазин сохранит данные вашей карты — в следующий раз можно будет их не вводить"; + +/* Текст со ссылкой информации об опциональном сохранении платёжных данных при платеже https://disk.yandex.ru/i/dcZY0utIfx634w */ +"RecurrencyAndSavePaymentData.link.saveData.required" = "Магазин сохранит данные вашей карты — в следующий раз можно будет их не вводить"; + /* Текст `После оплаты привяжем карту, чтобы` https://yadi.sk/i/_PWhW8MwuxCopQ */ -"SavePaymentMethod.BankCard.Force.Text" = "После оплаты привяжем карту, чтобы"; +"SavePaymentMethod.BankCard.Force.Text" = "Заплатив здесь, вы разрешаете запомнить карту 
и"; /* Текст `списывать деньги по запросу магазина` https://yadi.sk/i/_PWhW8MwuxCopQ */ -"SavePaymentMethod.BankCard.Force.hyperText" = "списывать деньги по запросу магазина"; +"SavePaymentMethod.BankCard.Force.hyperText" = "списывать деньги без вашего участия"; /* Текст `Привязать карту и` https://yadi.sk/i/Z2oi1Uun7nS-jA */ -"SavePaymentMethod.BankCard.UserPriority.Text" = "Привязать карту и"; +"SavePaymentMethod.BankCard.UserPriority.Text" = "После оплаты запомним эту карту: магазин сможет"; /* Текст `списывать деньги по запросу магазина` https://yadi.sk/i/Z2oi1Uun7nS-jA */ -"SavePaymentMethod.BankCard.UserPriority.hyperText" = "списывать деньги по запросу магазина"; +"SavePaymentMethod.BankCard.UserPriority.hyperText" = "списывать деньги без вашего участия"; /* Текст `После оплаты привяжем кошелёк: магазин сможет` https://yadi.sk/i/rFEZPSdXTgV1bw */ -"SavePaymentMethod.Wallet.Force.Text" = "После оплаты привяжем кошелёк: магазин сможет"; +"SavePaymentMethod.Wallet.Force.Text" = "Заплатив здесь, вы разрешаете запомнить этот кошелёк и"; /* Текст `списывать деньги без вашего участия` https://yadi.sk/i/rFEZPSdXTgV1bw */ "SavePaymentMethod.Wallet.Force.hyperText" = "списывать деньги без вашего участия"; /* Текст `Разрешить магазину` https://yadi.sk/i/o89CnEUSmNsM7g */ -"SavePaymentMethod.Wallet.UserPriority.Text" = "Разрешить магазину"; +"SavePaymentMethod.Wallet.UserPriority.Text" = "Магазин сможет"; /* Текст `списывать деньги без моего участия` https://yadi.sk/i/o89CnEUSmNsM7g */ -"SavePaymentMethod.Wallet.UserPriority.hyperText" = "списывать деньги без моего участия"; +"SavePaymentMethod.Wallet.UserPriority.hyperText" = "списывать деньги без вашего участия"; /* Текст на экране разрешения списывать деньги магазином с банковской карты https://yadi.sk/i/QOSvfo9hsOPs9Q */ -"SavePaymentMethodInfo.BankCard.Body" = "Это значит, что вы разрешаете ЮMoney списывать деньги по запросу магазина без отдельного подтверждения — с этой карты или с новой, при перевыпуске (если ваш банк умеет автоматически обновлять данные).\n\nОтменить привязку можно в любой момент — через службу поддержки магазина."; +"SavePaymentMethodInfo.BankCard.Body" = "Если вы согласитесь на автосписания, 
мы привяжем банковскую карту (в том числе использованную через Apple Pay)
 к магазину. После этого магазин сможет присылать запросы на автоматические списания денег — тогда платёж выполняется без дополнительного подтверждения с вашей стороны. \n\nАвтосписания продолжатся даже при перевыпуске карты, если ваш банк умеет автоматически обновлять данные. Отключить их и отвязать карту можно в любой момент — через службу поддержки магазина."; /* Заголовок на экране разрешения списывать деньги магазином с банковской карты https://yadi.sk/i/QOSvfo9hsOPs9Q */ -"SavePaymentMethodInfo.BankCard.Header" = "Разрешение списывать деньги по запросу магазина"; +"SavePaymentMethodInfo.BankCard.Header" = "Как работают автоматические списания"; /* Текст кнопки `Понятно` https://yadi.sk/i/4MbCtrW4qrtDcQ */ "SavePaymentMethodInfo.Button.GotIt" = "Понятно"; /* Текст на экране разрешения списывать деньги магазином с кошелька https://yadi.sk/i/4MbCtrW4qrtDcQ */ -"SavePaymentMethodInfo.Wallet.Body" = "Это значит, что вы разрешаете ЮMoney списывать деньги с кошелька по запросу магазина — без дополнительного подтверждения с вашей стороны. Отменить такие списания можно в любой момент — в настройках кошелька (на сайте ЮMoney)."; +"SavePaymentMethodInfo.Wallet.Body" = "Это значит, что вы разрешаете ЮMoney списывать деньги с кошелька по запросу магазина, без дополнительного подтверждения с вашей стороны. Отключить такие списания можно в любой момент — в настройках кошелька
 (на сайте ЮMoney)."; /* Заголовок на экране разрешения списывать деньги магазином с кошелька https://yadi.sk/i/4MbCtrW4qrtDcQ */ "SavePaymentMethodInfo.Wallet.Header" = "Разрешение списывать деньги без вашего участия"; @@ -214,16 +301,16 @@ "TermsOfService.Text" = "Нажимая кнопку, вы принимаете"; /* Текст условий сервиса с ссылкой на экране установки пароля для пользователя без установленной почты https://yadi.sk/i/DgL-5V4hQL15WQ */ -"Wallet.Authorization.addEmailTitle" = "Для чеков и уведомлений"; +"Wallet.Authorization.addEmailTitle" = "Укажите почту"; /* Текст условий сервиса с ссылкой на экране установки пароля для пользователя c установленной почтой https://yadi.sk/i/DgL-5V4hQL15WQ */ -"Wallet.Authorization.emailCheckboxTitle" = "Хочу получать новости сервиса, скидки, опросы: максимум раз в неделю"; +"Wallet.Authorization.emailCheckboxTitle" = "Для чеков и уведомлений"; /* Текст свитча согласия на рассылку на экране ввода почты https://yadi.sk/i/8BSuo7q_6CJzbg */ -"Wallet.Authorization.hardMigrationScreenButtonSubtitle" = "На лимиты, комиссии и остальные условия использования кошелька это никак не влияет: вот подробности "; +"Wallet.Authorization.hardMigrationScreenButtonSubtitle" = "Хочу получать рекламные предложения на почту"; /* Текст под полем ввода почты на экране ввода почты https://yadi.sk/i/8BSuo7q_6CJzbg */ -"Wallet.Authorization.hardMigrationScreenSubtitle" = "Раньше вы заходили в кошелёк с логином и паролем Яндекса, теперь нужен профиль ЮMoney.\nСейчас поможем его получить:\n\n— вы зайдёте с логином и паролем Яндекса,\n— разрешите ЮMoney доступ к имени и почте,\n— придумаете новый пароль.\n\nУ вас появится профиль ЮMoney с прежним кошельком внутри.\nДля входа — почта и пароль, для подтверждений — смс-коды."; +"Wallet.Authorization.hardMigrationScreenSubtitle" = "Для чеков и уведомлений"; /* Заголовок экрана про миграцию, который после нажатия на большой баннер на экране ввода почты/телефона при авторизации https://yadi.sk/i/_IMGLswOravIOw */ "Wallet.Authorization.hardMigrationScreenTitle" = "Пора перейти в ЮMoney"; @@ -235,10 +322,10 @@ "Wallet.Authorization.migrationBannerText" = "Если вы регистрировались до 21 октября — нужно перейти в ЮMoney"; /* Текст на экране про миграцию, который после нажатия на большой баннер на экране ввода почты/телефона при авторизации https://yadi.sk/i/_IMGLswOravIOw */ -"Wallet.Authorization.migrationScreenButtonSubtitle" = "На лимиты, комиссии и остальные условия использования кошелька это никак не влияет: вот подробности"; +"Wallet.Authorization.migrationScreenButtonSubtitle" = "Раньше вы заходили в кошелёк с логином и паролем Яндекса, теперь нужен профиль ЮMoney.\n\nСейчас поможем его получить:\n\n— вы зайдёте с логином и паролем Яндекса,\n— разрешите ЮMoney доступ к имени и почте,\n— придумаете новый пароль.\n\nУ вас появится профиль ЮMoney с прежним кошельком внутри.\nДля входа — почта и пароль, для подтверждений — смс-коды."; /* Текст с ссылкой под кнопкой на экране про миграцию, который после нажатия на большой баннер на экране ввода почты/телефона при авторизации https://yadi.sk/i/_IMGLswOravIOw */ -"Wallet.Authorization.migrationScreenSubtitle" = "Потому что теперь кошелёк — в ЮMoney, отдельно от аккаунта в Яндексе.\n\n— Что останется как раньше: номер кошелька, ваши настройки, условия использования.\n\n— Что поменяется: вместо логина (как в Яндексе) у вас будет почта или телефон. Пароль тоже можно обновить.\n\n— Сейчас нужно: войти в аккаунт Яндекса, где есть кошелёк."; +"Wallet.Authorization.migrationScreenSubtitle" = "На лимиты, комиссии и остальные условия использования кошелька это никак не влияет: вот подробности"; /* Заголовок экрана про миграцию, который после нажатия на немигрированный аккаунт на экране выбора аккаунта https://yadi.sk/i/_IMGLswOravIOw */ "Wallet.Authorization.migrationScreenTitle" = "Зачем куда-то переходить?"; @@ -250,16 +337,16 @@ "Wallet.Authorization.userWithEmailAgreementTitle" = "Нажимая кнопку, я подтверждаю осведомлённость и согласие со всеми юридическими условиями"; /* Текст `Привязать кошелек` https://yadi.sk/i/o89CnEUSmNsM7g */ -"Wallet.savePaymentMethod.title" = "Привязать кошелек"; +"Wallet.savePaymentMethod.title" = "Разрешить автосписания"; /* Текст `ЮMoney` https://yadi.sk/i/o89CnEUSmNsM7g */ "YooMoney.title" = "ЮMoney"; /* Текст, в информере, о сохранении автоплатежа https://disk.yandex.ru/i/QNJyBrfP52vQOw */ -"card.details.autopaymentPersists" = "После отвязки карты останутся автосписания. Отменить их можно через службу поддержки магазина."; +"card.details.autopaymentPersists" = "После удаления карты останутся автосписания. Отключить их можно через службу поддержки магазина"; /* Текст информации о работе автосписания https://disk.yandex.ru/i/r9l5HObi2jZy6A */ -"card.details.info.autopay.details" = "Если вы согласитесь на автосписания, мы привяжем банковскую карту к магазину. После этого магазин сможет присылать запросы на автоматические списания денег — тогда платёж выполняется без дополнительного подтверждения с вашей стороны.\nАвтосписания продолжатся даже при перевыпуске карты, если ваш банк умеет автоматически обновлять данные. Отменить их и отвязать карту можно в любой момент — через службу поддержки магазина."; +"card.details.info.autopay.details" = "Если вы согласитесь на автосписания, мы привяжем банковскую карту (в том числе использованную через Apple Pay) к магазину. После этого магазин сможет присылать запросы на автоматические списания денег — тогда платёж выполняется без дополнительного подтверждения с вашей стороны.\n\nАвтосписания продолжатся даже при перевыпуске карты, если ваш банк умеет автоматически обновлять данные. Отключить их и отвязать карту можно в любой момент — через службу поддержки магазина."; /* Заголовок информации о работе автосписания https://disk.yandex.ru/i/r9l5HObi2jZy6A */ "card.details.info.autopay.title" = "Как работают автоматические списания"; @@ -268,45 +355,41 @@ "card.details.info.more" = "Подробнее"; /* Текст информации об отвязке карты https://disk.yandex.ru/i/59heYTl9Q4L2fA */ -"card.details.info.unbind.details" = " - Для этого зайдите в настройки кошелька на сайте или в приложении ЮMoney. - В приложении: нажмите на свою аватарку, выберите «Банковские карты», смахните нужную карту влево и нажмите «Удалить». - На сайте: перейдите в настройки кошелька, откройте вкладку «Привязанные карты», найдите нужную карту и нажмите «Отвязать». - "; +"card.details.info.unbind.details" = "Для этого зайдите в настройки кошелька на сайте или в приложении ЮMoney.\n\nВ приложении: нажмите на свою аватарку, выберите «Банковские карты», смахните нужную карту влево и нажмите «Удалить».\n\nНа сайте: перейдите в настройки кошелька, откройте вкладку «Привязанные карты», найдите нужную карту и нажмите «Отвязать»."; /* Заголовок информации об отвязке карты https://disk.yandex.ru/i/59heYTl9Q4L2fA */ "card.details.info.unbind.title" = "Как отвязать карту от кошелька"; /* Текст `Отвязать карту` https://disk.yandex.ru/i/QNJyBrfP52vQOw */ -"card.details.unbind" = "Отвязать карту"; +"card.details.unbind" = "Удалить карту"; /* Текст нотификации об ошибке отвязки карты. Параметр - маска карты https://disk.yandex.ru/i/QNJyBrfP52vQOw */ -"card.details.unbind.fail" = "Не удалось отвязать карту %@"; +"card.details.unbind.fail" = "Не получилось удалить карту %@"; /* Текст нотификации об успешной отвязке карты. Параметр - маска карты https://disk.yandex.ru/i/JWC70LuzuJSeEw */ -"card.details.unbind.success" = "Карта %@ отвязана"; +"card.details.unbind.success" = "Карта %@ удалена"; /* Текст, ведущей назад, кнопки https://disk.yandex.ru/i/dcgivhF4QbURwA */ -"card.details.unwind" = "Вернуться"; +"card.details.unwind" = "Назад"; /* Текст, в информере, для карты привязанной к кошельку https://disk.yandex.ru/i/dcgivhF4QbURwA */ "card.details.yoocardUnbindDetails" = "Отвязать эту карту можно только в настройках кошелька"; -"settings.payment_methods.apple_pay" = "settings.payment_methods.apple_pay"; +"settings.payment_methods.apple_pay" = "Apple Pay"; -"settings.payment_methods.bank_card" = "settings.payment_methods.bank_card"; +"settings.payment_methods.bank_card" = "Банковская карта"; -"settings.payment_methods.sberbank" = "settings.payment_methods.sberbank"; +"settings.payment_methods.sberbank" = "СберБанк Онлайн"; -"settings.payment_methods.title" = "settings.payment_methods.title"; +"settings.payment_methods.title" = "Способы оплаты"; -"settings.payment_methods.yoo_money" = "settings.payment_methods.yoo_money"; +"settings.payment_methods.yoo_money" = "ЮMoney"; -"settings.test_mode.title" = "settings.test_mode.title"; +"settings.test_mode.title" = "Тестовый режим"; -"settings.title" = "settings.title"; +"settings.title" = "Настройки"; "settings.ui_customization.bank_card_scan_enabled" = "Сканирование банковской карты"; -"settings.ui_customization.yoo_money_logo" = "settings.ui_customization.yoo_money_logo"; +"settings.ui_customization.yoo_money_logo" = "Логотип ЮMoney"; diff --git a/YooKassaPayments/Public/TokenizationAssembly.swift b/YooKassaPayments/Public/TokenizationAssembly.swift index 6d04283f..5e0bf198 100644 --- a/YooKassaPayments/Public/TokenizationAssembly.swift +++ b/YooKassaPayments/Public/TokenizationAssembly.swift @@ -66,12 +66,13 @@ public enum TokenizationAssembly { tokenizationSettings: inputData.tokenizationSettings, testModeSettings: inputData.testModeSettings, isLoggingEnabled: inputData.isLoggingEnabled, - getSavePaymentMethod: makeGetSavePaymentMethod(inputData.savePaymentMethod), + getSavePaymentMethod: inputData.boolFromSavePaymentMethod, moneyAuthClientId: inputData.moneyAuthClientId, returnUrl: inputData.returnUrl, savePaymentMethod: inputData.savePaymentMethod, userPhoneNumber: inputData.userPhoneNumber, - cardScanning: inputData.cardScanning + cardScanning: inputData.cardScanning, + customerId: inputData.customerId ) let (viewController, moduleInput) = PaymentMethodsAssembly.makeModule( @@ -105,22 +106,3 @@ public enum TokenizationAssembly { return viewControllerToReturn } } - -private func makeGetSavePaymentMethod( - _ savePaymentMethod: SavePaymentMethod -) -> Bool? { - let getSavePaymentMethod: Bool? - - switch savePaymentMethod { - case .on: - getSavePaymentMethod = true - - case .off: - getSavePaymentMethod = false - - case .userSelects: - getSavePaymentMethod = nil - } - - return getSavePaymentMethod -} diff --git a/YooKassaPaymentsDemoApp/Resources/Info.plist b/YooKassaPaymentsDemoApp/Resources/Info.plist index e6e8bb6e..c092459d 100644 --- a/YooKassaPaymentsDemoApp/Resources/Info.plist +++ b/YooKassaPaymentsDemoApp/Resources/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - $(MARKETING_VERSION) + 6.2.5 CFBundleURLTypes @@ -42,6 +42,8 @@ NSCameraUsageDescription Automatic card recognition + NSPhotoLibraryUsageDescription + Automatic card recognition UIApplicationSupportsIndirectInputEvents UILaunchStoryboardName diff --git a/YooKassaPaymentsDemoApp/Resources/en.lproj/InfoPlist.strings b/YooKassaPaymentsDemoApp/Resources/en.lproj/InfoPlist.strings index f4d8b42d..f50d0d20 100644 --- a/YooKassaPaymentsDemoApp/Resources/en.lproj/InfoPlist.strings +++ b/YooKassaPaymentsDemoApp/Resources/en.lproj/InfoPlist.strings @@ -1,3 +1,5 @@ /* Название приложения */ "CFBundleDisplayName" = "mSDK"; +"NSPhotoLibraryUsageDescription" = "Scanning card number with camera"; + diff --git a/YooKassaPaymentsDemoApp/Resources/ru.lproj/InfoPlist.strings b/YooKassaPaymentsDemoApp/Resources/ru.lproj/InfoPlist.strings index f4d8b42d..26e05870 100644 --- a/YooKassaPaymentsDemoApp/Resources/ru.lproj/InfoPlist.strings +++ b/YooKassaPaymentsDemoApp/Resources/ru.lproj/InfoPlist.strings @@ -1,3 +1,5 @@ /* Название приложения */ "CFBundleDisplayName" = "mSDK"; +"NSPhotoLibraryUsageDescription" = "Сканирование номера карты"; + diff --git a/YooKassaPaymentsDemoApp/Source/UserStories/Root/RootViewController.swift b/YooKassaPaymentsDemoApp/Source/UserStories/Root/RootViewController.swift index ccea35d5..10a18c7b 100644 --- a/YooKassaPaymentsDemoApp/Source/UserStories/Root/RootViewController.swift +++ b/YooKassaPaymentsDemoApp/Source/UserStories/Root/RootViewController.swift @@ -365,7 +365,8 @@ final class RootViewController: UIViewController { customizationSettings: CustomizationSettings(mainScheme: .blueRibbon), savePaymentMethod: .userSelects, moneyAuthClientId: "hitm6hg51j1d3g1u3ln040bajiol903b", - applicationScheme: "yookassapaymentsexample://" + applicationScheme: "yookassapaymentsexample://", + customerId: "app.example.demo.payments.yookassa" )) // let inputData: TokenizationFlow = .bankCardRepeat(BankCardRepeatModuleInputData( diff --git a/deliver/metadata/app_icon.png b/deliver/metadata/app_icon.png new file mode 100644 index 00000000..8a200a9b Binary files /dev/null and b/deliver/metadata/app_icon.png differ diff --git a/deliver/metadata/copyright.txt b/deliver/metadata/copyright.txt new file mode 100644 index 00000000..bb9507cf --- /dev/null +++ b/deliver/metadata/copyright.txt @@ -0,0 +1 @@ +© 2021 ООО НКО «ЮМани» \ No newline at end of file diff --git a/deliver/metadata/en-US/description.txt b/deliver/metadata/en-US/description.txt new file mode 100644 index 00000000..a9d08cb2 --- /dev/null +++ b/deliver/metadata/en-US/description.txt @@ -0,0 +1 @@ +mSDK Demo App diff --git a/deliver/metadata/en-US/keywords.txt b/deliver/metadata/en-US/keywords.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/deliver/metadata/en-US/keywords.txt @@ -0,0 +1 @@ + diff --git a/deliver/metadata/en-US/marketing_url.txt b/deliver/metadata/en-US/marketing_url.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/deliver/metadata/en-US/marketing_url.txt @@ -0,0 +1 @@ + diff --git a/deliver/metadata/en-US/name.txt b/deliver/metadata/en-US/name.txt new file mode 100644 index 00000000..2d65c43b --- /dev/null +++ b/deliver/metadata/en-US/name.txt @@ -0,0 +1 @@ +mSDK \ No newline at end of file diff --git a/deliver/metadata/en-US/privacy_url.txt b/deliver/metadata/en-US/privacy_url.txt new file mode 100644 index 00000000..2e8f1c44 --- /dev/null +++ b/deliver/metadata/en-US/privacy_url.txt @@ -0,0 +1 @@ +https://yoomoney.ru/page?id=527708 \ No newline at end of file diff --git a/deliver/metadata/en-US/promotional_text.txt b/deliver/metadata/en-US/promotional_text.txt new file mode 100644 index 00000000..e69de29b diff --git a/deliver/metadata/en-US/release_notes.txt b/deliver/metadata/en-US/release_notes.txt new file mode 100644 index 00000000..e69de29b diff --git a/deliver/metadata/en-US/subtitle.txt b/deliver/metadata/en-US/subtitle.txt new file mode 100644 index 00000000..e69de29b diff --git a/deliver/metadata/en-US/support_url.txt b/deliver/metadata/en-US/support_url.txt new file mode 100644 index 00000000..e25a0353 --- /dev/null +++ b/deliver/metadata/en-US/support_url.txt @@ -0,0 +1 @@ +https://yoomoney.ru/feedback/ diff --git a/deliver/metadata/primary_category.txt b/deliver/metadata/primary_category.txt new file mode 100644 index 00000000..1db27f96 --- /dev/null +++ b/deliver/metadata/primary_category.txt @@ -0,0 +1 @@ +MZGenre.Finance diff --git a/deliver/metadata/primary_first_sub_category.txt b/deliver/metadata/primary_first_sub_category.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/deliver/metadata/primary_first_sub_category.txt @@ -0,0 +1 @@ + diff --git a/deliver/metadata/primary_second_sub_category.txt b/deliver/metadata/primary_second_sub_category.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/deliver/metadata/primary_second_sub_category.txt @@ -0,0 +1 @@ + diff --git a/deliver/metadata/review_information/demo_password.txt b/deliver/metadata/review_information/demo_password.txt new file mode 100644 index 00000000..d68b7c03 --- /dev/null +++ b/deliver/metadata/review_information/demo_password.txt @@ -0,0 +1 @@ +123456qW diff --git a/deliver/metadata/review_information/demo_user.txt b/deliver/metadata/review_information/demo_user.txt new file mode 100644 index 00000000..8c33a510 --- /dev/null +++ b/deliver/metadata/review_information/demo_user.txt @@ -0,0 +1 @@ +test-alfacard@yandex.ru \ No newline at end of file diff --git a/deliver/metadata/review_information/email_address.txt b/deliver/metadata/review_information/email_address.txt new file mode 100644 index 00000000..3316c401 --- /dev/null +++ b/deliver/metadata/review_information/email_address.txt @@ -0,0 +1 @@ +romanvt@yandex-team.ru diff --git a/deliver/metadata/review_information/first_name.txt b/deliver/metadata/review_information/first_name.txt new file mode 100644 index 00000000..47e96b38 --- /dev/null +++ b/deliver/metadata/review_information/first_name.txt @@ -0,0 +1 @@ +Roman diff --git a/deliver/metadata/review_information/last_name.txt b/deliver/metadata/review_information/last_name.txt new file mode 100644 index 00000000..dae2c959 --- /dev/null +++ b/deliver/metadata/review_information/last_name.txt @@ -0,0 +1 @@ +Tsirulnikov diff --git a/deliver/metadata/review_information/notes.txt b/deliver/metadata/review_information/notes.txt new file mode 100644 index 00000000..772252cb --- /dev/null +++ b/deliver/metadata/review_information/notes.txt @@ -0,0 +1 @@ +2FA password: pa$$w0rd diff --git a/deliver/metadata/review_information/phone_number.txt b/deliver/metadata/review_information/phone_number.txt new file mode 100644 index 00000000..41c593f2 --- /dev/null +++ b/deliver/metadata/review_information/phone_number.txt @@ -0,0 +1 @@ ++79219538416 diff --git a/deliver/metadata/ru/description.txt b/deliver/metadata/ru/description.txt new file mode 100644 index 00000000..b6a208a1 --- /dev/null +++ b/deliver/metadata/ru/description.txt @@ -0,0 +1 @@ +mSDK Example \ No newline at end of file diff --git a/deliver/metadata/ru/keywords.txt b/deliver/metadata/ru/keywords.txt new file mode 100644 index 00000000..e69de29b diff --git a/deliver/metadata/ru/marketing_url.txt b/deliver/metadata/ru/marketing_url.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/deliver/metadata/ru/marketing_url.txt @@ -0,0 +1 @@ + diff --git a/deliver/metadata/ru/name.txt b/deliver/metadata/ru/name.txt new file mode 100644 index 00000000..2d65c43b --- /dev/null +++ b/deliver/metadata/ru/name.txt @@ -0,0 +1 @@ +mSDK \ No newline at end of file diff --git a/deliver/metadata/ru/privacy_url.txt b/deliver/metadata/ru/privacy_url.txt new file mode 100644 index 00000000..2e8f1c44 --- /dev/null +++ b/deliver/metadata/ru/privacy_url.txt @@ -0,0 +1 @@ +https://yoomoney.ru/page?id=527708 \ No newline at end of file diff --git a/deliver/metadata/ru/promotional_text.txt b/deliver/metadata/ru/promotional_text.txt new file mode 100644 index 00000000..e69de29b diff --git a/deliver/metadata/ru/release_notes.txt b/deliver/metadata/ru/release_notes.txt new file mode 100644 index 00000000..e69de29b diff --git a/deliver/metadata/ru/subtitle.txt b/deliver/metadata/ru/subtitle.txt new file mode 100644 index 00000000..e69de29b diff --git a/deliver/metadata/ru/support_url.txt b/deliver/metadata/ru/support_url.txt new file mode 100644 index 00000000..e25a0353 --- /dev/null +++ b/deliver/metadata/ru/support_url.txt @@ -0,0 +1 @@ +https://yoomoney.ru/feedback/ diff --git a/deliver/metadata/secondary_category.txt b/deliver/metadata/secondary_category.txt new file mode 100644 index 00000000..732f4577 --- /dev/null +++ b/deliver/metadata/secondary_category.txt @@ -0,0 +1 @@ +MZGenre.Business diff --git a/deliver/metadata/secondary_first_sub_category.txt b/deliver/metadata/secondary_first_sub_category.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/deliver/metadata/secondary_first_sub_category.txt @@ -0,0 +1 @@ + diff --git a/deliver/metadata/secondary_second_sub_category.txt b/deliver/metadata/secondary_second_sub_category.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/deliver/metadata/secondary_second_sub_category.txt @@ -0,0 +1 @@ + diff --git a/deliver/metadata/trade_representative_contact_information/address_line1.txt b/deliver/metadata/trade_representative_contact_information/address_line1.txt new file mode 100644 index 00000000..6059e9c3 --- /dev/null +++ b/deliver/metadata/trade_representative_contact_information/address_line1.txt @@ -0,0 +1 @@ +16 Leo Tolstoy St. diff --git a/deliver/metadata/trade_representative_contact_information/city_name.txt b/deliver/metadata/trade_representative_contact_information/city_name.txt new file mode 100644 index 00000000..4f720eac --- /dev/null +++ b/deliver/metadata/trade_representative_contact_information/city_name.txt @@ -0,0 +1 @@ +Moscow diff --git a/deliver/metadata/trade_representative_contact_information/country.txt b/deliver/metadata/trade_representative_contact_information/country.txt new file mode 100644 index 00000000..eaef2c9e --- /dev/null +++ b/deliver/metadata/trade_representative_contact_information/country.txt @@ -0,0 +1 @@ +Russia diff --git a/deliver/metadata/trade_representative_contact_information/is_displayed_on_app_store.txt b/deliver/metadata/trade_representative_contact_information/is_displayed_on_app_store.txt new file mode 100644 index 00000000..c508d536 --- /dev/null +++ b/deliver/metadata/trade_representative_contact_information/is_displayed_on_app_store.txt @@ -0,0 +1 @@ +false diff --git a/deliver/metadata/trade_representative_contact_information/postal_code.txt b/deliver/metadata/trade_representative_contact_information/postal_code.txt new file mode 100644 index 00000000..4aba3330 --- /dev/null +++ b/deliver/metadata/trade_representative_contact_information/postal_code.txt @@ -0,0 +1 @@ +119021 diff --git a/deliver/metadata/trade_representative_contact_information/state.txt b/deliver/metadata/trade_representative_contact_information/state.txt new file mode 100644 index 00000000..4f720eac --- /dev/null +++ b/deliver/metadata/trade_representative_contact_information/state.txt @@ -0,0 +1 @@ +Moscow diff --git a/deliver/metadata/trade_representative_contact_information/trade_name.txt b/deliver/metadata/trade_representative_contact_information/trade_name.txt new file mode 100644 index 00000000..46e6aae2 --- /dev/null +++ b/deliver/metadata/trade_representative_contact_information/trade_name.txt @@ -0,0 +1 @@ +YooMoney, NBСO LLC