diff --git a/.github/workflows/health-api-library.check.yml b/.github/workflows/health-api-library.check.yml index 7ff2fc316..986bfa4fc 100644 --- a/.github/workflows/health-api-library.check.yml +++ b/.github/workflows/health-api-library.check.yml @@ -19,7 +19,7 @@ jobs: runs-on: macos-latest strategy: matrix: - target: [GiniHealthAPILibrary, GiniHealthAPILibraryPinning] + target: [GiniHealthAPILibrary] destination: ['platform=iOS Simulator,OS=17.2,name=iPhone SE (3rd generation)', 'platform=iOS Simulator,OS=17.5,name=iPhone 15'] steps: - uses: maxim-lobanov/setup-xcode@v1.6.0 @@ -35,12 +35,6 @@ jobs: cd HealthAPILibrary/GiniHealthAPILibrary swift package update - - name: Check package GiniHealthAPILibraryPinning - if: ${{ matrix.target == 'GiniHealthAPILibraryPinning' }} - run: | - cd HealthAPILibrary/GiniHealthAPILibraryPinning - swift package update - - name: Build sdk targets run: | xcodebuild -workspace GiniMobile.xcworkspace -scheme "${{ matrix.target }}" -destination "${{ matrix.destination }}" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO @@ -61,28 +55,3 @@ jobs: ONLY_ACTIVE_ARCH=NO CLIENT_ID="gini-mobile-test" CLIENT_SECRET="${{ secrets.GINI_MOBILE_TEST_CLIENT_SECRET }}" - - - name: Build pinning example app and run integration tests - if: ${{ matrix.target == 'GiniHealthAPILibraryPinning' }} - run: > - xcodebuild clean test - -project HealthAPILibrary/GiniHealthAPILibraryPinningExample/GiniHealthAPILibraryPinningExample.xcodeproj - -scheme "GiniHealthAPILibraryPinningExampleTests" - -destination "${{ matrix.destination }}" - -skip-testing:GiniHealthAPILibraryPinningExampleTests/HealthAPILibraryPinningWrongCertificatesTests - CODE_SIGN_IDENTITY="" - CODE_SIGNING_REQUIRED=NO - ONLY_ACTIVE_ARCH=NO - CLIENT_ID="gini-mobile-test" - CLIENT_SECRET="${{ secrets.GINI_MOBILE_TEST_CLIENT_SECRET }}" - && - xcodebuild clean test - -project HealthAPILibrary/GiniHealthAPILibraryPinningExample/GiniHealthAPILibraryPinningExample.xcodeproj - -scheme "GiniHealthAPILibraryPinningExampleTests" - -destination "${{ matrix.destination }}" - -only-testing:GiniHealthAPILibraryPinningExampleTests/HealthAPILibraryPinningWrongCertificatesTests - CODE_SIGN_IDENTITY="" - CODE_SIGNING_REQUIRED=NO - ONLY_ACTIVE_ARCH=NO - CLIENT_ID="gini-mobile-test" - CLIENT_SECRET="${{ secrets.GINI_MOBILE_TEST_CLIENT_SECRET }}" diff --git a/.github/workflows/health-api-library.release.yml b/.github/workflows/health-api-library.release.yml index 3b5648e81..baca7c899 100644 --- a/.github/workflows/health-api-library.release.yml +++ b/.github/workflows/health-api-library.release.yml @@ -44,21 +44,6 @@ jobs: "ci": "true" } - - name: Publish GiniHealthAPILibraryPinning package to the release repo - uses: maierj/fastlane-action@v3.1.0 - with: - lane: 'publish_swift_package' - options: > - { - "project_folder": "HealthAPILibrary", - "package_folder": "GiniHealthAPILibraryPinning", - "version_file_path": "HealthAPILibrary/GiniHealthAPILibraryPinning/Sources/GiniHealthAPILibraryPinning/GiniHealthAPILibraryPinningVersion.swift", - "git_tag": "${{ github.ref }}", - "repo_url": "https://github.com/gini/health-api-library-pinning-ios.git", - "repo_user": "${{ secrets.RELEASE_GITHUB_USER }}", - "repo_password": "${{ secrets.RELEASE_GITHUB_PASSWORD }}", - "ci": "true" - } release-documentation: needs: release diff --git a/.github/workflows/health-sdk.check.yml b/.github/workflows/health-sdk.check.yml index 4e577be19..d5f1c6493 100644 --- a/.github/workflows/health-sdk.check.yml +++ b/.github/workflows/health-sdk.check.yml @@ -20,7 +20,7 @@ jobs: runs-on: macos-latest strategy: matrix: - target: [GiniHealthSDK, GiniHealthSDKPinning] + target: [GiniHealthSDK] destination: ['platform=iOS Simulator,OS=17.2,name=iPhone SE (3rd generation)', 'platform=iOS Simulator,OS=17.5,name=iPhone 15'] steps: @@ -36,13 +36,7 @@ jobs: run: | cd HealthSDK/GiniHealthSDK swift package update - - - name: Check package GiniHealthSDKPinning - if: ${{ matrix.target == 'GiniHealthSDKPinning' }} - run: | - cd HealthSDK/GiniHealthSDKPinning - swift package update - + - name: Build sdk targets run: | xcodebuild -workspace GiniMobile.xcworkspace -scheme "${{ matrix.target }}" -destination "${{ matrix.destination }}" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO @@ -53,7 +47,7 @@ jobs: - name: Build example app run: | - xcodebuild -project HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample.xcodeproj -scheme "GiniHealthSDKPinningExample" -destination "${{ matrix.destination }}" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO + xcodebuild -project HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample.xcodeproj -scheme "GiniHealthSDKExample" -destination "${{ matrix.destination }}" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO - name: Extract Device Name id: extract_name @@ -70,9 +64,9 @@ jobs: run: > xcodebuild clean test -project HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample.xcodeproj - -scheme "GiniHealthSDKPinningExampleTests" + -scheme "GiniHealthSDKExampleTests" -destination "${{ matrix.destination }}" - -skip-testing:GiniHealthSDKPinningExampleTests/GiniHealthSDKPinningExampleWrongCertificatesTests + -skip-testing:GiniHealthSDKExampleTests/GiniHealthSDKPinningExampleWrongCertificatesTests CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO @@ -81,11 +75,11 @@ jobs: && xcodebuild clean test -project HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample.xcodeproj - -scheme "GiniHealthSDKPinningExampleTests" + -scheme "GiniHealthSDKExampleTests" -destination "${{ matrix.destination }}" - -only-testing:GiniHealthSDKPinningExampleTests/GiniHealthSDKPinningExampleWrongCertificatesTests + -only-testing:GiniHealthSDKExampleTests/GiniHealthSDKPinningExampleWrongCertificatesTests CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO CLIENT_ID="gini-mobile-test" - CLIENT_SECRET="${{ secrets.GINI_MOBILE_TEST_CLIENT_SECRET }}" \ No newline at end of file + CLIENT_SECRET="${{ secrets.GINI_MOBILE_TEST_CLIENT_SECRET }}" diff --git a/.github/workflows/health-sdk.publish.example.apps.firebase.yml b/.github/workflows/health-sdk.publish.example.apps.firebase.yml index 09ac69f93..fb60242b1 100644 --- a/.github/workflows/health-sdk.publish.example.apps.firebase.yml +++ b/.github/workflows/health-sdk.publish.example.apps.firebase.yml @@ -73,8 +73,8 @@ jobs: - name: Setup Credentials run: | - plutil -replace client_id -string "gini-mobile-test" HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/Credentials.plist - plutil -replace client_password -string "${{ secrets.GINI_MOBILE_TEST_CLIENT_SECRET }}" HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/Credentials.plist + plutil -replace client_id -string "gini-mobile-test" HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/CredentialsManager.swift + plutil -replace client_password -string "${{ secrets.GINI_MOBILE_TEST_CLIENT_SECRET }}" HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/CredentialsManager.swift - name: Archiving project run: | diff --git a/.github/workflows/health-sdk.publish.example.apps.yml b/.github/workflows/health-sdk.publish.example.apps.yml index 15d201229..4fe80f945 100644 --- a/.github/workflows/health-sdk.publish.example.apps.yml +++ b/.github/workflows/health-sdk.publish.example.apps.yml @@ -77,8 +77,10 @@ jobs: - name: Setup Credentials run: | - plutil -replace client_id -string "gini-mobile-test" HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/Credentials.plist - plutil -replace client_password -string "${{ secrets.GINI_MOBILE_TEST_CLIENT_SECRET }}" HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/Credentials.plist + sed -i '' \ + -e 's/clientID = "client_id"/clientID = "gini-mobile-test"/' \ + -e 's/clientPassword = "client_password"/clientPassword = "${{ secrets.GINI_MOBILE_TEST_CLIENT_SECRET }}"/' \ + HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/CredentialsManager.swift - name: Archiving project run: | diff --git a/.github/workflows/health-sdk.release.yml b/.github/workflows/health-sdk.release.yml index 5898a2ef1..e02abf0e9 100644 --- a/.github/workflows/health-sdk.release.yml +++ b/.github/workflows/health-sdk.release.yml @@ -44,22 +44,6 @@ jobs: "ci": "true" } - - name: Publish GiniHealthSDKPinning package to the release repo - uses: maierj/fastlane-action@v3.1.0 - with: - lane: 'publish_swift_package' - options: > - { - "project_folder": "HealthSDK", - "package_folder": "GiniHealthSDKPinning", - "version_file_path": "HealthSDK/GiniHealthSDKPinning/Sources/GiniHealthSDKPinning/GiniHealthSDKPinningVersion.swift", - "git_tag": "${{ github.ref }}", - "repo_url": "https://github.com/gini/health-sdk-pinning-ios.git", - "repo_user": "${{ secrets.RELEASE_GITHUB_USER }}", - "repo_password": "${{ secrets.RELEASE_GITHUB_PASSWORD }}", - "ci": "true" - } - release-documentation: needs: release uses: gini/gini-mobile-ios/.github/workflows/health-sdk.publish.docs.yml@main diff --git a/.github/workflows/internal-payment-sdk.check.yml b/.github/workflows/internal-payment-sdk.check.yml new file mode 100644 index 000000000..ceddba802 --- /dev/null +++ b/.github/workflows/internal-payment-sdk.check.yml @@ -0,0 +1,33 @@ +name: Check Gini Internal Payment SDK +on: + push: + paths: + - 'GiniComponents/GiniInternalPaymentSDK/**' + tags-ignore: + - '**' + workflow_call: + workflow_dispatch: + +jobs: + check: + runs-on: macos-latest + strategy: + matrix: + target: [GiniInternalPaymentSDK] + destination: ['platform=iOS Simulator,OS=17.2,name=iPhone SE (3rd generation)', 'platform=iOS Simulator,OS=17.5,name=iPhone 15'] + steps: + - uses: maxim-lobanov/setup-xcode@v1.6.0 + with: + xcode-version: '15.3' + + - name: Checkout + uses: actions/checkout@v4 + + - name: Check package GiniInternalPaymentSDK + run: | + cd GiniComponents/GiniInternalPaymentSDK + swift package update + + - name: Build sdk targets + run: | + xcodebuild -workspace GiniMobile.xcworkspace -scheme "${{ matrix.target }}" -destination "${{ matrix.destination }}" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO \ No newline at end of file diff --git a/.github/workflows/internal-payment-sdk.release.yml b/.github/workflows/internal-payment-sdk.release.yml new file mode 100644 index 000000000..cda0b5d89 --- /dev/null +++ b/.github/workflows/internal-payment-sdk.release.yml @@ -0,0 +1,43 @@ +name: Release Gini Internal Payment SDK +on: + push: + tags: + - 'GiniInternalPaymentSDK;[0-9]+.[0-9]+.[0-9]+' + workflow_dispatch: + +jobs: + check: + uses: gini/gini-mobile-ios/.github/workflows/internal-payment-sdk.check.yml@main + + release: + needs: check + runs-on: macos-latest + steps: + - uses: maxim-lobanov/setup-xcode@v1.6.0 + with: + xcode-version: '15.3' + + - name: Checkout + uses: actions/checkout@v4 + + - name: setup ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2.0' + bundler-cache: true + + - name: Publish GiniInternalPaymentSDK package to the release repo + uses: maierj/fastlane-action@v3.1.0 + with: + lane: 'publish_swift_package' + options: > + { + "project_folder": "GiniComponents", + "package_folder": "GiniInternalPaymentSDK", + "version_file_path": "GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/GiniInternalPaymentSDKVersion.swift", + "git_tag": "${{ github.ref }}", + "repo_url": "https://github.com/gini/internal-payment-sdk-ios.git", + "repo_user": "${{ secrets.RELEASE_GITHUB_USER }}", + "repo_password": "${{ secrets.RELEASE_GITHUB_PASSWORD }}", + "ci": "true" + } \ No newline at end of file diff --git a/.github/workflows/merchant-sdk.check.yml b/.github/workflows/merchant-sdk.check.yml index 5f0b5c8fe..530789445 100644 --- a/.github/workflows/merchant-sdk.check.yml +++ b/.github/workflows/merchant-sdk.check.yml @@ -79,4 +79,4 @@ jobs: CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO CLIENT_ID="gini-mobile-test" - CLIENT_SECRET="${{ secrets.GINI_MOBILE_TEST_CLIENT_SECRET }}" \ No newline at end of file + CLIENT_SECRET="${{ secrets.GINI_MOBILE_TEST_CLIENT_SECRET }}" diff --git a/BankAPILibrary/GiniBankAPILibrary/Tests/GiniBankAPILibraryTests/PaymentServiceTests.swift b/BankAPILibrary/GiniBankAPILibrary/Tests/GiniBankAPILibraryTests/PaymentServiceTests.swift index 6d1d51227..318abfffb 100644 --- a/BankAPILibrary/GiniBankAPILibrary/Tests/GiniBankAPILibraryTests/PaymentServiceTests.swift +++ b/BankAPILibrary/GiniBankAPILibrary/Tests/GiniBankAPILibraryTests/PaymentServiceTests.swift @@ -24,8 +24,8 @@ class PaymentServiceTests: XCTestCase { paymentService.paymentRequest(id:SessionManagerMock.paymentRequestId){ result in switch result { case .success(let request): - let requestID = String(request.links?.linksSelf?.split(separator: "/").last ?? "") - XCTAssertEqual(requestID, + let requestId = String(request.links?.linksSelf?.split(separator: "/").last ?? "") + XCTAssertEqual(requestId, SessionManagerMock.paymentRequestId, "payment request ids should match") expect.fulfill() @@ -59,8 +59,8 @@ class PaymentServiceTests: XCTestCase { paymentService.payment(id: "118edf41-102a-4b40-8753-df2f0634cb86"){ result in switch result { case .success(let payment): - let requestID = String(payment.links?.paymentRequest?.split(separator: "/").last ?? "") - XCTAssertEqual(requestID, + let requestId = String(payment.links?.paymentRequest?.split(separator: "/").last ?? "") + XCTAssertEqual(requestId, SessionManagerMock.paymentID, "payment request ids should match") expect.fulfill() diff --git a/BankSDK/GiniBankSDK/Sources/GiniBankSDK/Core/GiniBankUtils.swift b/BankSDK/GiniBankSDK/Sources/GiniBankSDK/Core/GiniBankUtils.swift index 87f5f2f21..b283d3c3c 100644 --- a/BankSDK/GiniBankSDK/Sources/GiniBankSDK/Core/GiniBankUtils.swift +++ b/BankSDK/GiniBankSDK/Sources/GiniBankSDK/Core/GiniBankUtils.swift @@ -184,7 +184,7 @@ public func receivePaymentRequestId(url: URL, completion: @escaping (Result + LastUpgradeVersion = "1540" + version = "1.7"> + buildImplicitDependencies = "YES" + buildArchitectures = "Automatic"> @@ -26,9 +27,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> - - + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> diff --git a/HealthSDK/GiniHealthSDKPinning/.swiftpm/xcode/xcshareddata/xcschemes/GiniHealthSDKPinning.xcscheme b/GiniComponents/GiniInternalPaymentSDK/.swiftpm/xcode/xcshareddata/xcschemes/GiniInternalPaymentSDK.xcscheme similarity index 74% rename from HealthSDK/GiniHealthSDKPinning/.swiftpm/xcode/xcshareddata/xcschemes/GiniHealthSDKPinning.xcscheme rename to GiniComponents/GiniInternalPaymentSDK/.swiftpm/xcode/xcshareddata/xcschemes/GiniInternalPaymentSDK.xcscheme index fbd6ad8ea..a0f943beb 100644 --- a/HealthSDK/GiniHealthSDKPinning/.swiftpm/xcode/xcshareddata/xcschemes/GiniHealthSDKPinning.xcscheme +++ b/GiniComponents/GiniInternalPaymentSDK/.swiftpm/xcode/xcshareddata/xcschemes/GiniInternalPaymentSDK.xcscheme @@ -1,10 +1,11 @@ + LastUpgradeVersion = "1540" + version = "1.7"> + buildImplicitDependencies = "YES" + buildArchitectures = "Automatic"> @@ -26,15 +27,16 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> @@ -60,9 +62,9 @@ diff --git a/GiniComponents/GiniInternalPaymentSDK/Package-release.swift b/GiniComponents/GiniInternalPaymentSDK/Package-release.swift new file mode 100644 index 000000000..a23630e3e --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Package-release.swift @@ -0,0 +1,32 @@ +// swift-tools-version: 5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "GiniInternalPaymentSDK", + defaultLocalization: "en", + platforms: [.iOS(.v13)], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "GiniInternalPaymentSDK", + targets: ["GiniInternalPaymentSDK"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + .package(name: "GiniHealthAPILibrary", url: "https://github.com/gini/health-api-library-ios.git", .exact("5.0.0")), + .package(name: "GiniUtilites", url: "https://github.com/gini/utilites-ios.git", .exact("1.1.0")), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "GiniInternalPayment", + dependencies: ["GiniHealthAPILibrary", "GiniUtilites"]), + .testTarget( + name: "GiniInternalPaymenTests", + dependencies: ["GiniInternalPaymentSDK"]), + ] +) diff --git a/GiniComponents/GiniInternalPaymentSDK/Package.swift b/GiniComponents/GiniInternalPaymentSDK/Package.swift new file mode 100644 index 000000000..f5326cc81 --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version: 5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "GiniInternalPaymentSDK", + defaultLocalization: "en", + platforms: [.iOS(.v13)], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "GiniInternalPaymentSDK", + targets: ["GiniInternalPaymentSDK"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + .package(name: "GiniHealthAPILibrary", path: "../../HealthAPILibrary/GiniHealthAPILibrary"), + .package(name: "GiniUtilites", path: "../../GiniComponents/GiniUtilites") + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "GiniInternalPaymentSDK", + dependencies: ["GiniHealthAPILibrary", "GiniUtilites"]), + .testTarget( + name: "GiniInternalPaymentSDKTests", + dependencies: ["GiniInternalPaymentSDK"]), + ] +) diff --git a/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/BanksView/BankSelectionConfiguration.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/BanksView/BankSelectionConfiguration.swift new file mode 100644 index 000000000..4bcf5c5e4 --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/BanksView/BankSelectionConfiguration.swift @@ -0,0 +1,62 @@ +// +// BanksBottomConfiguration.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import UIKit + +public struct BankSelectionConfiguration { + let descriptionAccentColor: UIColor + let descriptionFont: UIFont + let selectBankAccentColor: UIColor + let selectBankFont: UIFont + let closeTitleIcon: UIImage + let closeIconAccentColor: UIColor + + let bankCellBackgroundColor: UIColor + let bankCellIconBorderColor: UIColor + let bankCellNameFont: UIFont + let bankCellNameAccentColor: UIColor + let bankCellSelectedBorderColor: UIColor + let bankCellNotSelectedBorderColor: UIColor + let bankCellSelectionIndicatorImage: UIImage + + public init(descriptionAccentColor: UIColor, + descriptionFont: UIFont, + selectBankAccentColor: UIColor, + selectBankFont: UIFont, + closeTitleIcon: UIImage, + closeIconAccentColor: UIColor, + bankCellBackgroundColor: UIColor, + bankCellIconBorderColor: UIColor, + bankCellNameFont: UIFont, + bankCellNameAccentColor: UIColor, + bankCellSelectedBorderColor: UIColor, + bankCellNotSelectedBorderColor: UIColor, + bankCellSelectionIndicatorImage: UIImage) { + self.descriptionAccentColor = descriptionAccentColor + self.descriptionFont = descriptionFont + self.selectBankAccentColor = selectBankAccentColor + self.selectBankFont = selectBankFont + self.closeTitleIcon = closeTitleIcon + self.closeIconAccentColor = closeIconAccentColor + self.bankCellBackgroundColor = bankCellBackgroundColor + self.bankCellIconBorderColor = bankCellIconBorderColor + self.bankCellNameFont = bankCellNameFont + self.bankCellNameAccentColor = bankCellNameAccentColor + self.bankCellSelectedBorderColor = bankCellSelectedBorderColor + self.bankCellNotSelectedBorderColor = bankCellNotSelectedBorderColor + self.bankCellSelectionIndicatorImage = bankCellSelectionIndicatorImage + } +} + +public struct BanksBottomStrings { + let selectBankTitleText: String + let descriptionText: String + + public init(selectBankTitleText: String, descriptionText: String) { + self.selectBankTitleText = selectBankTitleText + self.descriptionText = descriptionText + } +} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/BankSelectionTableViewCell.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/BanksView/BankSelectionTableViewCell.swift similarity index 91% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/BankSelectionTableViewCell.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/BanksView/BankSelectionTableViewCell.swift index 779207b5d..e947c9533 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/BankSelectionTableViewCell.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/BanksView/BankSelectionTableViewCell.swift @@ -96,16 +96,16 @@ private extension BankSelectionTableViewCell { let isSelected = cellViewModel.shouldShowSelectionIcon bankImageView.image = cellViewModel.bankImageIcon - bankImageView.layer.borderColor = cellViewModel.bankIconBorderColor.cgColor - + bankImageView.layer.borderColor = cellViewModel.colors.bankIconBorderColor.cgColor + bankNameLabel.text = cellViewModel.bankName - bankNameLabel.font = cellViewModel.bankNameLabelFont - bankNameLabel.textColor = cellViewModel.bankNameLabelAccentColor - - cellView.backgroundColor = cellViewModel.backgroundColor + bankNameLabel.font = cellViewModel.bankNameFont + bankNameLabel.textColor = cellViewModel.colors.bankNameAccentColor + + cellView.backgroundColor = cellViewModel.colors.backgroundColor cellView.layer.borderWidth = isSelected ? Constants.selectedBorderWidth : Constants.notSelectedBorderWidth - cellView.layer.borderColor = isSelected ? cellViewModel.selectedBankBorderColor.cgColor : cellViewModel.notSelectedBankBorderColor.cgColor - + cellView.layer.borderColor = isSelected ? cellViewModel.colors.selectedBankBorderColor.cgColor : cellViewModel.colors.notSelectedBankBorderColor.cgColor + selectionIndicatorImageView.image = cellViewModel.selectionIndicatorImage selectionIndicatorImageView.isHidden = !isSelected } diff --git a/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/BanksView/BankSelectionTableViewCellModel.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/BanksView/BankSelectionTableViewCellModel.swift new file mode 100644 index 000000000..b87378306 --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/BanksView/BankSelectionTableViewCellModel.swift @@ -0,0 +1,46 @@ +// +// BankSelectionTableViewCellModel.swift +// GiniMerchantSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + +import UIKit +import GiniUtilites +import GiniHealthAPILibrary + +struct BankSelectionTableViewCellModelColors { + let backgroundColor: UIColor + let bankNameAccentColor: UIColor + let bankIconBorderColor: UIColor + let selectedBankBorderColor: UIColor + let notSelectedBankBorderColor: UIColor +} + +final class BankSelectionTableViewCellModel { + + private var isSelected: Bool = false + + var shouldShowSelectionIcon: Bool { + isSelected + } + + let bankName: String + let colors: BankSelectionTableViewCellModelColors + let bankNameFont: UIFont + let selectionIndicatorImage: UIImage + let bankImageIcon: UIImage + + init(paymentProvider: PaymentProviderAdditionalInfo, + bankNameFont: UIFont, + colors: BankSelectionTableViewCellModelColors, + selectionIndicatorImage: UIImage) { + self.isSelected = paymentProvider.isSelected + self.bankImageIcon = paymentProvider.paymentProvider.iconData.toImage + self.bankName = paymentProvider.paymentProvider.name + self.bankNameFont = bankNameFont + self.colors = colors + self.selectionIndicatorImage = selectionIndicatorImage + } +} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/BanksBottomView.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/BanksView/BanksBottomView.swift similarity index 81% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/BanksBottomView.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/BanksView/BanksBottomView.swift index ee74d4bfe..5ef5f590c 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/BanksBottomView.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/BanksView/BanksBottomView.swift @@ -5,15 +5,14 @@ // Copyright © 2024 Gini GmbH. All rights reserved. // - import UIKit import GiniUtilites -class BanksBottomView: BottomSheetViewController { +public final class BanksBottomView: BottomSheetViewController { var viewModel: BanksBottomViewModel - private let contentStackView = EmptyStackView(orientation: .vertical) + private let contentStackView = EmptyStackView().orientation(.vertical) private lazy var titleView: UIView = { let view = EmptyView() @@ -24,18 +23,18 @@ class BanksBottomView: BottomSheetViewController { private lazy var titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false - label.text = viewModel.selectBankTitleText - label.textColor = viewModel.selectBankLabelAccentColor - label.font = viewModel.selectBankLabelFont + label.text = viewModel.strings.selectBankTitleText + label.textColor = viewModel.configuration.selectBankAccentColor + label.font = viewModel.configuration.selectBankFont label.numberOfLines = 1 label.lineBreakMode = .byTruncatingTail return label }() private lazy var closeTitleIconImageView: UIImageView = { - let imageView = UIImageView(image: viewModel.closeTitleIcon.withRenderingMode(.alwaysTemplate)) + let imageView = UIImageView(image: viewModel.configuration.closeTitleIcon.withRenderingMode(.alwaysTemplate)) imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.tintColor = viewModel.closeIconAccentColor + imageView.tintColor = viewModel.configuration.closeIconAccentColor imageView.isUserInteractionEnabled = true imageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapOnCloseIcon))) imageView.isHidden = true @@ -47,9 +46,9 @@ class BanksBottomView: BottomSheetViewController { private lazy var descriptionLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false - label.text = viewModel.descriptionText - label.textColor = viewModel.descriptionLabelAccentColor - label.font = viewModel.descriptionLabelFont + label.text = viewModel.strings.descriptionText + label.textColor = viewModel.configuration.descriptionAccentColor + label.font = viewModel.configuration.descriptionFont label.numberOfLines = 0 return label }() @@ -73,30 +72,26 @@ class BanksBottomView: BottomSheetViewController { private let bottomView = EmptyView() - private let bottomStackView = EmptyStackView(orientation: .horizontal) + private let bottomStackView = EmptyStackView().orientation(.horizontal) private lazy var moreInformationView: MoreInformationView = { - let view = MoreInformationView() - let viewModel = MoreInformationViewModel() + let viewModel = viewModel.moreInformationViewModel viewModel.delegate = self - view.viewModel = viewModel - return view + return MoreInformationView(viewModel: viewModel) }() private lazy var poweredByGiniView: PoweredByGiniView = { - let view = PoweredByGiniView() - view.viewModel = PoweredByGiniViewModel() - return view + PoweredByGiniView(viewModel: viewModel.poweredByGiniViewModel) }() - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() setupView() } - init(viewModel: BanksBottomViewModel) { + public init(viewModel: BanksBottomViewModel, bottomSheetConfiguration: BottomSheetConfiguration) { self.viewModel = viewModel - super.init(nibName: nil, bundle: nil) + super.init(configuration: bottomSheetConfiguration) } required init?(coder: NSCoder) { @@ -200,28 +195,32 @@ extension BanksBottomView { } extension BanksBottomView: MoreInformationViewProtocol { - func didTapOnMoreInformation() { + public func didTapOnMoreInformation() { viewModel.didTapOnMoreInformation() } } extension BanksBottomView: UITableViewDataSource, UITableViewDelegate { - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + /// We indicate the number of rows we show in bank selection view + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { viewModel.paymentProviders.count } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + /// We create the bank selection cell + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell: BankSelectionTableViewCell = tableView.dequeueReusableCell(for: indexPath) let invoiceTableViewCellModel = viewModel.paymentProvidersViewModel(paymentProvider: viewModel.paymentProviders[indexPath.row]) cell.cellViewModel = invoiceTableViewCellModel return cell } - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + /// We indicate the height of a bank row + public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { viewModel.rowHeight } - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + /// BanksBottomView event when a bank is selected from the list + public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { viewModel.viewDelegate?.didSelectPaymentProvider(paymentProvider: viewModel.paymentProviders[indexPath.row].paymentProvider) } } diff --git a/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/BanksView/BanksBottomViewModel.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/BanksView/BanksBottomViewModel.swift new file mode 100644 index 000000000..b5e5a0ef9 --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/BanksView/BanksBottomViewModel.swift @@ -0,0 +1,131 @@ +// +// BanksBottomViewModel.swift +// GiniMerchantSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + +import UIKit +import GiniUtilites +import GiniHealthAPILibrary + +/** + BanksSelectionProtocol provides trigger events for the actions happening in the BankSelection view + */ +public protocol BanksSelectionProtocol: AnyObject { + func didSelectPaymentProvider(paymentProvider: GiniHealthAPILibrary.PaymentProvider) + func didTapOnMoreInformation() + func didTapOnClose() + func didTapOnContinueOnShareBottomSheet() + func didTapForwardOnInstallBottomSheet() + func didTapOnPayButton() +} + +struct PaymentProviderAdditionalInfo { + var isSelected: Bool + var isInstalled: Bool + let paymentProvider: GiniHealthAPILibrary.PaymentProvider +} + +public final class BanksBottomViewModel { + let configuration: BankSelectionConfiguration + let strings: BanksBottomStrings + let poweredByGiniViewModel: PoweredByGiniViewModel + let moreInformationViewModel: MoreInformationViewModel + public weak var viewDelegate: BanksSelectionProtocol? + public var documentId: String? + + var paymentProviders: [PaymentProviderAdditionalInfo] = [] + private var selectedPaymentProvider: GiniHealthAPILibrary.PaymentProvider? + + let maximumViewHeight: CGFloat = UIScreen.main.bounds.height - Constants.topPaddingView + let rowHeight: CGFloat = Constants.cellSizeHeight + var bottomViewHeight: CGFloat = 0 + var heightTableView: CGFloat = 0 + + private var urlOpener: URLOpener + + public init(paymentProviders: PaymentProviders, + selectedPaymentProvider: GiniHealthAPILibrary.PaymentProvider?, + configuration: BankSelectionConfiguration, + strings: BanksBottomStrings, + poweredByGiniConfiguration: PoweredByGiniConfiguration, + poweredByGiniStrings: PoweredByGiniStrings, + moreInformationConfiguration: MoreInformationConfiguration, + moreInformationStrings: MoreInformationStrings, + urlOpener: URLOpener = URLOpener(UIApplication.shared)) { + self.selectedPaymentProvider = selectedPaymentProvider + self.urlOpener = urlOpener + self.configuration = configuration + self.strings = strings + self.poweredByGiniViewModel = PoweredByGiniViewModel(configuration: poweredByGiniConfiguration, strings: poweredByGiniStrings) + self.moreInformationViewModel = MoreInformationViewModel(configuration: moreInformationConfiguration, strings: moreInformationStrings) + + self.paymentProviders = paymentProviders + .map({ PaymentProviderAdditionalInfo(isSelected: $0.id == selectedPaymentProvider?.id, + isInstalled: isPaymentProviderInstalled(paymentProvider: $0), + paymentProvider: $0)}) + .filter { $0.paymentProvider.gpcSupportedPlatforms.contains(.ios) || $0.paymentProvider.openWithSupportedPlatforms.contains(.ios) } + .sorted { + // First, sort by isInstalled + if $0.isInstalled != $1.isInstalled { + return $0.isInstalled && !$1.isInstalled + } + // Then sort by paymentProvider.index if both have the same isInstalled value + return ($0.paymentProvider.index ?? 0) < ($1.paymentProvider.index ?? 0) + } + self.calculateHeights() + } + + private func calculateHeights() { + let totalTableViewHeight = CGFloat(paymentProviders.count) * Constants.cellSizeHeight + let totalBottomViewHeight = Constants.blankBottomViewHeight + totalTableViewHeight + if totalBottomViewHeight > maximumViewHeight { + self.heightTableView = maximumViewHeight - Constants.blankBottomViewHeight + self.bottomViewHeight = maximumViewHeight + } else { + self.heightTableView = totalTableViewHeight + self.bottomViewHeight = totalTableViewHeight + Constants.blankBottomViewHeight + } + } + + func paymentProvidersViewModel(paymentProvider: PaymentProviderAdditionalInfo) -> BankSelectionTableViewCellModel { + let bankSelectionTableViewCellModelColors = BankSelectionTableViewCellModelColors( + backgroundColor: configuration.bankCellBackgroundColor, + bankNameAccentColor: configuration.bankCellNameAccentColor, + bankIconBorderColor: configuration.bankCellIconBorderColor, + selectedBankBorderColor: configuration.bankCellSelectedBorderColor, + notSelectedBankBorderColor: configuration.bankCellNotSelectedBorderColor + ) + return BankSelectionTableViewCellModel( + paymentProvider: paymentProvider, + bankNameFont: configuration.bankCellNameFont, + colors: bankSelectionTableViewCellModelColors, + selectionIndicatorImage: configuration.bankCellSelectionIndicatorImage + ) + } + + func didTapOnClose() { + viewDelegate?.didTapOnClose() + } + + func didTapOnMoreInformation() { + viewDelegate?.didTapOnMoreInformation() + } + + private func isPaymentProviderInstalled(paymentProvider: PaymentProvider) -> Bool { + if let urlAppScheme = URL(string: paymentProvider.appSchemeIOS) { + return urlOpener.canOpenLink(url: urlAppScheme) + } + return false + } +} + +extension BanksBottomViewModel { + enum Constants { + static let blankBottomViewHeight: CGFloat = 200.0 + static let cellSizeHeight: CGFloat = 64.0 + static let topPaddingView: CGFloat = 100.0 + } +} diff --git a/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/BottomSheet/BottomSheetConfiguration.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/BottomSheet/BottomSheetConfiguration.swift new file mode 100644 index 000000000..4fdc5a94d --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/BottomSheet/BottomSheetConfiguration.swift @@ -0,0 +1,19 @@ +// +// BottomSheetConfiguration.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import UIKit + +public struct BottomSheetConfiguration { + let backgroundColor: UIColor + let rectangleColor: UIColor + let dimmingBackgroundColor: UIColor + + public init(backgroundColor: UIColor, rectangleColor: UIColor, dimmingBackgroundColor: UIColor) { + self.backgroundColor = backgroundColor + self.rectangleColor = rectangleColor + self.dimmingBackgroundColor = dimmingBackgroundColor + } +} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/BottomSheetViewController.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/BottomSheet/BottomSheetViewController.swift similarity index 82% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/BottomSheetViewController.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/BottomSheet/BottomSheetViewController.swift index 3f45c44a5..897013da7 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/BottomSheetViewController.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/BottomSheet/BottomSheetViewController.swift @@ -9,19 +9,19 @@ import UIKit import GiniUtilites -class BottomSheetViewController: UIViewController { +open class BottomSheetViewController: UIViewController { // MARK: - UI /// Main bottom sheet container view private lazy var mainContainerView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = backgroundColor + view.backgroundColor = configuration.backgroundColor view.roundCorners(corners: [.topLeft, .topRight], radius: Constants.cornerRadiusView) view.layer.cornerRadius = Constants.cornerRadiusView view.clipsToBounds = true return view }() - + /// View to hold dynamic content private let contentView = EmptyView() @@ -31,40 +31,65 @@ class BottomSheetViewController: UIViewController { /// Top view bar private lazy var barLineView: UIView = { let view = UIView() - view.backgroundColor = rectangleColor + view.backgroundColor = configuration.rectangleColor view.layer.cornerRadius = Constants.cornerRadiusTopRectangle view.translatesAutoresizingMaskIntoConstraints = false return view }() - + /// Dimmed background view private lazy var dimmedView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = dimmingBackgroundColor + view.backgroundColor = configuration.dimmingBackgroundColor view.alpha = 0 return view }() - - let backgroundColor: UIColor = GiniColor.standard7.uiColor() - let rectangleColor: UIColor = GiniColor.standard5.uiColor() - let dimmingBackgroundColor: UIColor = GiniColor(lightModeColor: UIColor.black, - darkModeColor: UIColor.white).uiColor().withAlphaComponent(0.4) - var minHeight: CGFloat = 0 - + + private let configuration: BottomSheetConfiguration + public var minHeight: CGFloat = 0 + + // MARK: - Init + public init(configuration: BottomSheetConfiguration) { + self.configuration = configuration + super.init(nibName: nil, bundle: nil) + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: - View Setup - override func viewDidLoad() { + open override func viewDidLoad() { super.viewDidLoad() setupViews() setupGestures() } - - override func viewDidAppear(_ animated: Bool) { + + open override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) animatePresent() } - - private func setupViews() { +} + +// MARK: - Public +public extension BottomSheetViewController { + // sub-view controller will call this function to set content + func setContent(content: UIView) { + contentView.addSubview(content) + NSLayoutConstraint.activate([ + content.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + content.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + content.topAnchor.constraint(equalTo: contentView.topAnchor), + content.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + ]) + view.layoutIfNeeded() + } +} + +// MARK: - Private +private extension BottomSheetViewController { + func setupViews() { view.backgroundColor = .clear view.addSubview(dimmedView) NSLayoutConstraint.activate([ @@ -115,14 +140,14 @@ class BottomSheetViewController: UIViewController { ]) } - private func obtainTopAnchorMinHeightConstraint() -> CGFloat { + func obtainTopAnchorMinHeightConstraint() -> CGFloat { let window = UIApplication.shared.windows.filter {$0.isKeyWindow}.first let extraBottomSafeAreaConstant = window?.safeAreaInsets.bottom == 0 ? Constants.safeAreaBottomPadding : 0 // fix for small devices let topAnchorWithMinHeightConstant = view.frame.height - minHeight + extraBottomSafeAreaConstant return topAnchorWithMinHeightConstant } - private func setupGestures() { + func setupGestures() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapDimmedView)) dimmedView.addGestureRecognizer(tapGesture) @@ -132,11 +157,11 @@ class BottomSheetViewController: UIViewController { topBarView.addGestureRecognizer(panGesture) } - @objc private func handleTapDimmedView() { + @objc func handleTapDimmedView() { dismissBottomSheet() } - @objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) { + @objc func handlePanGesture(_ gesture: UIPanGestureRecognizer) { let translation = gesture.translation(in: view) // get drag direction let isDraggingDown = translation.y > 0 @@ -149,19 +174,23 @@ class BottomSheetViewController: UIViewController { // This state will occur when user is dragging self.mainContainerView.frame.origin.y = currentY + pannedHeight case .ended: - // When user stop dragging - // if fulfil the condition dismiss it, else move to original position - if pannedHeight >= Constants.minDismissiblePanHeight { - dismissBottomSheet() - } else { - self.mainContainerView.frame.origin.y = currentY - } + handlePanGestureEnded(pannedHeight: pannedHeight, currentY: currentY) default: break } } - private func animatePresent() { + private func handlePanGestureEnded(pannedHeight: CGFloat, currentY: CGFloat) { + // When user stop dragging + // if fulfil the condition dismiss it, else move to original position + if pannedHeight >= Constants.minDismissiblePanHeight { + dismissBottomSheet() + } else { + self.mainContainerView.frame.origin.y = currentY + } + } + + func animatePresent() { dimmedView.alpha = 0 // add more animation duration for smoothness UIView.animate(withDuration: 0.2) { [weak self] in @@ -178,18 +207,6 @@ class BottomSheetViewController: UIViewController { self?.dismiss(animated: false) }) } - - // sub-view controller will call this function to set content - func setContent(content: UIView) { - contentView.addSubview(content) - NSLayoutConstraint.activate([ - content.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - content.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - content.topAnchor.constraint(equalTo: contentView.topAnchor), - content.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) - ]) - view.layoutIfNeeded() - } } extension BottomSheetViewController { @@ -212,7 +229,7 @@ extension BottomSheetViewController { } } -extension UIViewController { +public extension UIViewController { func presentBottomSheet(viewController: BottomSheetViewController) { viewController.modalPresentationStyle = .overFullScreen present(viewController, animated: false, completion: nil) diff --git a/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/GiniInternalPaymentSDKVersion.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/GiniInternalPaymentSDKVersion.swift new file mode 100644 index 000000000..31e80878b --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/GiniInternalPaymentSDKVersion.swift @@ -0,0 +1,8 @@ +// +// GiniInternalPaymentSDKVersion.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + +public let GiniInternalPaymentSDKVersion = "1.0.0" diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/InstallAppBottomView.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/InstallApp/InstallAppBottomView.swift similarity index 86% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/InstallAppBottomView.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/InstallApp/InstallAppBottomView.swift index ec58f88f5..a85828035 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/InstallAppBottomView.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/InstallApp/InstallAppBottomView.swift @@ -9,11 +9,11 @@ import UIKit import GiniUtilites -class InstallAppBottomView: BottomSheetViewController { +public final class InstallAppBottomView: BottomSheetViewController { var viewModel: InstallAppBottomViewModel - private let contentStackView = EmptyStackView(orientation: .vertical) + private let contentStackView = EmptyStackView().orientation(.vertical) private let titleView = EmptyView() @@ -21,8 +21,8 @@ class InstallAppBottomView: BottomSheetViewController { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.text = viewModel.titleText - label.textColor = viewModel.titleLabelAccentColor - label.font = viewModel.titleLabelFont + label.textColor = viewModel.configuration.titleAccentColor + label.font = viewModel.configuration.titleFont label.numberOfLines = 0 label.lineBreakMode = .byTruncatingTail return label @@ -35,14 +35,14 @@ class InstallAppBottomView: BottomSheetViewController { imageView.translatesAutoresizingMaskIntoConstraints = false imageView.roundCorners(corners: .allCorners, radius: Constants.bankIconCornerRadius) imageView.layer.borderWidth = Constants.bankIconBorderWidth - imageView.layer.borderColor = viewModel.bankIconBorderColor.cgColor + imageView.layer.borderColor = viewModel.configuration.bankIconBorderColor.cgColor return imageView }() private let moreInformationView = EmptyView() private lazy var moreInformationStackView: UIStackView = { - let stackView = EmptyStackView(orientation: .horizontal) + let stackView = EmptyStackView().orientation(.horizontal) stackView.spacing = Constants.viewPaddingConstraint stackView.distribution = .fillProportionally return stackView @@ -51,8 +51,8 @@ class InstallAppBottomView: BottomSheetViewController { private lazy var moreInformationLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = viewModel.moreInformationLabelTextColor - label.font = viewModel.moreInformationLabelFont + label.textColor = viewModel.configuration.moreInformationTextColor + label.font = viewModel.configuration.moreInformationFont label.numberOfLines = 0 label.text = viewModel.moreInformationLabelText return label @@ -61,8 +61,8 @@ class InstallAppBottomView: BottomSheetViewController { private lazy var moreInformationButton: UIButton = { let button = UIButton(type: .system) button.translatesAutoresizingMaskIntoConstraints = false - button.setImage(viewModel.moreInformationIcon, for: .normal) - button.tintColor = viewModel.moreInformationAccentColor + button.setImage(viewModel.configuration.moreInformationIcon, for: .normal) + button.tintColor = viewModel.configuration.moreInformationAccentColor button.isUserInteractionEnabled = false return button }() @@ -70,16 +70,17 @@ class InstallAppBottomView: BottomSheetViewController { private lazy var continueButton: PaymentPrimaryButton = { let button = PaymentPrimaryButton() button.translatesAutoresizingMaskIntoConstraints = false - button.configure(with: viewModel.giniMerchantConfiguration.primaryButtonConfiguration) - button.customConfigure(paymentProviderColors: viewModel.paymentProviderColors, - text: viewModel.continueLabelText) + button.configure(with: viewModel.primaryButtonConfiguration) + button.customConfigure(text: viewModel.strings.continueLabelText, + textColor: viewModel.paymentProviderColors?.text.toColor(), + backgroundColor: viewModel.paymentProviderColors?.background.toColor()) return button }() private lazy var appStoreImageView: UIButton = { let button = UIButton(type: .custom) button.translatesAutoresizingMaskIntoConstraints = false - button.setImage(viewModel.appStoreIcon, for: .normal) + button.setImage(viewModel.configuration.appStoreIcon, for: .normal) button.imageView?.contentMode = .scaleAspectFit button.addTarget(self, action: #selector(tapOnAppStoreButton), for: .touchUpInside) return button @@ -89,24 +90,26 @@ class InstallAppBottomView: BottomSheetViewController { private let bottomView = EmptyView() - private let bottomStackView = EmptyStackView(orientation: .horizontal) + private let bottomStackView = EmptyStackView().orientation(.horizontal) private lazy var poweredByGiniView: PoweredByGiniView = { - let view = PoweredByGiniView() - view.viewModel = PoweredByGiniViewModel() - return view + PoweredByGiniView(viewModel: viewModel.poweredByGiniViewModel) }() - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() setupView() } - - init(viewModel: InstallAppBottomViewModel) { + + deinit { + NotificationCenter.default.removeObserver(self) + } + + public init(viewModel: InstallAppBottomViewModel, bottomSheetConfiguration: BottomSheetConfiguration) { self.viewModel = viewModel - super.init(nibName: nil, bundle: nil) + super.init(configuration: bottomSheetConfiguration) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -183,8 +186,7 @@ class InstallAppBottomView: BottomSheetViewController { bankIconImageView.widthAnchor.constraint(equalToConstant: Constants.bankIconSize), bankIconImageView.topAnchor.constraint(equalTo: bankView.topAnchor), bankIconImageView.bottomAnchor.constraint(equalTo: bankView.bottomAnchor), - bankIconImageView.centerXAnchor.constraint(equalTo: bankView.centerXAnchor), - + bankIconImageView.centerXAnchor.constraint(equalTo: bankView.centerXAnchor) ]) } diff --git a/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/InstallApp/InstallAppBottomViewModel.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/InstallApp/InstallAppBottomViewModel.swift new file mode 100644 index 000000000..efd339db4 --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/InstallApp/InstallAppBottomViewModel.swift @@ -0,0 +1,62 @@ +// +// InstallAppBottomViewModel.swift +// GiniMerchantSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + +import UIKit +import GiniUtilites +import GiniHealthAPILibrary + +public protocol InstallAppBottomViewProtocol: AnyObject { + func didTapOnContinue() +} + +public final class InstallAppBottomViewModel { + let primaryButtonConfiguration: ButtonConfiguration + let configuration: InstallAppConfiguration + let strings: InstallAppStrings + let poweredByGiniViewModel: PoweredByGiniViewModel + + let selectedPaymentProvider: PaymentProvider? + let paymentProviderColors: ProviderColors? + let bankImageIcon: UIImage + + public weak var viewDelegate: InstallAppBottomViewProtocol? + + var moreInformationLabelText: String { + isBankInstalled ? + strings.moreInformationTipPattern.replacingOccurrences(of: bankToReplaceString, with: selectedPaymentProvider?.name ?? "") : + strings.moreInformationNotePattern.replacingOccurrences(of: bankToReplaceString, with: selectedPaymentProvider?.name ?? "") + } + + let titleText: String + let bankToReplaceString = "[BANK]" + + var isBankInstalled: Bool { + selectedPaymentProvider?.appSchemeIOS.canOpenURLString() == true + } + + public init(selectedPaymentProvider: PaymentProvider?, + installAppConfiguration: InstallAppConfiguration, + strings: InstallAppStrings, + primaryButtonConfiguration: ButtonConfiguration, + poweredByGiniConfiguration: PoweredByGiniConfiguration, + poweredByGiniStrings: PoweredByGiniStrings) { + self.selectedPaymentProvider = selectedPaymentProvider + self.bankImageIcon = selectedPaymentProvider?.iconData.toImage ?? UIImage() + self.paymentProviderColors = selectedPaymentProvider?.colors + self.configuration = installAppConfiguration + self.strings = strings + self.primaryButtonConfiguration = primaryButtonConfiguration + self.poweredByGiniViewModel = PoweredByGiniViewModel(configuration: poweredByGiniConfiguration, strings: poweredByGiniStrings) + + titleText = strings.titlePattern.replacingOccurrences(of: bankToReplaceString, with: selectedPaymentProvider?.name ?? "") + } + + func didTapOnContinue() { + viewDelegate?.didTapOnContinue() + } +} diff --git a/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/InstallApp/InstallAppConfiguration.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/InstallApp/InstallAppConfiguration.swift new file mode 100644 index 000000000..567c2f109 --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/InstallApp/InstallAppConfiguration.swift @@ -0,0 +1,53 @@ +// +// InstallAppConfiguration.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import UIKit + +public struct InstallAppConfiguration { + let titleAccentColor: UIColor + let titleFont: UIFont + let moreInformationFont: UIFont + let moreInformationTextColor: UIColor + let moreInformationAccentColor: UIColor + let moreInformationIcon: UIImage + let appStoreIcon: UIImage + let bankIconBorderColor: UIColor + + public init(titleAccentColor: UIColor, + titleFont: UIFont, + moreInformationFont: UIFont, + moreInformationTextColor: UIColor, + moreInformationAccentColor: UIColor, + moreInformationIcon: UIImage, + appStoreIcon: UIImage, + bankIconBorderColor: UIColor) { + self.titleAccentColor = titleAccentColor + self.titleFont = titleFont + self.moreInformationFont = moreInformationFont + self.moreInformationTextColor = moreInformationTextColor + self.moreInformationAccentColor = moreInformationAccentColor + self.moreInformationIcon = moreInformationIcon + self.appStoreIcon = appStoreIcon + self.bankIconBorderColor = bankIconBorderColor + } +} + +public struct InstallAppStrings { + let titlePattern: String + let moreInformationTipPattern: String + let moreInformationNotePattern: String + let continueLabelText: String + + public init(titlePattern: String, + moreInformationTipPattern: String, + moreInformationNotePattern: String, + continueLabelText: String) { + self.titlePattern = titlePattern + self.moreInformationTipPattern = moreInformationTipPattern + self.moreInformationNotePattern = moreInformationNotePattern + self.continueLabelText = continueLabelText + } +} diff --git a/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/MoreInformation/MoreInformationConfiguration.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/MoreInformation/MoreInformationConfiguration.swift new file mode 100644 index 000000000..1e9f7380d --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/MoreInformation/MoreInformationConfiguration.swift @@ -0,0 +1,32 @@ +// +// MoreInformationConfiguration.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import UIKit + +public struct MoreInformationConfiguration { + let moreInformationAccentColor: UIColor + let moreInformationTextColor: UIColor + let moreInformationLinkFont: UIFont + let moreInformationIcon: UIImage + + public init(moreInformationAccentColor: UIColor, + moreInformationTextColor: UIColor, + moreInformationLinkFont: UIFont, + moreInformationIcon: UIImage) { + self.moreInformationAccentColor = moreInformationAccentColor + self.moreInformationTextColor = moreInformationTextColor + self.moreInformationLinkFont = moreInformationLinkFont + self.moreInformationIcon = moreInformationIcon + } +} + +public struct MoreInformationStrings { + let moreInformationActionablePartText: String + + public init(moreInformationActionablePartText: String) { + self.moreInformationActionablePartText = moreInformationActionablePartText + } +} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/MoreInformationView.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/MoreInformation/MoreInformationView.swift similarity index 82% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/MoreInformationView.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/MoreInformation/MoreInformationView.swift index e98db5be5..0d0c7a57a 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/MoreInformationView.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/MoreInformation/MoreInformationView.swift @@ -9,13 +9,8 @@ import UIKit import GiniUtilites -final class MoreInformationView: UIView { - var viewModel: MoreInformationViewModel! { - didSet { - setupView() - } - } - +public final class MoreInformationView: UIView { + private let viewModel: MoreInformationViewModel private let mainContainer = EmptyView() private lazy var moreInformationLabel: UILabel = { @@ -24,15 +19,15 @@ final class MoreInformationView: UIView { label.numberOfLines = 0 let attributes: [NSAttributedString.Key: Any] = [ - .foregroundColor: viewModel.moreInformationLabelTextColor, + .foregroundColor: viewModel.configuration.moreInformationTextColor, .underlineStyle: NSUnderlineStyle.single.rawValue, - .font: viewModel.moreInformationLabelLinkFont + .font: viewModel.configuration.moreInformationLinkFont ] - let moreInformationActionableAttributtedString = NSMutableAttributedString(string: viewModel.moreInformationActionablePartText, attributes: attributes) + let moreInformationActionableAttributtedString = NSMutableAttributedString(string: viewModel.strings.moreInformationActionablePartText, attributes: attributes) label.attributedText = moreInformationActionableAttributtedString let tapOnMoreInformation = UITapGestureRecognizer(target: self, - action: #selector(tapOnMoreInformationLabelAction(gesture:))) + action: #selector(self.tapOnMoreInformationLabelAction(gesture:))) label.isUserInteractionEnabled = true label.addGestureRecognizer(tapOnMoreInformation) @@ -43,16 +38,18 @@ final class MoreInformationView: UIView { private lazy var moreInformationButton: UIButton = { let button = UIButton(type: .system) button.translatesAutoresizingMaskIntoConstraints = false - button.setImage(viewModel.moreInformationIcon, for: .normal) - button.tintColor = viewModel.moreInformationAccentColor + button.setImage(viewModel.configuration.moreInformationIcon, for: .normal) + button.tintColor = viewModel.configuration.moreInformationAccentColor button.addTarget(self, action: #selector(tapOnMoreInformationButtonAction), for: .touchUpInside) return button }() - override init(frame: CGRect) { - super.init(frame: frame) + public init(viewModel: MoreInformationViewModel) { + self.viewModel = viewModel + super.init(frame: .zero) + setupView() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -86,7 +83,7 @@ final class MoreInformationView: UIView { @objc private func tapOnMoreInformationLabelAction(gesture: UITapGestureRecognizer) { if gesture.didTapAttributedTextInLabel(label: moreInformationLabel, - targetText: viewModel.moreInformationActionablePartText) { + targetText: viewModel.strings.moreInformationActionablePartText) { viewModel.tapOnMoreInformation() } } diff --git a/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/MoreInformation/MoreInformationViewModel.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/MoreInformation/MoreInformationViewModel.swift new file mode 100644 index 000000000..914a3c3af --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/MoreInformation/MoreInformationViewModel.swift @@ -0,0 +1,29 @@ +// +// MoreInformationViewModel.swift +// GiniMerchantSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + +import UIKit + +public protocol MoreInformationViewProtocol: AnyObject { + func didTapOnMoreInformation() +} + +public final class MoreInformationViewModel { + let configuration: MoreInformationConfiguration + let strings: MoreInformationStrings + + public weak var delegate: MoreInformationViewProtocol? + + public init(configuration: MoreInformationConfiguration, strings: MoreInformationStrings) { + self.configuration = configuration + self.strings = strings + } + + func tapOnMoreInformation() { + delegate?.didTapOnMoreInformation() + } +} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/ButtonConfiguration.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentButton/ButtonConfiguration.swift similarity index 94% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/ButtonConfiguration.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentButton/ButtonConfiguration.swift index 86719659d..37826b9a1 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/ButtonConfiguration.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentButton/ButtonConfiguration.swift @@ -11,6 +11,7 @@ public struct ButtonConfiguration { let backgroundColor: UIColor let borderColor: UIColor let titleColor: UIColor + let titleFont: UIFont let shadowColor: UIColor let cornerRadius: CGFloat let borderWidth: CGFloat @@ -31,6 +32,7 @@ public struct ButtonConfiguration { public init(backgroundColor: UIColor, borderColor: UIColor, titleColor: UIColor, + titleFont: UIFont, shadowColor: UIColor, cornerRadius: CGFloat, borderWidth: CGFloat, @@ -44,5 +46,6 @@ public struct ButtonConfiguration { self.borderWidth = borderWidth self.shadowRadius = shadowRadius self.withBlurEffect = withBlurEffect + self.titleFont = titleFont } } diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentPrimaryButton.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentButton/PaymentPrimaryButton.swift similarity index 60% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentPrimaryButton.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentButton/PaymentPrimaryButton.swift index 1a3e19a9e..c68c44af5 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentPrimaryButton.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentButton/PaymentPrimaryButton.swift @@ -8,13 +8,9 @@ import UIKit import GiniUtilites -import GiniHealthAPILibrary -final class PaymentPrimaryButton: UIView { - - private let giniConfiguration = GiniMerchantConfiguration.shared - - var didTapButton: (() -> Void)? +public final class PaymentPrimaryButton: UIView { + public var didTapButton: (() -> Void)? private lazy var contentView: UIView = { let view = EmptyView() @@ -38,7 +34,16 @@ final class PaymentPrimaryButton: UIView { return imageView }() - init() { + private lazy var rightImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.frame = CGRect(x: 0, y: 0, width: Constants.bankIconSize, height: Constants.bankIconSize) + return imageView + }() + + private var trailingConstraint: NSLayoutConstraint? + + public init() { super.init(frame: .zero) addSubview(contentView) contentView.addSubview(titleLabel) @@ -50,6 +55,7 @@ final class PaymentPrimaryButton: UIView { } private func setupConstraints() { + trailingConstraint = contentView.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor) NSLayoutConstraint.activate([ contentView.leadingAnchor.constraint(equalTo: leadingAnchor), contentView.trailingAnchor.constraint(equalTo: trailingAnchor), @@ -57,8 +63,8 @@ final class PaymentPrimaryButton: UIView { contentView.bottomAnchor.constraint(equalTo: bottomAnchor), contentView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), contentView.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), - contentView.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor) ]) + trailingConstraint?.isActive = true } private func setupLeftImageConstraints() { @@ -67,13 +73,22 @@ final class PaymentPrimaryButton: UIView { leftImageView.widthAnchor.constraint(equalToConstant: leftImageView.frame.width).isActive = true leftImageView.heightAnchor.constraint(equalToConstant: leftImageView.frame.height).isActive = true } + + private func setupRightImageConstraints() { + rightImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.contentTrailingPadding).isActive = true + rightImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true + rightImageView.widthAnchor.constraint(equalToConstant: rightImageView.frame.width).isActive = true + rightImageView.heightAnchor.constraint(equalToConstant: rightImageView.frame.height).isActive = true + trailingConstraint?.isActive = false + titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -(Constants.contentTrailingPadding + Constants.bankIconSize)).isActive = true + } @objc private func tapOnPayInvoiceView() { didTapButton?() } } -extension PaymentPrimaryButton { +public extension PaymentPrimaryButton { func configure(with configuration: ButtonConfiguration) { self.contentView.backgroundColor = configuration.backgroundColor self.contentView.layer.cornerRadius = configuration.cornerRadius @@ -81,26 +96,35 @@ extension PaymentPrimaryButton { self.contentView.layer.shadowColor = configuration.shadowColor.cgColor self.titleLabel.textColor = configuration.titleColor - self.titleLabel.font = giniConfiguration.font(for: .button) + self.titleLabel.font = configuration.titleFont } - func customConfigure(paymentProviderColors: ProviderColors?, text: String, leftImageData: Data? = nil) { - if let backgroundHexColor = paymentProviderColors?.background.toColor() { - contentView.backgroundColor = backgroundHexColor - } + func customConfigure(text: String, + textColor: UIColor?, + backgroundColor: UIColor?, + leftImageData: Data? = nil, + rightImageData: Data? = nil) { + contentView.backgroundColor = backgroundColor contentView.isUserInteractionEnabled = true titleLabel.text = text - if let textHexColor = paymentProviderColors?.text.toColor() { - titleLabel.textColor = textHexColor - } - // Left image appears only on Payment Review Screen + titleLabel.textColor = textColor + + // Configure left image if provided if let leftImageData { contentView.addSubview(leftImageView) setupLeftImageConstraints() leftImageView.roundCorners(corners: .allCorners, radius: Constants.bankIconCornerRadius) leftImageView.image = UIImage(data: leftImageData) } + + // Configure right image if provided + if let rightImageData { + contentView.addSubview(rightImageView) + setupRightImageConstraints() + rightImageView.roundCorners(corners: .allCorners, radius: Constants.bankIconCornerRadius) + rightImageView.image = UIImage(data: rightImageData) + } } } @@ -109,5 +133,6 @@ extension PaymentPrimaryButton { static let bankIconSize: CGFloat = 36 static let bankIconCornerRadius: CGFloat = 8 static let contentLeadingPadding: CGFloat = 19 + static let contentTrailingPadding: CGFloat = 8 } } diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentSecondaryButton.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentButton/PaymentSecondaryButton.swift similarity index 92% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentSecondaryButton.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentButton/PaymentSecondaryButton.swift index d13e21437..cbcbf8c8a 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentSecondaryButton.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentButton/PaymentSecondaryButton.swift @@ -9,25 +9,22 @@ import UIKit import GiniUtilites -final class PaymentSecondaryButton: UIView { - - private let giniMerchantConfiguration = GiniMerchantConfiguration.shared - - var didTapButton: (() -> Void)? - +public final class PaymentSecondaryButton: UIView { + public var didTapButton: (() -> Void)? + private lazy var contentView: UIView = { let view = EmptyView() view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapOnBankPicker))) return view }() - + private lazy var leftImageView: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.frame = CGRect(x: 0, y: 0, width: Constants.bankIconSize, height: Constants.bankIconSize) return imageView }() - + private lazy var titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false @@ -35,15 +32,15 @@ final class PaymentSecondaryButton: UIView { label.lineBreakMode = .byTruncatingTail return label }() - + private lazy var rightImageView: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.frame = CGRect(x: 0, y: 0, width: Constants.chevronIconSize, height: Constants.chevronIconSize) return imageView }() - - init() { + + public init() { super.init(frame: .zero) addSubview(contentView) contentView.addSubview(leftImageView) @@ -51,11 +48,11 @@ final class PaymentSecondaryButton: UIView { contentView.addSubview(rightImageView) setupConstraints() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + private func setupConstraints() { NSLayoutConstraint.activate([ contentView.leadingAnchor.constraint(equalTo: leadingAnchor), @@ -68,14 +65,14 @@ final class PaymentSecondaryButton: UIView { rightImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), ]) } - + private func activateImagesViewConstraints() { if !leftImageView.isHidden { leftImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.contentPadding).isActive = true leftImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true leftImageView.widthAnchor.constraint(equalToConstant: leftImageView.frame.width).isActive = true leftImageView.heightAnchor.constraint(equalToConstant: leftImageView.frame.height).isActive = true - + titleLabel.leadingAnchor.constraint(equalTo: leftImageView.trailingAnchor, constant: Constants.contentPadding).isActive = true leftImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor).isActive = true } else { @@ -88,29 +85,29 @@ final class PaymentSecondaryButton: UIView { rightImageView.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor).isActive = true } } - + @objc private func tapOnBankPicker() { didTapButton?() } } -extension PaymentSecondaryButton { +public extension PaymentSecondaryButton { func configure(with configuration: ButtonConfiguration) { contentView.layer.cornerRadius = configuration.cornerRadius contentView.layer.borderWidth = configuration.borderWidth contentView.layer.borderColor = configuration.borderColor.cgColor contentView.backgroundColor = configuration.backgroundColor - + leftImageView.layer.borderColor = configuration.borderColor.cgColor leftImageView.layer.borderWidth = configuration.borderWidth leftImageView.roundCorners(corners: .allCorners, radius: Constants.bankIconCornerRadius) - + titleLabel.textColor = configuration.titleColor - titleLabel.font = giniMerchantConfiguration.font(for: .input) + titleLabel.font = configuration.titleFont } - - func customConfigure(labelText: String, leftImageIcon: UIImage?, rightImageIcon: UIImage?, rightImageTintColor: UIColor, shouldShowLabel: Bool) { + + func customConfigure(labelText: String, leftImageIcon: UIImage?, rightImageIcon: UIImage?, rightImageTintColor: UIColor?, shouldShowLabel: Bool) { if let leftImageIcon { leftImageView.image = leftImageIcon leftImageView.isHidden = false diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentComponentBottomView.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentComponents/PaymentComponentBottomView.swift similarity index 80% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentComponentBottomView.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentComponents/PaymentComponentBottomView.swift index a6ff6c3f9..b70a53719 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentComponentBottomView.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentComponents/PaymentComponentBottomView.swift @@ -8,20 +8,20 @@ import UIKit import GiniUtilites -class PaymentComponentBottomView: BottomSheetViewController { +public final class PaymentComponentBottomView: BottomSheetViewController { private var paymentView: UIView private let contentView = EmptyView() - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() setupView() } - init(paymentView: UIView) { + public init(paymentView: UIView, bottomSheetConfiguration: BottomSheetConfiguration) { self.paymentView = paymentView - super.init(nibName: nil, bundle: nil) + super.init(configuration: bottomSheetConfiguration) } required init?(coder: NSCoder) { diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentComponentView.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentComponents/PaymentComponentView.swift similarity index 78% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentComponentView.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentComponents/PaymentComponentView.swift index fde2c00d6..ab82069d7 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentComponentView.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentComponents/PaymentComponentView.swift @@ -9,24 +9,18 @@ import UIKit import GiniUtilites -final class PaymentComponentView: UIView { - - var viewModel: PaymentComponentViewModel! { - didSet { - setupView() - } - } - - private let contentStackView = EmptyStackView(orientation: .vertical) - +public final class PaymentComponentView: UIView { + let viewModel: PaymentComponentViewModel + + private let contentStackView = EmptyStackView().orientation(.vertical) private let selectYourBankView = EmptyView() private lazy var selectYourBankLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false - label.text = viewModel.selectYourBankLabelText - label.textColor = viewModel.selectYourBankAccentColor - label.font = viewModel.selectYourBankLabelFont + label.text = viewModel.strings.selectYourBankLabelText + label.textColor = viewModel.configuration.selectYourBankAccentColor + label.font = viewModel.configuration.selectYourBankLabelFont label.numberOfLines = 0 return label }() @@ -34,7 +28,7 @@ final class PaymentComponentView: UIView { private let buttonsView = EmptyView() private lazy var buttonsStackView: UIStackView = { - let stackView = EmptyStackView(orientation: viewModel.showPaymentComponentInOneRow ? .horizontal : .vertical) + let stackView = EmptyStackView().orientation(viewModel.showPaymentComponentInOneRow ? .horizontal : .vertical) stackView.spacing = Constants.buttonsSpacing return stackView }() @@ -42,39 +36,38 @@ final class PaymentComponentView: UIView { private lazy var selectBankButton: PaymentSecondaryButton = { let button = PaymentSecondaryButton() button.translatesAutoresizingMaskIntoConstraints = false - button.configure(with: viewModel.giniMerchantConfiguration.secondaryButtonConfiguration) + button.configure(with: viewModel.secondaryButtonConfiguration) return button }() private lazy var payInvoiceButton: PaymentPrimaryButton = { let button = PaymentPrimaryButton() button.translatesAutoresizingMaskIntoConstraints = false - button.configure(with: viewModel.giniMerchantConfiguration.primaryButtonConfiguration) - button.customConfigure(paymentProviderColors: viewModel.paymentProviderColors, - text: viewModel.ctaButtonText) + button.configure(with: viewModel.primaryButtonConfiguration) + button.customConfigure(text: viewModel.strings.ctaLabelText, + textColor: viewModel.paymentProviderColors?.text.toColor(), + backgroundColor: viewModel.paymentProviderColors?.background.toColor()) return button }() private let bottomView = EmptyView() - private let bottomStackView = EmptyStackView(orientation: .horizontal) + private let bottomStackView = EmptyStackView().orientation(.horizontal) private lazy var moreInformationView: MoreInformationView = { - let view = MoreInformationView() - let viewModel = MoreInformationViewModel() + let viewModel = viewModel.moreInformationViewModel viewModel.delegate = self - view.viewModel = viewModel - return view + return MoreInformationView(viewModel: viewModel) }() private lazy var poweredByGiniView: PoweredByGiniView = { - let view = PoweredByGiniView() - view.viewModel = PoweredByGiniViewModel() - return view + PoweredByGiniView(viewModel: viewModel.poweredByGiniViewModel) }() - override init(frame: CGRect) { - super.init(frame: frame) + public init(viewModel: PaymentComponentViewModel) { + self.viewModel = viewModel + super.init(frame: .zero) + setupView() } required init?(coder: NSCoder) { @@ -83,8 +76,8 @@ final class PaymentComponentView: UIView { private func setupView() { self.translatesAutoresizingMaskIntoConstraints = false - self.backgroundColor = viewModel.backgroundColor - + self.backgroundColor = .clear + selectYourBankView.addSubview(selectYourBankLabel) contentStackView.addArrangedSubview(selectYourBankView) @@ -129,16 +122,23 @@ final class PaymentComponentView: UIView { let isPaymentComponentUsed = viewModel.isPaymentComponentUsed() selectYourBankView.isHidden = isPaymentComponentUsed moreInformationView.isHidden = isPaymentComponentUsed + if moreInformationView.isHidden && !bottomStackView.contains(poweredByGiniView) { + bottomView.isHidden = true + } } private func updateButtonsViews() { selectBankButton.customConfigure(labelText: viewModel.selectBankButtonText, leftImageIcon: viewModel.bankImageIcon, - rightImageIcon: viewModel.chevronDownIcon, - rightImageTintColor: viewModel.chevronDownIconColor, + rightImageIcon: viewModel.configuration.chevronDownIcon, + rightImageTintColor: viewModel.configuration.chevronDownIconColor, shouldShowLabel: viewModel.showPaymentComponentInOneRow ? !viewModel.hasBankSelected : true) payInvoiceButton.isHidden = !viewModel.hasBankSelected - selectBankButton.heightAnchor.constraint(equalToConstant: viewModel.showPaymentComponentInOneRow ? viewModel.minimumButtonsHeight : (viewModel.hasBankSelected ? viewModel.minimumButtonsHeight : Constants.defaultButtonHeihgt)).isActive = true + selectBankButton.heightAnchor.constraint(equalToConstant: heightConstantSelectBankButton).isActive = true + } + + var heightConstantSelectBankButton: Double { + viewModel.showPaymentComponentInOneRow ? viewModel.minimumButtonsHeight : (viewModel.hasBankSelected ? viewModel.minimumButtonsHeight : Constants.defaultButtonHeihgt) } private func activateContentStackViewConstraints() { @@ -191,7 +191,7 @@ final class PaymentComponentView: UIView { } extension PaymentComponentView: MoreInformationViewProtocol { - func didTapOnMoreInformation() { + public func didTapOnMoreInformation() { viewModel.tapOnMoreInformation() } } diff --git a/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentComponents/PaymentComponentViewModel.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentComponents/PaymentComponentViewModel.swift new file mode 100644 index 000000000..0b8d70ac6 --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentComponents/PaymentComponentViewModel.swift @@ -0,0 +1,130 @@ +// +// PaymentComponentViewModel.swift +// GiniMerchantSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import UIKit +import GiniHealthAPILibrary + +public protocol PaymentComponentViewProtocol: AnyObject { + func didTapOnMoreInformation(documentId: String?) + func didTapOnBankPicker(documentId: String?) + func didTapOnPayInvoice(documentId: String?) +} + +/** + Helping extension for using the PaymentComponentViewProtocol methods without the document ID. This should be kept by the document view model and passed hierarchically from there. + + */ +extension PaymentComponentViewProtocol { + public func didTapOnMoreInformation() { + didTapOnMoreInformation(documentId: nil) + } + public func didTapOnBankPicker() { + didTapOnBankPicker(documentId: nil) + } + public func didTapOnPayInvoice() { + didTapOnPayInvoice(documentId: nil) + } +} + +public final class PaymentComponentViewModel { + let primaryButtonConfiguration: ButtonConfiguration + let secondaryButtonConfiguration: ButtonConfiguration + let configuration: PaymentComponentsConfiguration + let strings: PaymentComponentsStrings + let poweredByGiniViewModel: PoweredByGiniViewModel + let moreInformationViewModel: MoreInformationViewModel + let paymentProviderColors: GiniHealthAPILibrary.ProviderColors? + let bankImageIcon: UIImage? + + private var paymentProviderScheme: String? + + public weak var delegate: PaymentComponentViewProtocol? + + public var documentId: String? + + var minimumButtonsHeight: CGFloat + + var hasBankSelected: Bool + + var paymentComponentConfiguration: PaymentComponentConfiguration? + + var shouldShowBrandedView: Bool { + paymentComponentConfiguration?.isPaymentComponentBranded ?? true + } + + var showPaymentComponentInOneRow: Bool { + paymentComponentConfiguration?.showPaymentComponentInOneRow ?? false + } + + var hideInfoForReturningUser: Bool { + paymentComponentConfiguration?.hideInfoForReturningUser ?? false + } + + var bankName: String? + + var selectBankButtonText: String { + showPaymentComponentInOneRow ? strings.placeholderBankNameText : bankName ?? strings.placeholderBankNameText + } + + public init(paymentProvider: GiniHealthAPILibrary.PaymentProvider?, + primaryButtonConfiguration: ButtonConfiguration, + secondaryButtonConfiguration: ButtonConfiguration, + configuration: PaymentComponentsConfiguration, + strings: PaymentComponentsStrings, + poweredByGiniConfiguration: PoweredByGiniConfiguration, + poweredByGiniStrings: PoweredByGiniStrings, + moreInformationConfiguration: MoreInformationConfiguration, + moreInformationStrings: MoreInformationStrings, + minimumButtonsHeight: CGFloat, + paymentComponentConfiguration: PaymentComponentConfiguration?) { + self.configuration = configuration + self.strings = strings + self.primaryButtonConfiguration = primaryButtonConfiguration + self.secondaryButtonConfiguration = secondaryButtonConfiguration + self.paymentComponentConfiguration = paymentComponentConfiguration + + self.hasBankSelected = paymentProvider != nil + self.bankImageIcon = paymentProvider?.iconData.toImage + self.paymentProviderColors = paymentProvider?.colors + self.paymentProviderScheme = paymentProvider?.appSchemeIOS + self.bankName = paymentProvider?.name + + self.minimumButtonsHeight = minimumButtonsHeight + + self.poweredByGiniViewModel = PoweredByGiniViewModel(configuration: poweredByGiniConfiguration, strings: poweredByGiniStrings) + self.moreInformationViewModel = MoreInformationViewModel(configuration: moreInformationConfiguration, strings: moreInformationStrings) + } + + func tapOnMoreInformation() { + delegate?.didTapOnMoreInformation(documentId: documentId) + } + + func tapOnBankPicker() { + delegate?.didTapOnBankPicker(documentId: documentId) + } + + func tapOnPayInvoiceView() { + savePaymentComponentViewUsageStatus() + delegate?.didTapOnPayInvoice(documentId: documentId) + } + + // Function to check if Payment was used at least once + func isPaymentComponentUsed() -> Bool { + return UserDefaults.standard.bool(forKey: Constants.paymentComponentViewUsedKey) + } + + // Function to save the boolean value indicating whether Payment was used + private func savePaymentComponentViewUsageStatus() { + UserDefaults.standard.set(true, forKey: Constants.paymentComponentViewUsedKey) + } +} + +extension PaymentComponentViewModel { + private enum Constants { + static let paymentComponentViewUsedKey = "kPaymentComponentViewUsed" + } +} diff --git a/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentComponents/PaymentComponentsConfiguration.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentComponents/PaymentComponentsConfiguration.swift new file mode 100644 index 000000000..a0f1914ec --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentComponents/PaymentComponentsConfiguration.swift @@ -0,0 +1,47 @@ +// +// PaymentComponentsConfiguration.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import UIKit + +public enum PaymentComponentScreenType { + case paymentComponent + case bankPicker + case paymentReview +} + +public struct PaymentComponentsConfiguration { + let selectYourBankLabelFont: UIFont + let selectYourBankAccentColor: UIColor + let chevronDownIcon: UIImage + let chevronDownIconColor: UIColor + let notInstalledBankTextColor: UIColor + + public init(selectYourBankLabelFont: UIFont, + selectYourBankAccentColor: UIColor, + chevronDownIcon: UIImage, + chevronDownIconColor: UIColor, + notInstalledBankTextColor: UIColor) { + self.selectYourBankLabelFont = selectYourBankLabelFont + self.selectYourBankAccentColor = selectYourBankAccentColor + self.chevronDownIcon = chevronDownIcon + self.chevronDownIconColor = chevronDownIconColor + self.notInstalledBankTextColor = notInstalledBankTextColor + } +} + +public struct PaymentComponentsStrings { + let selectYourBankLabelText: String + let placeholderBankNameText: String + let ctaLabelText: String + + public init(selectYourBankLabelText: String, + placeholderBankNameText: String, + ctaLabelText: String) { + self.selectYourBankLabelText = selectYourBankLabelText + self.placeholderBankNameText = placeholderBankNameText + self.ctaLabelText = ctaLabelText + } +} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentInfo.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo.swift similarity index 100% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentInfo.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo.swift diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentComponentConfiguration.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo/PaymentComponentConfiguration.swift similarity index 70% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentComponentConfiguration.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo/PaymentComponentConfiguration.swift index ede236749..7c27006d1 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentComponentConfiguration.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo/PaymentComponentConfiguration.swift @@ -23,9 +23,11 @@ public struct PaymentComponentConfiguration { */ var hideInfoForReturningUser: Bool - public init(isPaymentComponentBranded: Bool = true) { + public init(isPaymentComponentBranded: Bool = true, + showPaymentComponentInOneRow: Bool = false, + hideInfoForReturningUser: Bool = false) { self.isPaymentComponentBranded = isPaymentComponentBranded - self.showPaymentComponentInOneRow = false - self.hideInfoForReturningUser = false + self.showPaymentComponentInOneRow = showPaymentComponentInOneRow + self.hideInfoForReturningUser = hideInfoForReturningUser } } diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentInfoAnswerTableViewCell.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo/PaymentInfoAnswerTableViewCell.swift similarity index 92% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentInfoAnswerTableViewCell.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo/PaymentInfoAnswerTableViewCell.swift index 17a0a6007..253ee5503 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentInfoAnswerTableViewCell.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo/PaymentInfoAnswerTableViewCell.swift @@ -59,12 +59,12 @@ final class PaymentInfoAnswerTableViewCell: UITableViewCell, ReusableView { struct PaymentInfoAnswerTableViewModel { let answerAttributedText: NSAttributedString - let answerTextColor: UIColor = GiniColor.standard1.uiColor() - let answerLinkColor: UIColor = GiniColor.accent1.uiColor() + let answerTextColor: UIColor let answerLinkAttributes: [NSAttributedString.Key: Any] - init(answerAttributedText: NSAttributedString) { + init(answerAttributedText: NSAttributedString, answerTextColor: UIColor, answerLinkColor: UIColor) { self.answerAttributedText = answerAttributedText + self.answerTextColor = answerTextColor self.answerLinkAttributes = [.foregroundColor: answerLinkColor] } } diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentInfoBankCollectionViewCell.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo/PaymentInfoBankCollectionViewCell.swift similarity index 84% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentInfoBankCollectionViewCell.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo/PaymentInfoBankCollectionViewCell.swift index f77e4ba0e..c345bba3e 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentInfoBankCollectionViewCell.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo/PaymentInfoBankCollectionViewCell.swift @@ -53,18 +53,12 @@ final class PaymentInfoBankCollectionViewCell: UICollectionViewCell { } final class PaymentInfoBankCollectionViewCellModel { - private var bankImageIconData: Data? - var bankImageIcon: UIImage { - if let bankImageIconData { - return UIImage(data: bankImageIconData) ?? UIImage() - } - return UIImage() - } - - var borderColor: UIColor = GiniColor.standard5.uiColor() - - init(bankImageIconData: Data?) { - self.bankImageIconData = bankImageIconData + let bankImageIcon: UIImage + let borderColor: UIColor + + init(bankImageIconData: Data?, borderColor: UIColor) { + self.borderColor = borderColor + self.bankImageIcon = bankImageIconData?.toImage ?? UIImage() } } diff --git a/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo/PaymentInfoConfiguration.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo/PaymentInfoConfiguration.swift new file mode 100644 index 000000000..98cf539e0 --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo/PaymentInfoConfiguration.swift @@ -0,0 +1,103 @@ +// +// PaymentInfoConfiguration.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import UIKit + +public struct PaymentInfoConfiguration { + let giniFont: UIFont + let answersFont: UIFont + let answerCellTextColor: UIColor + let answerCellLinkColor: UIColor + let questionsTitleFont: UIFont + let questionsTitleColor: UIColor + let questionHeaderFont: UIFont + let questionHeaderTitleColor: UIColor + let questionHeaderMinusIcon: UIImage + let questionHeaderPlusIcon: UIImage + let bankCellBorderColor: UIColor + let payBillsTitleFont: UIFont + let payBillsTitleColor: UIColor + let payBillsDescriptionFont: UIFont + let linksFont: UIFont + let linksColor: UIColor + let separatorColor: UIColor + let backgroundColor: UIColor + + public init(giniFont: UIFont, + answersFont: UIFont, + answerCellTextColor: UIColor, + answerCellLinkColor: UIColor, + questionsTitleFont: UIFont, + questionsTitleColor: UIColor, + questionHeaderFont: UIFont, + questionHeaderTitleColor: UIColor, + questionHeaderMinusIcon: UIImage, + questionHeaderPlusIcon: UIImage, + bankCellBorderColor: UIColor, + payBillsTitleFont: UIFont, + payBillsTitleColor: UIColor, + payBillsDescriptionFont: UIFont, + linksFont: UIFont, + linksColor: UIColor, + separatorColor: UIColor, + backgroundColor: UIColor) { + self.giniFont = giniFont + self.answersFont = answersFont + self.answerCellTextColor = answerCellTextColor + self.answerCellLinkColor = answerCellLinkColor + self.questionsTitleFont = questionsTitleFont + self.questionsTitleColor = questionsTitleColor + self.questionHeaderFont = questionHeaderFont + self.questionHeaderTitleColor = questionHeaderTitleColor + self.questionHeaderMinusIcon = questionHeaderMinusIcon + self.questionHeaderPlusIcon = questionHeaderPlusIcon + self.bankCellBorderColor = bankCellBorderColor + self.payBillsTitleFont = payBillsTitleFont + self.payBillsTitleColor = payBillsTitleColor + self.payBillsDescriptionFont = payBillsDescriptionFont + self.linksFont = linksFont + self.linksColor = linksColor + self.separatorColor = separatorColor + self.backgroundColor = backgroundColor + } +} + +public struct PaymentInfoStrings { + let giniWebsiteText : String + let giniURLText: String + + let questionsTitleText: String + let answerPrivacyPolicyText: String + let privacyPolicyURLText: String + let titleText: String + let payBillsTitleText: String + let payBillsDescriptionText: String + + let answers: [String] + let questions: [String] + + public init(giniWebsiteText: String, + giniURLText: String, + questionsTitleText: String, + answerPrivacyPolicyText: String, + privacyPolicyURLText: String, + titleText: String, + payBillsTitleText: String, + payBillsDescriptionText: String, + answers: [String], + questions: [String]) { + self.answers = answers + self.questions = questions + self.giniURLText = giniURLText + self.giniWebsiteText = giniWebsiteText + self.titleText = titleText + self.payBillsTitleText = payBillsTitleText + self.payBillsDescriptionText = payBillsDescriptionText + self.answerPrivacyPolicyText = answerPrivacyPolicyText + self.privacyPolicyURLText = privacyPolicyURLText + self.questionsTitleText = questionsTitleText + } +} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentInfoQuestionHeaderViewCell.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo/PaymentInfoQuestionHeaderViewCell.swift similarity index 84% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentInfoQuestionHeaderViewCell.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo/PaymentInfoQuestionHeaderViewCell.swift index a64390641..485743bd5 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentInfoQuestionHeaderViewCell.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo/PaymentInfoQuestionHeaderViewCell.swift @@ -55,7 +55,7 @@ final class PaymentInfoQuestionHeaderViewCell: UIView { paragraphStyle.lineHeightMultiple = Constants.titleLineHeight titleLabel.attributedText = NSMutableAttributedString(string: viewModel.titleText, attributes: [NSAttributedString.Key.paragraphStyle: paragraphStyle]) - titleLabel.textColor = viewModel.titleTextColor + titleLabel.textColor = viewModel.titleColor titleLabel.font = viewModel.titleFont extendedImageView.image = viewModel.extendedIcon } @@ -77,17 +77,17 @@ final class PaymentInfoQuestionHeaderViewCell: UIView { } } -final class PaymentInfoQuestionHeaderViewModel { - var titleText: String - var titleFont: UIFont - let titleTextColor: UIColor = GiniColor.standard1.uiColor() - var extendedIcon: UIImage - - init(title: String, isExtended: Bool) { - self.titleText = title - let giniConfiguration = GiniMerchantConfiguration.shared - self.titleFont = giniConfiguration.font(for: .body1) - self.extendedIcon = (isExtended ? GiniMerchantImage.minus : GiniMerchantImage.plus).preferredUIImage() +struct PaymentInfoQuestionHeaderViewModel { + let titleText: String + let titleFont: UIFont + let titleColor: UIColor + let extendedIcon: UIImage + + init(titleText: String, titleFont: UIFont, titleColor: UIColor, extendedIcon: UIImage) { + self.titleText = titleText + self.titleFont = titleFont + self.titleColor = titleColor + self.extendedIcon = extendedIcon } } diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentInfoViewController.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo/PaymentInfoViewController.swift similarity index 83% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentInfoViewController.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo/PaymentInfoViewController.swift index a0a7a1e2f..686cbb4a9 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentInfoViewController.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo/PaymentInfoViewController.swift @@ -7,15 +7,11 @@ import UIKit +import GiniUtilites + +public final class PaymentInfoViewController: UIViewController { + let viewModel: PaymentInfoViewModel -class PaymentInfoViewController: UIViewController { - - var viewModel: PaymentInfoViewModel! { - didSet { - setupView() - } - } - private lazy var scrollView: UIScrollView = { let scrollView = UIScrollView() scrollView.showsVerticalScrollIndicator = false @@ -51,22 +47,20 @@ class PaymentInfoViewController: UIViewController { }() private lazy var poweredByGiniView: PoweredByGiniView = { - let view = PoweredByGiniView() - view.viewModel = PoweredByGiniViewModel() - return view + PoweredByGiniView(viewModel: viewModel.poweredByGiniViewModel) }() private lazy var payBillsTitleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false - label.font = viewModel.payBillsTitleFont - label.textColor = viewModel.payBillsTitleTextColor + label.font = viewModel.configuration.payBillsTitleFont + label.textColor = viewModel.configuration.payBillsTitleColor label.lineBreakMode = .byWordWrapping label.numberOfLines = 0 label.textAlignment = .left let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineHeightMultiple = Constants.payBillsTitleLineHeight - label.attributedText = NSMutableAttributedString(string: viewModel.payBillsTitleText, + label.attributedText = NSMutableAttributedString(string: viewModel.strings.payBillsTitleText, attributes: [NSAttributedString.Key.paragraphStyle: paragraphStyle]) return label }() @@ -88,14 +82,14 @@ class PaymentInfoViewController: UIViewController { private lazy var questionsTitleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false - label.font = viewModel.questionsTitleFont - label.textColor = viewModel.questionsTitleTextColor + label.font = viewModel.configuration.questionsTitleFont + label.textColor = viewModel.configuration.questionsTitleColor label.lineBreakMode = .byWordWrapping label.numberOfLines = 0 label.textAlignment = .left let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineHeightMultiple = Constants.questionsTitleLineHeight - label.attributedText = NSMutableAttributedString(string: viewModel.questionsTitleText, + label.attributedText = NSMutableAttributedString(string: viewModel.strings.questionsTitleText, attributes: [NSAttributedString.Key.paragraphStyle: paragraphStyle]) return label }() @@ -122,12 +116,22 @@ class PaymentInfoViewController: UIViewController { private var heightsQuestionsTableView: [NSLayoutConstraint] = [] - override func viewDidLoad() { + public init(viewModel: PaymentInfoViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewDidLoad() { super.viewDidLoad() - self.title = viewModel.titleText + self.title = viewModel.strings.titleText + self.setupView() } - override func viewDidDisappear(_ animated: Bool) { + public override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) NotificationCenter.default.post(name: .paymentInfoDissapeared, object: nil) } @@ -150,7 +154,7 @@ class PaymentInfoViewController: UIViewController { } private func setupViewAttributes() { - view.backgroundColor = viewModel.backgroundColor + view.backgroundColor = viewModel.configuration.backgroundColor } private func setupViewConstraints() { @@ -231,33 +235,33 @@ class PaymentInfoViewController: UIViewController { } extension PaymentInfoViewController: UICollectionViewDataSource { - func collectionView(_ collectionView: UICollectionView, + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PaymentInfoBankCollectionViewCell.identifier, for: indexPath) as? PaymentInfoBankCollectionViewCell else { return UICollectionViewCell() } - cell.cellViewModel = PaymentInfoBankCollectionViewCellModel(bankImageIconData: viewModel.paymentProviders[indexPath.row].iconData) + cell.cellViewModel = viewModel.infoBankCellModel(at: indexPath.row) return cell } - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return viewModel.paymentProviders.count } - func numberOfSections(in collectionView: UICollectionView) -> Int { + public func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 } } extension PaymentInfoViewController: UICollectionViewDelegateFlowLayout { - func collectionView(_ collectionView: UICollectionView, + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: Constants.bankIconsWidth, height: Constants.bankIconsHeight) } - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { let cellCount = Double(viewModel.paymentProviders.count) if cellCount > 0 { let cellWidth = Constants.bankIconsWidth @@ -277,53 +281,52 @@ extension PaymentInfoViewController: UICollectionViewDelegateFlowLayout { } extension PaymentInfoViewController: UITableViewDelegate, UITableViewDataSource { - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if viewModel.questions[section].isExtended { return 1 } return 0 } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell: PaymentInfoAnswerTableViewCell = tableView.dequeueReusableCell(for: indexPath) - let answerTableViewCellModel = PaymentInfoAnswerTableViewModel(answerAttributedText: viewModel.questions[indexPath.section].description) - cell.cellViewModel = answerTableViewCellModel + cell.cellViewModel = viewModel.infoAnswerCellModel(at: indexPath.section) return cell } - - func numberOfSections(in tableView: UITableView) -> Int { + + public func numberOfSections(in tableView: UITableView) -> Int { viewModel.questions.count } - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { let viewHeader = PaymentInfoQuestionHeaderViewCell(frame: CGRect(x: 0, y: 0, width: .greatestFiniteMagnitude, height: Constants.questionTitleHeight)) - viewHeader.headerViewModel = PaymentInfoQuestionHeaderViewModel(title: viewModel.questions[section].title, isExtended: viewModel.questions[section].isExtended) + viewHeader.headerViewModel = viewModel.infoQuestionHeaderViewModel(at: section) viewHeader.didTapSelectButton = { [weak self] in self?.extended(section: section) } return viewHeader } - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { Constants.questionTitleHeight } - func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + public func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { guard section < viewModel.questions.count - 1 else { return UIView() } let separatorView = UIView(frame: CGRect(x: 0, y: 0, width: .greatestFiniteMagnitude, height: Constants.questionSectionSeparatorHeight)) - separatorView.backgroundColor = viewModel.separatorColor + separatorView.backgroundColor = viewModel.configuration.separatorColor return separatorView } - func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { Constants.questionSectionSeparatorHeight } - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { UITableView.automaticDimension } - func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { + public func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { Constants.estimatedAnswerHeight } } diff --git a/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo/PaymentInfoViewModel.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo/PaymentInfoViewModel.swift new file mode 100644 index 000000000..edee9dd3d --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentInfo/PaymentInfoViewModel.swift @@ -0,0 +1,119 @@ +// +// PaymentInfoViewModel.swift +// GiniMerchantSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + +import UIKit +import GiniHealthAPILibrary + +struct FAQSection { + let title: String + var description: NSAttributedString + var isExtended: Bool +} + +public final class PaymentInfoViewModel { + let configuration: PaymentInfoConfiguration + let strings: PaymentInfoStrings + var paymentProviders: GiniHealthAPILibrary.PaymentProviders + let poweredByGiniViewModel: PoweredByGiniViewModel + + var payBillsDescriptionAttributedText: NSMutableAttributedString = NSMutableAttributedString() + var payBillsDescriptionLinkAttributes: [NSAttributedString.Key: Any] + var questions: [FAQSection] = [] + + public init(paymentProviders: GiniHealthAPILibrary.PaymentProviders, + configuration: PaymentInfoConfiguration, + strings: PaymentInfoStrings, + poweredByGiniConfiguration: PoweredByGiniConfiguration, + poweredByGiniStrings: PoweredByGiniStrings) { + self.paymentProviders = paymentProviders + self.configuration = configuration + self.strings = strings + self.poweredByGiniViewModel = PoweredByGiniViewModel(configuration: poweredByGiniConfiguration, strings: poweredByGiniStrings) + + payBillsDescriptionLinkAttributes = [.font: configuration.linksFont] + + configurePayBillsGiniLink() + setupQuestions() + } + + private func setupQuestions() { + for index in 0 ... strings.questions.count-1 { + let answerAttributedString = answerWithAttributes(answer: strings.answers[index]) + let questionSection = FAQSection(title: strings.questions[index], + description: textWithLinks(linkFont: configuration.linksFont, attributedString: answerAttributedString), + isExtended: false) + questions.append(questionSection) + } + } + + private func configurePayBillsGiniLink() { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineHeightMultiple = Constants.payBillsDescriptionLineHeight + paragraphStyle.paragraphSpacing = Constants.payBillsParagraphSpacing + payBillsDescriptionAttributedText = NSMutableAttributedString(string: strings.payBillsDescriptionText, + attributes: [.paragraphStyle: paragraphStyle, + .font: configuration.payBillsDescriptionFont, + .foregroundColor: configuration.payBillsTitleColor]) + payBillsDescriptionAttributedText = textWithLinks(linkFont: configuration.giniFont, + attributedString: payBillsDescriptionAttributedText) + } + + private func answerWithAttributes(answer: String) -> NSMutableAttributedString { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineHeightMultiple = Constants.answersLineHeight + paragraphStyle.paragraphSpacing = Constants.answersParagraphSpacing + let answerAttributedText = NSMutableAttributedString(string: answer, + attributes: [.font: configuration.answersFont, .paragraphStyle: paragraphStyle]) + return answerAttributedText + } + + private func textWithLinks(linkFont: UIFont, attributedString: NSMutableAttributedString) -> NSMutableAttributedString { + let attributedString = attributedString + let giniRange = (attributedString.string as NSString).range(of: strings.giniWebsiteText) + attributedString.addLinkToRange(link: strings.giniURLText, + range: giniRange, + linkFont: linkFont, + textToRemove: Constants.linkTextToRemove) + let privacyPolicyRange = (attributedString.string as NSString).range(of: strings.answerPrivacyPolicyText) + attributedString.addLinkToRange(link: strings.privacyPolicyURLText, + range: privacyPolicyRange, + linkFont: linkFont, + textToRemove: Constants.linkTextToRemove) + return attributedString + } + + func infoAnswerCellModel(at index: Int) -> PaymentInfoAnswerTableViewModel { + PaymentInfoAnswerTableViewModel(answerAttributedText: questions[index].description, + answerTextColor: configuration.answerCellTextColor, + answerLinkColor: configuration.answerCellLinkColor) + } + + func infoQuestionHeaderViewModel(at index: Int) -> PaymentInfoQuestionHeaderViewModel { + PaymentInfoQuestionHeaderViewModel(titleText: questions[index].title, + titleFont: configuration.questionHeaderFont, + titleColor: configuration.questionHeaderTitleColor, + extendedIcon: questions[index].isExtended ? configuration.questionHeaderMinusIcon : configuration.questionHeaderPlusIcon) + } + + func infoBankCellModel(at index: Int) -> PaymentInfoBankCollectionViewCellModel { + PaymentInfoBankCollectionViewCellModel(bankImageIconData: paymentProviders[index].iconData, + borderColor: configuration.bankCellBorderColor) + } +} + +extension PaymentInfoViewModel { + private enum Constants { + static let payBillsDescriptionLineHeight = 1.32 + static let payBillsParagraphSpacing = 10.0 + + static let answersLineHeight = 1.32 + static let answersParagraphSpacing = 10.0 + + static let linkTextToRemove = "[LINK]" + } +} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PageCollectionViewCell.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReview/PageCollectionViewCell.swift similarity index 82% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PageCollectionViewCell.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReview/PageCollectionViewCell.swift index abce28583..4489e46cb 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PageCollectionViewCell.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReview/PageCollectionViewCell.swift @@ -8,9 +8,9 @@ import UIKit import GiniUtilites -class PageCollectionViewCell: UICollectionViewCell, ReusableView { +public class PageCollectionViewCell: UICollectionViewCell, ReusableView { - var pageImageView: ZoomedImageView = { + public var pageImageView: ZoomedImageView = { let iv = ZoomedImageView() iv.setup() iv.clipsToBounds = true diff --git a/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReview/PaymentReviewConfiguration.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReview/PaymentReviewConfiguration.swift new file mode 100644 index 000000000..31ede43d7 --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReview/PaymentReviewConfiguration.swift @@ -0,0 +1,68 @@ +// +// PaymentReviewConfiguration.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import UIKit + +public struct PaymentReviewConfiguration { + let loadingIndicatorStyle: UIActivityIndicatorView.Style + let loadingIndicatorColor: UIColor + let infoBarLabelTextColor: UIColor + let infoBarBackgroundColor: UIColor + let mainViewBackgroundColor: UIColor + let infoContainerViewBackgroundColor: UIColor + let paymentReviewClose: UIImage + let backgroundColor: UIColor + let infoBarLabelFont: UIFont + let statusBarStyle: UIStatusBarStyle + let pageIndicatorTintColor: UIColor + let currentPageIndicatorTintColor: UIColor + let isInfoBarHidden: Bool + + public init(loadingIndicatorStyle: UIActivityIndicatorView.Style, + loadingIndicatorColor: UIColor, + infoBarLabelTextColor: UIColor, + infoBarBackgroundColor: UIColor, + mainViewBackgroundColor: UIColor, + infoContainerViewBackgroundColor: UIColor, + paymentReviewClose: UIImage, + backgroundColor: UIColor, + infoBarLabelFont: UIFont, + statusBarStyle: UIStatusBarStyle, + pageIndicatorTintColor: UIColor, + currentPageIndicatorTintColor: UIColor, + isInfoBarHidden: Bool) { + self.loadingIndicatorStyle = loadingIndicatorStyle + self.loadingIndicatorColor = loadingIndicatorColor + self.infoBarLabelTextColor = infoBarLabelTextColor + self.infoBarBackgroundColor = infoBarBackgroundColor + self.mainViewBackgroundColor = mainViewBackgroundColor + self.infoContainerViewBackgroundColor = infoContainerViewBackgroundColor + self.backgroundColor = backgroundColor + self.infoBarLabelFont = infoBarLabelFont + self.statusBarStyle = statusBarStyle + self.pageIndicatorTintColor = pageIndicatorTintColor + self.currentPageIndicatorTintColor = currentPageIndicatorTintColor + self.paymentReviewClose = paymentReviewClose + self.isInfoBarHidden = isInfoBarHidden + } +} + +public struct PaymentReviewStrings { + public let alertOkButtonTitle: String + public let infoBarMessage: String + public let defaultErrorMessage: String + public let createPaymentErrorMessage: String + + public init(alertOkButtonTitle: String, + infoBarMessage: String, + defaultErrorMessage: String, + createPaymentErrorMessage: String) { + self.alertOkButtonTitle = alertOkButtonTitle + self.infoBarMessage = infoBarMessage + self.defaultErrorMessage = defaultErrorMessage + self.createPaymentErrorMessage = createPaymentErrorMessage + } +} diff --git a/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReview/PaymentReviewModel.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReview/PaymentReviewModel.swift new file mode 100644 index 000000000..4b370e772 --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReview/PaymentReviewModel.swift @@ -0,0 +1,335 @@ +// +// PaymentReviewModer.swift +// GiniMerchantSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import UIKit +import GiniHealthAPILibrary +import GiniUtilites + +protocol PaymentReviewViewModelDelegate: AnyObject { + func presentInstallAppBottomSheet(bottomSheet: BottomSheetViewController) + func presentBankSelectionBottomSheet(bottomSheet: BottomSheetViewController) + func createPaymentRequestAndOpenBankApp() + func obtainPDFFromPaymentRequest() +} + +/// BottomSheetsProviderProtocol defines methods for providing custom bottom sheets. +public protocol BottomSheetsProviderProtocol: AnyObject { + func installAppBottomSheet() -> BottomSheetViewController + func shareInvoiceBottomSheet(qrCodeData: Data) -> BottomSheetViewController + func bankSelectionBottomSheet() -> UIViewController +} + +/// PaymentReviewProtocol combines the functionalities of PaymentReviewAPIProtocol, PaymentReviewTrackingProtocol, PaymentReviewSupportedFormatsProtocol, and PaymentReviewActionProtocol for comprehensive payment review management. +public typealias PaymentReviewProtocol = PaymentReviewAPIProtocol & PaymentReviewTrackingProtocol & PaymentReviewSupportedFormatsProtocol & PaymentReviewActionProtocol + +/// PaymentReviewAPIProtocol defines methods for handling payment review processes. +public protocol PaymentReviewAPIProtocol: AnyObject { + func createPaymentRequest(paymentInfo: PaymentInfo, completion: @escaping (Result) -> Void) + func shouldHandleErrorInternally(error: GiniError) -> Bool + func openPaymentProviderApp(requestId: String, universalLink: String) + func submitFeedback(for document: Document, updatedExtractions: [Extraction], completion: ((Result) -> Void)?) + func preview(for documentId: String, pageNumber: Int, completion: @escaping (Result) -> Void) + func obtainPDFURLFromPaymentRequest(paymentInfo: PaymentInfo, viewController: UIViewController) +} + +/// PaymentReviewTrackingProtocol defines methods for tracking user interactions during the payment review process. +public protocol PaymentReviewTrackingProtocol { + func trackOnPaymentReviewCloseKeyboardClicked() + func trackOnPaymentReviewCloseButtonClicked() + func trackOnPaymentReviewBankButtonClicked(providerName: String) +} + +/// PaymentReviewSupportedFormatsProtocol defines methods for checking supported formats in the payment review process. +public protocol PaymentReviewSupportedFormatsProtocol { + func supportsGPC() -> Bool + func supportsOpenWith() -> Bool +} + +/// PaymentReviewActionProtocol defines actions related to payment review processes. +public protocol PaymentReviewActionProtocol { + func updatedPaymentProvider(_ paymentProvider: PaymentProvider) + func openMoreInformationViewController() + func presentShareInvoiceBottomSheet(paymentRequestId: String, paymentInfo: PaymentInfo) +} + +/** + View model class for review screen + */ +public class PaymentReviewModel: NSObject { + + var onPreviewImagesFetched: (() -> Void)? + var reloadCollectionViewClosure: (() -> Void)? + var updateLoadingStatus: (() -> Void)? + var updateImagesLoadingStatus: (() -> Void)? + + var onErrorHandling: ((_ error: GiniError) -> Void)? + + var onCreatePaymentRequestErrorHandling: (() -> Void)? + + var onNewPaymentProvider: (() -> Void)? + + weak var viewModelDelegate: PaymentReviewViewModelDelegate? + weak var delegate: PaymentReviewProtocol? + weak var bottomSheetsProvider: BottomSheetsProviderProtocol? + + public var onPaymentRequestCreated: (() -> Void)? + + public var document: Document? + + public var extractions: [Extraction]? + + public var paymentInfo: PaymentInfo? + + public var documentId: String? + var selectedPaymentProvider: GiniHealthAPILibrary.PaymentProvider + + private var cellViewModels: [PageCollectionCellViewModel] = [PageCollectionCellViewModel]() { + didSet { + self.reloadCollectionViewClosure?() + } + } + + var numberOfCells: Int { + return cellViewModels.count + } + + var isLoading: Bool = false { + didSet { + self.updateLoadingStatus?() + } + } + + var isImagesLoading: Bool = false { + didSet { + self.updateImagesLoadingStatus?() + } + } + + let configuration: PaymentReviewConfiguration + let strings: PaymentReviewStrings + let containerConfiguration: PaymentReviewContainerConfiguration + let containerStrings: PaymentReviewContainerStrings + let defaultStyleInputFieldConfiguration: TextFieldConfiguration + let errorStyleInputFieldConfiguration: TextFieldConfiguration + let selectionStyleInputFieldConfiguration: TextFieldConfiguration + let primaryButtonConfiguration: ButtonConfiguration + let secondaryButtonConfiguration: ButtonConfiguration + let poweredByGiniConfiguration: PoweredByGiniConfiguration + let poweredByGiniStrings: PoweredByGiniStrings + let bottomSheetConfiguration: BottomSheetConfiguration + let showPaymentReviewCloseButton: Bool + var displayMode: DisplayMode + + public init(delegate: PaymentReviewProtocol, + bottomSheetsProvider: BottomSheetsProviderProtocol, + document: Document?, + extractions: [Extraction]?, + paymentInfo: PaymentInfo?, + selectedPaymentProvider: GiniHealthAPILibrary.PaymentProvider, + configuration: PaymentReviewConfiguration, + strings: PaymentReviewStrings, + containerConfiguration: PaymentReviewContainerConfiguration, + containerStrings: PaymentReviewContainerStrings, + defaultStyleInputFieldConfiguration: TextFieldConfiguration, + errorStyleInputFieldConfiguration: TextFieldConfiguration, + selectionStyleInputFieldConfiguration: TextFieldConfiguration, + primaryButtonConfiguration: ButtonConfiguration, + secondaryButtonConfiguration: ButtonConfiguration, + poweredByGiniConfiguration: PoweredByGiniConfiguration, + poweredByGiniStrings: PoweredByGiniStrings, + bottomSheetConfiguration: BottomSheetConfiguration, + showPaymentReviewCloseButton: Bool) { + self.delegate = delegate + self.bottomSheetsProvider = bottomSheetsProvider + self.configuration = configuration + self.strings = strings + self.documentId = document?.id + self.document = document + self.extractions = extractions + self.paymentInfo = paymentInfo + self.selectedPaymentProvider = selectedPaymentProvider + self.poweredByGiniConfiguration = poweredByGiniConfiguration + self.poweredByGiniStrings = poweredByGiniStrings + self.showPaymentReviewCloseButton = showPaymentReviewCloseButton + self.containerConfiguration = containerConfiguration + self.containerStrings = containerStrings + self.primaryButtonConfiguration = primaryButtonConfiguration + self.secondaryButtonConfiguration = secondaryButtonConfiguration + self.defaultStyleInputFieldConfiguration = defaultStyleInputFieldConfiguration + self.errorStyleInputFieldConfiguration = errorStyleInputFieldConfiguration + self.selectionStyleInputFieldConfiguration = selectionStyleInputFieldConfiguration + self.bottomSheetConfiguration = bottomSheetConfiguration + self.displayMode = document != nil ? .documentCollection : .bottomSheet + } + + func getCellViewModel(at indexPath: IndexPath) -> PageCollectionCellViewModel { + return cellViewModels[indexPath.section] + } + + private func createCellViewModel(previewImage: UIImage) -> PageCollectionCellViewModel { + return PageCollectionCellViewModel(preview: previewImage) + } + + func sendFeedback(updatedExtractions: [Extraction]) { + guard let document else { return } + delegate?.submitFeedback(for: document, updatedExtractions: updatedExtractions, completion: nil) + } + + func createPaymentRequest(paymentInfo: PaymentInfo, completion: ((_ paymentRequestId: String) -> ())? = nil) { + isLoading = true + delegate?.createPaymentRequest(paymentInfo: paymentInfo, completion: { [weak self] result in + self?.isLoading = false + switch result { + case let .success(requestId): + completion?(requestId) + case let .failure(error): + if self?.delegate?.shouldHandleErrorInternally(error: error) == true { + self?.onCreatePaymentRequestErrorHandling?() + } + } + }) + } + + func openInstallAppBottomSheet() { + guard let installAppBottomSheet = bottomSheetsProvider?.installAppBottomSheet() as? InstallAppBottomView else { return } + installAppBottomSheet.viewModel.viewDelegate = self + installAppBottomSheet.modalPresentationStyle = .overFullScreen + viewModelDelegate?.presentInstallAppBottomSheet(bottomSheet: installAppBottomSheet) + } + + func openOnboardingShareInvoiceBottomSheet(paymentRequestId: String, paymentInfo: PaymentInfo) { + delegate?.presentShareInvoiceBottomSheet(paymentRequestId: paymentRequestId, paymentInfo: paymentInfo) + } + + func openBankSelectionBottomSheet() { + guard let banksPickerBottomSheet = bottomSheetsProvider?.bankSelectionBottomSheet() as? BanksBottomView else { return } + banksPickerBottomSheet.modalPresentationStyle = .overFullScreen + banksPickerBottomSheet.viewModel.viewDelegate = self + viewModelDelegate?.presentBankSelectionBottomSheet(bottomSheet: banksPickerBottomSheet) + } + + func openPaymentProviderApp(requestId: String, universalLink: String) { + delegate?.openPaymentProviderApp(requestId: requestId, universalLink: universalLink) + } + + func fetchImages() { + self.isImagesLoading = true + let dispatchGroup = DispatchGroup() + let dispatchQueue = DispatchQueue(label: "imagesQueue") + let dispatchSemaphore = DispatchSemaphore(value: 0) + guard let document, let documentId else { return } + var vms = [PageCollectionCellViewModel]() + dispatchQueue.async { + for page in 1 ... document.pageCount { + dispatchGroup.enter() + + self.delegate?.preview(for: documentId, pageNumber: page, completion: { [weak self] result in + if let cellModel = self?.proccessPreview(result) { + vms.append(cellModel) + } + dispatchSemaphore.signal() + dispatchGroup.leave() + }) + dispatchSemaphore.wait() + } + + dispatchGroup.notify(queue: dispatchQueue) { + DispatchQueue.main.async { + self.isImagesLoading = false + self.cellViewModels.append(contentsOf: vms) + self.onPreviewImagesFetched?() + } + } + } + } + + private func proccessPreview(_ result: Result) -> PageCollectionCellViewModel? { + switch result { + case let .success(dataImage): + if let image = UIImage(data: dataImage) { + return createCellViewModel(previewImage: image) + } + case let .failure(error): + if delegate?.shouldHandleErrorInternally(error: error) == true { + onErrorHandling?(error) + } + } + return nil + } + + func paymentReviewContainerViewModel() -> PaymentReviewContainerViewModel { + PaymentReviewContainerViewModel(extractions: extractions, + paymentInfo: paymentInfo, + selectedPaymentProvider: selectedPaymentProvider, + configuration: containerConfiguration, + strings: containerStrings, + primaryButtonConfiguration: primaryButtonConfiguration, + secondaryButtonConfiguration: secondaryButtonConfiguration, + defaultStyleInputFieldConfiguration: defaultStyleInputFieldConfiguration, + errorStyleInputFieldConfiguration: errorStyleInputFieldConfiguration, + selectionStyleInputFieldConfiguration: selectionStyleInputFieldConfiguration, + poweredByGiniConfiguration: poweredByGiniConfiguration, + poweredByGiniStrings: poweredByGiniStrings, + displayMode: displayMode) + } +} + +extension PaymentReviewModel: InstallAppBottomViewProtocol { + public func didTapOnContinue() { + viewModelDelegate?.createPaymentRequestAndOpenBankApp() + } +} + +extension PaymentReviewModel: ShareInvoiceBottomViewProtocol { + public func didTapOnContinueToShareInvoice() { + viewModelDelegate?.obtainPDFFromPaymentRequest() + } +} + +extension PaymentReviewModel: BanksSelectionProtocol { + /** + Called when a payment provider is selected by the user. + + - Parameters: + - paymentProvider: The `PaymentProvider` object representing the selected payment provider. + - documentId: An optional `String` identifier for the document associated id with this payment. If `nil`, no document is associated. + + This function updates the current selected payment provider, notifies the delegate of the new provider, + and triggers any associated callback for handling the change in payment provider. + */ + public func didSelectPaymentProvider(paymentProvider: GiniHealthAPILibrary.PaymentProvider) { + selectedPaymentProvider = paymentProvider + delegate?.updatedPaymentProvider(paymentProvider) + onNewPaymentProvider?() + } + + /** + Called when the user taps on the "More Information" button was tapped on BanksSelection view + + This function notifies the delegate to open the "More Information" view controller. + */ + public func didTapOnMoreInformation() { + delegate?.openMoreInformationViewController() + } + + public func didTapOnClose() {} + + public func didTapOnContinueOnShareBottomSheet() {} + + public func didTapForwardOnInstallBottomSheet() {} + + public func didTapOnPayButton() {} + +} + +/** + View model class for collection view cell + + */ +public struct PageCollectionCellViewModel { + let preview: UIImage +} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentReviewViewController+PaymentReviewViewModelDelegate.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReview/PaymentReviewViewController+PaymentReviewViewModelDelegate.swift similarity index 67% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentReviewViewController+PaymentReviewViewModelDelegate.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReview/PaymentReviewViewController+PaymentReviewViewModelDelegate.swift index c1fd65c1d..5ef1673f1 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentReviewViewController+PaymentReviewViewModelDelegate.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReview/PaymentReviewViewController+PaymentReviewViewModelDelegate.swift @@ -15,7 +15,7 @@ extension PaymentReviewViewController: PaymentReviewViewModelDelegate { func createPaymentRequestAndOpenBankApp() { self.presentedViewController?.dismiss(animated: true) - if paymentInfoContainerView.noErrorsFound() { + if paymentInfoContainerView.inputFieldsHaveNoErrors() { createPaymentRequest() } } @@ -26,6 +26,11 @@ extension PaymentReviewViewController: PaymentReviewViewModelDelegate { } func obtainPDFFromPaymentRequest() { - model?.paymentComponentsController.obtainPDFURLFromPaymentRequest(paymentInfo: paymentInfoContainerView.obtainPaymentInfo(), viewController: self) + model.delegate?.obtainPDFURLFromPaymentRequest(paymentInfo: paymentInfoContainerView.obtainPaymentInfo(), viewController: self) + } + + func presentBankSelectionBottomSheet(bottomSheet: BottomSheetViewController) { + bottomSheet.minHeight = Constants.inputContainerHeight + presentBottomSheet(viewController: bottomSheet) } } diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentReviewViewController+UICollection.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReview/PaymentReviewViewController+UICollection.swift similarity index 90% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentReviewViewController+UICollection.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReview/PaymentReviewViewController+UICollection.swift index aef690937..ecde10a1f 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentReviewViewController+UICollection.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReview/PaymentReviewViewController+UICollection.swift @@ -15,15 +15,15 @@ extension PaymentReviewViewController: UICollectionViewDelegate, UICollectionVie public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 1 } public func numberOfSections(in collectionView: UICollectionView) -> Int { - model?.numberOfCells ?? 1 + model.numberOfCells } public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell: PageCollectionViewCell = collectionView.dequeueReusableCell(for: indexPath) cell.pageImageView.frame = CGRect(x: 0, y: 0, width: collectionView.frame.width, height: collectionView.frame.height) cell.pageImageView.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: Constants.bottomPaddingPageImageView, right: 0.0) - let cellModel = model?.getCellViewModel(at: indexPath) - cell.pageImageView.display(image: cellModel?.preview ?? UIImage()) + let cellModel = model.getCellViewModel(at: indexPath) + cell.pageImageView.display(image: cellModel.preview) return cell } diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentReviewViewController.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReview/PaymentReviewViewController.swift similarity index 65% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentReviewViewController.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReview/PaymentReviewViewController.swift index 987848449..d4eb466df 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentReviewViewController.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReview/PaymentReviewViewController.swift @@ -9,12 +9,14 @@ import UIKit import GiniUtilites import GiniHealthAPILibrary -private enum DisplayMode: Int { +/// Modes for displaying PaymentReview content in the UI. +public enum DisplayMode: Int { case bottomSheet case documentCollection } -public final class PaymentReviewViewController: BottomSheetController, UIGestureRecognizerDelegate { +/// A view controller for reviewing payment details +public final class PaymentReviewViewController: BottomSheetViewController, UIGestureRecognizerDelegate { private lazy var mainView = buildMainView() private lazy var closeButton = buildCloseButton() private lazy var infoBar = buildInfoBar() @@ -26,47 +28,25 @@ public final class PaymentReviewViewController: BottomSheetController, UIGesture lazy var pageControl = buildPageControl() private var infoBarBottomConstraint: NSLayoutConstraint? - private let screenBackgroundColor = GiniColor(lightModeColorName: .light7, darkModeColorName: .light7).uiColor() + private var showInfoBarOnce = true private var keyboardWillShowCalled = false - private var displayMode = DisplayMode.bottomSheet - - public var model: PaymentReviewModel? - var selectedPaymentProvider: PaymentProvider! - - public weak var trackingDelegate: GiniMerchantTrackingDelegate? - - public static func instantiate(with giniMerchant: GiniMerchant, document: Document?, extractions: [Extraction]?, paymentInfo: PaymentInfo?, selectedPaymentProvider: PaymentProvider, trackingDelegate: GiniMerchantTrackingDelegate? = nil, paymentComponentsController: PaymentComponentsController, isInfoBarHidden: Bool = true) -> PaymentReviewViewController { - let viewController = PaymentReviewViewController() - let viewModel = PaymentReviewModel(with: giniMerchant, - document: document, - extractions: extractions, - paymentInfo: paymentInfo, - selectedPaymentProvider: selectedPaymentProvider, - paymentComponentsController: paymentComponentsController) - viewController.model = viewModel - viewController.trackingDelegate = trackingDelegate - viewController.selectedPaymentProvider = selectedPaymentProvider - viewController.isInfoBarHidden = isInfoBarHidden - viewController.displayMode = document != nil ? .documentCollection : .bottomSheet - return viewController - } - public static func instantiate(with giniMerchant: GiniMerchant, data: DataForReview?, paymentInfo: PaymentInfo?, selectedPaymentProvider: PaymentProvider, trackingDelegate: GiniMerchantTrackingDelegate? = nil, paymentComponentsController: PaymentComponentsController) -> PaymentReviewViewController { - let viewController = PaymentReviewViewController() - let viewModel = PaymentReviewModel(with: giniMerchant, - document: data?.document, - extractions: data?.extractions, - paymentInfo: paymentInfo, - selectedPaymentProvider: selectedPaymentProvider, - paymentComponentsController: paymentComponentsController) - viewController.model = viewModel - viewController.trackingDelegate = trackingDelegate - viewController.selectedPaymentProvider = selectedPaymentProvider - return viewController + /// The model instance containing data and methods for handling the payment review process. + public let model: PaymentReviewModel + private var selectedPaymentProvider: GiniHealthAPILibrary.PaymentProvider + + init(viewModel: PaymentReviewModel, + selectedPaymentProvider: GiniHealthAPILibrary.PaymentProvider) { + self.model = viewModel + self.selectedPaymentProvider = selectedPaymentProvider + self.isInfoBarHidden = viewModel.configuration.isInfoBarHidden + super.init(configuration: self.model.bottomSheetConfiguration) } - let giniMerchantConfiguration = GiniMerchantConfiguration.shared + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } override public func viewDidLoad() { super.viewDidLoad() @@ -85,67 +65,81 @@ public final class PaymentReviewViewController: BottomSheetController, UIGesture } fileprivate func setupViewModel() { + model.onErrorHandling = { [weak self] error in + guard let self = self else { return } + self.showError(message: self.model.strings.defaultErrorMessage) + } + + model.onCreatePaymentRequestErrorHandling = { [weak self] () in + guard let self = self else { return } + self.showError(message: self.model.strings.createPaymentErrorMessage) + } + + if model.displayMode == .documentCollection { + setupViewModelWithDocument() + } + + model.onNewPaymentProvider = { [weak self] () in + self?.updatePaymentInfoContainerView() + } + + model.viewModelDelegate = self + } - model?.updateImagesLoadingStatus = { [weak self] () in + private func setupViewModelWithDocument() { + model.fetchImages() + + model.updateImagesLoadingStatus = { [weak self] () in DispatchQueue.main.async { [weak self] in - let isLoading = self?.model?.isImagesLoading ?? false + guard let self = self else { return } + let isLoading = self.model.isImagesLoading if isLoading { - self?.collectionView.showLoading(style: Constants.loadingIndicatorStyle, - color: GiniMerchantColorPalette.accent1.preferredColor(), - scale: Constants.loadingIndicatorScale) + self.collectionView.showLoading(style: self.model.configuration.loadingIndicatorStyle, + color: self.model.configuration.loadingIndicatorColor, + scale: Constants.loadingIndicatorScale) } else { - self?.collectionView.stopLoading() + self.collectionView.stopLoading() } } } - model?.updateLoadingStatus = { [weak self] () in + model.updateLoadingStatus = { [weak self] () in DispatchQueue.main.async { [weak self] in - let isLoading = self?.model?.isLoading ?? false + guard let self = self else { return } + let isLoading = self.model.isLoading if isLoading { - self?.view.showLoading(style: Constants.loadingIndicatorStyle, - color: GiniMerchantColorPalette.accent1.preferredColor(), - scale: Constants.loadingIndicatorScale) + self.view.showLoading(style: self.model.configuration.loadingIndicatorStyle, + color: self.model.configuration.loadingIndicatorColor, + scale: Constants.loadingIndicatorScale) } else { - self?.view.stopLoading() + self.view.stopLoading() } } } - model?.onErrorHandling = { [weak self] error in - self?.showError(message: NSLocalizedStringPreferredFormat("gini.merchant.errors.default", comment: "default error message")) - } - - model?.reloadCollectionViewClosure = { [weak self] () in + model.reloadCollectionViewClosure = { [weak self] () in DispatchQueue.main.async { self?.collectionView.reloadData() } } - model?.onPreviewImagesFetched = { [weak self] () in + model.onPreviewImagesFetched = { [weak self] () in DispatchQueue.main.async { self?.collectionView.reloadData() } } - - model?.onCreatePaymentRequestErrorHandling = { [weak self] () in - self?.showError(message: NSLocalizedStringPreferredFormat("gini.merchant.errors.failed.payment.request.creation", comment: "error for creating payment request")) - } - - if displayMode == .documentCollection { - model?.fetchImages() - } - model?.viewModelDelegate = self - - paymentInfoContainerView.model = PaymentReviewContainerViewModel(extractions: model?.extractions, paymentInfo: model?.paymentInfo, selectedPaymentProvider: selectedPaymentProvider) } override public func viewDidDisappear(_ animated: Bool) { unsubscribeFromNotifications() } + public override var preferredStatusBarStyle: UIStatusBarStyle { + return model.configuration.statusBarStyle + } + fileprivate func layoutUI() { - switch displayMode { + switch model.displayMode { case .documentCollection: layoutMainView() layoutPaymentInfoContainerView() @@ -161,34 +155,34 @@ public final class PaymentReviewViewController: BottomSheetController, UIGesture // MARK: - Pay Button Action func payButtonClicked() { - var event = TrackingEvent.init(type: PaymentReviewScreenEventType.onToTheBankButtonClicked) - event.info = ["paymentProvider": selectedPaymentProvider.name] - trackingDelegate?.onPaymentReviewScreenEvent(event: event) + model.delegate?.trackOnPaymentReviewBankButtonClicked(providerName: selectedPaymentProvider.name) view.endEditing(true) - if model?.paymentComponentsController.supportsGPC() == true { + guard paymentInfoContainerView.noErrorsFound() else { return } + guard paymentInfoContainerView.inputFieldsHaveNoErrors() else { return } + guard let delegate = model.delegate else { return } + if delegate.supportsGPC() { guard selectedPaymentProvider.appSchemeIOS.canOpenURLString() else { - model?.openInstallAppBottomSheet() + model.openInstallAppBottomSheet() return } - - if paymentInfoContainerView.noErrorsFound() { - createPaymentRequest() - } - } else if model?.paymentComponentsController.supportsOpenWith() == true { - if model?.paymentComponentsController.shouldShowOnboardingScreenFor() == true { - model?.openOnboardingShareInvoiceBottomSheet() - } else { - obtainPDFFromPaymentRequest() + createPaymentRequest() + } else if delegate.supportsOpenWith() { + if !paymentInfoContainerView.isTextFieldEmpty(textFieldType: .amountFieldTag) { + let paymentInfo = paymentInfoContainerView.obtainPaymentInfo() + model.createPaymentRequest(paymentInfo: paymentInfo, completion: { [weak self] requestId in + self?.model.openOnboardingShareInvoiceBottomSheet(paymentRequestId: requestId, paymentInfo: paymentInfo) + }) + sendFeedback(paymentInfo: paymentInfo) } } } func createPaymentRequest() { - if !paymentInfoContainerView.isTextFieldEmpty(texFieldType: .amountFieldTag) { + if !paymentInfoContainerView.isTextFieldEmpty(textFieldType: .amountFieldTag) { let paymentInfo = paymentInfoContainerView.obtainPaymentInfo() - model?.createPaymentRequest(paymentInfo: paymentInfo, completion: { [weak self] requestId in - self?.model?.openPaymentProviderApp(requestId: requestId, universalLink: paymentInfo.paymentUniversalLink) + model.createPaymentRequest(paymentInfo: paymentInfo, completion: { [weak self] requestId in + self?.model.openPaymentProviderApp(requestId: requestId, universalLink: paymentInfo.paymentUniversalLink) }) sendFeedback(paymentInfo: paymentInfo) } @@ -216,7 +210,17 @@ public final class PaymentReviewViewController: BottomSheetController, UIGesture value: paymentInfo.amount, name: "amount_to_pay") let updatedExtractions = [paymentRecipientExtraction, ibanExtraction, referenceExtraction, amoutToPayExtraction] - model?.sendFeedback(updatedExtractions: updatedExtractions) + model.sendFeedback(updatedExtractions: updatedExtractions) + } +} + +// MARK: - Instantiation +extension PaymentReviewViewController { + public static func instantiate(viewModel: PaymentReviewModel, + selectedPaymentProvider: GiniHealthAPILibrary.PaymentProvider) -> PaymentReviewViewController { + let viewController = PaymentReviewViewController(viewModel: viewModel, + selectedPaymentProvider: selectedPaymentProvider) + return viewController } } @@ -232,7 +236,7 @@ extension PaymentReviewViewController { /** Moves the root view up by the distance of keyboard height taking in account safeAreaInsets.bottom */ - (displayMode == .bottomSheet ? view : mainView) + (model.displayMode == .bottomSheet ? view : mainView) .bounds.origin.y = keyboardSize.height - view.safeAreaInsets.bottom keyboardWillShowCalled = true @@ -244,8 +248,13 @@ extension PaymentReviewViewController { keyboardWillShowCalled = false + /** + Moves back the root view origin to zero. Schedules it on the main dispatch queue to prevent + the view jumping if another keyboard is shown right after this one is hidden. + */ UIView.animate(withDuration: animationDuration, delay: 0.0, options: UIView.AnimationOptions(rawValue: animationCurve), animations: { [weak self] in - self?.view.bounds.origin.y = 0 + guard let self else { return } + (model.displayMode == .bottomSheet ? view : mainView)?.bounds.origin.y = 0 }, completion: nil) } @@ -277,23 +286,22 @@ extension PaymentReviewViewController { fileprivate func dismissKeyboardOnTap() { let tap = UITapGestureRecognizer(target: view, action: #selector(UIView.endEditing)) tap.cancelsTouchesInView = false - (displayMode == .bottomSheet ? view : mainView).addGestureRecognizer(tap) + (model.displayMode == .bottomSheet ? view : mainView).addGestureRecognizer(tap) } } - //MARK: - MainView fileprivate extension PaymentReviewViewController { func buildMainView() -> UIView { let view = UIView() - view.backgroundColor = GiniColor.standard7.uiColor() + view.backgroundColor = model.configuration.mainViewBackgroundColor return view } func layoutMainView() { mainView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(mainView) - mainView.backgroundColor = screenBackgroundColor + mainView.backgroundColor = model.configuration.backgroundColor NSLayoutConstraint.activate([ mainView.topAnchor.constraint(equalTo: view.topAnchor), mainView.bottomAnchor.constraint(equalTo: view.bottomAnchor), @@ -306,25 +314,41 @@ fileprivate extension PaymentReviewViewController { //MARK: - PaymentReviewContainerView fileprivate extension PaymentReviewViewController { func buildPaymentInfoContainerView() -> PaymentReviewContainerView { - let containerView = PaymentReviewContainerView() - containerView.backgroundColor = GiniColor.standard7.uiColor() + let containerView = PaymentReviewContainerView(viewModel: model.paymentReviewContainerViewModel()) + containerView.backgroundColor = model.configuration.infoContainerViewBackgroundColor containerView.roundCorners(corners: [.topLeft, .topRight], radius: Constants.cornerRadius) containerView.onPayButtonClicked = { [weak self] in self?.payButtonClicked() } + containerView.onBankSelectionButtonClicked = { [weak self] in + self?.model.openBankSelectionBottomSheet() + } return containerView } func layoutPaymentInfoContainerView() { paymentInfoContainerView.translatesAutoresizingMaskIntoConstraints = false - let container = displayMode == .bottomSheet ? (view ?? UIView()) : mainView + let container = model.displayMode == .bottomSheet ? (view ?? UIView()) : mainView container.addSubview(paymentInfoContainerView) + container.backgroundColor = .clear NSLayoutConstraint.activate([ paymentInfoContainerView.leadingAnchor.constraint(equalTo: container.leadingAnchor), paymentInfoContainerView.trailingAnchor.constraint(equalTo: container.trailingAnchor) ]) + + if model.displayMode == .documentCollection { + NSLayoutConstraint.activate([ + paymentInfoContainerView.bottomAnchor.constraint(equalTo: mainView.bottomAnchor) + ]) + } + } + + func updatePaymentInfoContainerView() { + self.presentedViewController?.dismiss(animated: true) + self.selectedPaymentProvider = model.selectedPaymentProvider + paymentInfoContainerView.updateSelectedPaymentProvider(model.selectedPaymentProvider) } } @@ -342,9 +366,10 @@ fileprivate extension PaymentReviewViewController { let flowLayout = UICollectionViewFlowLayout() flowLayout.minimumInteritemSpacing = Constants.collectionViewPadding flowLayout.minimumLineSpacing = Constants.collectionViewPadding + flowLayout.scrollDirection = .horizontal // Enable horizontal scrolling let collection = UICollectionView(frame: CGRect.zero, collectionViewLayout: flowLayout) - collection.backgroundColor = screenBackgroundColor + collection.backgroundColor = model.configuration.backgroundColor collection.delegate = self collection.dataSource = self collection.register(cellType: PageCollectionViewCell.self) @@ -353,11 +378,11 @@ fileprivate extension PaymentReviewViewController { func buildPageControl() -> UIPageControl { let control = UIPageControl() - control.pageIndicatorTintColor = GiniColor.standard4.uiColor() - control.currentPageIndicatorTintColor = GiniColor(lightModeColorName: .dark2, darkModeColorName: .light5).uiColor() - control.backgroundColor = screenBackgroundColor + control.pageIndicatorTintColor = model.configuration.pageIndicatorTintColor + control.currentPageIndicatorTintColor = model.configuration.currentPageIndicatorTintColor + control.backgroundColor = model.configuration.backgroundColor control.hidesForSinglePage = true - control.numberOfPages = model?.document?.pageCount ?? 1 + control.numberOfPages = model.document?.pageCount ?? 0 return control } @@ -370,7 +395,7 @@ fileprivate extension PaymentReviewViewController { containerCollectionView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor), containerCollectionView.trailingAnchor.constraint(equalTo: mainView.trailingAnchor), containerCollectionView.topAnchor.constraint(equalTo: mainView.topAnchor), - containerCollectionView.bottomAnchor.constraint(equalTo: paymentInfoContainerView.topAnchor), + containerCollectionView.bottomAnchor.constraint(equalTo: paymentInfoContainerView.topAnchor, constant: Constants.collectionViewBottomPadding), pageControl.heightAnchor.constraint(equalToConstant: Constants.pageControlHeight), collectionView.widthAnchor.constraint(equalTo: containerCollectionView.widthAnchor), @@ -385,8 +410,8 @@ fileprivate extension PaymentReviewViewController { fileprivate extension PaymentReviewViewController { func buildCloseButton() -> UIButton { let button = UIButton() - button.isHidden = true - button.setImage(GiniMerchantImage.paymentReviewClose.preferredUIImage(), for: .normal) + button.isHidden = !model.showPaymentReviewCloseButton + button.setImage(model.configuration.paymentReviewClose , for: .normal) button.addTarget(self, action: #selector(closeButtonClicked), for: .touchUpInside) return button } @@ -406,10 +431,10 @@ fileprivate extension PaymentReviewViewController { @objc func closeButtonClicked(_ sender: UIButton) { if (keyboardWillShowCalled) { - trackingDelegate?.onPaymentReviewScreenEvent(event: TrackingEvent.init(type: .onCloseKeyboardButtonClicked)) + model.delegate?.trackOnPaymentReviewCloseKeyboardClicked() view.endEditing(true) } else { - trackingDelegate?.onPaymentReviewScreenEvent(event: TrackingEvent.init(type: .onCloseButtonClicked)) + model.delegate?.trackOnPaymentReviewCloseButtonClicked() dismiss(animated: true, completion: nil) } } @@ -420,17 +445,17 @@ fileprivate extension PaymentReviewViewController { func buildInfoBar() -> UIView { let view = UIView() view.roundCorners(corners: [.topLeft, .topRight], radius: Constants.cornerRadius) - view.backgroundColor = GiniMerchantColorPalette.success1.preferredColor() + view.backgroundColor = model.configuration.infoBarBackgroundColor view.isHidden = isInfoBarHidden return view } func buildInfoBarLabel() -> UILabel { let label = UILabel() - label.textColor = GiniMerchantColorPalette.dark7.preferredColor() - label.font = GiniMerchantConfiguration.shared.font(for: .captions1) + label.textColor = model.configuration.infoBarLabelTextColor + label.font = model.configuration.infoBarLabelFont label.adjustsFontForContentSizeCategory = true - label.text = NSLocalizedStringPreferredFormat("gini.merchant.reviewscreen.infobar.message", comment: "info bar message") + label.text = model.strings.infoBarMessage label.textAlignment = .center label.numberOfLines = 0 return label @@ -440,7 +465,8 @@ fileprivate extension PaymentReviewViewController { infoBar.translatesAutoresizingMaskIntoConstraints = false infoBarLabel.translatesAutoresizingMaskIntoConstraints = false - view.insertSubview(infoBar, belowSubview: paymentInfoContainerView) + let container = model.displayMode == .bottomSheet ? (view ?? UIView()) : mainView + container.insertSubview(infoBar, belowSubview: paymentInfoContainerView) infoBar.addSubview(infoBarLabel) let bottomConstraint = infoBar.bottomAnchor.constraint(equalTo: paymentInfoContainerView.topAnchor, constant: Constants.infoBarHeight) @@ -491,8 +517,7 @@ extension PaymentReviewViewController { let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - let okAction = UIAlertAction(title: NSLocalizedStringPreferredFormat("gini.merchant.alert.ok.title", - comment: "ok title for action"), style: .default, handler: nil) + let okAction = UIAlertAction(title: model.strings.alertOkButtonTitle, style: .default, handler: nil) alertController.addAction(okAction) DispatchQueue.main.async { [weak self] in self?.present(alertController, animated: true, completion: nil) @@ -502,18 +527,18 @@ extension PaymentReviewViewController { extension PaymentReviewViewController { enum Constants { - static let animationDuration: CGFloat = 0.3 + static let animationDuration = 0.3 static let bottomPaddingPageImageView = 20.0 static let loadingIndicatorScale = 1.0 - static let loadingIndicatorStyle = UIActivityIndicatorView.Style.large static let closeButtonSide = 48.0 static let closeButtonPadding = 16.0 - static let infoBarHeight = 60.0 + static let infoBarHeight = 55.0 static let infoBarLabelPadding = 8.0 static let pageControlHeight = 20.0 static let collectionViewPadding = 10.0 - static let inputContainerHeight = 300.0 + static let inputContainerHeight = 375.0 static let cornerRadius = 12.0 - static let moveHeightInfoBar = 32.0 + static let moveHeightInfoBar = 24.0 + static let collectionViewBottomPadding = 10.0 } } diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/ZoomedImageView.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReview/ZoomedImageView.swift similarity index 95% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/ZoomedImageView.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReview/ZoomedImageView.swift index 2764e26f9..ded12b4ea 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/ZoomedImageView.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReview/ZoomedImageView.swift @@ -204,19 +204,23 @@ open class ZoomedImageView: UIScrollView { let xOffset = contentSize.width < bounds.width ? 0 : (contentSize.width - bounds.width)/2 let yOffset = contentSize.height < bounds.height ? 0 : (contentSize.height - bounds.height)/2 - switch imageContentMode { - case .aspectFit: - contentOffset = CGPoint.zero - case .aspectFill: - contentOffset = CGPoint(x: xOffset, y: yOffset) - case .heightFill: - contentOffset = CGPoint(x: xOffset, y: 0) - case .widthFill: - contentOffset = CGPoint(x: 0, y: yOffset) - } + contentOffset = getContentOffset(imageContentMode: imageContentMode, xOffset: xOffset, yOffset: yOffset) } } - + + private func getContentOffset(imageContentMode: ScaleMode, xOffset: CGFloat, yOffset: CGFloat) -> CGPoint { + switch imageContentMode { + case .aspectFit: + return CGPoint.zero + case .aspectFill: + return CGPoint(x: xOffset, y: yOffset) + case .heightFill: + return CGPoint(x: xOffset, y: 0) + case .widthFill: + return CGPoint(x: 0, y: yOffset) + } + } + private func setMaxMinZoomScalesForCurrentBounds() { // calculate min/max zoomscale let xScale = bounds.width / imageSize.width // the scale needed to perfectly fit the image width-wise diff --git a/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReviewContainer/PaymentReviewContainerConfiguration.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReviewContainer/PaymentReviewContainerConfiguration.swift new file mode 100644 index 000000000..1e943a2df --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReviewContainer/PaymentReviewContainerConfiguration.swift @@ -0,0 +1,85 @@ +// +// File.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + +import UIKit + +/// Configuration settings for the Payment Review container view. +public struct PaymentReviewContainerConfiguration { + let errorLabelTextColor: UIColor + let errorLabelFont: UIFont + let lockIcon: UIImage + let lockedFields: Bool + let showBanksPicker: Bool + let chevronDownIcon: UIImage? + let chevronDownIconColor: UIColor? + + /** + Initializes a new configuration for the Payment Review container view. + + - Parameters: + - errorLabelTextColor: The color of the error label text. + - errorLabelFont: The font used for the error label. + - lockIcon: The icon displayed to indicate locked fields. + - lockedFields: A flag indicating whether specific fields are locked for editing. + - showBanksPicker: A flag indicating whether the bank picker should be shown. + - chevronDownIcon: The icon for the chevron pointing downward, used in the UI. + - chevronDownIconColor: The color of the chevron down icon. + */ + public init(errorLabelTextColor: UIColor, + errorLabelFont: UIFont, + lockIcon: UIImage, + lockedFields: Bool, + showBanksPicker: Bool, + chevronDownIcon: UIImage?, + chevronDownIconColor: UIColor?) { + self.errorLabelTextColor = errorLabelTextColor + self.errorLabelFont = errorLabelFont + self.lockIcon = lockIcon + self.lockedFields = lockedFields + self.showBanksPicker = showBanksPicker + self.chevronDownIcon = chevronDownIcon + self.chevronDownIconColor = chevronDownIconColor + } +} + +public struct PaymentReviewContainerStrings { + let emptyCheckErrorMessage: String + let ibanCheckErrorMessage: String + let recipientFieldPlaceholder: String + let ibanFieldPlaceholder: String + let amountFieldPlaceholder: String + let usageFieldPlaceholder: String + let recipientErrorMessage: String + let ibanErrorMessage: String + let amountErrorMessage: String + let purposeErrorMessage: String + let payInvoiceLabelText: String + + public init(emptyCheckErrorMessage: String, + ibanCheckErrorMessage: String, + recipientFieldPlaceholder: String, + ibanFieldPlaceholder: String, + amountFieldPlaceholder: String, + usageFieldPlaceholder: String, + recipientErrorMessage: String, + ibanErrorMessage: String, + amountErrorMessage: String, + purposeErrorMessage: String, + payInvoiceLabelText: String) { + self.emptyCheckErrorMessage = emptyCheckErrorMessage + self.ibanCheckErrorMessage = ibanCheckErrorMessage + self.recipientFieldPlaceholder = recipientFieldPlaceholder + self.ibanFieldPlaceholder = ibanFieldPlaceholder + self.amountFieldPlaceholder = amountFieldPlaceholder + self.usageFieldPlaceholder = usageFieldPlaceholder + self.recipientErrorMessage = recipientErrorMessage + self.ibanErrorMessage = ibanErrorMessage + self.amountErrorMessage = amountErrorMessage + self.purposeErrorMessage = purposeErrorMessage + self.payInvoiceLabelText = payInvoiceLabelText + } +} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentReviewContainerView.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReviewContainer/PaymentReviewContainerView.swift similarity index 60% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentReviewContainerView.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReviewContainer/PaymentReviewContainerView.swift index 5a577e790..1c326590c 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentReviewContainerView.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReviewContainer/PaymentReviewContainerView.swift @@ -8,126 +8,52 @@ import UIKit import GiniUtilites +import GiniHealthAPILibrary -enum TextFieldType: Int { +/** + An enumeration representing the types of text fields used in the payment review interface. + Each case corresponds to a specific text field and is assigned a unique integer tag. + */ +public enum TextFieldType: Int { case recipientFieldTag = 1 case ibanFieldTag case amountFieldTag case usageFieldTag } -class PaymentReviewContainerView: UIView { - let ibanValidator = IBANValidator() - let giniMerchantConfiguration = GiniMerchantConfiguration.shared +/// The container for oayment review textfields +public final class PaymentReviewContainerView: UIView { + private let ibanValidator = IBANValidator() - private lazy var paymentInfoStackView: UIStackView = { - let stackView = EmptyStackView(orientation: .vertical) - stackView.distribution = .fill - stackView.spacing = Constants.stackViewSpacing - return stackView - }() - - private lazy var recipientStackView: UIStackView = { - let stackView = EmptyStackView(orientation: .vertical) - stackView.distribution = .fill - stackView.spacing = Constants.errorTopMargin - return stackView - }() - - private lazy var recipientTextFieldView: TextFieldWithLabelView = { - let textFieldView = TextFieldWithLabelView() - textFieldView.tag = TextFieldType.recipientFieldTag.rawValue - textFieldView.isUserInteractionEnabled = false - return textFieldView - }() - - private lazy var recipientErrorLabel: UILabel = { - let label = UILabel() - label.font = giniMerchantConfiguration.font(for: .captions2) - return label - }() - - private lazy var ibanAmountContainerStackView: UIStackView = { - let stackView = EmptyStackView(orientation: .vertical) - stackView.distribution = .fill - stackView.spacing = Constants.errorTopMargin - return stackView - }() - - private lazy var ibanAmountHorizontalStackView: UIStackView = { - let stackView = EmptyStackView(orientation: .horizontal) - stackView.distribution = .fill - stackView.spacing = Constants.errorTopMargin - return stackView - }() + private lazy var recipientErrorLabel = buildErrorLabel() + private lazy var usageErrorLabel = buildErrorLabel() + private lazy var ibanErrorLabel = buildErrorLabel() + private lazy var amountErrorLabel = buildErrorLabel() - private lazy var ibanTextFieldView: TextFieldWithLabelView = { - let textFieldView = TextFieldWithLabelView() - textFieldView.tag = TextFieldType.ibanFieldTag.rawValue - textFieldView.isUserInteractionEnabled = false - return textFieldView - }() - - private lazy var amountTextFieldView: TextFieldWithLabelView = { - let textFieldView = TextFieldWithLabelView() - textFieldView.tag = TextFieldType.amountFieldTag.rawValue - textFieldView.isUserInteractionEnabled = true - return textFieldView - }() + private let paymentInfoStackView = EmptyStackView().orientation(.vertical).distribution(.fill).spacing(Constants.stackViewSpacing) + private let recipientStackView = EmptyStackView().orientation(.vertical).distribution(.fill) + private let ibanAmountContainerStackView = EmptyStackView().orientation(.vertical).distribution(.fill) + private let ibanAmountHorizontalStackView = EmptyStackView().orientation(.horizontal).distribution(.fill).spacing(Constants.stackViewSpacing) - private lazy var ibanAmountErrorsHorizontalStackView: UIStackView = { - let stackView = EmptyStackView(orientation: .horizontal) - stackView.distribution = .fill - return stackView - }() + private let ibanAmountErrorsHorizontalStackView = EmptyStackView().orientation(.horizontal).distribution(.fill) + private let ibanErrorStackView = EmptyStackView().orientation(.vertical).distribution(.fill) + private let amountErrorStackView = EmptyStackView().orientation(.vertical).distribution(.fill) + private let usageStackView = EmptyStackView().orientation(.vertical).distribution(.fill) - private lazy var ibanErrorStackView: UIStackView = { - let stackView = EmptyStackView(orientation: .vertical) - stackView.distribution = .fill - return stackView - }() - - private lazy var ibanErrorLabel: UILabel = { - let label = UILabel() - label.font = giniMerchantConfiguration.font(for: .captions2) - return label - }() - - private lazy var amountErrorStackView: UIStackView = { - let stackView = EmptyStackView(orientation: .vertical) - stackView.distribution = .fill - return stackView - }() - - private lazy var amountErrorLabel: UILabel = { - let label = UILabel() - label.font = giniMerchantConfiguration.font(for: .captions2) - return label - }() - - private lazy var referenceNumberStackView: UIStackView = { - let stackView = EmptyStackView(orientation: .vertical) - stackView.distribution = .fill - stackView.spacing = Constants.errorTopMargin - return stackView - }() - - private lazy var usageTextFieldView: TextFieldWithLabelView = { - let textFieldView = TextFieldWithLabelView() - textFieldView.tag = TextFieldType.usageFieldTag.rawValue - textFieldView.isUserInteractionEnabled = false - return textFieldView - }() - - private lazy var usageErrorLabel: UILabel = { - let label = UILabel() - label.font = giniMerchantConfiguration.font(for: .captions2) - return label - }() + private lazy var recipientTextFieldView = buildTextFieldWithLabelView(tag: TextFieldType.recipientFieldTag.rawValue, isEditable: !viewModel.configuration.lockedFields) + private lazy var ibanTextFieldView = buildTextFieldWithLabelView(tag: TextFieldType.ibanFieldTag.rawValue, isEditable: !viewModel.configuration.lockedFields) + private lazy var amountTextFieldView = buildTextFieldWithLabelView(tag: TextFieldType.amountFieldTag.rawValue, isEditable: true) + private lazy var usageTextFieldView = buildTextFieldWithLabelView(tag: TextFieldType.usageFieldTag.rawValue, isEditable: !viewModel.configuration.lockedFields) private let buttonsView = EmptyView() - private let buttonsStackView = EmptyStackView(orientation: .horizontal) + private lazy var selectBankButton: PaymentSecondaryButton = { + let button = PaymentSecondaryButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.configure(with: viewModel.secondaryButtonConfiguration) + button.frame = CGRect(x: 0, y: 0, width: .greatestFiniteMagnitude, height: Constants.buttonViewHeight) + return button + }() private lazy var payInvoiceButton: PaymentPrimaryButton = { let button = PaymentPrimaryButton() @@ -137,12 +63,11 @@ class PaymentReviewContainerView: UIView { }() private let bottomView = EmptyView() - - private let bottomStackView = EmptyStackView(orientation: .horizontal) + private let buttonsStackView = EmptyStackView().orientation(.horizontal).spacing(Constants.buttonsSpacing) + private let bottomStackView = EmptyStackView().orientation(.horizontal) private lazy var poweredByGiniView: PoweredByGiniView = { - let view = PoweredByGiniView() - view.viewModel = PoweredByGiniViewModel() + let view = PoweredByGiniView(viewModel: viewModel.poweredByGiniViewModel) view.translatesAutoresizingMaskIntoConstraints = false return view }() @@ -153,17 +78,18 @@ class PaymentReviewContainerView: UIView { private var paymentInputFields: [TextFieldWithLabelView] = [] private var paymentInputFieldsErrorLabels: [UILabel] = [] private var coupledErrorLabels: [UILabel] = [] - var model: PaymentReviewContainerViewModel! { - didSet { - configureUI() - } - } - var onPayButtonClicked: (() -> Void)? - - override init(frame: CGRect) { - super.init(frame: frame) + private let viewModel: PaymentReviewContainerViewModel + /// A closure that is called when the pay button is clicked. + public var onPayButtonClicked: (() -> Void)? + /// A closure that is called when the banks selection button is clicked. + public var onBankSelectionButtonClicked: (() -> Void)? + + public init(viewModel: PaymentReviewContainerViewModel) { + self.viewModel = viewModel + super.init(frame: .zero) setupViewHierarchy() setupLayout() + configureUI() } required init?(coder: NSCoder) { @@ -189,12 +115,16 @@ class PaymentReviewContainerView: UIView { ibanAmountContainerStackView.addArrangedSubview(ibanAmountHorizontalStackView) ibanAmountContainerStackView.addArrangedSubview(ibanAmountErrorsHorizontalStackView) - referenceNumberStackView.addArrangedSubview(usageTextFieldView) - referenceNumberStackView.addArrangedSubview(usageErrorLabel) + usageStackView.addArrangedSubview(usageTextFieldView) + usageStackView.addArrangedSubview(usageErrorLabel) + if viewModel.configuration.showBanksPicker { + buttonsStackView.addArrangedSubview(selectBankButton) + } buttonsStackView.addArrangedSubview(payInvoiceButton) buttonsView.addSubview(buttonsStackView) + bottomStackView.addArrangedSubview(UIView()) bottomStackView.addArrangedSubview(poweredByGiniView) bottomView.addSubview(bottomStackView) @@ -202,11 +132,12 @@ class PaymentReviewContainerView: UIView { paymentInfoStackView.addArrangedSubview(ibanAmountContainerStackView) - paymentInfoStackView.addArrangedSubview(referenceNumberStackView) + paymentInfoStackView.addArrangedSubview(usageStackView) paymentInfoStackView.addArrangedSubview(buttonsView) paymentInfoStackView.addArrangedSubview(bottomView) + paymentInfoStackView.addArrangedSubview(UIView()) - addSubview(paymentInfoStackView) + self.addSubview(paymentInfoStackView) } // MARK: Layout & Constraints @@ -222,10 +153,10 @@ class PaymentReviewContainerView: UIView { private func setupContainerContraints() { NSLayoutConstraint.activate([ - paymentInfoStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Constants.leftRightPaymentInfoContainerPadding), - paymentInfoStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Constants.leftRightPaymentInfoContainerPadding), - paymentInfoStackView.topAnchor.constraint(equalTo: topAnchor, constant: Constants.topBottomPaymentInfoContainerPadding), - paymentInfoStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Constants.topBottomPaymentInfoContainerPadding) + paymentInfoStackView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: Constants.leftRightPaymentInfoContainerPadding), + paymentInfoStackView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -Constants.leftRightPaymentInfoContainerPadding), + paymentInfoStackView.topAnchor.constraint(equalTo: self.topAnchor, constant: viewModel.dispayMode == .bottomSheet ? 0 : Constants.topBottomPaymentInfoContainerPadding), + paymentInfoStackView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: viewModel.dispayMode == .bottomSheet ? 0 : -Constants.topBottomPaymentInfoContainerPadding) ]) } @@ -233,6 +164,7 @@ class PaymentReviewContainerView: UIView { setupViewModel() configurePaymentInputFields() configurePayButtonInitialState() + configureSelectBanksButton() hideErrorLabels() fillInInputFields() addDoneButtonForNumPad(amountTextFieldView) @@ -287,14 +219,10 @@ class PaymentReviewContainerView: UIView { ]) } - private func updateAmountIbanErrorState() { - ibanAmountErrorsHorizontalStackView.isHidden = coupledErrorLabels.allSatisfy { $0.isHidden } - } - // MARK: - Input fields configuration fileprivate func setupViewModel() { - model?.onExtractionFetched = { [weak self] () in + viewModel.onExtractionFetched = { [weak self] () in DispatchQueue.main.async { self?.fillInInputFields() } @@ -308,28 +236,22 @@ class PaymentReviewContainerView: UIView { } fileprivate func applyDefaultStyle(_ textFieldView: TextFieldWithLabelView) { - textFieldView.configure(configuration: giniMerchantConfiguration.defaultStyleInputFieldConfiguration) + textFieldView.configure(configuration: viewModel.defaultStyleInputFieldConfiguration) textFieldView.customConfigure(labelTitle: inputFieldPlaceholderText(textFieldView)) - textFieldView.textField.delegate = self - textFieldView.textField.tag = textFieldView.tag - - if let fieldIdentifier = TextFieldType(rawValue: textFieldView.tag), fieldIdentifier != .amountFieldTag { - textFieldView.textField.textColor = giniMerchantConfiguration.defaultStyleInputFieldConfiguration.placeholderForegroundColor - } - + textFieldView.delegate = self textFieldView.layer.masksToBounds = true } fileprivate func applyErrorStyle(_ textFieldView: TextFieldWithLabelView) { UIView.animate(withDuration: Constants.animationDuration) { - textFieldView.configure(configuration: self.giniMerchantConfiguration.errorStyleInputFieldConfiguration) + textFieldView.configure(configuration: self.viewModel.errorStyleInputFieldConfiguration) textFieldView.layer.masksToBounds = true } } fileprivate func applySelectionStyle(_ textFieldView: TextFieldWithLabelView) { - UIView.animate(withDuration: Constants.animationDuration) { [self] in - textFieldView.configure(configuration: self.giniMerchantConfiguration.selectionStyleInputFieldConfiguration) + UIView.animate(withDuration: Constants.animationDuration) { + textFieldView.configure(configuration: self.viewModel.selectionStyleInputFieldConfiguration) textFieldView.layer.masksToBounds = true } } @@ -340,31 +262,26 @@ class PaymentReviewContainerView: UIView { var text = "" switch fieldIdentifier { case .recipientFieldTag: - text = NSLocalizedStringPreferredFormat("gini.merchant.reviewscreen.recipient.placeholder", - comment: "placeholder text for recipient input field") + text = viewModel.strings.recipientFieldPlaceholder case .ibanFieldTag: - text = NSLocalizedStringPreferredFormat("gini.merchant.reviewscreen.iban.placeholder", - comment: "placeholder text for iban input field") + text = viewModel.strings.ibanFieldPlaceholder case .amountFieldTag: - text = NSLocalizedStringPreferredFormat("gini.merchant.reviewscreen.amount.placeholder", - comment: "placeholder text for amount input field") + text = viewModel.strings.amountFieldPlaceholder case .usageFieldTag: - text = NSLocalizedStringPreferredFormat("gini.merchant.reviewscreen.usage.placeholder", - comment: "placeholder text for usage input field") + text = viewModel.strings.usageFieldPlaceholder } fullString.append(NSAttributedString(string: text)) - if fieldIdentifier != .amountFieldTag { + if viewModel.configuration.lockedFields, fieldIdentifier != .amountFieldTag { appendLockIcon(fullString) } } - return fullString } fileprivate func appendLockIcon(_ string: NSMutableAttributedString) { let lockIconAttachment = NSTextAttachment() - let icon = GiniMerchantImage.lock.preferredUIImage() + let icon = viewModel.configuration.lockIcon lockIconAttachment.image = icon let height = Constants.lockIconHeight @@ -382,22 +299,9 @@ class PaymentReviewContainerView: UIView { if let fieldIdentifier = TextFieldType(rawValue: textFieldViewTag) { switch fieldIdentifier { case .amountFieldTag: - if amountTextFieldView.textField.hasText && !amountTextFieldView.textField.isReallyEmpty { - let decimalPart = amountToPay.value - if decimalPart > 0 { - applyDefaultStyle(textFieldView) - hideErrorLabel(textFieldTag: fieldIdentifier) - } else { - amountTextFieldView.text = "" - applyErrorStyle(textFieldView) - showErrorLabel(textFieldTag: fieldIdentifier) - } - } else { - applyErrorStyle(textFieldView) - showErrorLabel(textFieldTag: fieldIdentifier) - } + validateAmountTextField() case .ibanFieldTag, .recipientFieldTag, .usageFieldTag: - if textFieldView.textField.hasText && !textFieldView.textField.isReallyEmpty { + if textFieldView.hasText && !textFieldView.isReallyEmpty { applyDefaultStyle(textFieldView) hideErrorLabel(textFieldTag: fieldIdentifier) } else { @@ -408,12 +312,29 @@ class PaymentReviewContainerView: UIView { } } + fileprivate func validateAmountTextField() { + if amountTextFieldView.hasText && !amountTextFieldView.isReallyEmpty { + let decimalPart = amountToPay.value + if decimalPart > 0 { + applyDefaultStyle(amountTextFieldView) + hideErrorLabel(textFieldTag: .amountFieldTag) + } else { + amountTextFieldView.text = "" + applyErrorStyle(amountTextFieldView) + showErrorLabel(textFieldTag: .amountFieldTag) + } + } else { + applyErrorStyle(amountTextFieldView) + showErrorLabel(textFieldTag: .amountFieldTag) + } + } + fileprivate func textFieldViewWithTag(tag: Int) -> TextFieldWithLabelView { paymentInputFields.first(where: { $0.tag == tag }) ?? TextFieldWithLabelView() } fileprivate func validateIBANTextField(){ - if let ibanText = ibanTextFieldView.textField.text, ibanTextFieldView.textField.hasText { + if let ibanText = ibanTextFieldView.text { if ibanValidator.isValid(iban: ibanText) { applyDefaultStyle(ibanTextFieldView) hideErrorLabel(textFieldTag: .ibanFieldTag) @@ -451,8 +372,7 @@ class PaymentReviewContainerView: UIView { } fileprivate func fillInInputFields() { - guard let model else { return } - if let extractions = model.extractions { + if let extractions = viewModel.extractions { recipientTextFieldView.text = extractions.first(where: {$0.name == "payment_recipient"})?.value ibanTextFieldView.text = extractions.first(where: {$0.name == "iban"})?.value usageTextFieldView.text = extractions.first(where: {$0.name == "payment_purpose"})?.value @@ -461,7 +381,7 @@ class PaymentReviewContainerView: UIView { let amountToPayText = amountToPay.string amountTextFieldView.text = amountToPayText } - } else if let paymentInfo = model.paymentInfo { + } else if let paymentInfo = viewModel.paymentInfo { recipientTextFieldView.text = paymentInfo.recipient ibanTextFieldView.text = paymentInfo.iban usageTextFieldView.text = paymentInfo.purpose @@ -481,24 +401,19 @@ class PaymentReviewContainerView: UIView { switch textFieldTag { case .recipientFieldTag: errorLabel = recipientErrorLabel - errorMessage = NSLocalizedStringPreferredFormat("gini.merchant.errors.failed.recipient.non.empty.check", - comment: " recipient failed non empty check") + errorMessage = viewModel.strings.recipientErrorMessage case .ibanFieldTag: errorLabel = ibanErrorLabel - errorMessage = NSLocalizedStringPreferredFormat("gini.merchant.errors.failed.iban.non.empty.check", - comment: "iban failed non empty check") + errorMessage = viewModel.strings.ibanErrorMessage case .amountFieldTag: errorLabel = amountErrorLabel - errorMessage = NSLocalizedStringPreferredFormat("gini.merchant.errors.failed.amount.non.empty.check", - comment: "amount failed non empty check") + errorMessage = viewModel.strings.amountErrorMessage case .usageFieldTag: errorLabel = usageErrorLabel - errorMessage = NSLocalizedStringPreferredFormat("gini.merchant.errors.failed.purpose.non.empty.check", - comment: "purpose failed non empty check") + errorMessage = viewModel.strings.purposeErrorMessage } if errorLabel.isHidden { errorLabel.isHidden = false - errorLabel.textColor = GiniColor.feedback1.uiColor() errorLabel.text = errorMessage } updateAmountIbanErrorState() @@ -526,20 +441,18 @@ class PaymentReviewContainerView: UIView { // MARK: - Pay button fileprivate func disablePayButtonIfNeeded() { - payInvoiceButton.superview?.alpha = paymentInputFields.allSatisfy({ !$0.textField.isReallyEmpty }) && amountToPay.value > 0 ? 1 : Constants.payInvoiceInactiveAlpha + payInvoiceButton.alpha = paymentInputFields.allSatisfy({ !$0.isReallyEmpty }) && amountToPay.value > 0 ? 1 : Constants.payInvoiceInactiveAlpha } fileprivate func showValidationErrorLabel(textFieldTag: TextFieldType) { var errorLabel = UILabel() - var errorMessage = NSLocalizedStringPreferredFormat("gini.merchant.errors.failed.default.textfield.validation.check", - comment: "the field failed non empty check") + var errorMessage = viewModel.strings.emptyCheckErrorMessage switch textFieldTag { case .recipientFieldTag: errorLabel = recipientErrorLabel case .ibanFieldTag: errorLabel = ibanErrorLabel - errorMessage = NSLocalizedStringPreferredFormat("gini.merchant.errors.failed.iban.validation.check", - comment: "iban failed validation check") + errorMessage = viewModel.strings.ibanCheckErrorMessage case .amountFieldTag: errorLabel = amountErrorLabel case .usageFieldTag: @@ -547,18 +460,34 @@ class PaymentReviewContainerView: UIView { } if errorLabel.isHidden { errorLabel.isHidden = false - errorLabel.textColor = GiniColor.feedback1.uiColor() + errorLabel.text = errorMessage } updateAmountIbanErrorState() } + fileprivate func configureSelectBanksButton() { + selectBankButton.customConfigure(labelText: "", + leftImageIcon: viewModel.bankImageIcon, + rightImageIcon: viewModel.configuration.chevronDownIcon, + rightImageTintColor: viewModel.configuration.chevronDownIconColor, + shouldShowLabel: false) + selectBankButton.didTapButton = { [weak self] in + self?.tapOnBankPicker() + } + } + + @objc + private func tapOnBankPicker() { + onBankSelectionButtonClicked?() + } + fileprivate func configurePayButtonInitialState() { - guard let model else { return } - payInvoiceButton.configure(with: giniMerchantConfiguration.primaryButtonConfiguration) - payInvoiceButton.customConfigure(paymentProviderColors: model.selectedPaymentProvider.colors, - text: model.payInvoiceLabelText, - leftImageData: model.selectedPaymentProvider.iconData) + payInvoiceButton.configure(with: viewModel.primaryButtonConfiguration) + payInvoiceButton.customConfigure(text: viewModel.strings.payInvoiceLabelText, + textColor: viewModel.selectedPaymentProvider.colors.text.toColor(), + backgroundColor: viewModel.selectedPaymentProvider.colors.background.toColor(), + leftImageData: viewModel.configuration.showBanksPicker ? nil : viewModel.selectedPaymentProvider.iconData) disablePayButtonIfNeeded() payInvoiceButton.didTapButton = { [weak self] in self?.payButtonClicked() @@ -574,18 +503,22 @@ class PaymentReviewContainerView: UIView { action: #selector(doneWithAmountInputButtonTapped)) toolbarDone.items = [flexBarButton, barBtnDone] - textFieldView.textField.inputAccessoryView = toolbarDone + textFieldView.setInputAccesoryView(view: toolbarDone) } @objc fileprivate func doneWithAmountInputButtonTapped() { - amountTextFieldView.textField.endEditing(true) - amountTextFieldView.textField.resignFirstResponder() + _ = amountTextFieldView.endEditing(true) + _ = amountTextFieldView.resignFirstResponder() - if amountTextFieldView.textField.hasText && !amountTextFieldView.textField.isReallyEmpty { + if amountTextFieldView.hasText && !amountTextFieldView.isReallyEmpty { updateAmoutToPayWithCurrencyFormat() } } + private func updateAmountIbanErrorState() { + ibanAmountErrorsHorizontalStackView.isHidden = coupledErrorLabels.allSatisfy { $0.isHidden } + } + // MARK: - Pay Button Action fileprivate func payButtonClicked() { self.endEditing(true) @@ -602,6 +535,37 @@ class PaymentReviewContainerView: UIView { // MARK: - Helping functions + func textFieldText(textFieldType: TextFieldType) -> String? { + switch textFieldType { + case .recipientFieldTag: + return recipientTextFieldView.text + case .ibanFieldTag: + return ibanTextFieldView.text + case .amountFieldTag: + return amountTextFieldView.text + case .usageFieldTag: + return usageTextFieldView.text + } + } + + private func buildErrorLabel() -> UILabel { + let label = UILabel() + label.font = viewModel.configuration.errorLabelFont + label.textColor = viewModel.configuration.errorLabelTextColor + return label + } + + private func buildTextFieldWithLabelView(tag: Int, isEditable: Bool) -> TextFieldWithLabelView { + let textFieldView = TextFieldWithLabelView() + textFieldView.tag = tag + textFieldView.isUserInteractionEnabled = isEditable + return textFieldView + } +} + +// MARK: - Public + +public extension PaymentReviewContainerView { func noErrorsFound() -> Bool { // check if no errors labels are shown if (paymentInputFieldsErrorLabels.allSatisfy { $0.isHidden }) { @@ -611,43 +575,41 @@ class PaymentReviewContainerView: UIView { } } - func isTextFieldEmpty(texFieldType: TextFieldType) -> Bool { - switch texFieldType { - case .recipientFieldTag: - return recipientTextFieldView.textField.isReallyEmpty - case .ibanFieldTag: - return ibanTextFieldView.textField.isReallyEmpty - case .amountFieldTag: - return amountTextFieldView.textField.isReallyEmpty - case .usageFieldTag: - return usageTextFieldView.textField.isReallyEmpty - } + func inputFieldsHaveNoErrors() -> Bool { + paymentInputFieldsErrorLabels.allSatisfy { $0.isHidden } } func obtainPaymentInfo() -> PaymentInfo { let amountText = amountToPay.extractionString let paymentInfo = PaymentInfo(recipient: recipientTextFieldView.text ?? "", iban: ibanTextFieldView.text ?? "", - bic: "", amount: amountText, + bic: "", + amount: amountText, purpose: usageTextFieldView.text ?? "", - paymentUniversalLink: model.selectedPaymentProvider.universalLinkIOS, - paymentProviderId: model.selectedPaymentProvider.id) + paymentUniversalLink: viewModel.selectedPaymentProvider.universalLinkIOS, + paymentProviderId: viewModel.selectedPaymentProvider.id) return paymentInfo } - func textFieldText(texFieldType: TextFieldType) -> String? { - switch texFieldType { + func isTextFieldEmpty(textFieldType: TextFieldType) -> Bool { + switch textFieldType { case .recipientFieldTag: - return recipientTextFieldView.textField.text + return recipientTextFieldView.isReallyEmpty case .ibanFieldTag: - return ibanTextFieldView.textField.text + return ibanTextFieldView.isReallyEmpty case .amountFieldTag: - return amountTextFieldView.textField.text + return amountTextFieldView.isReallyEmpty case .usageFieldTag: - return usageTextFieldView.textField.text + return usageTextFieldView.isReallyEmpty } } + func updateSelectedPaymentProvider(_ paymentProvider: PaymentProvider) { + viewModel.selectedPaymentProvider = paymentProvider + viewModel.bankImageIcon = paymentProvider.iconData.toImage + configureSelectBanksButton() + configurePayButtonInitialState() + } } // MARK: - UITextFieldDelegate @@ -656,7 +618,7 @@ extension PaymentReviewContainerView: UITextFieldDelegate { /** Dissmiss the keyboard when return key pressed */ - func textFieldShouldReturn(_ textField: UITextField) -> Bool { + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true } @@ -664,9 +626,9 @@ extension PaymentReviewContainerView: UITextFieldDelegate { /** Updates amoutToPay, formated string with a currency and removes "0.00" value */ - func updateAmoutToPayWithCurrencyFormat() { - if amountTextFieldView.textField.hasText, let amountFieldText = amountTextFieldView.text { - if let priceValue = amountFieldText.toDecimal() { + public func updateAmoutToPayWithCurrencyFormat() { + if amountTextFieldView.hasText, let amountFieldText = amountTextFieldView.text { + if let priceValue = amountFieldText.decimal() { amountToPay.value = priceValue if priceValue > 0 { let amountToPayText = amountToPay.string @@ -677,7 +639,8 @@ extension PaymentReviewContainerView: UITextFieldDelegate { } } } - func textFieldDidBeginEditing(_ textField: UITextField) { + + public func textFieldDidBeginEditing(_ textField: UITextField) { applySelectionStyle(textFieldViewWithTag(tag: textField.tag)) // remove currency symbol and whitespaces for edit mode @@ -691,7 +654,7 @@ extension PaymentReviewContainerView: UITextFieldDelegate { } } - func textFieldDidEndEditing(_ textField: UITextField) { + public func textFieldDidEndEditing(_ textField: UITextField) { // add currency format when edit is finished if TextFieldType(rawValue: textField.tag) == .amountFieldTag { updateAmoutToPayWithCurrencyFormat() @@ -705,43 +668,46 @@ extension PaymentReviewContainerView: UITextFieldDelegate { disablePayButtonIfNeeded() } - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if TextFieldType(rawValue: textField.tag) == .amountFieldTag, let text = textField.text, let textRange = Range(range, in: text) { let updatedText = text.replacingCharacters(in: textRange, with: string) + adjustAmountValue(textField: textField, updatedText: updatedText) + disablePayButtonIfNeeded() + return false + } + return true + } - // Limit length to 7 digits - let onlyDigits = String(updatedText - .trimmingCharacters(in: .whitespaces) - .filter { c in c != "," && c != "."} - .prefix(7)) + private func adjustAmountValue(textField: UITextField, updatedText: String) { + // Limit length to 7 digits + let onlyDigits = String(updatedText + .trimmingCharacters(in: .whitespaces) + .filter { c in c != "," && c != "."} + .prefix(7)) - if let decimal = Decimal(string: onlyDigits) { - let decimalWithFraction = decimal / 100 + if let decimal = Decimal(string: onlyDigits) { + let decimalWithFraction = decimal / 100 - if let newAmount = Price.stringWithoutSymbol(from: decimalWithFraction)?.trimmingCharacters(in: .whitespaces) { - // Save the selected text range to restore the cursor position after replacing the text - let selectedRange = textField.selectedTextRange + if let newAmount = Price.stringWithoutSymbol(from: decimalWithFraction)?.trimmingCharacters(in: .whitespaces) { + // Save the selected text range to restore the cursor position after replacing the text + let selectedRange = textField.selectedTextRange - textField.text = newAmount - amountToPay.value = decimalWithFraction + textField.text = newAmount + amountToPay.value = decimalWithFraction - // Move the cursor position after the inserted character - if let selectedRange = selectedRange { - let countDelta = newAmount.count - text.count - let offset = countDelta == 0 ? 1 : countDelta - textField.moveSelectedTextRange(from: selectedRange.start, to: offset) - } + // Move the cursor position after the inserted character + if let selectedRange = selectedRange { + let countDelta = newAmount.count - (textField.text?.count ?? 0) + let offset = countDelta == 0 ? 1 : countDelta + textField.moveSelectedTextRange(from: selectedRange.start, to: offset) } } - disablePayButtonIfNeeded() - return false - } - return true + } } - func textFieldDidChangeSelection(_ textField: UITextField) { + public func textFieldDidChangeSelection(_ textField: UITextField) { disablePayButtonIfNeeded() } } @@ -750,7 +716,7 @@ extension PaymentReviewContainerView { enum Constants { static let buttonViewHeight = 56.0 static let leftRightPaymentInfoContainerPadding = 8.0 - static let topBottomPaymentInfoContainerPadding = 0.0 + static let topBottomPaymentInfoContainerPadding = 16.0 static let textFieldHeight = 56.0 static let errorLabelHeight = 12.0 static let amountWidth = 95.0 @@ -762,5 +728,6 @@ extension PaymentReviewContainerView { static let bottomViewHeight = 20.0 static let errorTopMargin = 9.0 static let lockIconHeight = 11.0 + static let buttonsSpacing = 8.0 } } diff --git a/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReviewContainer/PaymentReviewContainerViewModel.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReviewContainer/PaymentReviewContainerViewModel.swift new file mode 100644 index 000000000..20572f02f --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PaymentReviewContainer/PaymentReviewContainerViewModel.swift @@ -0,0 +1,88 @@ +// +// PaymentReviewContainerViewModel.swift +// GiniMerchantSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + +import Foundation +import GiniHealthAPILibrary +import GiniUtilites +import UIKit + +/// The view model for the Payment Review container view. +public final class PaymentReviewContainerViewModel { + var onExtractionFetched: (() -> Void)? + var selectedPaymentProvider: GiniHealthAPILibrary.PaymentProvider + let configuration: PaymentReviewContainerConfiguration + let strings: PaymentReviewContainerStrings + let primaryButtonConfiguration: ButtonConfiguration + let secondaryButtonConfiguration: ButtonConfiguration + let defaultStyleInputFieldConfiguration: TextFieldConfiguration + let errorStyleInputFieldConfiguration: TextFieldConfiguration + let selectionStyleInputFieldConfiguration: TextFieldConfiguration + let poweredByGiniViewModel: PoweredByGiniViewModel + var dispayMode: DisplayMode = .bottomSheet + var bankImageIcon: UIImage? + + /// An optional array of `Extraction` objects fetched during the payment review process. We use optional because we can rather have extractions fetched or payment information provided by user + public var extractions: [Extraction]? { + didSet { + self.onExtractionFetched?() + } + } + + /// An optional `PaymentInfo` object containing details about the payment. We use optional because we can rather have extractions fetched or payment information provided by user + public var paymentInfo: PaymentInfo? { + didSet { + self.onExtractionFetched?() + } + } + + /** + Initializes a new instance of `PaymentReviewContainerViewModel`. + + - Parameters: + - extractions: An optional array of `Extraction` objects representing fetched data. + - paymentInfo: An optional `PaymentInfo` object containing details about the payment. + - selectedPaymentProvider: The selected payment provider from the Gini Health API. + - configuration: The configuration settings for the payment review container. + - strings: The string resources for localizing the payment review UI. + - primaryButtonConfiguration: Configuration for the primary button in the UI. + - secondaryButtonConfiguration: Configuration for the secondary button in the UI. + - defaultStyleInputFieldConfiguration: Configuration for default-styled input fields. + - errorStyleInputFieldConfiguration: Configuration for input fields that display errors. + - selectionStyleInputFieldConfiguration: Configuration for input fields with selection styles. + - poweredByGiniConfiguration: Configuration settings for the "Powered by Gini" branding. + - poweredByGiniStrings: The string resources for localizing "Powered by Gini" UI elements. + - displayMode: The display mode indicating how the payment review interface should be presented. + */ + public init(extractions: [Extraction]?, + paymentInfo: PaymentInfo?, + selectedPaymentProvider: GiniHealthAPILibrary.PaymentProvider, + configuration: PaymentReviewContainerConfiguration, + strings: PaymentReviewContainerStrings, + primaryButtonConfiguration: ButtonConfiguration, + secondaryButtonConfiguration: ButtonConfiguration, + defaultStyleInputFieldConfiguration: TextFieldConfiguration, + errorStyleInputFieldConfiguration: TextFieldConfiguration, + selectionStyleInputFieldConfiguration: TextFieldConfiguration, + poweredByGiniConfiguration: PoweredByGiniConfiguration, + poweredByGiniStrings: PoweredByGiniStrings, + displayMode: DisplayMode) { + self.extractions = extractions + self.paymentInfo = paymentInfo + self.selectedPaymentProvider = selectedPaymentProvider + self.configuration = configuration + self.strings = strings + self.primaryButtonConfiguration = primaryButtonConfiguration + self.secondaryButtonConfiguration = secondaryButtonConfiguration + self.defaultStyleInputFieldConfiguration = defaultStyleInputFieldConfiguration + self.errorStyleInputFieldConfiguration = errorStyleInputFieldConfiguration + self.selectionStyleInputFieldConfiguration = selectionStyleInputFieldConfiguration + self.poweredByGiniViewModel = PoweredByGiniViewModel(configuration: poweredByGiniConfiguration, strings: poweredByGiniStrings) + self.dispayMode = displayMode + self.bankImageIcon = selectedPaymentProvider.iconData.toImage + } +} diff --git a/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PoweredByGini/PoweredByGiniConfiguration.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PoweredByGini/PoweredByGiniConfiguration.swift new file mode 100644 index 000000000..6ce27d5fd --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PoweredByGini/PoweredByGiniConfiguration.swift @@ -0,0 +1,29 @@ +// +// PoweredByGiniConfiguration.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import UIKit + +public struct PoweredByGiniConfiguration { + let poweredByGiniLabelFont: UIFont + let poweredByGiniLabelAccentColor: UIColor + let giniIcon: UIImage + + public init(poweredByGiniLabelFont: UIFont, + poweredByGiniLabelAccentColor: UIColor, + giniIcon: UIImage) { + self.poweredByGiniLabelFont = poweredByGiniLabelFont + self.poweredByGiniLabelAccentColor = poweredByGiniLabelAccentColor + self.giniIcon = giniIcon + } +} + +public struct PoweredByGiniStrings { + let poweredByGiniText: String + + public init(poweredByGiniText: String) { + self.poweredByGiniText = poweredByGiniText + } +} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PoweredByGiniView.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PoweredByGini/PoweredByGiniView.swift similarity index 82% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PoweredByGiniView.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PoweredByGini/PoweredByGiniView.swift index 31211dd5c..7b6dce9e8 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PoweredByGiniView.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PoweredByGini/PoweredByGiniView.swift @@ -9,22 +9,16 @@ import UIKit import GiniUtilites -final class PoweredByGiniView: UIView { - - var viewModel: PoweredByGiniViewModel! { - didSet { - setupView() - } - } - +public final class PoweredByGiniView: UIView { + private let viewModel: PoweredByGiniViewModel private let mainContainer = EmptyView() private lazy var poweredByGiniLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false - label.text = viewModel.poweredByGiniLabelText - label.textColor = viewModel.poweredByGiniLabelAccentColor - label.font = viewModel.poweredByGiniLabelFont + label.text = viewModel.strings.poweredByGiniText + label.textColor = viewModel.configuration.poweredByGiniLabelAccentColor + label.font = viewModel.configuration.poweredByGiniLabelFont label.numberOfLines = Constants.textNumberOfLines label.adjustsFontSizeToFitWidth = true label.textAlignment = .right @@ -32,14 +26,16 @@ final class PoweredByGiniView: UIView { }() private lazy var giniImageView: UIImageView = { - let imageView = UIImageView(image: viewModel.giniIcon) + let imageView = UIImageView(image: viewModel.configuration.giniIcon) imageView.frame = CGRect(x: 0, y: 0, width: Constants.widthGiniLogo, height: Constants.heightGiniLogo) imageView.translatesAutoresizingMaskIntoConstraints = false return imageView }() - override init(frame: CGRect) { - super.init(frame: frame) + public init(viewModel: PoweredByGiniViewModel) { + self.viewModel = viewModel + super.init(frame: .zero) + setupView() } required init?(coder: NSCoder) { diff --git a/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PoweredByGini/PoweredByGiniViewModel.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PoweredByGini/PoweredByGiniViewModel.swift new file mode 100644 index 000000000..b222dc8ca --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/PoweredByGini/PoweredByGiniViewModel.swift @@ -0,0 +1,19 @@ +// +// PoweredByGiniViewModel.swift +// GiniMerchantSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + +import UIKit + +public final class PoweredByGiniViewModel { + let strings: PoweredByGiniStrings + let configuration: PoweredByGiniConfiguration + + public init(configuration: PoweredByGiniConfiguration, strings: PoweredByGiniStrings) { + self.strings = strings + self.configuration = configuration + } +} diff --git a/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/ShareInvoice/ShareInvoiceBottomView.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/ShareInvoice/ShareInvoiceBottomView.swift new file mode 100644 index 000000000..314bd6d48 --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/ShareInvoice/ShareInvoiceBottomView.swift @@ -0,0 +1,335 @@ +// +// ShareInvoiceBottomView.swift +// GiniMerchantSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + +import UIKit +import GiniUtilites + +public final class ShareInvoiceBottomView: BottomSheetViewController { + + var viewModel: ShareInvoiceBottomViewModel + + private let contentStackView = EmptyStackView().orientation(.vertical) + + private let titleView = EmptyView() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = viewModel.titleText + label.textColor = viewModel.configuration.titleAccentColor + label.font = viewModel.configuration.titleFont + label.numberOfLines = 0 + label.lineBreakMode = .byTruncatingTail + label.textAlignment = .center + return label + }() + + private let qrCodeView = EmptyView() + + private lazy var qrImageView: UIImageView = { + let imageView = UIImageView(image: viewModel.qrCodeData.toImage) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.frame = CGRect(x: 0, y: 0, width: Constants.qrCodeImageSize, height: Constants.qrCodeImageSize) + return imageView + }() + + private let descriptionView = EmptyView() + + private lazy var descriptionLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = viewModel.descriptionLabelText + label.textColor = viewModel.configuration.descriptionAccentColor + label.font = viewModel.configuration.descriptionFont + label.numberOfLines = 0 + label.lineBreakMode = .byTruncatingTail + label.textAlignment = .center + return label + }() + + private let continueView = EmptyView() + + private lazy var continueButton: PaymentPrimaryButton = { + let button = PaymentPrimaryButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.configure(with: viewModel.primaryButtonConfiguration) + button.customConfigure(text: viewModel.continueButtonText, + textColor: viewModel.paymentProviderColors?.text.toColor(), + backgroundColor: viewModel.paymentProviderColors?.background.toColor(), + rightImageData: viewModel.bankImageIcon) + return button + }() + + private let brandView = EmptyView() + private let brandStackView = EmptyStackView().orientation(.horizontal).distribution(.fillEqually) + + private lazy var poweredByGiniView: PoweredByGiniView = { + PoweredByGiniView(viewModel: viewModel.poweredByGiniViewModel) + }() + + private let bottomView = EmptyView() + private lazy var paymentInfoView: UIView = { + let emptyView = EmptyView() + emptyView.roundCorners(corners: .allCorners, radius: Constants.paymentInfoCornerRadius) + emptyView.layer.borderColor = viewModel.configuration.paymentInfoBorderColor.cgColor + emptyView.layer.borderWidth = Constants.paymentInfoBorderWidth + emptyView.backgroundColor = .clear + return emptyView + }() + + private lazy var paymentInfoStackView = generatePaymentInfoViews() + + public override func viewDidLoad() { + super.viewDidLoad() + setupView() + } + + public init(viewModel: ShareInvoiceBottomViewModel, bottomSheetConfiguration: BottomSheetConfiguration) { + self.viewModel = viewModel + super.init(configuration: bottomSheetConfiguration) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + setupViewHierarchy() + setupLayout() + setButtonsState() + } + + private func setupViewHierarchy() { + // Create and configure the UIScrollView + let scrollView = UIScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.showsVerticalScrollIndicator = false + scrollView.bounces = true + + // Add contentStackView to the UIScrollView + scrollView.addSubview(contentStackView) + contentStackView.translatesAutoresizingMaskIntoConstraints = false + + // Apply constraints for contentStackView + setupContentStackViewConstraints(in: scrollView) + + // Set up the content hierarchy + titleView.addSubview(titleLabel) + contentStackView.addArrangedSubview(titleView) + + qrCodeView.addSubview(qrImageView) + contentStackView.addArrangedSubview(qrCodeView) + + brandStackView.addArrangedSubview(UIView()) + brandStackView.addArrangedSubview(poweredByGiniView) + brandStackView.addArrangedSubview(UIView()) + brandView.addSubview(brandStackView) + contentStackView.addArrangedSubview(brandView) + + continueView.addSubview(continueButton) + contentStackView.addArrangedSubview(continueView) + + descriptionView.addSubview(descriptionLabel) + contentStackView.addArrangedSubview(descriptionView) + + paymentInfoView.addSubview(paymentInfoStackView) + bottomView.addSubview(paymentInfoView) + contentStackView.addArrangedSubview(bottomView) + + // Calculate and update scrollView height dynamically + DispatchQueue.main.async { + self.updateScrollViewHeight(scrollView: scrollView) + } + // Add the UIScrollView to the main container + self.setContent(content: scrollView) + } + + private func setupContentStackViewConstraints(in scrollView: UIScrollView) { + NSLayoutConstraint.activate([ + contentStackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + contentStackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + contentStackView.topAnchor.constraint(equalTo: scrollView.topAnchor), + contentStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + contentStackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor) + ]) + } + + // Function to dynamically update scrollView height + private func updateScrollViewHeight(scrollView: UIScrollView) { + // Force layout to calculate the content size + scrollView.layoutIfNeeded() + let contentHeight = contentStackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height + + // Adjust the scrollView height + let scrollViewHeight = contentHeight + (2 * Constants.viewPaddingConstraint) + scrollView.heightAnchor.constraint(equalToConstant: scrollViewHeight).isActive = true + + // If needed, adjust bottom sheet constraints or animations + self.view.layoutIfNeeded() + } + + + private func setupLayout() { + setupTitleViewConstraints() + setupQRCodeImageConstraints() + setupDescriptionViewConstraints() + setupContinueButtonConstraints() + setupPoweredByGiniConstraints() + setupPaymentInfoViewConstraints() + } + + private func setButtonsState() { + continueButton.didTapButton = { [weak self] in + self?.tapOnContinueButton() + } + } + + private func setupTitleViewConstraints() { + NSLayoutConstraint.activate([ + titleLabel.leadingAnchor.constraint(equalTo: titleView.leadingAnchor, constant: Constants.viewPaddingConstraint), + titleLabel.trailingAnchor.constraint(equalTo: titleView.trailingAnchor, constant: -Constants.viewPaddingConstraint), + titleLabel.topAnchor.constraint(equalTo: titleView.topAnchor, constant: Constants.topBottomPaddingConstraint), + titleLabel.bottomAnchor.constraint(equalTo: titleView.bottomAnchor, constant: -Constants.topBottomPaddingConstraint) + ]) + } + + private func setupQRCodeImageConstraints() { + NSLayoutConstraint.activate([ + qrImageView.centerXAnchor.constraint(equalTo: qrCodeView.centerXAnchor), + qrImageView.widthAnchor.constraint(equalToConstant: Constants.qrCodeImageSize), + qrImageView.heightAnchor.constraint(equalToConstant: Constants.qrCodeImageSize), + qrImageView.topAnchor.constraint(equalTo: qrCodeView.topAnchor), + qrImageView.bottomAnchor.constraint(equalTo: qrCodeView.bottomAnchor) + ]) + } + + private func setupDescriptionViewConstraints() { + NSLayoutConstraint.activate([ + descriptionLabel.leadingAnchor.constraint(equalTo: descriptionView.leadingAnchor, constant: Constants.viewPaddingConstraint), + descriptionLabel.trailingAnchor.constraint(equalTo: descriptionView.trailingAnchor, constant: -Constants.viewPaddingConstraint), + descriptionLabel.topAnchor.constraint(equalTo: descriptionView.topAnchor, constant: Constants.topBottomPaddingConstraint), + descriptionLabel.bottomAnchor.constraint(equalTo: descriptionView.bottomAnchor, constant: -Constants.bottomDescriptionConstraint) + ]) + } + + private func setupContinueButtonConstraints() { + NSLayoutConstraint.activate([ + continueButton.leadingAnchor.constraint(equalTo: continueView.leadingAnchor, constant: Constants.viewPaddingConstraint), + continueButton.trailingAnchor.constraint(equalTo: continueView.trailingAnchor, constant: -Constants.viewPaddingConstraint), + continueButton.heightAnchor.constraint(equalToConstant: Constants.continueButtonViewHeight), + continueButton.topAnchor.constraint(equalTo: continueView.topAnchor, constant: Constants.topBottomPaddingConstraint), + continueButton.bottomAnchor.constraint(equalTo: continueView.bottomAnchor) + ]) + } + + private func setupPoweredByGiniConstraints() { + NSLayoutConstraint.activate([ + brandStackView.leadingAnchor.constraint(equalTo: brandView.leadingAnchor, constant: Constants.viewPaddingConstraint), + brandStackView.trailingAnchor.constraint(equalTo: brandView.trailingAnchor, constant: -Constants.viewPaddingConstraint), + brandStackView.topAnchor.constraint(equalTo: brandView.topAnchor), + brandStackView.bottomAnchor.constraint(equalTo: brandView.bottomAnchor), + brandStackView.heightAnchor.constraint(equalToConstant: Constants.brandViewHeight) + ]) + } + + private func setupPaymentInfoViewConstraints() { + NSLayoutConstraint.activate([ + paymentInfoView.leadingAnchor.constraint(equalTo: bottomView.leadingAnchor, constant: Constants.viewPaddingConstraint), + paymentInfoView.trailingAnchor.constraint(equalTo: bottomView.trailingAnchor, constant: -Constants.viewPaddingConstraint), + paymentInfoView.bottomAnchor.constraint(equalTo: bottomView.bottomAnchor), + paymentInfoView.topAnchor.constraint(equalTo: bottomView.topAnchor), + paymentInfoView.heightAnchor.constraint(equalToConstant: Constants.bottomViewHeight), + paymentInfoStackView.leadingAnchor.constraint(equalTo: paymentInfoView.leadingAnchor, constant: Constants.viewPaddingConstraint), + paymentInfoStackView.trailingAnchor.constraint(equalTo: paymentInfoView.trailingAnchor, constant: -Constants.viewPaddingConstraint), + paymentInfoStackView.topAnchor.constraint(equalTo: paymentInfoView.topAnchor, constant: Constants.viewPaddingConstraint), + paymentInfoStackView.bottomAnchor.constraint(equalTo: paymentInfoView.bottomAnchor, constant: -Constants.viewPaddingConstraint) + ]) + } + + @objc + private func tapOnContinueButton() { + viewModel.didTapOnContinue() + } + + @objc + private func tapOnAppStoreButton() { + openPaymentProvidersAppStoreLink(urlString: viewModel.selectedPaymentProvider?.appStoreUrlIOS) + } + + private func openPaymentProvidersAppStoreLink(urlString: String?) { + guard let urlString = urlString else { + print("AppStore link unavailable for this payment provider") + return + } + if let url = URL(string: urlString), UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } + } + + private func generatePaymentInfoViews() -> UIStackView { + let paymentInfoStackView = createStackView(distribution: .fillEqually, spacing: Constants.viewPaddingConstraint, orientation: .vertical) + [ + generateInfoStackView(title: viewModel.strings.recipientLabelText, subtitle: viewModel.paymentInfo?.recipient), + generateInfoStackView(title: viewModel.strings.ibanLabelText, subtitle: viewModel.paymentInfo?.iban), + generateAmountPurposeStackView() + ].forEach { paymentInfoStackView.addArrangedSubview($0) } + return paymentInfoStackView + } + + private func generateInfoStackView(title: String, subtitle: String?) -> UIStackView { + let stackView = createStackView(distribution: .fill, spacing: Constants.paymentInfoFieldsSpacing, orientation: .vertical) + stackView.addArrangedSubview(createLabel(text: title, isTitle: true)) + stackView.addArrangedSubview(createLabel(text: subtitle ?? "", isTitle: false)) + return stackView + } + + private func generateAmountPurposeStackView() -> UIStackView { + let amountPurposeStackView = createStackView(distribution: .fillEqually, spacing: Constants.viewPaddingConstraint, orientation: .horizontal) + + let amountStackView = generateInfoStackView(title: viewModel.strings.amountLabelText, subtitle: viewModel.paymentInfo?.amount) + let purposeStackView = generateInfoStackView(title: viewModel.strings.purposeLabelText, subtitle: viewModel.paymentInfo?.purpose) + + [amountStackView, purposeStackView].forEach { amountPurposeStackView.addArrangedSubview($0) } + return amountPurposeStackView + } + + private func createLabel(text: String, isTitle: Bool) -> UILabel { + let label = UILabel() + label.text = text + label.textAlignment = .left + label.font = isTitle ? viewModel.configuration.titlePaymentInfoFont : viewModel.configuration.subtitlePaymentInfoFont + label.textColor = isTitle ? viewModel.configuration.titlePaymentInfoTextColor : viewModel.configuration.subtitlePaymentInfoTextColor + return label + } + + private func createStackView(distribution: UIStackView.Distribution, spacing: CGFloat, orientation: NSLayoutConstraint.Axis) -> UIStackView { + let stackView = EmptyStackView() + stackView.distribution = distribution + stackView.spacing = spacing + stackView.axis = orientation + return stackView + } +} + +extension ShareInvoiceBottomView { + enum Constants { + static let viewPaddingConstraint = 16.0 + static let topBottomPaddingConstraint = 10.0 + static let bottomDescriptionConstraint = 20.0 + static let continueButtonViewHeight = 56.0 + static let topAnchorAppsViewConstraint = 20.0 + static let trailingAppsViewConstraint = 40.0 + static let topAnchorTipViewConstraint = 5.0 + static let brandViewHeight = 44.0 + static let bottomViewHeight = 190.0 + static let qrCodeImageSize = 208.0 + static let paymentInfoBorderWidth = 1.0 + static let paymentInfoCornerRadius = 16.0 + static let paymentInfoFieldsSpacing = 4.0 + } +} diff --git a/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/ShareInvoice/ShareInvoiceBottomViewModel.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/ShareInvoice/ShareInvoiceBottomViewModel.swift new file mode 100644 index 000000000..fd566cb3f --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/ShareInvoice/ShareInvoiceBottomViewModel.swift @@ -0,0 +1,86 @@ +// +// ShareInvoiceBottomViewModel.swift +// GiniMerchantSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + +import UIKit +import GiniHealthAPILibrary + +/// A protocol for handling actions from the Onboarding Share Invoice bottom view +public protocol ShareInvoiceBottomViewProtocol: AnyObject { + func didTapOnContinueToShareInvoice() +} + +struct SingleApp { + var title: String + var image: UIImage? + var isMoreButton: Bool +} + +/// The view model for the Share Invoice bottom view. +public final class ShareInvoiceBottomViewModel { + let configuration: ShareInvoiceConfiguration + let strings: ShareInvoiceStrings + let primaryButtonConfiguration: ButtonConfiguration + let poweredByGiniViewModel: PoweredByGiniViewModel + + var selectedPaymentProvider: GiniHealthAPILibrary.PaymentProvider? + var paymentProviderColors: GiniHealthAPILibrary.ProviderColors? + + /// A weak reference to the delegate conforming to `ShareInvoiceBottomViewProtocol`. + public weak var viewDelegate: ShareInvoiceBottomViewProtocol? + + let bankToReplaceString = "[BANK]" + let titleText: String + let descriptionLabelText: String + let bankImageIcon: Data + let qrCodeData: Data + let continueButtonText: String + + /// An optional identifier for the document ID being shared in order to pass it back to the delegates + public var documentId: String? + public var paymentInfo: PaymentInfo? + + var appsMocked: [SingleApp] = [] + + /** + Initializes a new instance of `ShareInvoiceBottomViewModel`. + + - Parameters: + - selectedPaymentProvider: The selected payment provider, if available. + - configuration: Configuration settings for sharing the invoice. + - strings: String resources for localizing the share invoice UI. + - primaryButtonConfiguration: Configuration for the primary button in the UI. + - poweredByGiniConfiguration: Configuration for the "Powered by Gini" branding. + - poweredByGiniStrings: String resources for localizing "Powered by Gini" UI elements. + */ + public init(selectedPaymentProvider: GiniHealthAPILibrary.PaymentProvider?, + configuration: ShareInvoiceConfiguration, + strings: ShareInvoiceStrings, + primaryButtonConfiguration: ButtonConfiguration, + poweredByGiniConfiguration: PoweredByGiniConfiguration, + poweredByGiniStrings: PoweredByGiniStrings, + qrCodeData: Data, + paymentInfo: PaymentInfo?) { + self.selectedPaymentProvider = selectedPaymentProvider + self.bankImageIcon = selectedPaymentProvider?.iconData ?? Data() + self.paymentProviderColors = selectedPaymentProvider?.colors + self.configuration = configuration + self.strings = strings + self.primaryButtonConfiguration = primaryButtonConfiguration + self.poweredByGiniViewModel = PoweredByGiniViewModel(configuration: poweredByGiniConfiguration, strings: poweredByGiniStrings) + self.qrCodeData = qrCodeData + self.paymentInfo = paymentInfo + + titleText = strings.titleTextPattern.replacingOccurrences(of: bankToReplaceString, with: selectedPaymentProvider?.name ?? "") + descriptionLabelText = strings.descriptionTextPattern.replacingOccurrences(of: bankToReplaceString, with: selectedPaymentProvider?.name ?? "") + continueButtonText = strings.continueLabelText.replacingOccurrences(of: bankToReplaceString, with: selectedPaymentProvider?.name ?? "") + } + + func didTapOnContinue() { + viewDelegate?.didTapOnContinueToShareInvoice() + } +} diff --git a/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/ShareInvoice/ShareInvoiceConfiguration.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/ShareInvoice/ShareInvoiceConfiguration.swift new file mode 100644 index 000000000..01791f83f --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/ShareInvoice/ShareInvoiceConfiguration.swift @@ -0,0 +1,68 @@ +// +// ShareInvoiceSingleAppConfiguration.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import UIKit + +public struct ShareInvoiceConfiguration { + public let titleFont: UIFont + public let titleAccentColor: UIColor + public let descriptionFont: UIFont + public let descriptionTextColor: UIColor + public let descriptionAccentColor: UIColor + public let paymentInfoBorderColor: UIColor + public let titlePaymentInfoTextColor: UIColor + public let titlePaymentInfoFont: UIFont + public let subtitlePaymentInfoTextColor: UIColor + public let subtitlePaymentInfoFont: UIFont + + public init(titleFont: UIFont, + titleAccentColor: UIColor, + descriptionFont: UIFont, + descriptionTextColor: UIColor, + descriptionAccentColor: UIColor, + paymentInfoBorderColor: UIColor, + titlePaymentInfoTextColor: UIColor, + subtitlePaymentInfoTextColor: UIColor, + titlepaymentInfoFont: UIFont, + subtitlePaymentInfoFont: UIFont) { + self.titleFont = titleFont + self.titleAccentColor = titleAccentColor + self.descriptionFont = descriptionFont + self.descriptionTextColor = descriptionTextColor + self.descriptionAccentColor = descriptionAccentColor + self.paymentInfoBorderColor = paymentInfoBorderColor + self.titlePaymentInfoTextColor = titlePaymentInfoTextColor + self.subtitlePaymentInfoTextColor = subtitlePaymentInfoTextColor + self.titlePaymentInfoFont = titlepaymentInfoFont + self.subtitlePaymentInfoFont = subtitlePaymentInfoFont + } +} + +public struct ShareInvoiceStrings { + let continueLabelText: String + let titleTextPattern: String + let descriptionTextPattern: String + let recipientLabelText: String + let amountLabelText: String + let ibanLabelText: String + let purposeLabelText: String + + public init(continueLabelText: String, + titleTextPattern: String, + descriptionTextPattern: String, + recipientLabelText: String, + amountLabelText: String, + ibanLabelText: String, + purposeLabelText: String) { + self.continueLabelText = continueLabelText + self.titleTextPattern = titleTextPattern + self.descriptionTextPattern = descriptionTextPattern + self.recipientLabelText = recipientLabelText + self.amountLabelText = amountLabelText + self.ibanLabelText = ibanLabelText + self.purposeLabelText = purposeLabelText + } +} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/ShareInvoiceSingleAppView.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/ShareInvoice/ShareInvoiceSingleAppView.swift similarity index 81% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/ShareInvoiceSingleAppView.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/ShareInvoice/ShareInvoiceSingleAppView.swift index 29bf6bb3e..57caf92b2 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/ShareInvoiceSingleAppView.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/ShareInvoice/ShareInvoiceSingleAppView.swift @@ -5,7 +5,6 @@ // Copyright © 2024 Gini GmbH. All rights reserved. // - import UIKit import GiniUtilites @@ -21,8 +20,6 @@ class ShareInvoiceSingleAppView: UIView { private let titleLabel: UILabel = { let label = UILabel() label.textAlignment = .center - label.textColor = GiniColor.standard3.uiColor() - label.font = GiniMerchantConfiguration.shared.font(for: .captions2) label.numberOfLines = 0 label.translatesAutoresizingMaskIntoConstraints = false return label @@ -58,14 +55,21 @@ class ShareInvoiceSingleAppView: UIView { } // Function to configure view - func configure(image: UIImage?, title: String?, isMoreButton: Bool) { - imageView.image = image + func configure(image: UIImage?, + imageBorderColor: UIColor, + imageBackgroundColor: UIColor, + title: String?, + titleColor: UIColor, + titleFont: UIFont, + isMoreButton: Bool) { titleLabel.text = title - imageView.layer.borderColor = GiniColor.standard3.uiColor().cgColor + titleLabel.textColor = titleColor + titleLabel.font = titleFont + + imageView.image = image + imageView.layer.borderColor = imageBorderColor.cgColor imageView.layer.borderWidth = isMoreButton ? 1 : 0 - let giniColor = GiniColor(lightModeColor: .white, - darkModeColor: GiniMerchantColorPalette.light3.preferredColor()) - imageView.backgroundColor = isMoreButton ? .clear : giniColor.uiColor() + imageView.backgroundColor = isMoreButton ? .clear : imageBackgroundColor imageView.contentMode = isMoreButton ? .center : .scaleAspectFit } } diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/TextFieldWithLabelView.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/TextField/TextFieldWithLabelView.swift similarity index 71% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/TextFieldWithLabelView.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/TextField/TextFieldWithLabelView.swift index ae795981a..483602a97 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/TextFieldWithLabelView.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/TextField/TextFieldWithLabelView.swift @@ -8,35 +8,22 @@ import UIKit import GiniUtilites -final class TextFieldWithLabelView: UIView { - private lazy var configuration = GiniMerchantConfiguration.shared - - var text: String? { - get { - return textField.text - } - set { - textField.text = newValue - textField.accessibilityValue = newValue - } - } - +public final class TextFieldWithLabelView: UIView { private lazy var titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false - label.font = configuration.font(for: .caption2) label.adjustsFontForContentSizeCategory = true return label }() - lazy var textField: UITextField = { + private lazy var textField: UITextField = { let textField = UITextField() textField.translatesAutoresizingMaskIntoConstraints = false textField.adjustsFontForContentSizeCategory = true return textField }() - override init(frame: CGRect) { + public override init(frame: CGRect) { super.init(frame: frame) setupView() setupConstraints() @@ -58,7 +45,7 @@ final class TextFieldWithLabelView: UIView { self.layer.borderWidth = configuration.borderWidth self.layer.borderColor = configuration.borderColor.cgColor self.backgroundColor = configuration.backgroundColor - self.textField.textColor = configuration.textColor + self.textField.textColor = isUserInteractionEnabled ? configuration.textColor : configuration.placeholderForegroundColor self.textField.attributedPlaceholder = NSAttributedString(string: "", attributes: [.foregroundColor: configuration.placeholderForegroundColor]) self.titleLabel.textColor = configuration.placeholderForegroundColor @@ -69,6 +56,15 @@ final class TextFieldWithLabelView: UIView { titleLabel.accessibilityValue = labelTitle.string } + func customConfigure(labelTitle: String) { + titleLabel.text = labelTitle + titleLabel.accessibilityValue = labelTitle + } + + func setInputAccesoryView(view: UIView?) { + textField.inputAccessoryView = view + } + private func setupConstraints() { NSLayoutConstraint.activate([ titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: Constants.topBottomPadding), @@ -83,6 +79,52 @@ final class TextFieldWithLabelView: UIView { } } +public extension TextFieldWithLabelView { + var text: String? { + get { + return textField.text + } + set { + textField.text = newValue + textField.accessibilityValue = newValue + } + } + + var hasText: Bool { + textField.hasText + } + + var isReallyEmpty: Bool { + return textField.isReallyEmpty + } + + override var tag: Int { + get { + return textField.tag + } + set { + textField.tag = newValue + } + } + + var delegate: UITextFieldDelegate? { + get { + return textField.delegate + } + set { + textField.delegate = newValue + } + } + + override func endEditing(_ force: Bool) -> Bool { + textField.endEditing(true) + } + + override func resignFirstResponder() -> Bool { + textField.resignFirstResponder() + } +} + private extension TextFieldWithLabelView { enum Constants { static let leftRightPadding: CGFloat = 12 diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/UITextField+Utils.swift b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/TextField/UITextField+Utils.swift similarity index 96% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/UITextField+Utils.swift rename to GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/TextField/UITextField+Utils.swift index 85d9d09d9..f95f0cfa3 100644 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/UITextField+Utils.swift +++ b/GiniComponents/GiniInternalPaymentSDK/Sources/GiniInternalPaymentSDK/TextField/UITextField+Utils.swift @@ -1,11 +1,13 @@ // // UITextField+Utils.swift -// GiniHealth +// GiniUtilites // // Copyright © 2024 Gini GmbH. All rights reserved. // import UIKit +import GiniUtilites + public extension UITextField { var isReallyEmpty: Bool { return text?.trimmingCharacters(in: .whitespaces).isEmpty ?? true diff --git a/GiniComponents/GiniInternalPaymentSDK/Tests/GiniInternalPaymentSDKTests/GiniPaymentComponentsTests.swift b/GiniComponents/GiniInternalPaymentSDK/Tests/GiniInternalPaymentSDKTests/GiniPaymentComponentsTests.swift new file mode 100644 index 000000000..9a4daf33d --- /dev/null +++ b/GiniComponents/GiniInternalPaymentSDK/Tests/GiniInternalPaymentSDKTests/GiniPaymentComponentsTests.swift @@ -0,0 +1,12 @@ +import XCTest +@testable import GiniInternalPaymentSDK + +final class GiniInternalPaymentSDKTests: XCTestCase { + func testExample() throws { + // XCTest Documentation + // https://developer.apple.com/documentation/xctest + + // Defining Test Cases and Test Methods + // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods + } +} diff --git a/GiniComponents/GiniUtilites/Package-release.swift b/GiniComponents/GiniUtilites/Package-release.swift new file mode 100644 index 000000000..7816cbfe0 --- /dev/null +++ b/GiniComponents/GiniUtilites/Package-release.swift @@ -0,0 +1,25 @@ +// swift-tools-version: 5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "GiniUtilites", + defaultLocalization: "en", + platforms: [.iOS(.v13)], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "GiniUtilites", + targets: ["GiniUtilites"]), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "GiniUtilites"), + .testTarget( + name: "GiniUtilitesTests", + dependencies: ["GiniUtilites"]), + ] +) diff --git a/GiniComponents/GiniUtilites/Sources/GiniUtilites/Extensions/Data.swift b/GiniComponents/GiniUtilites/Sources/GiniUtilites/Extensions/Data.swift new file mode 100644 index 000000000..b79572609 --- /dev/null +++ b/GiniComponents/GiniUtilites/Sources/GiniUtilites/Extensions/Data.swift @@ -0,0 +1,14 @@ +// +// Data.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + +import UIKit + +extension Data { + public var toImage: UIImage { + return UIImage(data: self) ?? UIImage() + } +} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/Extensions/NotificationsName.swift b/GiniComponents/GiniUtilites/Sources/GiniUtilites/Extensions/NotificationName.swift similarity index 72% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/Extensions/NotificationsName.swift rename to GiniComponents/GiniUtilites/Sources/GiniUtilites/Extensions/NotificationName.swift index 37423330b..d86de6fb3 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/Extensions/NotificationsName.swift +++ b/GiniComponents/GiniUtilites/Sources/GiniUtilites/Extensions/NotificationName.swift @@ -1,5 +1,5 @@ // -// NotificationsName.swift +// NotificationName.swift // // Copyright © 2024 Gini GmbH. All rights reserved. // @@ -7,6 +7,6 @@ import Foundation -extension Notification.Name { +public extension Notification.Name { static let paymentInfoDissapeared = Notification.Name("paymentInfoDissapeared") } diff --git a/GiniComponents/GiniUtilites/Sources/GiniUtilites/Extensions/String.swift b/GiniComponents/GiniUtilites/Sources/GiniUtilites/Extensions/String.swift index 46fe74325..bd3b27851 100644 --- a/GiniComponents/GiniUtilites/Sources/GiniUtilites/Extensions/String.swift +++ b/GiniComponents/GiniUtilites/Sources/GiniUtilites/Extensions/String.swift @@ -12,18 +12,25 @@ public extension String { return UIColor(hex: "#\(self)FF") } - func toDecimal() -> Decimal? { - let formatter = NumberFormatter() - formatter.numberStyle = .currency - formatter.currencySymbol = "" - return formatter.number(from: self)?.decimalValue - } - - func canOpenURLString() -> Bool { if let url = URL(string: self) , UIApplication.shared.canOpenURL(url) { return true } return false } + + /** + Returns a decimal value + + - parameter inputFieldString: String from input field. + + - returns: decimal value in current locale. + */ + + func decimal() -> Decimal? { + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.currencySymbol = "" + return formatter.number(from: self)?.decimalValue + } } diff --git a/GiniComponents/GiniUtilites/Sources/GiniUtilites/Extensions/UINavigationController.swift b/GiniComponents/GiniUtilites/Sources/GiniUtilites/Extensions/UINavigationController.swift new file mode 100644 index 000000000..769db3c20 --- /dev/null +++ b/GiniComponents/GiniUtilites/Sources/GiniUtilites/Extensions/UINavigationController.swift @@ -0,0 +1,33 @@ +// +// UINavigationController.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import UIKit + +extension UINavigationController { + public func pushViewController(viewController: UIViewController, animated: Bool, completion: @escaping () -> Void) { + pushViewController(viewController, animated: animated) + + if animated, let coordinator = transitionCoordinator { + coordinator.animate(alongsideTransition: nil) { _ in + completion() + } + } else { + completion() + } + } + + public func popViewController(animated: Bool, completion: @escaping () -> Void) { + popViewController(animated: animated) + + if animated, let coordinator = transitionCoordinator { + coordinator.animate(alongsideTransition: nil) { _ in + completion() + } + } else { + completion() + } + } +} diff --git a/GiniComponents/GiniUtilites/Sources/GiniUtilites/Font/UIFont.swift b/GiniComponents/GiniUtilites/Sources/GiniUtilites/Font/UIFont.swift index 155efc9ea..b339ee508 100644 --- a/GiniComponents/GiniUtilites/Sources/GiniUtilites/Font/UIFont.swift +++ b/GiniComponents/GiniUtilites/Sources/GiniUtilites/Font/UIFont.swift @@ -18,6 +18,6 @@ extension UIFont.TextStyle { public static let button: UIFont.TextStyle = .init(rawValue: "kButton") public static let body1: UIFont.TextStyle = .init(rawValue: "kBody1") public static let body2: UIFont.TextStyle = .init(rawValue: "kBody2") - public static let captions1: UIFont.TextStyle = .init(rawValue: "kCaption1") - public static let captions2: UIFont.TextStyle = .init(rawValue: "kCaption2") + public static let captions1: UIFont.TextStyle = .init(rawValue: "kCaptions1") + public static let captions2: UIFont.TextStyle = .init(rawValue: "kCaptions2") } diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniLocalization.swift b/GiniComponents/GiniUtilites/Sources/GiniUtilites/GiniLocalization.swift similarity index 83% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniLocalization.swift rename to GiniComponents/GiniUtilites/Sources/GiniUtilites/GiniLocalization.swift index aea38abfe..0589c10be 100644 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniLocalization.swift +++ b/GiniComponents/GiniUtilites/Sources/GiniUtilites/GiniLocalization.swift @@ -1,6 +1,6 @@ // // GiniLocalization.swift -// GiniHealth +// GiniUtilies // // Copyright © 2024 Gini GmbH. All rights reserved. // @@ -19,70 +19,70 @@ public enum GiniLocalization: String, CaseIterable { /** A utility for retrieving localized strings from the client's bundle or SDK bundle. */ -enum GiniLocalized { - +public enum GiniLocalized { + /** Retrieves a localized string for the given key. According localization GiniHealthConfiguration localization field and with check for client app locallizaton - + - Parameters: - key: The key to search for in the strings file. - fallbackKey: The fallback key to use if the primary key is not found. - comment: The corresponding comment for the key. - + - Returns: The localized string for the given key. */ - static func string(_ key: String, fallbackKey: String? = nil, comment: String) -> String { - let locale = GiniHealthConfiguration.shared.customLocalization?.rawValue + public static func string(_ key: String, fallbackKey: String? = nil, comment: String, locale: String?, bundle: Bundle) -> String { + let locale = locale ?? getLanguageCode() ?? GiniLocalization.en.rawValue let clientAppBundle = Bundle.main - + if let clientString = overridedString(key, locale: locale, comment: comment, bundle: clientAppBundle) { return clientString } else if let fallbackKey = fallbackKey, let fallbackClientString = overridedString(fallbackKey, locale: locale, comment: comment, bundle: clientAppBundle) { return fallbackClientString - } else if let sdkString = overridedString(key, locale: locale, comment: comment, bundle: giniHealthBundleResource()) { + } else if let sdkString = overridedString(key, locale: locale, comment: comment, bundle: bundle) { return sdkString } - return localizationString(fallbackKey ?? "", locale: locale, comment: comment, bundle: giniHealthBundleResource()) + return localizationString(fallbackKey ?? "", locale: locale, comment: comment, bundle: bundle) } - + /** Checks if the localized string exists in the localized strings. - + - Parameters: - key: The key to search for in the strings file. - locale: The locale for the localized string. - bundle: The bundle to search for the localized string. - + - Returns: The localized string if it exists, otherwise nil. */ private static func overridedString(_ key: String, locale: String?, comment: String, bundle: Bundle) -> String? { let value = localizationString(key, locale: locale, comment: comment, bundle: bundle) return value.lowercased() == key.lowercased() ? nil : value } - + /** Retrieves the localized string based on the key and locale in specifyed bundle. - + - Parameters: - key: The key to search for in the strings file. - locale: The locale for the localized string. - bundle: The bundle to search for the localized string. - + - Returns: The localized string for the given key. */ private static func localizationString(_ key: String, locale: String?, comment: String, bundle: Bundle) -> String { let localizedBundle = localizedBundle(parentBundle: bundle, localeKey: locale) return NSLocalizedString(key, tableName: nil, bundle: localizedBundle ?? bundle, value: "", comment: comment) } - + /** Retrieves the localized bundle based on the locale key. - + - Parameters: - parentBundle: The parent bundle to search for the localized bundle. - localeKey: The key representing the locale for the localized bundle. - + - Returns: The localized bundle if found, otherwise nil. */ private static func localizedBundle(parentBundle: Bundle, localeKey: String?) -> Bundle? { @@ -93,4 +93,15 @@ enum GiniLocalized { } return bundle } + + /** + Retrieve the default language code of the system + */ + private static func getLanguageCode() -> String? { + let locale = Locale.current + if let languageCode = locale.languageCode { + return languageCode + } + return nil + } } diff --git a/GiniComponents/GiniUtilites/Sources/GiniUtilites/GiniUtilitesVersion.swift b/GiniComponents/GiniUtilites/Sources/GiniUtilites/GiniUtilitesVersion.swift index bf69045a4..458da5f35 100644 --- a/GiniComponents/GiniUtilites/Sources/GiniUtilites/GiniUtilitesVersion.swift +++ b/GiniComponents/GiniUtilites/Sources/GiniUtilites/GiniUtilitesVersion.swift @@ -5,4 +5,4 @@ // Copyright © 2024 Gini GmbH. All rights reserved. // -public let GiniUtilitesVersion = "1.0.0" +public let GiniUtilitesVersion = "1.1.0" diff --git a/GiniComponents/GiniUtilites/Sources/GiniUtilites/Logger.swift b/GiniComponents/GiniUtilites/Sources/GiniUtilites/Logger.swift new file mode 100644 index 000000000..b1f2d8f88 --- /dev/null +++ b/GiniComponents/GiniUtilites/Sources/GiniUtilites/Logger.swift @@ -0,0 +1,46 @@ +// +// Logger.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + + +import Foundation +import os + +public enum LogEvent { + case error + case success + case warning + case custom(String) + + var value: String { + switch self { + case .error: return "❌" + case .success: return "✅" + case .warning: return "⚠️" + case .custom(let emoji): return emoji + } + } +} + +public enum LogLevel { + case none + case debug +} + +public func Log(_ message: String, + event: LogEvent) { + let prefix = event.value + + // When having the `OS_ACTIVITY_MODE` disabled, NSLog messages are not printed + if ProcessInfo.processInfo.environment["OS_ACTIVITY_MODE"] == "disable" { + print(prefix, message) + } + if #available(macOS 10.12, *) { + os_log("%@ %@", prefix, message) + } else { + // Fallback on earlier versions + } +} diff --git a/GiniComponents/GiniUtilites/Sources/GiniUtilites/TextFieldConfiguration.swift b/GiniComponents/GiniUtilites/Sources/GiniUtilites/TextFieldConfiguration.swift index 44289e3d9..5ac234f59 100644 --- a/GiniComponents/GiniUtilites/Sources/GiniUtilites/TextFieldConfiguration.swift +++ b/GiniComponents/GiniUtilites/Sources/GiniUtilites/TextFieldConfiguration.swift @@ -11,11 +11,11 @@ public struct TextFieldConfiguration { public let backgroundColor: UIColor public let borderColor: UIColor public let textColor: UIColor + public let textFont: UIFont public let cornerRadius: CGFloat public let borderWidth: CGFloat public let placeholderForegroundColor: UIColor - /// Text Field configuration initalizer /// - Parameters: /// - backgroundColor: the textField's background color @@ -28,12 +28,14 @@ public struct TextFieldConfiguration { public init(backgroundColor: UIColor, borderColor: UIColor, textColor: UIColor, + textFont: UIFont, cornerRadius: CGFloat, borderWidth: CGFloat, placeholderForegroundColor: UIColor) { self.backgroundColor = backgroundColor self.borderColor = borderColor self.textColor = textColor + self.textFont = textFont self.cornerRadius = cornerRadius self.borderWidth = borderWidth self.placeholderForegroundColor = placeholderForegroundColor diff --git a/GiniComponents/GiniUtilites/Sources/GiniUtilites/URLOpener.swift b/GiniComponents/GiniUtilites/Sources/GiniUtilites/URLOpener.swift index d588170bc..6064b71ce 100644 --- a/GiniComponents/GiniUtilites/Sources/GiniUtilites/URLOpener.swift +++ b/GiniComponents/GiniUtilites/Sources/GiniUtilites/URLOpener.swift @@ -20,7 +20,7 @@ public struct URLOpener { public init(_ application: URLOpenerProtocol) { self.application = application } - + /// Opens AppStore with the provided URL /// /// - Parameters: @@ -28,16 +28,23 @@ public struct URLOpener { /// - completion: called after opening is completed /// param is true if website was opened successfully /// param is false if opening failed + public func openLink(url: URL, completion: GiniOpenLinkCompletionBlock?) { if application.canOpenURL(url) { application.open(url, options: [:], completionHandler: completion) } else { - Task { @MainActor in - completion?(false) + if #available(iOS 13, *) { + Task { @MainActor in + completion?(false) + } + } else { + DispatchQueue.main.async { + completion?(false) + } } } } - + public func canOpenLink(url: URL) -> Bool { application.canOpenURL(url) } diff --git a/GiniComponents/GiniUtilites/Sources/GiniUtilites/Views/EmptyStackView.swift b/GiniComponents/GiniUtilites/Sources/GiniUtilites/Views/EmptyStackView.swift index beb9dce25..58e150839 100644 --- a/GiniComponents/GiniUtilites/Sources/GiniUtilites/Views/EmptyStackView.swift +++ b/GiniComponents/GiniUtilites/Sources/GiniUtilites/Views/EmptyStackView.swift @@ -9,14 +9,34 @@ import UIKit public class EmptyStackView: UIStackView { - public init(orientation: NSLayoutConstraint.Axis) { + public init() { super.init(frame: .zero) - self.axis = orientation self.translatesAutoresizingMaskIntoConstraints = false self.backgroundColor = .clear } - + required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } + +extension EmptyStackView { + @discardableResult + public func spacing(_ spacing: CGFloat) -> EmptyStackView { + self.spacing = spacing + return self + } + + @discardableResult + public func orientation(_ orientation: NSLayoutConstraint.Axis) -> EmptyStackView { + self.axis = orientation + return self + } + + @discardableResult + public func distribution(_ distribution: UIStackView.Distribution) -> EmptyStackView { + self.distribution = distribution + return self + } +} + diff --git a/GiniMobile.xcworkspace/contents.xcworkspacedata b/GiniMobile.xcworkspace/contents.xcworkspacedata index 8501e3edf..7109c98d9 100644 --- a/GiniMobile.xcworkspace/contents.xcworkspacedata +++ b/GiniMobile.xcworkspace/contents.xcworkspacedata @@ -4,6 +4,9 @@ + + @@ -38,17 +41,11 @@ location = "container:" name = "GiniHealthAPILibrary"> + location = "group:HealthAPILibrary/GiniHealthAPILibrary"> - - - - - - + location = "group:HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample.xcodeproj"> : Resource { var fullUrlString: String? @@ -104,39 +111,54 @@ struct APIResource: Resource { return urlString case .payment(let id): return "/paymentRequests/\(id)/payment" - case .pdfWithQRCode(paymentRequestId: let paymentRequestId): + case .pdfWithQRCode(let paymentRequestId, _): return "/paymentRequests/\(paymentRequestId)" } } var defaultHeaders: HTTPHeaders { + // Define common headers + let acceptHeader = ["Accept": ContentType.content( + version: apiVersion, + subtype: nil, + mimeSubtype: MimeSubtype.json.rawValue + ).value] + + // Helper method to construct Content-Type header + func contentTypeHeader(subtype: String?, mimeSubtype: String) -> HTTPHeaders { + return ["Content-Type": ContentType.content( + version: apiVersion, + subtype: subtype, + mimeSubtype: mimeSubtype + ).value] + } + switch method { - case .createDocument(_, _, let mimeSubType, let documentType): - return ["Accept": ContentType.content(version: apiVersion, - subtype: nil, - mimeSubtype: "json").value, - "Content-Type": ContentType.content(version: apiVersion, - subtype: documentType?.name, - mimeSubtype: mimeSubType).value - ] - case .file(_): - return [:] - case .paymentProviders, .paymentProvider(_), .paymentRequests(_, _) : - return ["Accept": ContentType.content(version: apiVersion, - subtype: nil, - mimeSubtype: "json").value] - case .pdfWithQRCode(_): - return ["Accept": ContentType.content(version: apiVersion, - subtype: nil, - mimeSubtype: "qr+pdf").value] - default: - return ["Accept": ContentType.content(version: apiVersion, - subtype: nil, - mimeSubtype: "json").value, - "Content-Type": ContentType.content(version: apiVersion, - subtype: nil, - mimeSubtype: "json").value - ] + case .createDocument(_, _, let mimeSubType, let documentType): + return acceptHeader.merging(contentTypeHeader( + subtype: documentType?.name, + mimeSubtype: mimeSubType.rawValue + )) { _, new in new } + + case .file(_): + return [:] + + case .paymentProviders, .paymentProvider(_), .paymentRequests(_, _): + return acceptHeader + + case .pdfWithQRCode(_, let mimeSubtype): + return ["Accept": ContentType.content( + version: apiVersion, + subtype: nil, + mimeSubtype: mimeSubtype.rawValue + ).value] + + default: + // Default headers for other cases + return acceptHeader.merging(contentTypeHeader( + subtype: nil, + mimeSubtype: MimeSubtype.json.rawValue + )) { _, new in new } } } diff --git a/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/Core/Extensions/Data.swift b/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/Core/Extensions/Data.swift index dcc0de764..66b0c66ea 100644 --- a/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/Core/Extensions/Data.swift +++ b/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/Core/Extensions/Data.swift @@ -45,7 +45,7 @@ extension Data { } } - func isImage() -> Bool { + public func isImage() -> Bool { return UIImage(data: self) != nil } diff --git a/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/Core/GiniHealthAPI.swift b/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/Core/GiniHealthAPI.swift index 7159bdc82..2538936fa 100644 --- a/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/Core/GiniHealthAPI.swift +++ b/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/Core/GiniHealthAPI.swift @@ -100,6 +100,47 @@ extension GiniHealthAPI { self.sessionDelegate = sessionDelegate } + /** + * Creates a Gini Health API Library with certificate pinning configuration. + * + * - Parameter client: The Gini Health API client credentials + * - Parameter api: The Gini Health API that the library interacts with. `APIDomain.default` by default + * - Parameter userApi: The Gini User API that the library interacts with. `UserDomain.default` by default + * - Parameter pinningConfig: Configuration for certificate pinning. Format ["PinnedDomains" : ["PublicKeyHashes"]] + * - Parameter logLevel: The log level. `LogLevel.none` by default. + */ + public init(client: Client, + api: APIDomain = .default, + userApi: UserDomain = .default, + pinningConfig: [String: [String]], + logLevel: LogLevel = .none) { + self.init(client: client, + api: api, + userApi: userApi, + logLevel: logLevel, + sessionDelegate: GiniSessionDelegate(pinningConfig: pinningConfig)) + } + + /** + * Creates a Gini Health API Library to be used with a transparent proxy and a custom api access token source and certificate pinning configuration. + * + * - Parameter customApiDomain: A custom api domain string. + * - Parameter alternativeTokenSource: A protocol for using custom api access token + * - Parameter pinningConfig: Configuration for certificate pinning. Format ["PinnedDomains" : ["PublicKeyHashes"]] + * - Parameter logLevel: The log level. `LogLevel.none` by default. + */ + public init(customApiDomain: String, + alternativeTokenSource: AlternativeTokenSource, + apiVersion: Int, + pinningConfig: [String: [String]], + logLevel: LogLevel = .none) { + self.init(customApiDomain: customApiDomain, + alternativeTokenSource: alternativeTokenSource, + apiVersion: apiVersion, + logLevel: logLevel, + sessionDelegate: GiniSessionDelegate(pinningConfig: pinningConfig)) + } + public func build() -> GiniHealthAPI { // Save client information save(client) diff --git a/HealthAPILibrary/GiniHealthAPILibraryPinning/Sources/GiniHealthAPILibraryPinning/GiniSessionDelegate.swift b/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/Core/SSLPinning/GiniSessionDelegate.swift similarity index 100% rename from HealthAPILibrary/GiniHealthAPILibraryPinning/Sources/GiniHealthAPILibraryPinning/GiniSessionDelegate.swift rename to HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/Core/SSLPinning/GiniSessionDelegate.swift diff --git a/HealthAPILibrary/GiniHealthAPILibraryPinning/Sources/GiniHealthAPILibraryPinning/SSLPinningManager.swift b/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/Core/SSLPinning/SSLPinningManager.swift similarity index 100% rename from HealthAPILibrary/GiniHealthAPILibraryPinning/Sources/GiniHealthAPILibraryPinning/SSLPinningManager.swift rename to HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/Core/SSLPinning/SSLPinningManager.swift diff --git a/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/DefaultDocumentService.swift b/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/DefaultDocumentService.swift index 60993d264..0aa2c5dac 100644 --- a/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/DefaultDocumentService.swift +++ b/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/DefaultDocumentService.swift @@ -56,7 +56,7 @@ public final class DefaultDocumentService: DefaultDocumentServiceProtocol { case .composite(let compositeDocumentInfo): let resource = APIResource.init(method: .createDocument(fileName: fileName, docType: docType, - mimeSubType: "json", + mimeSubType: .json, documentType: type), apiDomain: apiDomain, apiVersion: apiVersion, @@ -67,7 +67,7 @@ public final class DefaultDocumentService: DefaultDocumentServiceProtocol { case .partial(let data): let resource = APIResource.init(method: .createDocument(fileName: fileName, docType: docType, - mimeSubType: "json", + mimeSubType: .json, documentType: type), apiDomain: apiDomain, apiVersion: apiVersion, @@ -211,31 +211,74 @@ public final class DefaultDocumentService: DefaultDocumentServiceProtocol { } /** - * Submits the analysis feedback for a given document. + * Submits the analysis feedback for a given document ID. * - * - Parameter document: The document for which feedback should be sent - * - Parameter extractions: The document's updated extractions - * - Parameter completion: A completion callback + * - Parameter documentId: The ID of the document for which feedback should be sent. + * - Parameter extractions: The document's updated extractions. + * - Parameter completion: A completion callback. + */ + public func submitFeedback(for documentId: String, + with extractions: [Extraction], + completion: @escaping CompletionResult) { + submitFeedback(resourceHandler: sessionManager.data, + documentId: documentId, + with: extractions, + completion: completion) + } + + /** + * Submits the analysis feedback with compound extractions (e.g., "line items") for a given document ID. + * + * - Parameter documentId: The ID of the document for which feedback should be sent. + * - Parameter extractions: The document's updated extractions. + * - Parameter compoundExtractions: The document's updated compound extractions. + * - Parameter completion: A completion callback. + */ + public func submitFeedback(for documentId: String, + with extractions: [Extraction], + and compoundExtractions: [String: [[Extraction]]], + completion: @escaping CompletionResult) { + submitFeedback(resourceHandler: sessionManager.data, + documentId: documentId, + with: extractions, + and: compoundExtractions, + completion: completion) + } + + /** + * Submits the analysis feedback for a given document. + * + * - Parameter document: The document for which feedback should be sent. + * - Parameter extractions: The document's updated extractions. + * - Parameter completion: A completion callback. */ public func submitFeedback(for document: Document, with extractions: [Extraction], completion: @escaping CompletionResult) { - submitFeedback(resourceHandler: sessionManager.data, for: document, with: extractions, completion: completion) + submitFeedback( + resourceHandler: sessionManager.data, + documentId: document.id, + with: extractions, + completion: completion) } - + /** - * Submits the analysis feedback with compound extractions (e.g., "line items") for a given document. + * Submits the analysis feedback with compound extractions (e.g., "line items") for a given document. * - * - Parameter document: The document for which feedback should be sent - * - Parameter extractions: The document's updated extractions - * - Parameter compoundExtractions: The document's updated compound extractions - * - Parameter completion: A completion callback + * - Parameter document: The document for which feedback should be sent. + * - Parameter extractions: The document's updated extractions. + * - Parameter compoundExtractions: The document's updated compound extractions. + * - Parameter completion: A completion callback. */ public func submitFeedback(for document: Document, with extractions: [Extraction], and compoundExtractions: [String: [[Extraction]]], completion: @escaping CompletionResult) { - submitFeedback(resourceHandler: sessionManager.data, for: document, with: extractions, and: compoundExtractions, completion: completion) + submitFeedback(resourceHandler: sessionManager.data, + documentId: document.id, + with: extractions, + and: compoundExtractions, + completion: completion) } /** diff --git a/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/Document.swift b/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/Document.swift index 3393656ac..f8c40c5b7 100644 --- a/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/Document.swift +++ b/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/Document.swift @@ -247,6 +247,7 @@ extension Document: Decodable { - parameter id: The document's unique identifier. - parameter name: The document's file name. - parameter links: Links to related resources, such as extractions, document, processed, layout or pages. + - parameter pageCount: The document's number of pages. - parameter sourceClassification: The document's source classification. We recommend to use `scanned` or `composite`. - parameter expirationDate: The document's expiration date. @@ -256,6 +257,7 @@ extension Document: Decodable { id: String, name: String, links: Links, + pageCount: Int, sourceClassification: SourceClassification, expirationDate: Date?) { self.init(compositeDocuments: [], @@ -263,7 +265,7 @@ extension Document: Decodable { id: id, name: name, origin: .upload, - pageCount: 1, + pageCount: pageCount, pages: [], links: links, partialDocuments: [], diff --git a/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/DocumentService.swift b/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/DocumentService.swift index 92ca0106a..b4400eaba 100644 --- a/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/DocumentService.swift +++ b/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/DocumentService.swift @@ -350,30 +350,47 @@ extension DocumentService { return width * height } - func submitFeedback(resourceHandler: ResourceDataHandler>, - for document: Document, - with extractions: [Extraction], - completion: @escaping CompletionResult) { - guard let json = try? JSONEncoder().encode(ExtractionsFeedback(feedback: extractions)) else { + private func submitFeedback(resourceHandler: ResourceDataHandler>, + documentId: String, + extractions: [Extraction], + compoundExtractions: [String: [[Extraction]]]? = nil, + completion: @escaping CompletionResult) { + let feedbackData: Encodable + if let compoundExtractions = compoundExtractions { + feedbackData = CompoundExtractionsFeedback(extractions: extractions, compoundExtractions: compoundExtractions) + } else { + feedbackData = ExtractionsFeedback(feedback: extractions) + } + + guard let json = try? JSONEncoder().encode(feedbackData) else { assertionFailure("The extractions provided cannot be encoded") return } - let resource = APIResource(method: .feedback(forDocumentId: document.id), + let resource = APIResource(method: .feedback(forDocumentId: documentId), apiDomain: apiDomain, apiVersion: apiVersion, httpMethod: .post, body: json) - resourceHandler(resource, { result in + resourceHandler(resource) { result in switch result { case .success: completion(.success(())) case .failure(let error): completion(.failure(error)) } - - }) + } + } + + func submitFeedback(resourceHandler: ResourceDataHandler>, + for document: Document, + with extractions: [Extraction], + completion: @escaping CompletionResult) { + submitFeedback(resourceHandler: resourceHandler, + documentId: document.id, + extractions: extractions, + completion: completion) } func submitFeedback(resourceHandler: ResourceDataHandler>, @@ -381,30 +398,34 @@ extension DocumentService { with extractions: [Extraction], and compoundExtractions: [String: [[Extraction]]], completion: @escaping CompletionResult) { - guard let json = try? JSONEncoder().encode( - CompoundExtractionsFeedback(extractions: extractions, compoundExtractions: compoundExtractions) - ) else { - assertionFailure("The extractions provided cannot be encoded") - return - } - - let resource = APIResource(method: .feedback(forDocumentId: document.id), - apiDomain: apiDomain, - apiVersion: apiVersion, - httpMethod: .post, - body: json) - - resourceHandler(resource, { result in - switch result { - case .success: - completion(.success(())) - case .failure(let error): - completion(.failure(error)) - } - - }) + submitFeedback(resourceHandler: resourceHandler, + documentId: document.id, + extractions: extractions, + compoundExtractions: compoundExtractions, + completion: completion) } + func submitFeedback(resourceHandler: ResourceDataHandler>, + documentId: String, + with extractions: [Extraction], + completion: @escaping CompletionResult) { + submitFeedback(resourceHandler: resourceHandler, + documentId: documentId, + extractions: extractions, + completion: completion) + } + + func submitFeedback(resourceHandler: ResourceDataHandler>, + documentId: String, + with extractions: [Extraction], + and compoundExtractions: [String: [[Extraction]]], + completion: @escaping CompletionResult) { + submitFeedback(resourceHandler: resourceHandler, + documentId: documentId, + extractions: extractions, + compoundExtractions: compoundExtractions, + completion: completion) + } } // MARK: - Fileprivate diff --git a/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/ExtractionResult.swift b/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/ExtractionResult.swift index a59388eb9..630f96517 100644 --- a/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/ExtractionResult.swift +++ b/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/ExtractionResult.swift @@ -7,29 +7,6 @@ import Foundation -/** - Payment State types from payment state from extraction result - */ -public enum PaymentState: String { - case payable = "Payable" - case other = "Other" -} -/** - Extraction Types for extraction result - */ -public enum ExtractionType: String { - case paymentState = "payment_state" - case containsMultipleDocs = "contains_multiple_docs" - case paymentDueDate = "payment_due_date" - case amountToPay = "amount_to_pay" - case paymentRecipient = "payment_recipient" - case iban = "iban" - case paymentPurpose = "payment_purpose" - case doctorName = "medical_service_provider" - case bic = "bic" - case invoiceDate = "invoice_date" -} - /** * Data model for a document extraction result. */ diff --git a/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/Payments/PaymentService.swift b/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/Payments/PaymentService.swift index 2ac8157de..0eaec2172 100644 --- a/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/Payments/PaymentService.swift +++ b/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/Documents/Payments/PaymentService.swift @@ -122,6 +122,21 @@ public final class PaymentService: PaymentServiceProtocol { public func pdfWithQRCode(paymentRequestId: String, completion: @escaping CompletionResult){ pdfWithQRCode(paymentRequestId: paymentRequestId, + mimeSubtype: .pdf, + resourceHandler: sessionManager.data, + completion: completion) + } + + /** + * Returns a QR Code image with a payment request in PNG format. + * + * - Parameter paymentRequestId: The payment request's unique identifier. + * - Parameter completion: A completion callback, returning the QR Code image in PNG format on success. + */ + public func qrCodeImage(paymentRequestId: String, + completion: @escaping CompletionResult) { + pdfWithQRCode(paymentRequestId: paymentRequestId, + mimeSubtype: .png, resourceHandler: sessionManager.data, completion: completion) } @@ -358,10 +373,12 @@ extension PaymentService { } func pdfWithQRCode(paymentRequestId: String, + mimeSubtype: MimeSubtype, resourceHandler: ResourceDataHandler>, completion: @escaping CompletionResult) { - let resource = APIResource(method: .pdfWithQRCode(paymentRequestId: paymentRequestId), - apiDomain: apiDomain, + let resource = APIResource(method: .pdfWithQRCode(paymentRequestId: paymentRequestId, + mimeSubtype: mimeSubtype), + apiDomain: apiDomain, apiVersion: apiVersion, httpMethod: .get) resourceHandler(resource, { result in diff --git a/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/GiniHealthAPILibraryVersion.swift b/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/GiniHealthAPILibraryVersion.swift index 6c290cd15..8fa40f0ad 100644 --- a/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/GiniHealthAPILibraryVersion.swift +++ b/HealthAPILibrary/GiniHealthAPILibrary/Sources/GiniHealthAPILibrary/GiniHealthAPILibraryVersion.swift @@ -5,4 +5,4 @@ // Copyright © 2024 Gini GmbH. All rights reserved. // -public let GiniHealthAPILibraryVersion = "4.3.1" +public let GiniHealthAPILibraryVersion = "5.0.0" diff --git a/HealthAPILibrary/GiniHealthAPILibrary/Tests/GiniHealthAPILibraryTests/APIResourceTests.swift b/HealthAPILibrary/GiniHealthAPILibrary/Tests/GiniHealthAPILibraryTests/APIResourceTests.swift index 76e51aa8a..27109b17e 100644 --- a/HealthAPILibrary/GiniHealthAPILibrary/Tests/GiniHealthAPILibraryTests/APIResourceTests.swift +++ b/HealthAPILibrary/GiniHealthAPILibrary/Tests/GiniHealthAPILibraryTests/APIResourceTests.swift @@ -67,7 +67,7 @@ final class APIResourceTests: XCTestCase { func testDocumentCreation() { let resource = APIResource<[Document]>(method: .createDocument(fileName: "invoice.jpg", docType: .invoice, - mimeSubType: "jpeg", + mimeSubType: .jpeg, documentType: .partial(Data(count: 0))), apiDomain: .default, apiVersion: versionAPI, @@ -80,7 +80,7 @@ final class APIResourceTests: XCTestCase { func testDocumentCreationWithoutFilename() { let resource = APIResource<[Document]>(method: .createDocument(fileName: nil, docType: .invoice, - mimeSubType: "jpeg", + mimeSubType: .jpeg, documentType: .partial(Data(count: 0))), apiDomain: .default, apiVersion: versionAPI, @@ -93,7 +93,7 @@ final class APIResourceTests: XCTestCase { func testDocumentCreationWithoutDoctype() { let resource = APIResource<[Document]>(method: .createDocument(fileName: "invoice.jpg", docType: nil, - mimeSubType: "jpeg", + mimeSubType: .jpeg, documentType: .partial(Data(count: 0))), apiDomain: .default, apiVersion: versionAPI, @@ -106,7 +106,7 @@ final class APIResourceTests: XCTestCase { func testDocumentCreationWithoutQueryParameters() { let resource = APIResource<[Document]>(method: .createDocument(fileName: nil, docType: nil, - mimeSubType: "jpeg", + mimeSubType: .jpeg, documentType: .partial(Data(count: 0))), apiDomain: .default, apiVersion: versionAPI, @@ -119,7 +119,7 @@ final class APIResourceTests: XCTestCase { func testDocumentCreationContentTypeV3() { let resource = APIResource<[Document]>(method: .createDocument(fileName: nil, docType: nil, - mimeSubType: "jpeg", + mimeSubType: .jpeg, documentType: nil), apiDomain: .default, apiVersion: versionAPI, @@ -131,7 +131,7 @@ final class APIResourceTests: XCTestCase { func testDocumentCreationContentTypeV3Partial() { let resource = APIResource<[Document]>(method: .createDocument(fileName: nil, docType: nil, - mimeSubType: "jpeg", + mimeSubType: .jpeg, documentType: .partial(Data(count: 0))), apiDomain: .default, apiVersion: versionAPI, @@ -144,7 +144,7 @@ final class APIResourceTests: XCTestCase { let compositeDocumentInfo = CompositeDocumentInfo(partialDocuments: []) let resource = APIResource<[Document]>(method: .createDocument(fileName: nil, docType: nil, - mimeSubType: "jpeg", + mimeSubType: .jpeg, documentType: .composite(compositeDocumentInfo)), apiDomain: .default, apiVersion: versionAPI, diff --git a/HealthAPILibrary/GiniHealthAPILibrary/Tests/GiniHealthAPILibraryTests/PaymentServiceTests.swift b/HealthAPILibrary/GiniHealthAPILibrary/Tests/GiniHealthAPILibraryTests/PaymentServiceTests.swift index d48c6b97d..9817959c3 100644 --- a/HealthAPILibrary/GiniHealthAPILibrary/Tests/GiniHealthAPILibraryTests/PaymentServiceTests.swift +++ b/HealthAPILibrary/GiniHealthAPILibrary/Tests/GiniHealthAPILibraryTests/PaymentServiceTests.swift @@ -77,8 +77,8 @@ class PaymentServiceTests: XCTestCase { paymentService.paymentRequest(id:SessionManagerMock.paymentRequestId){ result in switch result { case .success(let request): - let requestID = String(request.links?.linksSelf.split(separator: "/").last ?? "") - XCTAssertEqual(requestID, + let requestId = String(request.links?.linksSelf.split(separator: "/").last ?? "") + XCTAssertEqual(requestId, SessionManagerMock.paymentRequestId, "payment request ids should match") expect.fulfill() @@ -108,8 +108,8 @@ class PaymentServiceTests: XCTestCase { paymentService.payment(id: "118edf41-102a-4b40-8753-df2f0634cb86"){ result in switch result { case .success(let payment): - let requestID = String(payment.links?.paymentRequest?.split(separator: "/").last ?? "") - XCTAssertEqual(requestID, + let requestId = String(payment.links?.paymentRequest?.split(separator: "/").last ?? "") + XCTAssertEqual(requestId, SessionManagerMock.paymentID, "payment request ids should match") expect.fulfill() diff --git a/HealthAPILibrary/GiniHealthAPILibraryExample/GiniHealthAPILibraryExample.xcodeproj/project.pbxproj b/HealthAPILibrary/GiniHealthAPILibraryExample/GiniHealthAPILibraryExample.xcodeproj/project.pbxproj index 4c6be6929..d4b93ba6c 100644 --- a/HealthAPILibrary/GiniHealthAPILibraryExample/GiniHealthAPILibraryExample.xcodeproj/project.pbxproj +++ b/HealthAPILibrary/GiniHealthAPILibraryExample/GiniHealthAPILibraryExample.xcodeproj/project.pbxproj @@ -7,8 +7,8 @@ objects = { /* Begin PBXBuildFile section */ - F466A5D9270F26C300FB1364 /* GiniHealthAPILibraryPinning in Frameworks */ = {isa = PBXBuildFile; productRef = F466A5D8270F26C300FB1364 /* GiniHealthAPILibraryPinning */; }; - F466A5DC270F27B300FB1364 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F466A5DB270F27B300FB1364 /* AuthenticationServices.framework */; }; + 3A54ED2D2C917D5500EB5EED /* GiniHealthAPILibrary in Resources */ = {isa = PBXBuildFile; fileRef = 3A54ED2C2C917D5500EB5EED /* GiniHealthAPILibrary */; }; + 3AC048EE2C9061A5007C81FE /* GiniHealthAPILibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 3AC048ED2C9061A5007C81FE /* GiniHealthAPILibrary */; }; F46DE04C2705FC21002F7420 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46DE04B2705FC21002F7420 /* AppDelegate.swift */; }; F46DE04E2705FC21002F7420 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46DE04D2705FC21002F7420 /* SceneDelegate.swift */; }; F46DE0502705FC21002F7420 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46DE04F2705FC21002F7420 /* ViewController.swift */; }; @@ -30,7 +30,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - F466A5D7270F26B200FB1364 /* GiniHealthAPILibraryPinning */ = {isa = PBXFileReference; lastKnownFileType = folder; name = GiniHealthAPILibraryPinning; path = ../GiniHealthAPILibraryPinning; sourceTree = ""; }; + 3A54ED2C2C917D5500EB5EED /* GiniHealthAPILibrary */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = GiniHealthAPILibrary; path = ../GiniHealthAPILibrary; sourceTree = ""; }; F466A5DA270F27B300FB1364 /* HealthAPILibraryExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = HealthAPILibraryExample.entitlements; sourceTree = ""; }; F466A5DB270F27B300FB1364 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; }; F46DE0482705FC21002F7420 /* GiniHealthAPILibraryExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GiniHealthAPILibraryExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -51,8 +51,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - F466A5DC270F27B300FB1364 /* AuthenticationServices.framework in Frameworks */, - F466A5D9270F26C300FB1364 /* GiniHealthAPILibraryPinning in Frameworks */, + 3AC048EE2C9061A5007C81FE /* GiniHealthAPILibrary in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -66,10 +65,18 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 3A54ED2B2C917D2F00EB5EED /* Packages */ = { + isa = PBXGroup; + children = ( + 3A54ED2C2C917D5500EB5EED /* GiniHealthAPILibrary */, + ); + name = Packages; + sourceTree = ""; + }; F46DE03F2705FC21002F7420 = { isa = PBXGroup; children = ( - F46DE05F2705FCAC002F7420 /* Packages */, + 3A54ED2B2C917D2F00EB5EED /* Packages */, F46DE04A2705FC21002F7420 /* GiniHealthAPILibraryExample */, F4FD486F270F1F7D007F1EFA /* GiniHealthAPILibraryExampleTests */, F46DE0492705FC21002F7420 /* Products */, @@ -101,14 +108,6 @@ path = GiniHealthAPILibraryExample; sourceTree = ""; }; - F46DE05F2705FCAC002F7420 /* Packages */ = { - isa = PBXGroup; - children = ( - F466A5D7270F26B200FB1364 /* GiniHealthAPILibraryPinning */, - ); - name = Packages; - sourceTree = ""; - }; F46DE0622705FEF4002F7420 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -143,7 +142,7 @@ ); name = GiniHealthAPILibraryExample; packageProductDependencies = ( - F466A5D8270F26C300FB1364 /* GiniHealthAPILibraryPinning */, + 3AC048ED2C9061A5007C81FE /* GiniHealthAPILibrary */, ); productName = HealthAPILibraryExample; productReference = F46DE0482705FC21002F7420 /* GiniHealthAPILibraryExample.app */; @@ -210,6 +209,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3A54ED2D2C917D5500EB5EED /* GiniHealthAPILibrary in Resources */, F46DE0582705FC22002F7420 /* LaunchScreen.storyboard in Resources */, F46DE0552705FC22002F7420 /* Assets.xcassets in Resources */, F46DE0532705FC21002F7420 /* Main.storyboard in Resources */, @@ -544,9 +544,9 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ - F466A5D8270F26C300FB1364 /* GiniHealthAPILibraryPinning */ = { + 3AC048ED2C9061A5007C81FE /* GiniHealthAPILibrary */ = { isa = XCSwiftPackageProductDependency; - productName = GiniHealthAPILibraryPinning; + productName = GiniHealthAPILibrary; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/HealthAPILibrary/GiniHealthAPILibraryExample/GiniHealthAPILibraryExample/ViewController.swift b/HealthAPILibrary/GiniHealthAPILibraryExample/GiniHealthAPILibraryExample/ViewController.swift index f99440623..b5d56d63f 100644 --- a/HealthAPILibrary/GiniHealthAPILibraryExample/GiniHealthAPILibraryExample/ViewController.swift +++ b/HealthAPILibrary/GiniHealthAPILibraryExample/GiniHealthAPILibraryExample/ViewController.swift @@ -7,7 +7,6 @@ import UIKit import GiniHealthAPILibrary -import GiniHealthAPILibraryPinning class ViewController: UIViewController { override func viewDidLoad() { diff --git a/HealthAPILibrary/GiniHealthAPILibraryPinning/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/HealthAPILibrary/GiniHealthAPILibraryPinning/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/HealthAPILibrary/GiniHealthAPILibraryPinning/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/HealthAPILibrary/GiniHealthAPILibraryPinning/GiniHealth_Logo.png b/HealthAPILibrary/GiniHealthAPILibraryPinning/GiniHealth_Logo.png deleted file mode 100644 index 9c9658294..000000000 Binary files a/HealthAPILibrary/GiniHealthAPILibraryPinning/GiniHealth_Logo.png and /dev/null differ diff --git a/HealthAPILibrary/GiniHealthAPILibraryPinning/LICENSE b/HealthAPILibrary/GiniHealthAPILibraryPinning/LICENSE deleted file mode 100644 index 7aac2489b..000000000 --- a/HealthAPILibrary/GiniHealthAPILibraryPinning/LICENSE +++ /dev/null @@ -1,12 +0,0 @@ -Copyright (c) 2019-2021, Gini GmbH -All rights reserved. - -The Gini Health API Library Pinning is licensed through Gini GmbH ("Gini") and may not be -used, altered or copied in any way without explicit permission by Gini. The -terms of usage are defined in a separate usage agreement between Gini and the -licensee, where the licensee can gain access to a non-exclusive, -non-transferable usage right which is restricted for the time of a contractual -relationship between Gini and the licensee. - -For license related inquiries contact Gini via the email address -technical-support@gini.net. diff --git a/HealthAPILibrary/GiniHealthAPILibraryPinning/Package-release.swift b/HealthAPILibrary/GiniHealthAPILibraryPinning/Package-release.swift deleted file mode 100644 index 12529f6ab..000000000 --- a/HealthAPILibrary/GiniHealthAPILibraryPinning/Package-release.swift +++ /dev/null @@ -1,32 +0,0 @@ -// swift-tools-version:5.5 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let package = Package( - name: "GiniHealthAPILibraryPinning", - platforms: [.iOS(.v12)], - products: [ - // Products define the executables and libraries a package produces, and make them visible to other packages. - .library( - name: "GiniHealthAPILibraryPinning", - targets: ["GiniHealthAPILibraryPinning"]), - ], - dependencies: [ - // Dependencies declare other packages that this package depends on. - // .package(url: /* package url */, from: "1.0.0"), - - .package(name: "GiniHealthAPILibrary", url: "https://github.com/gini/health-api-library-ios.git", .exact("4.3.1")) - ], - targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. - .target( - name: "GiniHealthAPILibraryPinning", - dependencies: ["GiniHealthAPILibrary"]), - - .testTarget( - name: "GiniHealthAPILibraryPinningTests", - dependencies: ["GiniHealthAPILibraryPinning"]), - ] -) diff --git a/HealthAPILibrary/GiniHealthAPILibraryPinning/README.md b/HealthAPILibrary/GiniHealthAPILibraryPinning/README.md deleted file mode 100644 index cd5900500..000000000 --- a/HealthAPILibrary/GiniHealthAPILibraryPinning/README.md +++ /dev/null @@ -1,31 +0,0 @@ -

- -

- -# Gini Health API Library Pinning for iOS - -[![Platform](https://img.shields.io/badge/platform-iOS-lightgrey.svg)]() -[![Swift version](https://img.shields.io/badge/swift-5.0-orange.svg)]() -[![Swift package manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange?style=flat-square)]() - -The Gini Health API Library Pinning provides ways to interact with the Gini Health API and therefore, adds the possiblity to scan documents and extract information from them. The library supports certificate pinning. - -## Documentation - -Further documentation with information about how install and integrate it can be found in our [website](https://developer.gini.net/gini-mobile-ios/GiniHealthAPILibrary/index.html). - -## Requirements - -- iOS 12+ -- Xcode 12+ - -## Author - -Gini GmbH, hello@gini.net - -## License - -The Gini Health API Library Pinning for iOS is licensed under a Private License. See [the license](https://developer.gini.net/gini-mobile-ios/GiniHealthAPILibrary/license.html) for more info. - -> ⚠️ **Important:** Always make sure to ship all license notices and permissions with your application. - diff --git a/HealthAPILibrary/GiniHealthAPILibraryPinning/Sources/GiniHealthAPILibraryPinning/GiniHealthAPI+Pinning.swift b/HealthAPILibrary/GiniHealthAPILibraryPinning/Sources/GiniHealthAPILibraryPinning/GiniHealthAPI+Pinning.swift deleted file mode 100644 index 143c1cacd..000000000 --- a/HealthAPILibrary/GiniHealthAPILibraryPinning/Sources/GiniHealthAPILibraryPinning/GiniHealthAPI+Pinning.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// GiniHealthAPI+Pinning.swift -// GiniHealthAPI -// -// Created by Enrique del Pozo Gómez on 1/21/18. -// - -import GiniHealthAPILibrary -import Foundation - -public extension GiniHealthAPI.Builder { - - /** - * Creates a Gini Health API Library with certificate pinning configuration. - * - * - Parameter client: The Gini Health API client credentials - * - Parameter api: The Gini Health API that the library interacts with. `APIDomain.default` by default - * - Parameter userApi: The Gini User API that the library interacts with. `UserDomain.default` by default - * - Parameter pinningConfig: Configuration for certificate pinning. Format ["PinnedDomains" : ["PublicKeyHashes"]] - * - Parameter logLevel: The log level. `LogLevel.none` by default. - */ - init(client: Client, - api: APIDomain = .default, - userApi: UserDomain = .default, - pinningConfig: [String: [String]], - logLevel: LogLevel = .none) { - self.init(client: client, - api: api, - userApi: userApi, - logLevel: logLevel, - sessionDelegate: GiniSessionDelegate(pinningConfig: pinningConfig)) - } - - /** - * Creates a Gini Health API Library to be used with a transparent proxy and a custom api access token source and certificate pinning configuration. - * - * - Parameter customApiDomain: A custom api domain string. - * - Parameter alternativeTokenSource: A protocol for using custom api access token - * - Parameter pinningConfig: Configuration for certificate pinning. Format ["PinnedDomains" : ["PublicKeyHashes"]] - * - Parameter logLevel: The log level. `LogLevel.none` by default. - */ - init(customApiDomain: String, - alternativeTokenSource: AlternativeTokenSource, - apiVersion: Int, - pinningConfig: [String: [String]], - logLevel: LogLevel = .none) { - self.init(customApiDomain: customApiDomain, - alternativeTokenSource: alternativeTokenSource, - apiVersion: apiVersion, - logLevel: logLevel, - sessionDelegate: GiniSessionDelegate(pinningConfig: pinningConfig)) - } -} diff --git a/HealthAPILibrary/GiniHealthAPILibraryPinning/Sources/GiniHealthAPILibraryPinning/GiniHealthAPILibraryPinningVersion.swift b/HealthAPILibrary/GiniHealthAPILibraryPinning/Sources/GiniHealthAPILibraryPinning/GiniHealthAPILibraryPinningVersion.swift deleted file mode 100644 index 9930acbb7..000000000 --- a/HealthAPILibrary/GiniHealthAPILibraryPinning/Sources/GiniHealthAPILibraryPinning/GiniHealthAPILibraryPinningVersion.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// GiniHealthAPILibraryPinningVersion.swift -// -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -public let GiniHealthAPILibraryPinningVersion = "4.3.1" diff --git a/HealthAPILibrary/GiniHealthAPILibraryPinning/Tests/GiniHealthAPILibraryPinningTests/GiniHealthAPILibraryPinningTests.swift b/HealthAPILibrary/GiniHealthAPILibraryPinning/Tests/GiniHealthAPILibraryPinningTests/GiniHealthAPILibraryPinningTests.swift deleted file mode 100644 index 962880fc6..000000000 --- a/HealthAPILibrary/GiniHealthAPILibraryPinning/Tests/GiniHealthAPILibraryPinningTests/GiniHealthAPILibraryPinningTests.swift +++ /dev/null @@ -1,7 +0,0 @@ -import XCTest -@testable import GiniHealthAPILibraryPinning - -final class GiniHealthAPILibraryPinningTests: XCTestCase { - func testExample() throws { - } -} diff --git a/HealthAPILibrary/GiniHealthAPILibraryPinningExample/GiniHealthAPILibraryPinningExample.xcodeproj/project.pbxproj b/HealthAPILibrary/GiniHealthAPILibraryPinningExample/GiniHealthAPILibraryPinningExample.xcodeproj/project.pbxproj deleted file mode 100644 index b712b4f4b..000000000 --- a/HealthAPILibrary/GiniHealthAPILibraryPinningExample/GiniHealthAPILibraryPinningExample.xcodeproj/project.pbxproj +++ /dev/null @@ -1,568 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 55; - objects = { - -/* Begin PBXBuildFile section */ - F466A5D9270F26C300FB1364 /* GiniHealthAPILibraryPinning in Frameworks */ = {isa = PBXBuildFile; productRef = F466A5D8270F26C300FB1364 /* GiniHealthAPILibraryPinning */; }; - F466A5DC270F27B300FB1364 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F466A5DB270F27B300FB1364 /* AuthenticationServices.framework */; }; - F46DE04C2705FC21002F7420 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46DE04B2705FC21002F7420 /* AppDelegate.swift */; }; - F46DE04E2705FC21002F7420 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46DE04D2705FC21002F7420 /* SceneDelegate.swift */; }; - F46DE0502705FC21002F7420 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46DE04F2705FC21002F7420 /* ViewController.swift */; }; - F46DE0532705FC21002F7420 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F46DE0512705FC21002F7420 /* Main.storyboard */; }; - F46DE0552705FC22002F7420 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F46DE0542705FC22002F7420 /* Assets.xcassets */; }; - F46DE0582705FC22002F7420 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F46DE0562705FC22002F7420 /* LaunchScreen.storyboard */; }; - F4A183D02833E90200873F5F /* IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A183CE2833E90200873F5F /* IntegrationTests.swift */; }; - F4A183D12833E90200873F5F /* GiniHealthAPILibraryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A183CF2833E90200873F5F /* GiniHealthAPILibraryTests.swift */; }; - F4A183D42833F40300873F5F /* GiniHealthAPILibraryPinningIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A183D32833F40300873F5F /* GiniHealthAPILibraryPinningIntegrationTests.swift */; }; - F4A183DD2835279300873F5F /* GiniHealthAPILibraryPinningIntegrationWrongCertificatesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A183DC2835279300873F5F /* GiniHealthAPILibraryPinningIntegrationWrongCertificatesTests.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - F4FD4872270F1F7D007F1EFA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = F46DE0402705FC21002F7420 /* Project object */; - proxyType = 1; - remoteGlobalIDString = F46DE0472705FC21002F7420; - remoteInfo = HealthAPILibraryExample; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXFileReference section */ - F466A5D7270F26B200FB1364 /* GiniHealthAPILibraryPinning */ = {isa = PBXFileReference; lastKnownFileType = folder; name = GiniHealthAPILibraryPinning; path = ../GiniHealthAPILibraryPinning; sourceTree = ""; }; - F466A5DB270F27B300FB1364 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; }; - F46DE0482705FC21002F7420 /* GiniHealthAPILibraryPinningExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GiniHealthAPILibraryPinningExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; - F46DE04B2705FC21002F7420 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - F46DE04D2705FC21002F7420 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - F46DE04F2705FC21002F7420 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - F46DE0522705FC21002F7420 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - F46DE0542705FC22002F7420 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - F46DE0572705FC22002F7420 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - F46DE0592705FC22002F7420 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - F4A183CC2833C06E00873F5F /* GiniHealthAPILibraryPinningExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GiniHealthAPILibraryPinningExample.entitlements; sourceTree = ""; }; - F4A183CE2833E90200873F5F /* IntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntegrationTests.swift; sourceTree = ""; }; - F4A183CF2833E90200873F5F /* GiniHealthAPILibraryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GiniHealthAPILibraryTests.swift; sourceTree = ""; }; - F4A183D22833EB2C00873F5F /* GiniHealthAPILibraryPinning */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = GiniHealthAPILibraryPinning; path = ../GiniHealthAPILibraryPinning; sourceTree = ""; }; - F4A183D32833F40300873F5F /* GiniHealthAPILibraryPinningIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiniHealthAPILibraryPinningIntegrationTests.swift; sourceTree = ""; }; - F4A183DC2835279300873F5F /* GiniHealthAPILibraryPinningIntegrationWrongCertificatesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiniHealthAPILibraryPinningIntegrationWrongCertificatesTests.swift; sourceTree = ""; }; - F4FD486E270F1F7D007F1EFA /* GiniHealthAPILibraryPinningExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GiniHealthAPILibraryPinningExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - F46DE0452705FC21002F7420 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - F466A5DC270F27B300FB1364 /* AuthenticationServices.framework in Frameworks */, - F466A5D9270F26C300FB1364 /* GiniHealthAPILibraryPinning in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F4FD486B270F1F7D007F1EFA /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - F46DE03F2705FC21002F7420 = { - isa = PBXGroup; - children = ( - F4A183CD2833E90200873F5F /* GiniHealthAPILibraryPinningExampleTests */, - F4A183CC2833C06E00873F5F /* GiniHealthAPILibraryPinningExample.entitlements */, - F46DE05F2705FCAC002F7420 /* Packages */, - F46DE04A2705FC21002F7420 /* GiniHealthAPILibraryExample */, - F46DE0492705FC21002F7420 /* Products */, - F46DE0622705FEF4002F7420 /* Frameworks */, - ); - sourceTree = ""; - }; - F46DE0492705FC21002F7420 /* Products */ = { - isa = PBXGroup; - children = ( - F46DE0482705FC21002F7420 /* GiniHealthAPILibraryPinningExample.app */, - F4FD486E270F1F7D007F1EFA /* GiniHealthAPILibraryPinningExampleTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - F46DE04A2705FC21002F7420 /* GiniHealthAPILibraryExample */ = { - isa = PBXGroup; - children = ( - F46DE04B2705FC21002F7420 /* AppDelegate.swift */, - F46DE04D2705FC21002F7420 /* SceneDelegate.swift */, - F46DE04F2705FC21002F7420 /* ViewController.swift */, - F46DE0512705FC21002F7420 /* Main.storyboard */, - F46DE0542705FC22002F7420 /* Assets.xcassets */, - F46DE0562705FC22002F7420 /* LaunchScreen.storyboard */, - F46DE0592705FC22002F7420 /* Info.plist */, - ); - path = GiniHealthAPILibraryExample; - sourceTree = ""; - }; - F46DE05F2705FCAC002F7420 /* Packages */ = { - isa = PBXGroup; - children = ( - F466A5D7270F26B200FB1364 /* GiniHealthAPILibraryPinning */, - F4A183D22833EB2C00873F5F /* GiniHealthAPILibraryPinning */, - ); - name = Packages; - sourceTree = ""; - }; - F46DE0622705FEF4002F7420 /* Frameworks */ = { - isa = PBXGroup; - children = ( - F466A5DB270F27B300FB1364 /* AuthenticationServices.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - F4A183CD2833E90200873F5F /* GiniHealthAPILibraryPinningExampleTests */ = { - isa = PBXGroup; - children = ( - F4A183CE2833E90200873F5F /* IntegrationTests.swift */, - F4A183CF2833E90200873F5F /* GiniHealthAPILibraryTests.swift */, - F4A183D32833F40300873F5F /* GiniHealthAPILibraryPinningIntegrationTests.swift */, - F4A183DC2835279300873F5F /* GiniHealthAPILibraryPinningIntegrationWrongCertificatesTests.swift */, - ); - path = GiniHealthAPILibraryPinningExampleTests; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - F46DE0472705FC21002F7420 /* GiniHealthAPILibraryPinningExample */ = { - isa = PBXNativeTarget; - buildConfigurationList = F46DE05C2705FC22002F7420 /* Build configuration list for PBXNativeTarget "GiniHealthAPILibraryPinningExample" */; - buildPhases = ( - F46DE0442705FC21002F7420 /* Sources */, - F46DE0452705FC21002F7420 /* Frameworks */, - F46DE0462705FC21002F7420 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = GiniHealthAPILibraryPinningExample; - packageProductDependencies = ( - F466A5D8270F26C300FB1364 /* GiniHealthAPILibraryPinning */, - ); - productName = HealthAPILibraryExample; - productReference = F46DE0482705FC21002F7420 /* GiniHealthAPILibraryPinningExample.app */; - productType = "com.apple.product-type.application"; - }; - F4FD486D270F1F7D007F1EFA /* GiniHealthAPILibraryPinningExampleTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = F4FD4876270F1F7D007F1EFA /* Build configuration list for PBXNativeTarget "GiniHealthAPILibraryPinningExampleTests" */; - buildPhases = ( - F4FD486A270F1F7D007F1EFA /* Sources */, - F4FD486B270F1F7D007F1EFA /* Frameworks */, - F4FD486C270F1F7D007F1EFA /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - F4FD4873270F1F7D007F1EFA /* PBXTargetDependency */, - ); - name = GiniHealthAPILibraryPinningExampleTests; - productName = HealthAPILibraryExampleTests; - productReference = F4FD486E270F1F7D007F1EFA /* GiniHealthAPILibraryPinningExampleTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - F46DE0402705FC21002F7420 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1300; - LastUpgradeCheck = 1300; - TargetAttributes = { - F46DE0472705FC21002F7420 = { - CreatedOnToolsVersion = 13.0; - }; - F4FD486D270F1F7D007F1EFA = { - CreatedOnToolsVersion = 13.0; - TestTargetID = F46DE0472705FC21002F7420; - }; - }; - }; - buildConfigurationList = F46DE0432705FC21002F7420 /* Build configuration list for PBXProject "GiniHealthAPILibraryPinningExample" */; - compatibilityVersion = "Xcode 13.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = F46DE03F2705FC21002F7420; - productRefGroup = F46DE0492705FC21002F7420 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - F46DE0472705FC21002F7420 /* GiniHealthAPILibraryPinningExample */, - F4FD486D270F1F7D007F1EFA /* GiniHealthAPILibraryPinningExampleTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - F46DE0462705FC21002F7420 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F46DE0582705FC22002F7420 /* LaunchScreen.storyboard in Resources */, - F46DE0552705FC22002F7420 /* Assets.xcassets in Resources */, - F46DE0532705FC21002F7420 /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F4FD486C270F1F7D007F1EFA /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - F46DE0442705FC21002F7420 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F46DE0502705FC21002F7420 /* ViewController.swift in Sources */, - F46DE04C2705FC21002F7420 /* AppDelegate.swift in Sources */, - F46DE04E2705FC21002F7420 /* SceneDelegate.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F4FD486A270F1F7D007F1EFA /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F4A183D02833E90200873F5F /* IntegrationTests.swift in Sources */, - F4A183D42833F40300873F5F /* GiniHealthAPILibraryPinningIntegrationTests.swift in Sources */, - F4A183D12833E90200873F5F /* GiniHealthAPILibraryTests.swift in Sources */, - F4A183DD2835279300873F5F /* GiniHealthAPILibraryPinningIntegrationWrongCertificatesTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - F4FD4873270F1F7D007F1EFA /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = F46DE0472705FC21002F7420 /* GiniHealthAPILibraryPinningExample */; - targetProxy = F4FD4872270F1F7D007F1EFA /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - F46DE0512705FC21002F7420 /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - F46DE0522705FC21002F7420 /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - F46DE0562705FC22002F7420 /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - F46DE0572705FC22002F7420 /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - F46DE05A2705FC22002F7420 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.6; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - F46DE05B2705FC22002F7420 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.6; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - F46DE05D2705FC22002F7420 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = GiniHealthAPILibraryPinningExample.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = JA825X8F7Z; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = GiniHealthAPILibraryExample/Info.plist; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.gini.healthapilibrary.pinning.example; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - F46DE05E2705FC22002F7420 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = GiniHealthAPILibraryPinningExample.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = JA825X8F7Z; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = GiniHealthAPILibraryExample/Info.plist; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.gini.healthapilibrary.pinning.example; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - F4FD4874270F1F7D007F1EFA /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = JA825X8F7Z; - GENERATE_INFOPLIST_FILE = YES; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.gini.HealthAPILibraryExampleTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GiniHealthAPILibraryPinningExample.app/GiniHealthAPILibraryPinningExample"; - }; - name = Debug; - }; - F4FD4875270F1F7D007F1EFA /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = JA825X8F7Z; - GENERATE_INFOPLIST_FILE = YES; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.gini.HealthAPILibraryExampleTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GiniHealthAPILibraryPinningExample.app/GiniHealthAPILibraryPinningExample"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - F46DE0432705FC21002F7420 /* Build configuration list for PBXProject "GiniHealthAPILibraryPinningExample" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F46DE05A2705FC22002F7420 /* Debug */, - F46DE05B2705FC22002F7420 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - F46DE05C2705FC22002F7420 /* Build configuration list for PBXNativeTarget "GiniHealthAPILibraryPinningExample" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F46DE05D2705FC22002F7420 /* Debug */, - F46DE05E2705FC22002F7420 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - F4FD4876270F1F7D007F1EFA /* Build configuration list for PBXNativeTarget "GiniHealthAPILibraryPinningExampleTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F4FD4874270F1F7D007F1EFA /* Debug */, - F4FD4875270F1F7D007F1EFA /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - -/* Begin XCSwiftPackageProductDependency section */ - F466A5D8270F26C300FB1364 /* GiniHealthAPILibraryPinning */ = { - isa = XCSwiftPackageProductDependency; - productName = GiniHealthAPILibraryPinning; - }; -/* End XCSwiftPackageProductDependency section */ - }; - rootObject = F46DE0402705FC21002F7420 /* Project object */; -} diff --git a/HealthAPILibrary/GiniHealthAPILibraryPinningExample/GiniHealthAPILibraryPinningExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/HealthAPILibrary/GiniHealthAPILibraryPinningExample/GiniHealthAPILibraryPinningExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a62..000000000 --- a/HealthAPILibrary/GiniHealthAPILibraryPinningExample/GiniHealthAPILibraryPinningExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/HealthAPILibrary/GiniHealthAPILibraryPinningExample/GiniHealthAPILibraryPinningExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/HealthAPILibrary/GiniHealthAPILibraryPinningExample/GiniHealthAPILibraryPinningExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/HealthAPILibrary/GiniHealthAPILibraryPinningExample/GiniHealthAPILibraryPinningExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/HealthAPILibrary/GiniHealthAPILibraryPinningExample/GiniHealthAPILibraryPinningExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/HealthAPILibrary/GiniHealthAPILibraryPinningExample/GiniHealthAPILibraryPinningExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 28cb07b45..000000000 --- a/HealthAPILibrary/GiniHealthAPILibraryPinningExample/GiniHealthAPILibraryPinningExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,16 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "TrustKit", - "repositoryURL": "https://github.com/datatheorem/TrustKit.git", - "state": { - "branch": null, - "revision": "3e98aeb36aff65067cdae55bca7be7f1157deffb", - "version": "2.0.0" - } - } - ] - }, - "version": 1 -} diff --git a/HealthAPILibrary/GiniHealthAPILibraryPinningExample/GiniHealthAPILibraryPinningExample.xcodeproj/xcshareddata/xcschemes/GiniHealthAPILibraryPinningExample.xcscheme b/HealthAPILibrary/GiniHealthAPILibraryPinningExample/GiniHealthAPILibraryPinningExample.xcodeproj/xcshareddata/xcschemes/GiniHealthAPILibraryPinningExample.xcscheme deleted file mode 100644 index dc0394a1e..000000000 --- a/HealthAPILibrary/GiniHealthAPILibraryPinningExample/GiniHealthAPILibraryPinningExample.xcodeproj/xcshareddata/xcschemes/GiniHealthAPILibraryPinningExample.xcscheme +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HealthAPILibrary/GiniHealthAPILibraryPinningExample/GiniHealthAPILibraryPinningExample.xcodeproj/xcshareddata/xcschemes/GiniHealthAPILibraryPinningExampleTests.xcscheme b/HealthAPILibrary/GiniHealthAPILibraryPinningExample/GiniHealthAPILibraryPinningExample.xcodeproj/xcshareddata/xcschemes/GiniHealthAPILibraryPinningExampleTests.xcscheme deleted file mode 100644 index 37e80cd08..000000000 --- a/HealthAPILibrary/GiniHealthAPILibraryPinningExample/GiniHealthAPILibraryPinningExample.xcodeproj/xcshareddata/xcschemes/GiniHealthAPILibraryPinningExampleTests.xcscheme +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HealthSDK/GiniHealthSDK/Documentation/jazzy-theme/assets/img/Integration guide/BankSelectionBottomSheet.png b/HealthSDK/GiniHealthSDK/Documentation/jazzy-theme/assets/img/Integration guide/BankSelectionBottomSheet.png index 50e5d0f32..4b884ebf6 100644 Binary files a/HealthSDK/GiniHealthSDK/Documentation/jazzy-theme/assets/img/Integration guide/BankSelectionBottomSheet.png and b/HealthSDK/GiniHealthSDK/Documentation/jazzy-theme/assets/img/Integration guide/BankSelectionBottomSheet.png differ diff --git a/HealthSDK/GiniHealthSDK/Documentation/jazzy-theme/assets/img/Integration guide/InvoiceListWithPaymentComponent.png b/HealthSDK/GiniHealthSDK/Documentation/jazzy-theme/assets/img/Integration guide/InvoiceListWithPaymentComponent.png index 4fc0f82dc..cf19bcd21 100644 Binary files a/HealthSDK/GiniHealthSDK/Documentation/jazzy-theme/assets/img/Integration guide/InvoiceListWithPaymentComponent.png and b/HealthSDK/GiniHealthSDK/Documentation/jazzy-theme/assets/img/Integration guide/InvoiceListWithPaymentComponent.png differ diff --git a/HealthSDK/GiniHealthSDK/Documentation/jazzy-theme/assets/img/Integration guide/PaymentFeatureInformationScreen.png b/HealthSDK/GiniHealthSDK/Documentation/jazzy-theme/assets/img/Integration guide/PaymentFeatureInformationScreen.png index e8074e9d9..e50154de6 100644 Binary files a/HealthSDK/GiniHealthSDK/Documentation/jazzy-theme/assets/img/Integration guide/PaymentFeatureInformationScreen.png and b/HealthSDK/GiniHealthSDK/Documentation/jazzy-theme/assets/img/Integration guide/PaymentFeatureInformationScreen.png differ diff --git a/HealthSDK/GiniHealthSDK/Documentation/jazzy-theme/assets/img/Integration guide/PaymentReviewScreen.png b/HealthSDK/GiniHealthSDK/Documentation/jazzy-theme/assets/img/Integration guide/PaymentReviewScreen.png index fd98ca4b1..5e81a179b 100644 Binary files a/HealthSDK/GiniHealthSDK/Documentation/jazzy-theme/assets/img/Integration guide/PaymentReviewScreen.png and b/HealthSDK/GiniHealthSDK/Documentation/jazzy-theme/assets/img/Integration guide/PaymentReviewScreen.png differ diff --git a/HealthSDK/GiniHealthSDK/Documentation/sections/Documentation.md b/HealthSDK/GiniHealthSDK/Documentation/sections/Documentation.md index b62cd7b5a..f4255b87d 100644 --- a/HealthSDK/GiniHealthSDK/Documentation/sections/Documentation.md +++ b/HealthSDK/GiniHealthSDK/Documentation/sections/Documentation.md @@ -15,13 +15,13 @@ The Gini Health API provides an information extraction service for analyzing hea ## Requirements -- iOS 12.0+ +- iOS 13.0+ - Xcode 15.3+ **Note:** In order to have better analysis results it is highly recommended to enable only devices with 8MP camera and flash. These devices would be: -* iPhones with iOS 12 or higher. +* iPhones with iOS 13 or higher. * iPad Pro devices (iPad Air 2 and iPad Mini 4 have 8MP camera but no flash). ## Author diff --git a/HealthSDK/GiniHealthSDK/Documentation/source/Customization guide.md b/HealthSDK/GiniHealthSDK/Documentation/source/Customization guide.md index b95ffb564..f378fcf32 100644 --- a/HealthSDK/GiniHealthSDK/Documentation/source/Customization guide.md +++ b/HealthSDK/GiniHealthSDK/Documentation/source/Customization guide.md @@ -18,7 +18,7 @@ We provide a global color palette `GiniColors.xcassets` which you are free to ov For example, if you want to override Accent01 color you need to create an Accent01.colorset with your wished value in your main bundle. The custom colors are then applied to all screens. -Find the names of the color resources in the color palette (you can also view it in Figma [here](https://www.figma.com/design/wBBjc38iihjxrKMnLfbOD4/iOS-Gini-Health-SDK-4.1-UI-Customisation?node-id=8905-975&t=vNb6FqqGtzIAVdJl-1)). +Find the names of the color resources in the color palette (you can also view it in Figma [here](https://www.figma.com/design/fHf3b3XxE59wymH7gvoMrJ/iOS-Gini-Health-SDK-5.0-UI-Customisation)). ### Images @@ -30,7 +30,7 @@ If you want to override specific SDK images: ### Typography We provide global typography based on text appearance styles from UIFont.TextStyle. -Preview our typography and find the names of the style resources (you can also view it in Figma [here](https://www.figma.com/design/wBBjc38iihjxrKMnLfbOD4/iOS-Gini-Health-SDK-4.1-UI-Customisation?node-id=8906-1104&t=vNb6FqqGtzIAVdJl-1)). +Preview our typography and find the names of the style resources (you can also view it in Figma [here](https://www.figma.com/design/fHf3b3XxE59wymH7gvoMrJ/iOS-Gini-Health-SDK-5.0-UI-Customisation?node-id=12906-11636&t=vzclpYe0B8kEePKJ-4)). In the example below you can see to override a font for `.body1` @@ -44,10 +44,10 @@ health.setConfiguration(configuration) ### Text Text customization is done via overriding of string resources. -For example you would like to customize pay invoice button label in the Payment Component: +For example you would like to customize pay invoice button label in the Payment Review screen: 1. Find a string key for a text that you would like to customize. -For the [Pay the invoice button label](https://www.figma.com/design/wBBjc38iihjxrKMnLfbOD4/iOS-Gini-Health-SDK-4.1-UI-Customisation?node-id=8987-2854&t=vNb6FqqGtzIAVdJl-1) in the Payment Component we use `ginihealth.paymentcomponent.payInvoice.label`. +For the [To the banking app button label](https://www.figma.com/design/fHf3b3XxE59wymH7gvoMrJ/iOS-Gini-Health-SDK-5.0-UI-Customisation?node-id=12909-10929&t=vzclpYe0B8kEePKJ-4) in the Payment Review screen we use `gini.health.paymentcomponent.to.banking.app.label`. 2. Add the string key with a desired value to `Localizable.strings` in your app. ### Supporting dark mode @@ -56,7 +56,7 @@ We support dark mode in our SDK. If you decide to customize the color palette, p ## Payment Component -You can also view the UI customisation guide in Figma [here](https://www.figma.com/design/wBBjc38iihjxrKMnLfbOD4/iOS-Gini-Health-SDK-4.1-UI-Customisation?node-id=8987-2854&t=vNb6FqqGtzIAVdJl-1). +You can also view the UI customisation guide in Figma [here](https://www.figma.com/design/fHf3b3XxE59wymH7gvoMrJ/iOS-Gini-Health-SDK-5.0-UI-Customisation?node-id=12906-23094&t=vzclpYe0B8kEePKJ-4). For configuring the the payment component height use `paymentComponentButtonsHeight` configuration option: @@ -71,46 +71,35 @@ healthSDK.setConfiguration(config) **Note:** To copy text from Figma you need to have a Figma account. If you don't have one, you can create one for free. - + ## Bank Selection Bottom Sheet -You can also view the UI customisation guide in Figma [here](https://www.figma.com/design/wBBjc38iihjxrKMnLfbOD4/iOS-Gini-Health-SDK-4.1-UI-Customisation?node-id=9008-1654&t=vNb6FqqGtzIAVdJl-1). +You can also view the UI customisation guide in Figma [here](https://www.figma.com/design/fHf3b3XxE59wymH7gvoMrJ/iOS-Gini-Health-SDK-5.0-UI-Customisation?node-id=12907-10274&t=vzclpYe0B8kEePKJ-4). **Note:** To copy text from Figma you need to have a Figma account. If you don't have one, you can create one for free. - + ## Payment Feature Info Screen -You can also view the UI customisation guide in Figma [here](https://www.figma.com/design/wBBjc38iihjxrKMnLfbOD4/iOS-Gini-Health-SDK-4.1-UI-Customisation?node-id=9044-1582&t=vNb6FqqGtzIAVdJl-1). +You can also view the UI customisation guide in Figma [here](https://www.figma.com/design/fHf3b3XxE59wymH7gvoMrJ/iOS-Gini-Health-SDK-5.0-UI-Customisation?node-id=12907-11429&t=vzclpYe0B8kEePKJ-4). **Note:** To copy text from Figma you need to have a Figma account. If you don't have one, you can create one for free. - + ## Payment Review screen -You can also view the UI customisation guide in Figma [here](https://www.figma.com/design/wBBjc38iihjxrKMnLfbOD4/iOS-Gini-Health-SDK-4.1-UI-Customisation?node-id=9008-1300&t=vNb6FqqGtzIAVdJl-1). +You can also view the UI customisation guide in Figma [here](https://www.figma.com/design/fHf3b3XxE59wymH7gvoMrJ/iOS-Gini-Health-SDK-5.0-UI-Customisation?node-id=12907-12507&t=vzclpYe0B8kEePKJ-4). **Note:** To copy text from Figma you need to have a Figma account. If you don't have one, you can create one for free. - + > **Note:** > - PaymentReviewViewController contains the following configuration options: -> - paymentReviewStatusBarStyle: Sets the status bar style on the payment review screen. Only if `View controller-based status bar appearance` = `YES` in `Info.plist`. -> - showPaymentReviewCloseButton: If set to true, a floating close button will be shown in the top right corner of the screen. -Default value is false. - -For enabling `showPaymentReviewCloseButton`: - -```swift -let giniConfiguration = GiniHealthConfiguration() -config.showPaymentReviewCloseButton = true -healthSDK.setConfiguration(config) -``` - +> - paymentReviewStatusBarStyle: Sets the status bar style on the payment review screen. Only if `View controller-based status bar appearance` = `YES` in `Info.plist`. \ No newline at end of file diff --git a/HealthSDK/GiniHealthSDK/Documentation/source/Event tracking guide.md b/HealthSDK/GiniHealthSDK/Documentation/source/Event tracking guide.md index ed885a3b2..819c720b9 100644 --- a/HealthSDK/GiniHealthSDK/Documentation/source/Event tracking guide.md +++ b/HealthSDK/GiniHealthSDK/Documentation/source/Event tracking guide.md @@ -35,5 +35,4 @@ Event types are partitioned into different domains according to the screens that | Domain | Event type | Additional info keys | Comment | | --- | --- | --- | --- | | Payment Review Screen | `onToTheBankButtonClicked` |`"paymentProvider"`| User tapped "To the banking app" button from the payment review screen | -| Payment Review Screen | `onCloseButtonClicked` || User tapped "close" button and closed the payment review screen | | Payment Review Screen | `onCloseKeyboardButtonClicked` || User tapped "close" button and keyboard will be hidden from the payment review screen | diff --git a/HealthSDK/GiniHealthSDK/Documentation/source/Installation.md b/HealthSDK/GiniHealthSDK/Documentation/source/Installation.md index 7760cc223..3220c23f2 100644 --- a/HealthSDK/GiniHealthSDK/Documentation/source/Installation.md +++ b/HealthSDK/GiniHealthSDK/Documentation/source/Installation.md @@ -10,13 +10,6 @@ Once you have your Swift package set up, adding `GiniHealthSDK` as a dependency ```swift dependencies: [ - .package(url: "https://github.com/gini/health-sdk-ios.git", .exact("4.3.0")) -] -``` - -In case that you want to use [the certificate pinning](https://www.ssl.com/blogs/what-is-certificate-pinning/#:~:text=Certificate%20pinning%20is%20a%20security,(Transport%20Layer%20Security)%20protocols.) in the library, add `GiniHealthAPILibraryPinning`: -```swift -dependencies: [ - .package(url: "https://github.com/gini/health-sdk-pinning-ios.git", .exact("4.3.0")) + .package(url: "https://github.com/gini/health-sdk-ios.git", .exact("5.0.0")) ] ``` diff --git a/HealthSDK/GiniHealthSDK/Documentation/source/Integration.md b/HealthSDK/GiniHealthSDK/Documentation/source/Integration.md index 6244f5e16..4bc117e20 100644 --- a/HealthSDK/GiniHealthSDK/Documentation/source/Integration.md +++ b/HealthSDK/GiniHealthSDK/Documentation/source/Integration.md @@ -1,7 +1,7 @@ Integration ============================= -The Gini Health SDK for iOS provides all the UI and functionality needed to use the Gini Health API in your app to extract payment and health information from invoices. The payment information can be reviewed and then the invoice can be paid using any available payment provider app (e.g., banking app). +The Gini Health SDK for iOS provides all the UI and functionality needed to use the Gini Health API in your app to extract payment and health information from invoices and from digital payment orders. The payment information can be reviewed and then the invoice/orders can be paid using any available payment provider app (e.g., banking app). The Gini Health API provides an information extraction service for analyzing health invoices. Specifically, it extracts information such as the document sender or the payment relevant information (amount to pay, IBAN, etc.). In addition it also provides a secure channel for sharing payment related information between clients. @@ -17,7 +17,7 @@ You should have received Gini Health API client credentials from us. Please get You can easy initialize `GiniHealthAPI` with the client credentials: ```swift -let apiLib = GiniHealthAPI.Builder(client: client).build() +private lazy var merchant = GiniHealth(id: clientID, secret: clientPassword, domain: clientDomain) ``` If you want to use a transparent proxy with your own authentication you can specify your own domain and add `AlternativeTokenSource` protocol implementation: @@ -45,29 +45,9 @@ private class MyAlternativeTokenSource: AlternativeTokenSource { ## Certificate pinning (optional) -If you want to use _Certificate pinning_, provide metadata for the upload process, you can pass both your public key pinning configuration as follows: +If you want to use _Certificate pinning_, provide metadata for the upload process, you can pass your public key pinning configuration as follows: ```swift - let yourPublicPinningConfig = [ - "health-api.gini.net": [ - // old *.gini.net public key - "cNzbGowA+LNeQ681yMm8ulHxXiGojHE8qAjI+M7bIxU=", - // new *.gini.net public key, active from around June 2020 - "zEVdOCzXU8euGVuMJYPr3DUU/d1CaKevtr0dW0XzZNo=", - ], - "user.gini.net": [ - // old *.gini.net public key - "cNzbGowA+LNeQ681yMm8ulHxXiGojHE8qAjI+M7bIxU=", - // new *.gini.net public key, active from around June 2020 - "zEVdOCzXU8euGVuMJYPr3DUU/d1CaKevtr0dW0XzZNo=", - ], - ] - let apiLib = GiniHealthAPI - .Builder(client: Client(id: "your-id", - secret: "your-secret", - domain: "your-domain"), - api: .default, - pinningConfig: yourPublicPinningConfig) - .build() + private lazy var health = GiniHealth(id: clientID, secret: clientPassword, domain: clientDomain, pinningConfig: ["PinnedDomains" : ["PublicKeyHashes"]]) ``` ## GiniHealth initialization @@ -121,7 +101,7 @@ The method returns success and `true` value if `payment_state` was extracted. ```swift for giniDocument in dataDocuments { dispatchGroup.enter() - self.paymentComponentsController.checkIfDocumentIsPayable(docId: createdDocument.id, completion: { [weak self] result in + self.health.checkIfDocumentIsPayable(docId: createdDocument.id, completion: { [weak self] result in switch result { // ... } @@ -133,26 +113,18 @@ dispatchGroup.notify(queue: .main) { } ``` -## Integrate the Payment component +## Integrate the Payment flow -We provide a custom payment component view to help users pay the invoice/document. +We provide a custom payment flow for the users to pay the invoice/document/digital payment . Please follow the steps below for the payment component integration. -### 1. Create an instance of the `PaymentComponentsController`. +### 1. Create an instance of the `GiniHealth`. ```swift -let paymentComponentsController = PaymentComponentsController(giniHealth: health) + private lazy var health = GiniHealth(id: clientID, secret: clientPassword, domain: clientDomain) + health.paymentDelegate = self // where self is your viewController ``` - -### 2. Load the payment providers - -You will load the list of the payment providers by calling the `loadPaymentProviders` function from the `PaymentComponentsController` and conform to the `PaymentComponentsControllerProtocol`. - -```swift -paymentComponentsController.delegate = self // where self is your viewController -paymentComponentsController.loadPaymentProviders() -``` - +* `paymentDelegate` is a delegate for `PaymentComponentsControllerProtocol` * `PaymentComponentsControllerProtocol` provides information when the `PaymentComponentsController` is loading. You can show/hide an `UIActivityIndicator` based on that. @@ -164,105 +136,24 @@ It should be sufficient to call paymentComponentsController.loadPaymentProviderA > - We effectively handle situations where there are no payment providers available. > - Based on the payment provider's colors, the `UIView` will automatically change its color. -### 3. Show the Payment Component view - -In this step you will show a payment component view and conform to the `PaymentComponentViewProtocol`. - -Depending on the value of `isPayable`, incorporate the corresponding payment component view into your cells using this function: - -```swift -public func paymentView(documentId: String) -> UIView -``` - -> - We suggest placing this `UIView` within a vertical `UIStackView`. Additionally, in the `prepareForReuse()` function of each cell, remove the payment component view if it exists. -> - Furthermore, employing automatic dimension height in the `UITableView` containing the cells is recommended. - -* `PaymentComponentViewProtocol` is the view protocol and provides events handlers when the user tapped on various areas on the payment component view (more information icon, bank/payment provider picker, the pay invoice button and etc.). - -> - Make sure you properly link `PaymentComponentsControllerProtocol` and `PaymentComponentViewProtocol` delegates to get notified. - -## Show PaymentInfoViewController - -The `PaymentInfoViewController` displays information and an FAQ section about the payment feature. -It requires a `PaymentComponentsController` instance (see `Integrate the Payment component` step 1). - -> **Note:** -> - The `PaymentInfoViewController` can be presented modally, used in a container view or pushed to a navigation view controller. Make sure to add your own navigation around the provided views. - -> ⚠️ **Important:** -> - The `PaymentInfoViewController` presentation should happen in `func didTapOnMoreInformation(documentId: String?)` inside `PaymentComponentViewProtocol` implementation without animation since SDK handles the animation during the presentation.(`Integrate the Payment component` step 3). - -```swift -func didTapOnMoreInformation(documentId: String?) { - let paymentInfoViewController = paymentComponentsController.paymentInfoViewController() - self.yourInvoicesListViewController.navigationController?.pushViewController(paymentInfoViewController, - animated: false) -} - ``` - -## Show BankSelectionBottomSheet - -The `BankSelectionBottomSheet` displays a list of available banks for the user to choose from. -If a banking app is not installed it will also display its AppStore link. -The `BankSelectionBottomSheet` presentation requires a `PaymentComponentsController` instance from the `Integrate the Payment component` step 1. - -> **Note:** -> - We strongly recommend to present `BankSelectionBottomSheet` modally with a `.overFullScreen` presentation style. - -> ⚠️ **Important:** -> - The `BankSelectionBottomSheet` presentation should happen in `func didTapOnBankPicker(documentId: String?)` inside -`PaymentComponentViewProtocol` implementation without animation since SDK handles the animation during the presentation (see `Integrate the Payment component` step 3). +### 2. Start the Payment Flow +Once you initialize the healthSDK, there is a function that you should call when users taps on your CTA pay button: ```swift -func didTapOnBankPicker(documentId: String?) { - let bankSelectionBottomSheet = paymentComponentsController.bankSelectionBottomSheet() - bankSelectionBottomSheet.modalPresentationStyle = .overFullScreen - self.yourInvoicesListViewController.present(bankSelectionBottomSheet, - animated: false) - } - ``` - -## Show PaymentReviewViewController +health.startPaymentFlow(documentId: documentId, paymentInfo: paymentInfo, navigationController: navigationController, trackingDelegate: self) +Initiates the payment flow for a specified document and payment information. -The `PaymentReviewViewController` displays an invoice's pages and extractions. It also lets users pay the invoice with the bank they selected in the `BankSelectionBottomSheet`. - -The `PaymentReviewViewController` presentation requires a `PaymentComponentsController` instance from the `Integrate the Payment component` step 1 and `documentId`. - -> **Note:** -> - The `PaymentReviewViewController` can be presented modally, used in a container view or pushed to a navigation view controller. Make sure to add your own navigation around the provided views. - -> ⚠️ **Important:** -> - The `PaymentReviewViewController` presentation should happen in `func didTapOnBankPicker(documentId: String?)` inside -`PaymentComponentViewProtocol` implementation without animation since SDK handles the animation during the presentation (see `Integrate the Payment component` step 3). - -```swift - func didTapOnPayInvoice(documentId: String?) { - guard let documentId else { return } - paymentComponentsController.loadPaymentReviewScreenFor(documentID: documentId, trackingDelegate: self) { [weak self] viewController, error in - if let error { - self?.showErrorsIfAny() - } else if let viewController { - viewController.modalTransitionStyle = .coverVertical - viewController.modalPresentationStyle = .overCurrentContext - self?.yourInvoicesListViewController.present(viewController, animated: false) - } - } - } + - Parameters: + - documentId: An optional identifier for the document associated with the payment flow. + - paymentInfo: An optional `PaymentInfo` object containing the payment details. + - navigationController: The `UINavigationController` used to present subsequent view controllers in the payment flow. + - trackingDelegate: The `GiniHealthTrackingDelegate` provides event information that happens on PaymentReviewScreen. ``` -> **Note:** -> - PaymentReviewViewController contains the following configuration options: -> - paymentReviewStatusBarStyle: Sets the status bar style on the payment review screen. Only if `View controller-based status bar appearance` = `YES` in `Info.plist`. -> - showPaymentReviewCloseButton: If set to true, a floating close button will be shown in the top right corner of the screen. -Default value is false. - -For enabling `showPaymentReviewCloseButton`: - +### Optional: +We also provide trust marker information for creating a subview that displays the available banks and their respective numbers. See Figma [here](https://www.figma.com/design/fHf3b3XxE59wymH7gvoMrJ/iOS-Gini-Health-SDK-5.0-UI-Customisation?node-id=12906-13711&node-type=instance&t=fLL9Yl3dPpmV51U0-0) +For that please call next method: ```swift -let giniConfiguration = GiniHealthConfiguration() -config.showPaymentReviewCloseButton = true -. -. -. -healthSDK.setConfiguration(config) + let logos = health.fetchBankLogos().logos // for the first two payment providers available + let additionalBankNumberToShow = health.fetchBankLogos().additionalBankCount // for the number of additional payment providers available ``` diff --git a/HealthSDK/GiniHealthSDK/Documentation/source/Testing.md b/HealthSDK/GiniHealthSDK/Documentation/source/Testing.md index c5a5ee42b..0ff9b7fce 100644 --- a/HealthSDK/GiniHealthSDK/Documentation/source/Testing.md +++ b/HealthSDK/GiniHealthSDK/Documentation/source/Testing.md @@ -30,7 +30,7 @@ After you've set the client credentials in the example banking app and installed #### Payment component After following the integration steps above you'll arrive at the `Payment Invoice list screen`, which already has integrated the `Payment Component`. -The following screenshot shows a sample list of invoices where the `PaymentComponent` is shown for each invoice. +The following screenshot shows a sample list of invoices. Tapping on a CTA button will show the `Payment Component` in a bottom sheet if a payment provider isn't selected already.
@@ -38,7 +38,7 @@ The following screenshot shows a sample list of invoices where the `PaymentCompo #### Bank Selection Bottom sheet -You should see the `Gini-Test-Payment-Provider` preselected in every payment component view. By clicking the picker you should see the `BankSelectionBottomSheet` with the list of available banking apps (including `Gini-Test-Payment-Provider` and other testing and production apps). +By clicking the picker you should see the `BankSelectionBottomSheet` with the list of available banking apps (including `Gini-Test-Payment-Provider` and other testing and production apps).
@@ -46,7 +46,7 @@ You should see the `Gini-Test-Payment-Provider` preselected in every payment com #### More information and FAQ -By clicking either the more information or the info icon on the `Payment Component` view you should see the `Payment feature Info screen` with information about the payment feature and an FAQ section. +By clicking the more information in the bottom `Payment Component` view you should see the `Payment feature Info screen` with information about the payment feature and an FAQ section.
@@ -54,9 +54,9 @@ By clicking either the more information or the info icon on the `Payment Compone #### Payment Review -By clicking the `Pay the invoice` button on a `Payment Component` view you should see the `Payment Review screen`, which shows the invoice's pages and the payment information. It also allows editing the payment information. The `To the banking app` button should have the icon and colors of the banking app, which was selected in the payment component view. +By clicking the `Continue to overview` button on a bottom `Payment Component` view you should see the `Payment Review screen`, which shows the invoice's pages and the payment information. It also allows editing the payment information. The `To the banking app` button should have the colors of the banking app, which was selected in the payment component view. There should also be a bank picker with selected payment provider in the left down part of the view. You can tap on it and change the payment provider. -Check that the extractions and the document preview are shown and then press the `Pay` button: +Check that the extractions and the document preview are shown and then press the `To the banking app` button:
@@ -89,12 +89,33 @@ The following is an example for the url `gini-pay://payment-requester`: open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { if url.host == "payment-requester" { + processBankUrl(url: url) // hadle incoming url from the banking app } return true } ``` +Here you can obtain the `paymentRequestId` and check the payment status: + +```swift +func processBankUrl(url: URL) { + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return } + if let queryItems = components.queryItems { + if let paymentRequestId = queryItems.first(where: { $0.name == "paymentRequestId" })?.value { + health.getPaymentRequest(by: paymentRequestId) { [weak self] result in + switch result { + case .success(let paymentRequest): + print("paymentStatus: \(PaymentStatus(rawValue: paymentRequest.status))") + case .failure(let error): + print("Failed to retrieve payment request: \(error.localizedDescription)") + } + } + } + } +} +``` + With these steps completed you have verified that your app, the Gini Health API, the Gini Health SDK and the Gini Bank SDK work together correctly. diff --git a/HealthSDK/GiniHealthSDK/Package-release.swift b/HealthSDK/GiniHealthSDK/Package-release.swift index 9d76c3d18..7ef08fb48 100644 --- a/HealthSDK/GiniHealthSDK/Package-release.swift +++ b/HealthSDK/GiniHealthSDK/Package-release.swift @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "GiniHealthSDK", defaultLocalization: "en", - platforms: [.iOS(.v12)], + platforms: [.iOS(.v13)], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( @@ -16,7 +16,9 @@ let package = Package( dependencies: [ // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), - .package(name: "GiniHealthAPILibrary", url: "https://github.com/gini/health-api-library-ios.git", .exact("4.3.1")), + .package(name: "GiniHealthAPILibrary", url: "https://github.com/gini/health-api-library-ios.git", .exact("5.0.0")), + .package(name: "GiniInternalPaymentSDK", url: "https://github.com/gini/internal-payment-sdk-ios", .exact("1.0.0")), + .package(name: "GiniUtilites", url: "https://github.com/gini/utilites-ios.git", .exact("1.1.0")), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -24,7 +26,7 @@ let package = Package( .target( name: "GiniHealthSDK", - dependencies: ["GiniHealthAPILibrary"]), + dependencies: ["GiniHealthAPILibrary", "GiniInternalPaymentSDK", "GiniUtilites"]), .testTarget( name: "GiniHealthSDKTests", dependencies: ["GiniHealthSDK"]), diff --git a/HealthSDK/GiniHealthSDK/Package.swift b/HealthSDK/GiniHealthSDK/Package.swift index 97dc45c13..a47901ae3 100644 --- a/HealthSDK/GiniHealthSDK/Package.swift +++ b/HealthSDK/GiniHealthSDK/Package.swift @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "GiniHealthSDK", defaultLocalization: "en", - platforms: [.iOS(.v12)], + platforms: [.iOS(.v13)], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( @@ -17,6 +17,8 @@ let package = Package( // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), .package(name: "GiniHealthAPILibrary", path: "../../HealthAPILibrary/GiniHealthAPILibrary"), + .package(name: "GiniInternalPaymentSDK", path: "../../GiniComponents/GiniInternalPaymentSDK"), + .package(name: "GiniUtilites", path: "../../GiniComponents/GiniUtilites") ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -24,7 +26,7 @@ let package = Package( .target( name: "GiniHealthSDK", - dependencies: ["GiniHealthAPILibrary"]), + dependencies: ["GiniHealthAPILibrary", "GiniInternalPaymentSDK", "GiniUtilites"]), .testTarget( name: "GiniHealthSDKTests", dependencies: ["GiniHealthSDK"], diff --git a/HealthSDK/GiniHealthSDK/README.md b/HealthSDK/GiniHealthSDK/README.md index fffecd4dd..5a77d72af 100644 --- a/HealthSDK/GiniHealthSDK/README.md +++ b/HealthSDK/GiniHealthSDK/README.md @@ -23,17 +23,17 @@ We are providing example app for Swift. This app demonstrates how to integrate t An example banking app is available in the [Gini Mobile iOS Monorepo](https://github.com/gini/gini-mobile-ios/tree/main/BankSDK/GiniBankSDKExample) repository. To check the redirection to the Banking app please run Bank example before the Health example. You can use the same Gini Health API client credentials in the example banking app as in your app, if not otherwise specified. -To inject your API credentials into the Health and Bank example apps you need to fill in your credentials in [Credentials.plist](https://github.com/gini/gini-mobile-ios/blob/main/BankSDK/GiniBankSDKExample/GiniBankSDKExampleBank/Credentials.plist) and [Credentials.plist](https://github.com/gini/gini-mobile-ios/blob/main/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/Credentials.plist), respectively. +To inject your API credentials into the Health and Bank example apps you need to fill in your credentials in [Credentials.plist](https://github.com/gini/gini-mobile-ios/blob/main/BankSDK/GiniBankSDKExample/GiniBankSDKExampleBank/Credentials.plist) and [CredentialsManager.swift](https://github.com/gini/gini-mobile-ios/blob/main/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/CredentialsManager.swift), respectively. ## Requirements -- iOS 12+ +- iOS 13+ - Xcode 15+ **Note:** In order to have better analysis results it is highly recommended to enable only devices with 8MP camera and flash. These devices would be: -* iPhones with iOS 12 or higher. +* iPhones with iOS 13 or higher. * iPad Pro devices (iPad Air 2 and iPad Mini 4 have 8MP camera but no flash). ## Author diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/CompositeDocument.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/CompositeDocument.swift new file mode 100644 index 000000000..1e1e35e42 --- /dev/null +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/CompositeDocument.swift @@ -0,0 +1,21 @@ +// +// CompositeDocument.swift +// GiniHealthSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import Foundation + +/// Composite document information +public struct CompositeDocument { + + /// Composite document URL. Similar to this: https://api.gini.net/documents/12345678-9123-11e2-bfd6-000000000000 + public let document: URL + + /// The composite document’s unique identifier. + public var id: String? { + guard let id = document.absoluteString.split(separator: "/").last else { return nil } + return String(id) + } +} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/CompositeDocumentInfo.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/CompositeDocumentInfo.swift new file mode 100644 index 000000000..c83be553d --- /dev/null +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/CompositeDocumentInfo.swift @@ -0,0 +1,25 @@ +// +// CompositeDocumentInfo.swift +// GiniHealthSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import Foundation + +/// Information used to create a composite document +public struct CompositeDocumentInfo { + + /// Array containing all the partial documents used to create a composite document. + public let partialDocuments: [PartialDocumentInfo] + + public init(partialDocuments: [PartialDocumentInfo]) { + self.partialDocuments = partialDocuments + } +} + +// MARK: - Decodable + +extension CompositeDocumentInfo: Encodable { + +} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/Document+Layout.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/Document+Layout.swift new file mode 100644 index 000000000..abbfaf5e6 --- /dev/null +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/Document+Layout.swift @@ -0,0 +1,67 @@ +// +// Document+Layout.swift +// GiniHealthSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import Foundation + +extension Document.Layout { + /// A document's page layout, indicating its size, textZones, regions and its page number + public struct Page: Decodable { + /// Page number + public let number: Int + /// Page width + public let sizeX: Double + /// Page height + public let sizeY: Double + /// Page textZones + public let textZones: [TextZone] + /// Page regions + public let regions: [Region]? + } + + /// A document's page layout region, indicating its origin, size, type, lines and words + public struct Region: Decodable { + /// Top-Left X origin + public let l: Double + /// Top-Left Y origin + public let t: Double + /// Region width + public let w: Double + /// Region height + public let h: Double + /// Region type + public let type: String? + /// (Optional) Amount of lines in that region + public let lines: [Region]? + /// (Optional) Amount of words in that region + public let wds: [Word]? + } + + /// A document's page text zone, containing an array of regions + public struct TextZone: Decodable { + public let paragraphs: [Region] + } + + /// Word contained within a text region + public struct Word: Decodable { + /// Top-Left X origin + public let l: Double + /// Top-Left Y origin + public let t: Double + /// Word width + public let w: Double + /// Word height + public let h: Double + /// Word font size + public let fontSize: Double + /// Word font family + public let fontFamily: String + /// Indicates if the font style is bold + public let bold: Bool + /// Text contained in the word + public let text: String + } +} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/Document.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/Document.swift new file mode 100644 index 000000000..4cb014e25 --- /dev/null +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/Document.swift @@ -0,0 +1,225 @@ +// +// Document.swift +// GiniHealthSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import Foundation +import GiniHealthAPILibrary + +/// Data model that represents a Document entity +public struct Document { + + /// (Optional) Array containing the path of every composite document + public let compositeDocuments: [CompositeDocument]? + /// The document's creation date. + public let creationDate: Date + /// The document's unique identifier. + public let id: String + /// The document's file name. + public let name: String + /// The document's origin. + public let origin: Origin + /// The number of pages. + public let pageCount: Int + /// The document's pages. + public let pages: [Page]? + /// Links to related resources, such as extractions, document, processed, layout or pages. + public let links: Links + /// (Optional) Array containing the path of every partial document info + public let partialDocuments: [PartialDocumentInfo]? + /// The processing state of the document. + public let progress: Progress + /// The document's source classification. + public let sourceClassification: SourceClassification + /// The document's expiration date. + public let expirationDate: Date? +} + +extension Document { + /** + It's the easiest way to initialize a `Document` if you are receiving a customized JSON structure from your proxy backend. + + - parameter creationDate: The document's creation date. + - parameter id: The document's unique identifier. + - parameter name: The document's file name. + - parameter links: Links to related resources, such as extractions, document, processed, layout or pages. + - parameter pageCount: The document's number of pages. + - parameter sourceClassification: The document's source classification. We recommend to use `scanned` or `composite`. + - parameter expirationDate: The document's expiration date. + + - note: Custom networking only. + */ + public init(creationDate: Date, + id: String, + name: String, + links: Links, + pageCount: Int, + sourceClassification: SourceClassification, + expirationDate: Date?) { + self.init(compositeDocuments: [], + creationDate: creationDate, + id: id, + name: name, + origin: .upload, + pageCount: pageCount, + pages: [], + links: links, + partialDocuments: [], + progress: .completed, + sourceClassification: sourceClassification, + expirationDate: expirationDate) + } +} + +extension Document { + /** + * The possible states of documents. The availability of a document's extractions, layout and preview images are + * depending on the document's progress. + */ + public enum Progress: String, Decodable { + /// Indicates that the document is fully processed. Preview images, extractions and the layout are available. + case completed = "COMPLETED" + + /// Indicates that the document is not fully processed yet. + /// There are no extractions, layout or preview images available. + case pending = "PENDING" + + /// The document is processed, but there was an error during processing, so it is very likely that neither the + /// extractions, layout or preview images are available + case error = "ERROR" + } + + /// The origin of an uploaded document. + public enum Origin: String, Decodable { + /// When a document comes from an upload + case upload = "UPLOAD" + + /// Unknown origin + case unknown = "UNKNOWN" + } + + /// The possible source classifications of a document. + public enum SourceClassification: String, Decodable { + /// A composite document created by one or several partial documents + case composite = "COMPOSITE" + /// A "native" document, usually a PDF document. + case native = "NATIVE" + /// A scanned document, usually the result of a photographed or scanned document. + case scanned = "SCANNED" + /// A scanned document with the ocr information on top. + case sandwich = "SANDWICH" + /// A text document. + case text = "TEXT" + } + + /// A document's page, consisting of an array of number and its page number + public struct Page { + /// Page number + public let number: Int + /// Page image urls array, along with their sizes + public let images: [(size: Size, url: URL)] + + //swiftlint:disable nesting + enum CodingKeys: String, CodingKey { + case number = "pageNumber" + case images + } + + /// Page size + public enum Size: String, Decodable { + /// 750x900 + case small = "750x900" + + /// 1280x1810 + case big = "1280x1810" + } + + } + + /// Links to related resources, such as extractions, document, processed or layout. + public struct Links { + /** + An initializer for a `Links` structure if you are receiving a customized JSON structure from your proxy backend. + For this particular case all links will be pointed to the document's link. + + - parameter giniAPIDocumentURL: The document's link received from the Gini API. This must be the same URL that you received in the `Location` header from the Gini API. For example "https://pay-api.gini.net/documents/626626a0-749f-11e2-bfd6-000000000000". + + - note: Custom networking only. + */ + public init(giniAPIDocumentURL: URL) { + self.extractions = giniAPIDocumentURL + self.layout = giniAPIDocumentURL + self.processed = giniAPIDocumentURL + self.document = giniAPIDocumentURL + self.pages = nil + } + + public let extractions: URL + public let layout: URL + public let processed: URL + public let document: URL + public let pages: URL? + } + + /// The document's layout, formed by an array of pages + public struct Layout { + /// Layout pages + public let pages: [Page] + } + + /// The document types, used as a hint during the analysis. + public enum DocType: String, Codable { + case bankStatement = "BankStatement" + case contract = "Contract" + case invoice = "Invoice" + case receipt = "Receipt" + case reminder = "Reminder" + case remittanceSlip = "RemittanceSlip" + case travelExpenseReport = "TravelExpenseReport" + case other = "Other" + } + + /// The V2 document's type. Used when creating documents in multipage mode. + public enum TypeV2 { + /// Partial document, consisting of pdf/image/qrCode data + case partial(Data) + /// Composite document, made of partial documents + case composite(CompositeDocumentInfo) + + var name: String { + switch self { + case .partial: + return "partial" + case .composite: + return "composite" + } + } + } + + /** + * The metadata contains any custom information regarding the upload (used later for reporting), + * creating HTTP headers with an specific format. + */ + public struct Metadata { + internal let healthMeta: GiniHealthAPILibrary.Document.Metadata + + /** + * The document metadata initializer with the branch ID (i.e: the BLZ of a Bank in Germany) and additional + * headers. + * + * - Parameter branchId: The branch id (i.e: the BLZ of a Bank in Germany) + * - Parameter additionalHeaders: Additional headers for the metadata. i.e: ["customerId":"123456"] + */ + public init(branchId: String? = nil, additionalHeaders: [String: String]? = nil) { + healthMeta = GiniHealthAPILibrary.Document.Metadata(branchId: branchId, additionalHeaders: additionalHeaders) + } + } +} + +extension Document: Equatable { + public static func == (lhs: Document, rhs: Document) -> Bool { + lhs.id == rhs.id + } +} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/DocumentService.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/DocumentService.swift new file mode 100644 index 000000000..819c7b8a3 --- /dev/null +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/DocumentService.swift @@ -0,0 +1,297 @@ +// +// DefaultDocumentService.swift +// GiniHealthSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import Foundation +import GiniHealthAPILibrary + +/// The default document service. By default interacts with the `APIDomain.default` api. +public final class DefaultDocumentService { + + private let docService: GiniHealthAPILibrary.DefaultDocumentService + + init(docService: GiniHealthAPILibrary.DefaultDocumentService) { + self.docService = docService + self.docService.apiDomain = .default + self.docService.apiVersion = GiniHealth.Constants.defaultVersionAPI + } + + /** + * Creates a partial document from a given image `Data` or a composite document for given partial documents. + * + * - Parameter fileName: The document's filename + * - Parameter docType: The document's docType + * - Parameter type: The V2 document's type. It could be either partial or composite type. + * - Parameter metadata: The document's metadata + * - Parameter completion: A completion callback, returning the created document on success + */ + public func createDocument(fileName: String?, + docType: Document.DocType?, + type: Document.TypeV2, + metadata: Document.Metadata?, + completion: @escaping CompletionResult) { + docService.createDocument(fileName: fileName, + docType: GiniHealthAPILibrary.Document.DocType(rawValue: docType?.rawValue ?? ""), + type: type.toHealthType(), + metadata: metadata?.healthMeta, + completion: { result in + switch result { + case .success(let document): + completion(.success(Document(healthDocument: document))) + case .failure(let error): + completion(.failure(GiniError.decorator(error))) + } + }) + + } + + /** + * Deletes a document + * + * - Parameter document: Document to be deleted + * - Parameter completion: A completion callback + */ + public func delete(_ document: Document, completion: @escaping CompletionResult) { + docService.delete(document.toHealthDocument(), + completion: { result in + switch result { + case .success(let item): + completion(.success(item)) + case .failure(let error): + completion(.failure(GiniError.decorator(error))) + } + }) + } + + /** + * Fetches the user documents, with the possibility to retrieve them paginated + * + * - Parameter limit: Limit of documents to retrieve + * - Parameter offset: Document's offset + * - Parameter completion: A completion callback, returning the document list on success + */ + public func documents(limit: Int?, offset: Int?, completion: @escaping CompletionResult<[Document]>) { + docService.documents(limit: limit, + offset: offset, + completion: { result in + switch result { + case .success(let documents): + completion(.success(documents.compactMap { Document(healthDocument: $0) })) + case .failure(let error): + completion(.failure(GiniError.decorator(error))) + } + }) + } + + /** + * Retrieves a document for a given document id + * + * - Parameter id: The document's unique identifier + * - Parameter completion: A completion callback, returning the requested document on success + */ + public func fetchDocument(with id: String, completion: @escaping CompletionResult) { + docService.fetchDocument(with: id, + completion: { result in + switch result { + case .success(let item): + completion(.success(Document(healthDocument: item))) + case .failure(let error): + completion(.failure(GiniError.decorator(error))) + } + }) + } + + /** + * Retrieves the extractions for a given document. + * + * - Parameter document: Document to get the extractions for + * - Parameter cancellationToken: Token use to stopped the analysis when a user cancels it + * - Parameter completion: A completion callback, returning the extraction list on success + */ + public func extractions(for document: Document, + cancellationToken: CancellationToken, + completion: @escaping CompletionResult) { + docService.extractions(for: document.toHealthDocument(), + cancellationToken: cancellationToken.healthToken, + completion: { result in + switch result { + case .success(let healthExtractionResult): + completion(.success(ExtractionResult(healthExtractionResult: healthExtractionResult))) + case .failure(let error): + completion(.failure(GiniError.decorator(error))) + } + }) + } + + /** + * Retrieves the layout of a given document + * + * - Parameter id: The document's unique identifier + * - Parameter completion: A completion callback, returning the requested document layout on success + */ + public func layout(for document: Document, completion: @escaping CompletionResult) { + docService.layout(for: document.toHealthDocument(), + completion: { result in + switch result { + case .success(let item): + completion(.success(Document.Layout(healthLayout: item))) + case .failure(let error): + completion(.failure(GiniError.decorator(error))) + } + }) + } + + /** + * Retrieves the pages of a given document + * + * - Parameter id: The document's unique identifier + * - Parameter completion: A completion callback, returning the requested document layout on success + */ + public func pages(in document: Document, completion: @escaping CompletionResult<[Document.Page]>) { + docService.pages(in: document.toHealthDocument(), + completion: { result in + switch result { + case .success(let pages): + completion(.success(pages.compactMap { Document.Page(healthPage: $0) })) + case .failure(let error): + completion(.failure(GiniError.decorator(error))) + } + }) + } + + /// Private helper function to handle feedback submission. + /// + /// - Parameters: + /// - documentId: The ID of the document for which feedback should be sent. + /// - extractions: The document's updated extractions. + /// - compoundExtractions: The document's updated compound extractions, if any. + /// - completion: A completion callback. + private func submitFeedback(documentId: String, + extractions: [Extraction], + compoundExtractions: [String: [[Extraction]]]? = nil, + completion: @escaping CompletionResult) { + let healthExtractions = extractions.map { $0.toHealthExtraction() } + let healthCompoundExtractions = compoundExtractions?.mapValues { mapCompoundExtraction($0) } ?? [:] + + docService.submitFeedback(for: documentId, + with: healthExtractions, + and: healthCompoundExtractions, + completion: { result in + switch result { + case .success: + completion(.success(())) + case .failure(let error): + completion(.failure(GiniError.decorator(error))) + } + } + ) + } + + /** + * Submits the analysis feedback for a given document. + * + * - Parameter document: The document for which feedback should be sent. + * - Parameter extractions: The document's updated extractions. + * - Parameter completion: A completion callback. + */ + public func submitFeedback(for document: Document, + with extractions: [Extraction], + completion: @escaping CompletionResult) { + submitFeedback(documentId: document.id, + extractions: extractions, + completion: completion) + } + + /** + * Submits the analysis feedback with compound extractions (e.g., "line items") for a given document. + * + * - Parameter document: The document for which feedback should be sent. + * - Parameter extractions: The document's updated extractions. + * - Parameter compoundExtractions: The document's updated compound extractions. + * - Parameter completion: A completion callback. + */ + public func submitFeedback(for document: Document, + with extractions: [Extraction], + and compoundExtractions: [String: [[Extraction]]], + completion: @escaping CompletionResult) { + submitFeedback(documentId: document.id, + extractions: extractions, + compoundExtractions: compoundExtractions, + completion: completion) + } + + /** + * Submits the analysis feedback for a document using only its ID. + * + * - Parameter documentId: The ID of the document for which feedback should be sent. + * - Parameter extractions: The document's updated extractions. + * - Parameter completion: A completion callback. + */ + public func submitFeedback(for documentId: String, + with extractions: [Extraction], + completion: @escaping CompletionResult) { + submitFeedback(documentId: documentId, + extractions: extractions, + completion: completion) + } + + /** + * Submits the analysis feedback with compound extractions for a document using only its ID. + * + * - Parameter documentId: The ID of the document for which feedback should be sent. + * - Parameter extractions: The document's updated extractions. + * - Parameter compoundExtractions: The document's updated compound extractions. + * - Parameter completion: A completion callback. + */ + public func submitFeedback(for documentId: String, + with extractions: [Extraction], + and compoundExtractions: [String: [[Extraction]]], + completion: @escaping CompletionResult) { + submitFeedback(documentId: documentId, + extractions: extractions, + compoundExtractions: compoundExtractions, + completion: completion) + } + + + private func mapCompoundExtraction(_ compoundExtraction: [[Extraction]]) -> [[GiniHealthAPILibrary.Extraction]] { + return compoundExtraction.map { $0.map { $0.toHealthExtraction() } } + } + + /** + * Retrieves the page preview of a document for a given page + * + * - Parameter documentId: Document id to get the preview for + * - Parameter pageNumber: The document's page number starting from 1 + * - Parameter completion: A completion callback, returning the requested page preview as Data on success + */ + public func preview(for documentId: String, + pageNumber: Int, + completion: @escaping CompletionResult) { + docService.preview(for: documentId, + pageNumber: pageNumber, + completion: { result in + switch result { + case .success(let data): + completion(.success(data)) + case .failure(let error): + completion(.failure(GiniError.decorator(error))) + } + }) + } + + public func file(urlString: String, completion: @escaping CompletionResult){ + docService.file(urlString: urlString, + completion: { result in + switch result { + case .success(let data): + completion(.success(data)) + case .failure(let error): + completion(.failure(GiniError.decorator(error))) + } + }) + } +} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/Extraction.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/Extraction.swift new file mode 100644 index 000000000..7d04bc21c --- /dev/null +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/Extraction.swift @@ -0,0 +1,119 @@ +// +// Extraction.swift +// GiniHealthSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import Foundation + +/** + * Data model for a document extraction. + */ +@objcMembers final public class Extraction: NSObject { + + /// The extraction's box. Only available for some extractions. + public let box: Box? + /// The available candidates for this extraction. + public let candidates: String? + /// The extraction's entity. + public let entity: String + /// The extraction's value + public var value: String + /// The extraction's name + public var name: String? + + /// The extraction's box attributes. + @objcMembers final public class Box: NSObject { + public let height: Double + public let left: Double + public let page: Int + public let top: Double + public let width: Double + + public init(height: Double, left: Double, page: Int, top: Double, width: Double) { + self.height = height + self.left = left + self.page = page + self.top = top + self.width = width + } + } + + /// A extraction candidate, containing a box, an entity and a its value. + @objcMembers final public class Candidate: NSObject { + public let box: Box? + public let entity: String + public let value: String + + public init(box: Box?, entity: String, value: String) { + self.box = box + self.entity = entity + self.value = value + } + } + + public init(box: Box?, candidates: String?, entity: String, value: String, name: String?) { + self.box = box + self.candidates = candidates + self.entity = entity + self.value = value + self.name = name + } + +} + +// MARK: - Decodable + +extension Extraction: Decodable {} +extension Extraction.Box: Decodable {} +extension Extraction.Candidate: Decodable {} + +// MARK: - isEqual + +extension Extraction { + + public override func isEqual(_ object: Any?) -> Bool { + + guard let other = object as? Extraction else { return false } + + return self.box == other.box && + self.candidates == other.candidates && + self.entity == other.entity && + self.name == other.name && + self.value == other.value + } +} + +extension Extraction { + + public override var debugDescription: String { + return "(\(name ?? "") : \(value))" + } +} + +extension Extraction.Box { + + public override func isEqual(_ object: Any?) -> Bool { + + guard let other = object as? Extraction.Box else { return false } + + return self.height == other.height && + self.left == other.left && + self.page == other.page && + self.top == other.top && + self.width == other.width + } +} + +extension Extraction.Candidate { + + public override func isEqual(_ object: Any?) -> Bool { + + guard let other = object as? Extraction.Candidate else { return false } + + return self.box == other.box && + self.entity == other.entity && + self.value == other.value + } +} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/ExtractionResult.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/ExtractionResult.swift new file mode 100644 index 000000000..2758ca651 --- /dev/null +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/ExtractionResult.swift @@ -0,0 +1,59 @@ +// +// ExtractionResult.swift +// GiniHealthSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import Foundation + +/** + Payment State types from payment state from extraction result + */ +public enum PaymentState: String { + case payable = "Payable" + case other = "Other" +} +/** + Extraction Types for extraction result + */ +public enum ExtractionType: String { + case paymentState = "payment_state" + case containsMultipleDocs = "contains_multiple_docs" + case paymentDueDate = "payment_due_date" + case amountToPay = "amount_to_pay" + case paymentRecipient = "payment_recipient" + case iban = "iban" + case paymentPurpose = "payment_purpose" + case doctorName = "medical_service_provider" + case bic = "bic" + case invoiceDate = "invoice_date" +} + +/** +* Data model for a document extraction result. +*/ +@objcMembers final public class ExtractionResult: NSObject { + + /// The specific extractions. + public let extractions: [Extraction] + + /// The payment compound extractions. + public var payment: [[Extraction]]? + + /// The line item compound extractions. + public var lineItems: [[Extraction]]? + + public init(extractions: [Extraction], payment: [[Extraction]]?, lineItems: [[Extraction]]?) { + self.extractions = extractions + self.payment = payment + self.lineItems = lineItems + super.init() + } + + convenience init(extractionsContainer: ExtractionsContainer) { + self.init(extractions: extractionsContainer.extractions, + payment: extractionsContainer.compoundExtractions?["payment"], + lineItems: extractionsContainer.compoundExtractions?["lineItems"]) + } +} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/ExtractionsContainer.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/ExtractionsContainer.swift new file mode 100644 index 000000000..297d61ccc --- /dev/null +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/ExtractionsContainer.swift @@ -0,0 +1,56 @@ +// +// ExtractionsContainer.swift +// GiniHealthSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import Foundation + +public struct ExtractionsContainer { + let extractions: [Extraction] + let compoundExtractions: [String : [[Extraction]]]? + let candidates: [Extraction.Candidate] + + enum CodingKeys: String, CodingKey { + case extractions + case compoundExtractions + case candidates + } +} + +// MARK: - Decodable + +extension ExtractionsContainer: Decodable { + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let decodedExtractions = try container.decode([String : Extraction].self, + forKey: .extractions) + let decodedCompoundExtractions = try container.decodeIfPresent([String : [[String : Extraction]]].self, + forKey: .compoundExtractions) + let decodedCandidates = try container.decodeIfPresent([String : [Extraction.Candidate]].self, + forKey: .candidates) ?? [:] + + extractions = decodedExtractions.map(ExtractionsContainer.mapExtraction) + + compoundExtractions = decodedCompoundExtractions?.mapValues(ExtractionsContainer.mapCompoundExtractions) + + candidates = decodedCandidates.flatMap { $0.value } + } +} + +private extension ExtractionsContainer { + private static func mapExtraction(key: String, value: Extraction) -> Extraction { + let extraction = value + extraction.name = key + return extraction + } + + private static func mapCompoundExtractions(extractionDictionaries: [[String : Extraction]]) -> [[Extraction]] { + return extractionDictionaries.map { extractionsDictionary in + extractionsDictionary.map(mapExtraction) + } + } +} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/GiniError.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/GiniError.swift new file mode 100644 index 000000000..3dd9da0f8 --- /dev/null +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/GiniError.swift @@ -0,0 +1,40 @@ +// +// GiniError.swift +// GiniHealthSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import Foundation +import GiniHealthAPILibrary + +public protocol GiniErrorProtocol { + var message: String { get } + var response: HTTPURLResponse? { get } + var data: Data? { get } +} + +public enum GiniError: Error, GiniErrorProtocol, Equatable { + case decorator(GiniHealthAPILibrary.GiniError) + + public var message: String { + switch self { + case .decorator(let giniError): + return giniError.message + } + } + + public var response: HTTPURLResponse? { + switch self { + case .decorator(let giniError): + return giniError.response + } + } + + public var data: Data? { + switch self { + case .decorator(let giniError): + return giniError.data + } + } +} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/GiniLog.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/GiniLog.swift new file mode 100644 index 000000000..275ed1b00 --- /dev/null +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/GiniLog.swift @@ -0,0 +1,13 @@ +// +// GiniLog.swift +// GiniHealthSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import Foundation + +public enum LogLevel { + case none + case debug +} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/Mapping.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/Mapping.swift new file mode 100644 index 000000000..886306e88 --- /dev/null +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/Mapping.swift @@ -0,0 +1,247 @@ +// +// Mapping.swift +// GiniHealthSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + +import Foundation +import GiniHealthAPILibrary +import GiniInternalPaymentSDK + +//MARK: - Mapping Extraction +extension Extraction { + convenience init(healthExtraction: GiniHealthAPILibrary.Extraction) { + self.init(box: nil, + candidates: healthExtraction.candidates, + entity: healthExtraction.entity, + value: healthExtraction.value, + name: healthExtraction.name) + } + + func toHealthExtraction() -> GiniHealthAPILibrary.Extraction { + return GiniHealthAPILibrary.Extraction(box: nil, + candidates: candidates, + entity: entity, + value: value, + name: name) + } +} + +extension ExtractionResult { + convenience init(healthExtractionResult: GiniHealthAPILibrary.ExtractionResult) { + let extractions = healthExtractionResult.extractions.map { Extraction(healthExtraction: $0) } + let payment = healthExtractionResult.payment?.map { $0.map { Extraction(healthExtraction: $0) } } + let lineItems = healthExtractionResult.lineItems?.map { $0.map { Extraction(healthExtraction: $0) } } + + self.init(extractions: extractions, + payment: payment, + lineItems: lineItems) + } + + func toHealthExtractionResult() -> GiniHealthAPILibrary.ExtractionResult { + let healthExtractions = extractions.map { $0.toHealthExtraction() } + let healthPayment = payment?.map { $0.map { $0.toHealthExtraction() } } + let healthLineItems = lineItems?.map { $0.map { $0.toHealthExtraction() } } + return GiniHealthAPILibrary.ExtractionResult(extractions: healthExtractions, + payment: healthPayment, + lineItems: healthLineItems) + } +} + +//MARK: - PaymentProvider + +extension PaymentProvider { + public init(healthPaymentProvider: GiniHealthAPILibrary.PaymentProvider) { + let openWithPlatforms = healthPaymentProvider.openWithSupportedPlatforms.compactMap { PlatformSupported(rawValue: $0.rawValue) } + let gpcSupportedPlatforms = healthPaymentProvider.gpcSupportedPlatforms.compactMap { PlatformSupported(rawValue: $0.rawValue) } + let colors = ProviderColors(healthProviderColors: healthPaymentProvider.colors) + let minAppVersions: MinAppVersions? + if let healthMinAppVersions = healthPaymentProvider.minAppVersion { + minAppVersions = MinAppVersions(healthMinAppVersions: healthMinAppVersions) + } else { + minAppVersions = nil + } + + self.init(id: healthPaymentProvider.id, + name: healthPaymentProvider.name, + appSchemeIOS: healthPaymentProvider.appSchemeIOS, + minAppVersion: minAppVersions, + colors: colors, + iconData: healthPaymentProvider.iconData, + appStoreUrlIOS: healthPaymentProvider.appStoreUrlIOS, + universalLinkIOS: healthPaymentProvider.universalLinkIOS, + index: healthPaymentProvider.index, + gpcSupportedPlatforms: gpcSupportedPlatforms, + openWithSupportedPlatforms: openWithPlatforms) + } + + func toHealthPaymentProvider() -> GiniHealthAPILibrary.PaymentProvider { + let gpcSupportedPlatforms = self.gpcSupportedPlatforms.compactMap { GiniHealthAPILibrary.PlatformSupported(rawValue: $0.rawValue) } + let openWithPlatforms = openWithSupportedPlatforms.compactMap { GiniHealthAPILibrary.PlatformSupported(rawValue: $0.rawValue) } + + return GiniHealthAPILibrary.PaymentProvider(id: id, + name: name, + appSchemeIOS: appSchemeIOS, + minAppVersion: minAppVersion?.healthMinAppVersions, + colors: colors.toHealthProviderColors(), + iconData: iconData, + appStoreUrlIOS: appStoreUrlIOS, + universalLinkIOS: universalLinkIOS, + index: index, + gpcSupportedPlatforms: gpcSupportedPlatforms, + openWithSupportedPlatforms:openWithPlatforms) + } +} + +extension ProviderColors { + init(healthProviderColors: GiniHealthAPILibrary.ProviderColors) { + self.init(background: healthProviderColors.background, + text: healthProviderColors.text) + } + + func toHealthProviderColors() -> GiniHealthAPILibrary.ProviderColors { + return GiniHealthAPILibrary.ProviderColors(background: background, + text: text) + } +} + +extension MinAppVersions { + init(healthMinAppVersions: GiniHealthAPILibrary.MinAppVersions) { + self.healthMinAppVersions = healthMinAppVersions + } +} + +//MARK: - Document + +extension Document { + init(healthDocument: GiniHealthAPILibrary.Document) { + self.init(compositeDocuments: healthDocument.compositeDocuments?.compactMap { CompositeDocument(document: $0.document) }, + creationDate: healthDocument.creationDate, + id: healthDocument.id, + name: healthDocument.name, + origin: Origin(rawValue: healthDocument.origin.rawValue) ?? .unknown, + pageCount: healthDocument.pageCount, + pages: healthDocument.pages?.compactMap { Document.Page(healthPage: $0) }, + links: Links(giniAPIDocumentURL: healthDocument.links.extractions), + partialDocuments: healthDocument.partialDocuments?.compactMap { PartialDocumentInfo(document: $0.document, rotationDelta: $0.rotationDelta) }, + progress: Progress(rawValue: healthDocument.progress.rawValue) ?? .completed, + sourceClassification: SourceClassification(rawValue: healthDocument.sourceClassification.rawValue) ?? .scanned, + expirationDate: healthDocument.expirationDate) + } + + func toHealthDocument() -> GiniHealthAPILibrary.Document { + GiniHealthAPILibrary.Document(creationDate: creationDate, + id: id, + name: name, + links: GiniHealthAPILibrary.Document.Links(giniAPIDocumentURL: links.extractions), + pageCount: pageCount, + sourceClassification: GiniHealthAPILibrary.Document.SourceClassification(rawValue: sourceClassification.rawValue) ?? .scanned, + expirationDate: expirationDate) + } +} + +extension Document.Page { + init(healthPage: GiniHealthAPILibrary.Document.Page) { + let images = healthPage.images.compactMap { (size: Document.Page.Size(healthSize: $0.size), url: $0.url) } + self.init(number: healthPage.number, images: images) + } +} + +extension Document.Page.Size { + init(healthSize: GiniHealthAPILibrary.Document.Page.Size) { + self.init(rawValue: healthSize.rawValue)! + } +} + +extension Document.Layout { + init(healthLayout: GiniHealthAPILibrary.Document.Layout) { + self.init(pages: healthLayout.pages.compactMap { Document.Layout.Page(healthPage: $0) }) + } +} + +extension Document.Layout.Page { + init(healthPage: GiniHealthAPILibrary.Document.Layout.Page) { + self.init(number: healthPage.number, + sizeX: healthPage.sizeX, + sizeY: healthPage.sizeY, + textZones: healthPage.textZones.compactMap { Document.Layout.TextZone(healthTextZone: $0) }, + regions: healthPage.regions?.compactMap { Document.Layout.Region(healthRegion: $0) }) + } +} + +extension Document.Layout.Region { + init(healthRegion: GiniHealthAPILibrary.Document.Layout.Region) { + self.init(l: healthRegion.l, + t: healthRegion.t, + w: healthRegion.w, + h: healthRegion.h, + type: healthRegion.type, + lines: healthRegion.lines?.compactMap { Document.Layout.Region.init(healthRegion: $0) }, + wds: healthRegion.wds?.compactMap { Document.Layout.Word.init(healthWord: $0) }) + } +} + +extension Document.Layout.Word { + init(healthWord: GiniHealthAPILibrary.Document.Layout.Word) { + self.init(l: healthWord.l, + t: healthWord.t, + w: healthWord.w, + h: healthWord.h, + fontSize: healthWord.fontSize, + fontFamily: healthWord.fontFamily, + bold: healthWord.bold, + text: healthWord.text) + } +} + +extension Document.Layout.TextZone { + init(healthTextZone: GiniHealthAPILibrary.Document.Layout.TextZone) { + self.init(paragraphs: healthTextZone.paragraphs.compactMap { Document.Layout.Region(healthRegion: $0) }) + } +} + +extension CompositeDocumentInfo { + func toHealthCompositeDocumentInfo() -> GiniHealthAPILibrary.CompositeDocumentInfo { + GiniHealthAPILibrary.CompositeDocumentInfo(partialDocuments: partialDocuments.map { GiniHealthAPILibrary.PartialDocumentInfo(document: $0.document) }) + } +} + +extension Document.TypeV2 { + func toHealthType() -> GiniHealthAPILibrary.Document.TypeV2 { + switch self { + case .partial(let data): + return .partial(data) + case .composite(let info): + return .composite(info.toHealthCompositeDocumentInfo()) + } + } +} + +//MARK: - Log + +extension LogLevel { + func toHealthLogLevel() -> GiniHealthAPILibrary.LogLevel { + switch self { + case .debug: + return .debug + case .none: + return .none + } + } +} + +//MARK: - PaymentProvider + +extension GiniInternalPaymentSDK.PaymentInfo { + init(paymentConponentsInfo: GiniHealthSDK.PaymentInfo) { + self.init(recipient: paymentConponentsInfo.recipient, + iban: paymentConponentsInfo.iban, + bic: paymentConponentsInfo.bic, + amount: paymentConponentsInfo.amount, + purpose: paymentConponentsInfo.purpose, + paymentUniversalLink: paymentConponentsInfo.paymentUniversalLink, + paymentProviderId: paymentConponentsInfo.paymentProviderId) + } +} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/PartialDocumentInfo.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/PartialDocumentInfo.swift new file mode 100644 index 000000000..0bb833915 --- /dev/null +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/PartialDocumentInfo.swift @@ -0,0 +1,43 @@ +// +// PartialDocumentInfo.swift +// GiniHealthSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import Foundation + +/// Partial document info used to create a composite document +public struct PartialDocumentInfo { + /// Partial document url + public var document: URL? + /// Partial document rotation delta [0-360º]. + public var rotationDelta: Int + + /// The partial document’s unique identifier. + public var id: String? { + guard let id = document?.absoluteString.split(separator: "/").last else { return nil } + return String(id) + } + + enum CodingKeys: String, CodingKey { + case document + case rotationDelta + } + + public init(document: URL?, rotationDelta: Int = 0) { + self.document = document + self.rotationDelta = rotationDelta + } +} + +// MARK: - Decodable + +extension PartialDocumentInfo: Codable { + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + document = try container.decodeIfPresent(URL.self, forKey: .document) + rotationDelta = try container.decodeIfPresent(Int.self, forKey: .rotationDelta) ?? 0 + } +} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/PaymentInfo.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/PaymentInfo.swift new file mode 100644 index 000000000..d5ce21482 --- /dev/null +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/PaymentInfo.swift @@ -0,0 +1,39 @@ +// +// PaymentInfo.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + +import Foundation +/** + Model object for payment information + */ + +public struct PaymentInfo { + + public var recipient: String + public var iban: String + public var bic: String + public var amount: String + public var purpose: String + public var paymentUniversalLink: String + public var paymentProviderId: String + + public init(recipient: String, iban: String, bic: String, amount: String, purpose: String, paymentUniversalLink: String, paymentProviderId: String) { + self.recipient = recipient + self.iban = iban + self.bic = bic + self.amount = amount + self.purpose = purpose + self.paymentUniversalLink = paymentUniversalLink + self.paymentProviderId = paymentProviderId + } + + public var isComplete: Bool { + !recipient.isEmpty && + !iban.isEmpty && + !amount.isEmpty && + !purpose.isEmpty + } +} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/PaymentProvider.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/PaymentProvider.swift new file mode 100644 index 000000000..42bbff544 --- /dev/null +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/PaymentProvider.swift @@ -0,0 +1,78 @@ +// +// PaymentProvider.swift +// GiniHealthSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + +import Foundation +import GiniHealthAPILibrary +/** + Struct for payment provider + */ +public struct PaymentProvider: Codable { + public var id: String + public var name: String + public var appSchemeIOS: String + public var colors: ProviderColors + public var minAppVersion: MinAppVersions? + public var iconData: Data + public var appStoreUrlIOS: String? + public var universalLinkIOS: String + public var index: Int? + public var gpcSupportedPlatforms: [PlatformSupported] + public var openWithSupportedPlatforms: [PlatformSupported] + + public init(id: String, name: String, appSchemeIOS: String, minAppVersion: MinAppVersions?, colors: ProviderColors, iconData: Data, appStoreUrlIOS: String?, universalLinkIOS: String, index: Int?, gpcSupportedPlatforms: [PlatformSupported], openWithSupportedPlatforms: [PlatformSupported]) { + self.id = id + self.name = name + self.appSchemeIOS = appSchemeIOS + self.minAppVersion = minAppVersion + self.colors = colors + self.iconData = iconData + self.appStoreUrlIOS = appStoreUrlIOS + self.universalLinkIOS = universalLinkIOS + self.index = index + self.gpcSupportedPlatforms = gpcSupportedPlatforms + self.openWithSupportedPlatforms = openWithSupportedPlatforms + } +} +public typealias PaymentProviders = [PaymentProvider] + +extension PaymentProvider: Equatable { + public static func == (lhs: PaymentProvider, rhs: PaymentProvider) -> Bool { + lhs.id == rhs.id + } +} + +/** + Struct for MinAppVersions in payment provider response + */ +public struct MinAppVersions: Codable { + internal let healthMinAppVersions: GiniHealthAPILibrary.MinAppVersions + + public init(ios: String?, android: String?) { + self.healthMinAppVersions = GiniHealthAPILibrary.MinAppVersions(ios: ios, android: android) + } +} + +/** + Struct for payment provider colors in payment provider response + */ +public struct ProviderColors: Codable { + public var background: String + public var text: String + public init(background: String, text: String) { + self.background = background + self.text = text + } +} + +/** + Enum for platforms supported by payment providers. We now support iOS and Android + */ +public enum PlatformSupported: String, Codable { + case ios + case android +} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/SessionManager.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/SessionManager.swift new file mode 100644 index 000000000..70d070077 --- /dev/null +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Adapter/SessionManager.swift @@ -0,0 +1,35 @@ +// +// SessionManager.swift +// GiniHealthSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import Foundation +import GiniHealthAPILibrary + +public typealias CompletionResult = (Result) -> Void + +/// Cancellation token needed during the analysis process +public final class CancellationToken { + internal let healthToken: GiniHealthAPILibrary.CancellationToken + + /// Indicates if the analysis has been cancelled + public var isCancelled: Bool { + get { healthToken.isCancelled } + set { healthToken.isCancelled = newValue } + } + + public init() { + self.healthToken = GiniHealthAPILibrary.CancellationToken() + } + + public init(healthToken: GiniHealthAPILibrary.CancellationToken) { + self.healthToken = healthToken + } + + /// Cancels the current task + public func cancel() { + healthToken.cancel() + } +} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/ButtonConfiguration.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/ButtonConfiguration.swift deleted file mode 100644 index 764f6201b..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/ButtonConfiguration.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// ButtonConfiguration.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit - -public struct ButtonConfiguration { - let backgroundColor: UIColor - let borderColor: UIColor - let titleColor: UIColor - let shadowColor: UIColor - let cornerRadius: CGFloat - let borderWidth: CGFloat - let shadowRadius: CGFloat - - let withBlurEffect: Bool - - /// Button configuration initalizer - /// - Parameters: - /// - backgroundColor: the button's background color - /// - borderColor: the button's border color - /// - titleColor: the button's title color - /// - shadowColor: the button's color of the shadow - /// - cornerRadius: the button's corner radius - /// - borderWidth: the button's border width - /// - shadowRadius: the button's shadow radius - /// - withBlurEffect: adds a blur effect on the button ignoring the background color and making it translucent - public init(backgroundColor: UIColor, - borderColor: UIColor, - titleColor: UIColor, - shadowColor: UIColor, - cornerRadius: CGFloat, - borderWidth: CGFloat, - shadowRadius: CGFloat, - withBlurEffect: Bool) { - self.backgroundColor = backgroundColor - self.borderColor = borderColor - self.titleColor = titleColor - self.shadowColor = shadowColor - self.cornerRadius = cornerRadius - self.borderWidth = borderWidth - self.shadowRadius = shadowRadius - self.withBlurEffect = withBlurEffect - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/CollectionFlowLayout.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/CollectionFlowLayout.swift deleted file mode 100644 index 353b04956..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/CollectionFlowLayout.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// CollectionFlowLayout.swift -// GiniHealth -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -import UIKit - -class CollectionFlowLayout: UICollectionViewFlowLayout{ - override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { - invalidateLayout(with: invalidationContext(forBoundsChange: newBounds)) - return super.shouldInvalidateLayout(forBoundsChange: newBounds) - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/GiniHealthColors.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/GiniHealthColors.swift deleted file mode 100644 index fdc1fce90..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/GiniHealthColors.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// GiniHealthColors.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -import UIKit - -extension UIColor { - struct GiniHealthColors { - static let accent1 = UIColorPreferred(named: "Accent01") - static let accent2 = UIColorPreferred(named: "Accent02") - static let accent3 = UIColorPreferred(named: "Accent03") - static let accent4 = UIColorPreferred(named: "Accent04") - static let accent5 = UIColorPreferred(named: "Accent05") - - static let dark1 = UIColorPreferred(named: "Dark01") - static let dark2 = UIColorPreferred(named: "Dark02") - static let dark3 = UIColorPreferred(named: "Dark03") - static let dark4 = UIColorPreferred(named: "Dark04") - static let dark5 = UIColorPreferred(named: "Dark05") - static let dark6 = UIColorPreferred(named: "Dark06") - static let dark7 = UIColorPreferred(named: "Dark07") - - static let light1 = UIColorPreferred(named: "Light01") - static let light2 = UIColorPreferred(named: "Light02") - static let light3 = UIColorPreferred(named: "Light03") - static let light4 = UIColorPreferred(named: "Light04") - static let light5 = UIColorPreferred(named: "Light05") - static let light6 = UIColorPreferred(named: "Light06") - static let light7 = UIColorPreferred(named: "Light07") - - static let feedback1 = UIColorPreferred(named: "Feedback01") - static let feedback2 = UIColorPreferred(named: "Feedback02") - static let feedback3 = UIColorPreferred(named: "Feedback03") - static let feedback4 = UIColorPreferred(named: "Feedback04") - - static let success1 = UIColorPreferred(named: "Success01") - static let success2 = UIColorPreferred(named: "Success02") - static let success3 = UIColorPreferred(named: "Success03") - static let success4 = UIColorPreferred(named: "Success04") - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/NSAttributedString.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/NSAttributedString.swift deleted file mode 100644 index 06bd21441..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/NSAttributedString.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// NSAttributedString.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit - -extension NSMutableAttributedString { - func addLinkToRange(link: String, range: NSRange, linkFont: UIFont, textToRemove: String?) { - var attributes: [NSAttributedString.Key: Any] = [ - .font: linkFont - ] - if range.length > 0, let url = URL(string: link) { - attributes[.link] = url - self.addAttributes(attributes, range: range) - if let textToRemove { - self.mutableString.replaceOccurrences(of: textToRemove, - with: "", - options: .caseInsensitive, - range: range) - } - } - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/String.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/String.swift deleted file mode 100644 index 3c4220808..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/String.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// String.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -import UIKit - -extension String { - func toColor() -> UIColor? { - return UIColor(hex: String.rgbaHexFrom(rgbHex: self)) - } - - func canOpenURLString() -> Bool { - if let url = URL(string: self) { - if UIApplication.shared.canOpenURL(url) { - return true - } - } - return false - } -} - -public extension String { - var numberValue: NSNumber? { - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - return formatter.number(from: self) - } - - static func rgbaHexFrom(rgbHex: String) -> String { - return "#\(rgbHex)FF" - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/UIColor+Utils.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/UIColor+Utils.swift deleted file mode 100644 index 1d0f4dec3..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/UIColor+Utils.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// UIColor+Utils.swift -// GiniHealth -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -import UIKit -public extension UIColor { - static func from(giniColor: GiniColor) -> UIColor { - if #available(iOS 13, *) { - return UIColor { (UITraitCollection: UITraitCollection) -> UIColor in - if UITraitCollection.userInterfaceStyle == .dark { - /// Return the color for Dark Mode - return giniColor.darkModeColor - } else { - /// Return the color for Light Mode - return giniColor.lightModeColor - } - } - } else { - /// Return a fallback color for iOS 12 and lower. - return giniColor.lightModeColor - } - } - - static func from(hex: UInt) -> UIColor { - return UIColor( - red: CGFloat((hex & 0xFF0000) >> 16) / 255.0, - green: CGFloat((hex & 0x00FF00) >> 8) / 255.0, - blue: CGFloat(hex & 0x0000FF) / 255.0, - alpha: CGFloat(1.0) - ) - } - - convenience init?(hex: String) { - let r, g, b, a: CGFloat - - if hex.hasPrefix("#") { - let start = hex.index(hex.startIndex, offsetBy: 1) - let hexColor = String(hex[start...]) - - if hexColor.count == 8 { - let scanner = Scanner(string: hexColor) - var hexNumber: UInt64 = 0 - - if scanner.scanHexInt64(&hexNumber) { - r = CGFloat((hexNumber & 0xff000000) >> 24) / 255 - g = CGFloat((hexNumber & 0x00ff0000) >> 16) / 255 - b = CGFloat((hexNumber & 0x0000ff00) >> 8) / 255 - a = CGFloat(hexNumber & 0x000000ff) / 255 - - self.init(red: r, green: g, blue: b, alpha: a) - return - } - } - } - - return nil - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/UIFont.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/UIFont.swift deleted file mode 100644 index 0bf3c9819..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/UIFont.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// UIFont.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit - -extension UIFont.TextStyle { - public static let headline1: UIFont.TextStyle = .init(rawValue: "kHeadline1") - public static let headline2: UIFont.TextStyle = .init(rawValue: "kHeadline2") - public static let headline3: UIFont.TextStyle = .init(rawValue: "kHeadline3") - public static let linkBold: UIFont.TextStyle = .init(rawValue: "kLinkBold") - public static let subtitle1: UIFont.TextStyle = .init(rawValue: "kSubtitle1") - public static let subtitle2: UIFont.TextStyle = .init(rawValue: "kSubtitle2") - public static let input: UIFont.TextStyle = .init(rawValue: "kInput") - public static let button: UIFont.TextStyle = .init(rawValue: "kButton") - public static let body1: UIFont.TextStyle = .init(rawValue: "kBody1") - public static let body2: UIFont.TextStyle = .init(rawValue: "kBody2") - public static let caption1: UIFont.TextStyle = .init(rawValue: "kCaption1") - public static let caption2: UIFont.TextStyle = .init(rawValue: "kCaption2") -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/UITapGestureRecognizer.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/UITapGestureRecognizer.swift deleted file mode 100644 index b74760e25..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/UITapGestureRecognizer.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// UITapGestureRecognizer.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit - -extension UITapGestureRecognizer { - func didTapAttributedTextInLabel(label: UILabel, targetText: String) -> Bool { - guard let attributedString = label.attributedText, let lblText = label.text else { return false } - let targetRange = (lblText as NSString).range(of: targetText) - //IMPORTANT label correct font for NSTextStorage needed - let mutableAttribString = NSMutableAttributedString(attributedString: attributedString) - mutableAttribString.addAttributes( - [NSAttributedString.Key.font: label.font ?? UIFont.smallSystemFontSize], - range: NSRange(location: 0, length: attributedString.length) - ) - // Create instances of NSLayoutManager, NSTextContainer and NSTextStorage - let layoutManager = NSLayoutManager() - let textContainer = NSTextContainer(size: CGSize.zero) - let textStorage = NSTextStorage(attributedString: mutableAttribString) - - // Configure layoutManager and textStorage - layoutManager.addTextContainer(textContainer) - textStorage.addLayoutManager(layoutManager) - - // Configure textContainer - textContainer.lineFragmentPadding = 0.0 - textContainer.lineBreakMode = label.lineBreakMode - textContainer.maximumNumberOfLines = label.numberOfLines - let labelSize = label.bounds.size - textContainer.size = labelSize - - // Find the tapped character location and compare it to the specified range - let locationOfTouchInLabel = self.location(in: label) - let textBoundingBox = layoutManager.usedRect(for: textContainer) - let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x, - y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y) - let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, - y: locationOfTouchInLabel.y - textContainerOffset.y) - let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, - in: textContainer, - fractionOfDistanceBetweenInsertionPoints: nil) - return NSLocationInRange(indexOfCharacter, targetRange) - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/UIView+Utils.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/UIView+Utils.swift deleted file mode 100644 index 8110c30c5..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Extensions/UIView+Utils.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// UIView+Utils.swift -// GiniHealth -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -import UIKit - -// MARK: - Adds round corners to any UIView, configurable with UIRectCorner, radius - -public extension UIView { - func roundCorners(corners: UIRectCorner, radius: CGFloat) { - self.clipsToBounds = true - self.layer.cornerRadius = radius - var masked = CACornerMask() - if corners.contains(.topLeft) { masked.insert(.layerMinXMinYCorner) } - if corners.contains(.topRight) { masked.insert(.layerMaxXMinYCorner) } - if corners.contains(.bottomLeft) { masked.insert(.layerMinXMaxYCorner) } - if corners.contains(.bottomRight) { masked.insert(.layerMaxXMaxYCorner) } - self.layer.maskedCorners = masked - } -} - -// MARK: - Adds loading indicator to any UIView, configurable with UIActivityIndicatorView.Style, color and scale - -public extension UIView { - func showLoading(style: UIActivityIndicatorView.Style? = .whiteLarge, color: UIColor? = .orange, scale: CGFloat? = 1.0) { - let loading = UIActivityIndicatorView(style: style ?? .whiteLarge) - if let color = color { - loading.color = color - } - loading.contentScaleFactor = scale ?? 1.0 - loading.translatesAutoresizingMaskIntoConstraints = false - loading.startAnimating() - loading.hidesWhenStopped = true - addSubview(loading) - loading.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true - loading.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true - } - - func stopLoading() { - removeActivityIndicator() - } - - func removeActivityIndicator() { - let activityIndicators = subviews.filter { $0 is UIActivityIndicatorView } as? [UIActivityIndicatorView] - - activityIndicators?.forEach { activityIndicator in - activityIndicator.stopAnimating() - activityIndicator.removeFromSuperview() - } - } -} - -// MARK: - Adds Blur effect to any UIView, configurable with UIBlurEffect.Style - -public extension UIView { - func applyBlurEffect(style: UIBlurEffect.Style? = .regular) { - let blurEffect = UIBlurEffect(style: style ?? .regular) - let blurEffectView = UIVisualEffectView(effect: blurEffect) - blurEffectView.frame = bounds - blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - addSubview(blurEffectView) - } - - func removeBlurEffect() { - let blurredEffectViews = subviews.filter { $0 is UIVisualEffectView } - blurredEffectViews.forEach { blurView in - blurView.removeFromSuperview() - } - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniColor.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniColor.swift deleted file mode 100644 index 63dd80025..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniColor.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// GiniColor.swift -// GiniHealth -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -import UIKit - -/** - The `GiniColor` class allows to customize color for the light and the dark modes. - */ - -@objc public class GiniColor : NSObject { - var lightModeColor: UIColor - var darkModeColor: UIColor - - /** - Creates a GiniColor with the colors for the light and dark modes - - - parameter lightModeColor: color for the light mode - - parameter darkModeColor: color for the dark mode - */ - public init(lightModeColor: UIColor, darkModeColor: UIColor) { - self.lightModeColor = lightModeColor - self.darkModeColor = darkModeColor - } - - func uiColor() -> UIColor { - if #available(iOS 13, *) { - return UIColor { (UITraitCollection: UITraitCollection) -> UIColor in - if UITraitCollection.userInterfaceStyle == .dark { - /// Return the color for Dark Mode - return self.darkModeColor - } else { - /// Return the color for Light Mode - return self.lightModeColor - } - } - } else { - /// Return a fallback color for iOS 12 and lower. - return self.lightModeColor - } - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealth+PaymentComponentsConfigurationProvider.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealth+PaymentComponentsConfigurationProvider.swift new file mode 100644 index 000000000..b7c4a912f --- /dev/null +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealth+PaymentComponentsConfigurationProvider.swift @@ -0,0 +1,175 @@ +// +// GiniHealth+PaymentComponentsConfigurationProvider.swift +// GiniHealthSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import UIKit +import GiniUtilites +import GiniInternalPaymentSDK + +extension GiniHealth: PaymentComponentsConfigurationProvider { + public var defaultStyleInputFieldConfiguration: TextFieldConfiguration { + GiniHealthConfiguration.shared.defaultStyleInputFieldConfiguration + } + + public var errorStyleInputFieldConfiguration: TextFieldConfiguration { + GiniHealthConfiguration.shared.errorStyleInputFieldConfiguration + } + + public var selectionStyleInputFieldConfiguration: TextFieldConfiguration { + GiniHealthConfiguration.shared.selectionStyleInputFieldConfiguration + } + + public var showPaymentReviewCloseButton: Bool { + false + } + + + public var paymentComponentButtonsHeight: CGFloat { + GiniHealthConfiguration.shared.paymentComponentButtonsHeight + } + + public var paymentReviewContainerConfiguration: PaymentReviewContainerConfiguration { + PaymentReviewContainerConfiguration( + errorLabelTextColor: GiniColor.feedback1.uiColor(), + errorLabelFont: GiniHealthConfiguration.shared.font(for: .captions2), + lockIcon: GiniHealthImage.lock.preferredUIImage(), + lockedFields: GiniHealthConfiguration.shared.useInvoiceWithoutDocument ? true : false, + showBanksPicker: true, + chevronDownIcon: GiniHealthImage.chevronDown.preferredUIImage(), + chevronDownIconColor: GiniColor(lightModeColorName: .light7, darkModeColorName: .light1).uiColor() + ) + } + + public var installAppConfiguration: InstallAppConfiguration { + InstallAppConfiguration( + titleAccentColor: GiniColor.standard2.uiColor(), + titleFont: GiniHealthConfiguration.shared.font(for: .subtitle1), + moreInformationFont: GiniHealthConfiguration.shared.font(for: .captions1), + moreInformationTextColor: GiniColor.standard3.uiColor(), + moreInformationAccentColor: GiniColor.standard3.uiColor(), + moreInformationIcon: GiniHealthImage.info.preferredUIImage(), + appStoreIcon: GiniHealthImage.appStore.preferredUIImage(), + bankIconBorderColor: GiniColor.standard5.uiColor() + ) + } + + public var bottomSheetConfiguration: BottomSheetConfiguration { + BottomSheetConfiguration( + backgroundColor: GiniColor.standard7.uiColor(), + rectangleColor: GiniColor.standard5.uiColor(), + dimmingBackgroundColor: GiniColor(lightModeColor: UIColor.black, darkModeColor: UIColor.white).uiColor().withAlphaComponent(0.4) + ) + } + + public var shareInvoiceConfiguration: ShareInvoiceConfiguration { + ShareInvoiceConfiguration( + titleFont: GiniHealthConfiguration.shared.font(for: .subtitle1), + titleAccentColor: GiniColor.standard2.uiColor(), + descriptionFont: GiniHealthConfiguration.shared.font(for: .captions1), + descriptionTextColor: GiniColor.standard3.uiColor(), + descriptionAccentColor: GiniColor.standard3.uiColor(), + paymentInfoBorderColor: GiniColor.standard5.uiColor(), + titlePaymentInfoTextColor: GiniColor.standard4.uiColor(), + subtitlePaymentInfoTextColor: GiniColor.standard1.uiColor(), + titlepaymentInfoFont: GiniHealthConfiguration.shared.font(for: .captions2), + subtitlePaymentInfoFont: GiniHealthConfiguration.shared.font(for: .body2) + ) + } + + public var paymentInfoConfiguration: PaymentInfoConfiguration { + PaymentInfoConfiguration( + giniFont: GiniHealthConfiguration.shared.font(for: .button), + answersFont: GiniHealthConfiguration.shared.font(for: .body2), + answerCellTextColor: GiniColor.standard1.uiColor(), + answerCellLinkColor: GiniColor.accent1.uiColor(), + questionsTitleFont: GiniHealthConfiguration.shared.font(for: .subtitle1), + questionsTitleColor: GiniColor.standard1.uiColor(), + questionHeaderFont: GiniHealthConfiguration.shared.font(for: .body1), + questionHeaderTitleColor: GiniColor.standard1.uiColor(), + questionHeaderMinusIcon: GiniHealthImage.minus.preferredUIImage(), + questionHeaderPlusIcon: GiniHealthImage.plus.preferredUIImage(), + bankCellBorderColor: GiniColor.standard5.uiColor(), + payBillsTitleFont: GiniHealthConfiguration.shared.font(for: .subtitle1), + payBillsTitleColor: GiniColor.standard1.uiColor(), + payBillsDescriptionFont: GiniHealthConfiguration.shared.font(for: .body2), + linksFont: GiniHealthConfiguration.shared.font(for: .linkBold), + linksColor: GiniColor.accent1.uiColor(), + separatorColor: GiniColor.standard5.uiColor(), + backgroundColor: GiniColor.standard7.uiColor() + ) + } + + public var bankSelectionConfiguration: BankSelectionConfiguration { + BankSelectionConfiguration( + descriptionAccentColor: GiniColor.standard3.uiColor(), + descriptionFont: GiniHealthConfiguration.shared.font(for: .captions1), + selectBankAccentColor: GiniColor.standard2.uiColor(), + selectBankFont: GiniHealthConfiguration.shared.font(for: .subtitle1), + closeTitleIcon: GiniHealthImage.close.preferredUIImage(), + closeIconAccentColor: GiniColor.standard2.uiColor(), + bankCellBackgroundColor: GiniColor.standard7.uiColor(), + bankCellIconBorderColor: GiniColor.standard5.uiColor(), + bankCellNameFont: GiniHealthConfiguration.shared.font(for: .body1), + bankCellNameAccentColor: GiniColor.standard1.uiColor(), + bankCellSelectedBorderColor: GiniColor.accent1.uiColor(), + bankCellNotSelectedBorderColor: GiniColor.standard5.uiColor(), + bankCellSelectionIndicatorImage: GiniHealthImage.selectionIndicator.preferredUIImage() + ) + } + + public var paymentComponentsConfiguration: PaymentComponentsConfiguration { + PaymentComponentsConfiguration( + selectYourBankLabelFont: GiniHealthConfiguration.shared.font(for: .subtitle2), + selectYourBankAccentColor: GiniColor.standard1.uiColor(), + chevronDownIcon: GiniHealthImage.chevronDown.preferredUIImage(), + chevronDownIconColor: GiniColor(lightModeColorName: .light7, darkModeColorName: .light1).uiColor(), + notInstalledBankTextColor: GiniColor.standard4.uiColor() + ) + } + + public var paymentReviewConfiguration: PaymentReviewConfiguration { + PaymentReviewConfiguration( + loadingIndicatorStyle: UIActivityIndicatorView.Style.large, + loadingIndicatorColor: GiniHealthColorPalette.accent1.preferredColor(), + infoBarLabelTextColor: GiniHealthColorPalette.dark7.preferredColor(), + infoBarBackgroundColor: GiniHealthColorPalette.success1.preferredColor(), + mainViewBackgroundColor: GiniColor.standard7.uiColor(), + infoContainerViewBackgroundColor: GiniColor.standard7.uiColor(), + paymentReviewClose: GiniHealthImage.paymentReviewClose.preferredUIImage(), + backgroundColor: GiniColor(lightModeColorName: .light7, darkModeColorName: .light7).uiColor(), + infoBarLabelFont: GiniHealthConfiguration.shared.font(for: .captions1), + statusBarStyle: GiniHealthConfiguration.shared.paymentReviewStatusBarStyle, + pageIndicatorTintColor: GiniColor.standard4.uiColor(), + currentPageIndicatorTintColor: GiniColor(lightModeColorName: .dark2, darkModeColorName: .light5).uiColor(), + isInfoBarHidden: GiniHealthConfiguration.shared.useInvoiceWithoutDocument ? true : false + ) + } + + public var poweredByGiniConfiguration: PoweredByGiniConfiguration { + PoweredByGiniConfiguration( + poweredByGiniLabelFont: GiniHealthConfiguration.shared.font(for: .captions2), + poweredByGiniLabelAccentColor: GiniColor.standard4.uiColor(), + giniIcon: GiniHealthImage.logo.preferredUIImage() + ) + } + + public var moreInformationConfiguration: MoreInformationConfiguration { + MoreInformationConfiguration( + moreInformationAccentColor: GiniColor.standard2.uiColor(), + moreInformationTextColor: GiniColor.standard4.uiColor(), + moreInformationLinkFont: GiniHealthConfiguration.shared.font(for: .captions2), + moreInformationIcon: GiniHealthImage.info.preferredUIImage() + ) + } + + public var primaryButtonConfiguration: GiniInternalPaymentSDK.ButtonConfiguration { + GiniHealthConfiguration.shared.primaryButtonConfiguration + } + + public var secondaryButtonConfiguration: GiniInternalPaymentSDK.ButtonConfiguration { + GiniHealthConfiguration.shared.secondaryButtonConfiguration + } +} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealth+PaymentComponentsStringsProvider.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealth+PaymentComponentsStringsProvider.swift new file mode 100644 index 000000000..1a5cda919 --- /dev/null +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealth+PaymentComponentsStringsProvider.swift @@ -0,0 +1,163 @@ +// +// GiniHealth+PaymentComponentsStringsProvider.swift +// GiniHealthSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import GiniInternalPaymentSDK + +extension GiniHealth: PaymentComponentsStringsProvider { + public var paymentReviewContainerStrings: PaymentReviewContainerStrings { + PaymentReviewContainerStrings( + emptyCheckErrorMessage: NSLocalizedStringPreferredFormat("gini.health.errors.failed.default.textfield.validation.check", + comment: "the field failed non empty check"), + ibanCheckErrorMessage: NSLocalizedStringPreferredFormat("gini.health.errors.failed.iban.validation.check", + comment: "iban failed validation check"), + recipientFieldPlaceholder: NSLocalizedStringPreferredFormat("gini.health.reviewscreen.recipient.placeholder", + comment: "placeholder text for recipient input field"), + ibanFieldPlaceholder: NSLocalizedStringPreferredFormat("gini.health.reviewscreen.iban.placeholder", + comment: "placeholder text for iban input field"), + amountFieldPlaceholder: NSLocalizedStringPreferredFormat("gini.health.reviewscreen.amount.placeholder", + comment: "placeholder text for amount input field"), + usageFieldPlaceholder: NSLocalizedStringPreferredFormat("gini.health.reviewscreen.usage.placeholder", + comment: "placeholder text for usage input field"), + recipientErrorMessage: NSLocalizedStringPreferredFormat("gini.health.errors.failed.recipient.non.empty.check", + comment: "recipient failed non empty check"), + ibanErrorMessage: NSLocalizedStringPreferredFormat("gini.health.errors.failed.iban.non.empty.check", + comment: "iban failed non empty check"), + amountErrorMessage: NSLocalizedStringPreferredFormat("gini.health.errors.failed.amount.non.empty.check", + comment: "amount failed non empty check"), + purposeErrorMessage: NSLocalizedStringPreferredFormat("gini.health.errors.failed.purpose.non.empty.check", + comment: "purpose failed non empty check"), + payInvoiceLabelText: NSLocalizedStringPreferredFormat("gini.health.reviewscreen.banking.app.button.label", + comment: "Title label used for the pay invoice button default") + ) + } + + public var paymentComponentsStrings: PaymentComponentsStrings { + PaymentComponentsStrings( + selectYourBankLabelText: NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.select.your.bank.label", + comment: "Text for the select your bank label that's above the payment provider picker"), + placeholderBankNameText: NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.your.bank.label", + comment: "Placeholder text used when there isn't a payment provider app installed"), + ctaLabelText: GiniHealthConfiguration.shared.showPaymentReviewScreen ? + NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.continue.to.overview.label", + comment: "Title label used for the pay invoice button when overview is available") : + NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.to.banking.app.label", + comment: "Title label used for the pay invoice button when you jump to the banking app") + ) + } + + public var installAppStrings: InstallAppStrings { + InstallAppStrings( + titlePattern: NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.install.app.bottom.sheet.title", + comment: "Install App Bottom sheet title"), + moreInformationTipPattern: NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.install.app.bottom.sheet.tip.description", + comment: "Text for tip information label"), + moreInformationNotePattern: NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.install.app.bottom.sheet.notes.description", + comment: "Text for notes information label"), + continueLabelText: NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.install.app.bottom.sheet.continue.button.text", + comment: "Title label used for the Continue button") + ) + } + + public var shareInvoiceStrings: ShareInvoiceStrings { + ShareInvoiceStrings( + continueLabelText: NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.share.invoice.bottom.sheet.continue.button.text", + comment: "Title label used for the Continue button"), + titleTextPattern: NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.share.invoice.bottom.sheet.title", + comment: "Share Invoice Bottom sheet title"), + descriptionTextPattern: NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.share.invoice.bottom.sheet.description", + comment: "Text description for share bottom sheet"), + recipientLabelText: NSLocalizedStringPreferredFormat("gini.health.reviewscreen.recipient.placeholder", + comment: "placeholder text for recipient input field"), + amountLabelText: NSLocalizedStringPreferredFormat("gini.health.reviewscreen.amount.placeholder", + comment: "placeholder text for amount input field"), + ibanLabelText: NSLocalizedStringPreferredFormat("gini.health.reviewscreen.iban.placeholder", + comment: "placeholder text for iban input field"), + purposeLabelText: NSLocalizedStringPreferredFormat("gini.health.reviewscreen.usage.placeholder", + comment: "placeholder text for usage input field") + ) + } + + public var paymentInfoStrings: PaymentInfoStrings { + PaymentInfoStrings( + giniWebsiteText: NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.payment.info.pay.bills.description.clickable.text", + comment: "Word range that's clickable in pay bills description"), + giniURLText: NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.payment.info.gini.link", + comment: "Gini website link url"), + questionsTitleText: NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.payment.info.questions.title.label", + comment: "Payment Info questions title label text"), + answerPrivacyPolicyText: NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.payment.info.questions.answer.clickable.text", + comment: "Payment info answers clickable privacy policy"), + privacyPolicyURLText: NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.payment.info.gini.privacypolicy.link", + comment: "Gini privacy policy link url"), + titleText: NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.payment.info.title.label", + comment: "Payment Info title label text"), + payBillsTitleText: NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.payment.info.pay.bills.title.label", + comment: "Payment Info pay bills title label text"), + payBillsDescriptionText: NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.payment.info.pay.bills.description.label", + comment: "Payment Info pay bills description text"), + answers: [NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.payment.info.questions.answer.1", + comment: "Answers description for question 1"), + NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.payment.info.questions.answer.2", + comment: "Answers description for question 2"), + NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.payment.info.questions.answer.3", + comment: "Answers description for question 3"), + NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.payment.info.questions.answer.4", + comment: "Answers description for question 4"), + NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.payment.info.questions.answer.5", + comment: "Answers description for question 5"), + NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.payment.info.questions.answer.6", + comment: "Answers description for question 6")], + questions: [NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.payment.info.questions.question.1", + comment: "Questions titles for question 1"), + NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.payment.info.questions.question.2", + comment: "Questions titles for question 2"), + NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.payment.info.questions.question.3", + comment: "Questions titles for question 3"), + NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.payment.info.questions.question.4", + comment: "Questions titles for question 4"), + NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.payment.info.questions.question.5", + comment: "Questions titles for question 5"), + NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.payment.info.questions.question.6", + comment: "Questions titles for question 6")] + ) + } + + public var banksBottomStrings: BanksBottomStrings { + BanksBottomStrings( + selectBankTitleText: NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.select.bank.label", + comment: "Select bank text from the top label on payment providers bottom sheet"), + descriptionText: NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.payment.providers.list.description", + comment: "Top description text on payment providers bottom sheet") + ) + } + + public var paymentReviewStrings: PaymentReviewStrings { + PaymentReviewStrings( + alertOkButtonTitle: NSLocalizedStringPreferredFormat("gini.health.alert.ok.title", + comment: "ok title for action"), + infoBarMessage: NSLocalizedStringPreferredFormat("gini.health.reviewscreen.infobar.message", + comment: "info bar message"), + defaultErrorMessage: NSLocalizedStringPreferredFormat("gini.health.errors.default", + comment: "default error message"), + createPaymentErrorMessage: NSLocalizedStringPreferredFormat("gini.health.errors.failed.payment.request.creation", + comment: "error for creating payment request") + ) + } + + public var poweredByGiniStrings: PoweredByGiniStrings { + PoweredByGiniStrings( + poweredByGiniText: NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.powered.by.gini.label", comment: "") + ) + } + + public var moreInformationStrings: MoreInformationStrings { + MoreInformationStrings( + moreInformationActionablePartText: NSLocalizedStringPreferredFormat("gini.health.paymentcomponent.more.information.underlined.part", + comment: "Text for more information actionable part from the label") + ) + } +} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealth.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealth.swift index 95d00113f..08277ce28 100644 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealth.swift +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealth.swift @@ -7,6 +7,8 @@ import UIKit import GiniHealthAPILibrary +import GiniUtilites +import GiniInternalPaymentSDK /** Delegate to inform about the current status of the Gini Health SDK. @@ -18,9 +20,9 @@ public protocol GiniHealthDelegate: AnyObject { /** Called when the payment request was successfully created - - parameter paymentRequestID: Id of created payment request. + - parameter paymentRequestId: Id of created payment request. */ - func didCreatePaymentRequest(paymentRequestID: String) + func didCreatePaymentRequest(paymentRequestId: String) /** Error handling. If delegate is set and error is going to be handled internally the method should return true. @@ -64,20 +66,112 @@ public struct DataForReview { public var documentService: DefaultDocumentService /// reponsible for the payment processing. public var paymentService: PaymentService - private var bankProviders: [PaymentProvider] = [] + /// delegate to inform about the current status of the Gini Health SDK. public weak var delegate: GiniHealthDelegate? + /// delegate to inform about the changes into PaymentComponentsController + public weak var paymentDelegate: PaymentComponentsControllerProtocol? + + private var bankProviders: [PaymentProvider] = [] + + /// Configuration for the payment component, controlling its branding and display options. + public var paymentComponentConfiguration: PaymentComponentConfiguration = PaymentComponentConfiguration(isPaymentComponentBranded: true, + showPaymentComponentInOneRow: false, + hideInfoForReturningUser: (GiniHealthConfiguration.shared.showPaymentReviewScreen ? false : true)) + + public var paymentComponentsController: PaymentComponentsController! + + /** + Initializes a new instance of GiniHealth. + + This initializer creates a GiniHealth instance by first constructing a Client object with the provided client credentials (id, secret, domain) + + - Parameters: + - id: The client ID provided by Gini when you register your application. This is a unique identifier for your application. + - secret: The client secret provided by Gini alongside the client ID. This is used to authenticate your application to the Gini API. + - domain: The domain associated with your client credentials. This is used to scope the client credentials to a specific domain. + - logLevel: The log level. `LogLevel.none` by default. + */ + public init(id: String, + secret: String, + domain: String, + apiVersion: Int = Constants.defaultVersionAPI, + logLevel: LogLevel = .none) { + let client = Client(id: id, secret: secret, domain: domain) + self.giniApiLib = GiniHealthAPI.Builder(client: client, api: .default, logLevel: logLevel.toHealthLogLevel()).build() + self.documentService = DefaultDocumentService(docService: giniApiLib.documentService()) + self.paymentService = giniApiLib.paymentService(apiDomain: APIDomain.default, apiVersion: apiVersion) + super.init() + self.paymentComponentsController = PaymentComponentsController(giniHealth: self) + self.paymentComponentsController.delegate = self + } /** - Returns a GiniHealth instance + Initializes a new instance of GiniHealth. - - parameter giniApiLib: GiniHealthAPI initialized with client's credentials + This initializer creates a GiniHealth instance by first constructing a Client object with the provided client credentials (id, secret, domain) + + - Parameters: + - id: The client ID provided by Gini when you register your application. This is a unique identifier for your application. + - secret: The client secret provided by Gini alongside the client ID. This is used to authenticate your application to the Gini API. + - domain: The domain associated with your client credentials. This is used to scope the client credentials to a specific domain. + - pinningConfig: Configuration for certificate pinning. Format ["PinnedDomains" : ["PublicKeyHashes"]] + - logLevel: The log level. `LogLevel.none` by default. + */ + public init(id: String, + secret: String, + domain: String, + apiVersion: Int = Constants.defaultVersionAPI, + pinningConfig: [String: [String]], + logLevel: LogLevel = .none) { + let client = Client(id: id, secret: secret, domain: domain, apiVersion: apiVersion) + self.giniApiLib = GiniHealthAPI.Builder(client: client, + pinningConfig: pinningConfig, + logLevel: logLevel.toHealthLogLevel()).build() + self.documentService = DefaultDocumentService(docService: giniApiLib.documentService()) + self.paymentService = giniApiLib.paymentService(apiDomain: APIDomain.default, apiVersion: apiVersion) + super.init() + self.paymentComponentsController = PaymentComponentsController(giniHealth: self) + self.paymentComponentsController.delegate = self + } + + /** + Initializes a new instance of GiniHealth. + + - Parameter giniApiLib: The GiniHealthAPI instance used for document and payment services. */ - public init(with giniApiLib: GiniHealthAPI){ + public init(giniApiLib: GiniHealthAPI) { self.giniApiLib = giniApiLib - self.documentService = giniApiLib.documentService() - self.paymentService = giniApiLib.paymentService() + self.documentService = DefaultDocumentService(docService: giniApiLib.documentService()) + self.paymentService = giniApiLib.paymentService(apiDomain: .default, apiVersion: Constants.defaultVersionAPI) + super.init() + self.paymentComponentsController = PaymentComponentsController(giniHealth: self) } + + /** + Initiates the payment flow for a specified document and payment information. + - Parameters: + - documentId: An optional identifier for the document associated id with the payment flow. + - paymentInfo: An optional `PaymentInfo` object containing the payment details. + - navigationController: The `UINavigationController` used to present subsequent view controllers in the payment flow. + - trackingDelegate: The `GiniHealthTrackingDelegate` provides event information that happens on PaymentReviewScreen. + + This method sets up the payment flow by storing the provided document ID, payment information, and navigation controller. + If a `selectedPaymentProvider` is available, it either presents the payment review screen or the payment view bottom sheet, + depending on the configuration. If no payment provider is selected, it directly presents the payment view bottom sheet. + */ + public func startPaymentFlow(documentId: String?, paymentInfo: GiniHealthSDK.PaymentInfo?, navigationController: UINavigationController, trackingDelegate: GiniHealthTrackingDelegate?) { + paymentComponentsController.startPaymentFlow(documentId: documentId, paymentInfo: paymentInfo, navigationController: navigationController, trackingDelegate: trackingDelegate) + } + + /** + Fetches bank logos for the available payment providers. + + - Returns: A tuple containing an array of logo data and the count of additional banks, if any. + */ + public func fetchBankLogos() -> (logos: [Data]?, additionalBankCount: Int?) { + return paymentComponentsController.fetchBankLogos() + } /** Getting a list of the installed banking apps which support Gini Pay Connect functionality. @@ -91,32 +185,31 @@ public struct DataForReview { */ private func fetchInstalledBankingApps(completion: @escaping (Result) -> Void) { fetchBankingApps { result in - switch result { - case .success(let providers): - for provider in providers { - DispatchQueue.main.async { - if let url = URL(string:provider.appSchemeIOS) { - if UIApplication.shared.canOpenURL(url) { - self.bankProviders.append(provider) - } - } - } - } - DispatchQueue.main.async { + DispatchQueue.main.async { + switch result { + case .success(let providers): + self.updateBankProviders(providers: providers) + if self.bankProviders.count > 0 { completion(.success(self.bankProviders)) } else { completion(.failure(.noInstalledApps)) } - } - case let .failure(error): - DispatchQueue.main.async { - completion(.failure(error)) + case let .failure(error): + completion(.failure(GiniHealthError.apiError(error))) } } } } - + + private func updateBankProviders(providers: PaymentProviders) { + for provider in providers { + if let url = URL(string:provider.appSchemeIOS), UIApplication.shared.canOpenURL(url) { + self.bankProviders.append(provider) + } + } + } + /** Getting a list of the banking apps supported by SDK @@ -127,14 +220,14 @@ public struct DataForReview { In case of failure error provided by API. */ - public func fetchBankingApps(completion: @escaping (Result) -> Void) { + public func fetchBankingApps(completion: @escaping (Result) -> Void) { paymentService.paymentProviders { result in switch result { case let .success(providers): - self.bankProviders = providers + self.bankProviders = providers.map { PaymentProvider(healthPaymentProvider: $0) } completion(.success(self.bankProviders)) case let .failure(error): - completion(.failure(.apiError(error))) + completion(.failure(GiniError.decorator(error))) } } } @@ -328,14 +421,14 @@ public struct DataForReview { In case of failure error from the server side. */ - public func createPaymentRequest(paymentInfo: PaymentInfo, completion: @escaping (Result) -> Void) { + public func createPaymentRequest(paymentInfo: GiniInternalPaymentSDK.PaymentInfo, completion: @escaping (Result) -> Void) { paymentService.createPaymentRequest(sourceDocumentLocation: "", paymentProvider: paymentInfo.paymentProviderId, recipient: paymentInfo.recipient, iban: paymentInfo.iban, bic: "", amount: paymentInfo.amount, purpose: paymentInfo.purpose) { result in DispatchQueue.main.async { switch result { - case let .success(requestID): - completion(.success(requestID)) + case let .success(requestId): + completion(.success(requestId)) case let .failure(error): - completion(.failure(.apiError(error))) + completion(.failure(GiniError.decorator(error))) } } } @@ -346,7 +439,7 @@ public struct DataForReview { openUrl called on main thread. - Parameters: - - requestID: Id of the created payment request. + - requestId: Id of the created payment request. - universalLink: Universal link for the selected payment provider */ public func openPaymentProviderApp(requestID: String, universalLink: String, urlOpener: URLOpener = URLOpener(UIApplication.shared), completion: GiniOpenLinkCompletionBlock? = nil) { @@ -432,11 +525,53 @@ public struct DataForReview { } } } + + /** + Retrieves a payment request by ID. + + - Parameters: + - id: The ID of the payment request to retrieve. + - completion: An action for processing asynchronous data received from the service with Result type as a parameter. Result is a value that represents either a success or a failure, including an associated value in each case. + Completion block called on main thread. + In success, it includes the retrieved payment request. + In case of failure, error from the server side. + + */ + public func getPaymentRequest(by id: String, + completion: @escaping (Result) -> Void) { + paymentService.paymentRequest(id: id) { result in + DispatchQueue.main.async { + switch result { + case let .success(paymentRequest): + completion(.success(paymentRequest)) + case let .failure(error): + completion(.failure(GiniError.decorator(error))) + } + } + } + } + + /// A static string representing the current version of the Gini Health SDK. + public static var versionString: String { + return GiniHealthSDKVersion + } +} + +extension GiniHealth: PaymentComponentsControllerProtocol { + public func isLoadingStateChanged(isLoading: Bool) { + paymentDelegate?.isLoadingStateChanged(isLoading: isLoading) + } + + public func didFetchedPaymentProviders() { + paymentDelegate?.didFetchedPaymentProviders() + } + + } extension GiniHealth { - private enum Constants { + public enum Constants { + public static let defaultVersionAPI = 4 static let hasMultipleDocuments = "true" } } - diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealthColors.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealthColors.swift new file mode 100644 index 000000000..8ec0a67e4 --- /dev/null +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealthColors.swift @@ -0,0 +1,84 @@ +// +// GiniHealthColors.swift +// GiniHealthSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import UIKit +import GiniUtilites + +enum GiniHealthColorPalette: String { + case accent1 = "Accent01" + case accent2 = "Accent02" + case accent3 = "Accent03" + case accent4 = "Accent04" + case accent5 = "Accent05" + + case dark1 = "Dark01" + case dark2 = "Dark02" + case dark3 = "Dark03" + case dark4 = "Dark04" + case dark5 = "Dark05" + case dark6 = "Dark06" + case dark7 = "Dark07" + + case light1 = "Light01" + case light2 = "Light02" + case light3 = "Light03" + case light4 = "Light04" + case light5 = "Light05" + case light6 = "Light06" + case light7 = "Light07" + + case feedback1 = "Feedback01" + case feedback2 = "Feedback02" + case feedback3 = "Feedback03" + case feedback4 = "Feedback04" + + case success1 = "Success01" + case success2 = "Success02" + case success3 = "Success03" + case success4 = "Success04" +} + +extension GiniHealthColorPalette { + func preferredColor() -> UIColor { + let name = self.rawValue + if let mainBundleColor = UIColor(named: name, in: Bundle.main, compatibleWith: nil) { + return mainBundleColor + } + guard let color = UIColor(named: name, in: giniHealthBundleResource(), compatibleWith: nil) else { + fatalError("The color named '\(name)' does not exist.") + } + return color + } +} + +extension GiniColor { + static let standard1 = GiniColor(lightModeColorName: .dark1, darkModeColorName: .light1) + static let standard2 = GiniColor(lightModeColorName: .dark2, darkModeColorName: .light2) + static let standard3 = GiniColor(lightModeColorName: .dark3, darkModeColorName: .light3) + static let standard4 = GiniColor(lightModeColorName: .dark4, darkModeColorName: .light4) + static let standard5 = GiniColor(lightModeColorName: .dark5, darkModeColorName: .light5) + static let standard6 = GiniColor(lightModeColorName: .dark6, darkModeColorName: .light6) + static let standard7 = GiniColor(lightModeColorName: .dark7, darkModeColorName: .light7) + + static let accent1 = GiniColor(lightModeColorName: .accent1, darkModeColorName: .accent1) + + static let feedback1 = GiniColor(lightModeColorName: .feedback1, darkModeColorName: .feedback1) +} + +extension GiniColor { + /** + Creates a GiniColor with the color names for the light and dark modes + + - parameter lightModeColorName: color name for the light mode + - parameter darkModeColorName: color name for the dark mode + */ + convenience init(lightModeColorName: GiniHealthColorPalette, darkModeColorName: GiniHealthColorPalette) { + let lightColor = lightModeColorName.preferredColor() + let darkColor = darkModeColorName.preferredColor() + self.init(lightModeColor: lightColor, darkModeColor: darkColor) + } +} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealthConfiguration.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealthConfiguration.swift index cef5fd6aa..74786f886 100644 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealthConfiguration.swift +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealthConfiguration.swift @@ -1,16 +1,18 @@ // // GiniHealthConfiguration.swift -// GiniHealth +// GiniHealthSDK // // Copyright © 2024 Gini GmbH. All rights reserved. // import UIKit +import GiniUtilites +import GiniInternalPaymentSDK /** The `GiniHealthConfiguration` class allows customizations to the look of the Gini Health SDK. If there are limitations regarding which API can be used, this is clearly stated for the specific attribute. - + - note: Text can also be set by using the appropriate keys in a `Localizable.strings` file in the projects bundle. The library will prefer whatever value is set in the following order: attribute in configuration, key in strings file in project bundle, key in strings file in `GiniHealth` bundle. @@ -19,32 +21,30 @@ import UIKit in project bundle, asset file in `GiniHealth` bundle. See the avalible images for overriding in `GiniImages.xcassets`. */ public final class GiniHealthConfiguration: NSObject { - + private let fontProvider = FontProvider() + /** Singleton to make configuration internally accessible in all classes of the Gini Health SDK. */ - static var shared = GiniHealthConfiguration() - + public static var shared = GiniHealthConfiguration() + /** Returns a `GiniHealthConfiguration` instance which allows to set individual configurations to change the look and feel of the Gini Health SDK. - + - returns: Instance of `GiniHealthConfiguration`. */ - public override init() {} - - // MARK: - Payment review screen + public override init() { + super.init() + } + + // MARK: - Payment component view /** - Set to `true` to show a close button on the payment review screen. - */ - @objc public var showPaymentReviewCloseButton = false - - /** - Sets the status bar style on the payment review screen. Only if `View controller-based status bar appearance` = `YES` in info.plist. + Set to `true` to use the payment component view as bottom view */ - @objc public var paymentReviewStatusBarStyle: UIStatusBarStyle = .default - + public var useBottomPaymentComponentView = true + /** Height of the buttons from the Payment Component View */ @@ -55,14 +55,22 @@ public final class GiniHealthConfiguration: NSObject { } } } - + + // MARK: - Payment review screen + + /** + Set to `false` to hide the payment review screen and jump straight to payment + */ + public var showPaymentReviewScreen = true + // MARK: - Button configuration options /** A configuration that defines the appearance of the primary button, including its background color, border color, title color, shadow color, corner radius, border width, shadow radius, and whether to apply a blur effect. It is used for buttons on different UI elements: Payment Component View, Payment Review Screen. */ - public lazy var primaryButtonConfiguration = ButtonConfiguration(backgroundColor: .GiniHealthColors.accent1.withAlphaComponent(0.4), + public lazy var primaryButtonConfiguration = ButtonConfiguration(backgroundColor: GiniHealthColorPalette.accent1.preferredColor().withAlphaComponent(0.4), borderColor: .clear, titleColor: .white, + titleFont: font(for: .button), shadowColor: .clear, cornerRadius: 12, borderWidth: 0, @@ -71,65 +79,49 @@ public final class GiniHealthConfiguration: NSObject { /** A configuration that defines the appearance of the secondary button, including its background color, border color, title color, shadow color, corner radius, border width, shadow radius, and whether to apply a blur effect. It is used for buttons on different UI elements: Payment Component View. */ - public lazy var secondaryButtonConfiguration = ButtonConfiguration(backgroundColor: GiniColor(lightModeColor: UIColor.GiniHealthColors.dark6, - darkModeColor: UIColor.GiniHealthColors.light6).uiColor(), - borderColor: GiniColor(lightModeColor: UIColor.GiniHealthColors.dark5, - darkModeColor: UIColor.GiniHealthColors.light5).uiColor(), - titleColor: GiniColor(lightModeColor: UIColor.GiniHealthColors.dark1, - darkModeColor: UIColor.GiniHealthColors.light1).uiColor(), + public lazy var secondaryButtonConfiguration = ButtonConfiguration(backgroundColor: GiniColor.standard6.uiColor(), + borderColor: GiniColor.standard5.uiColor(), + titleColor: GiniColor.standard1.uiColor(), + titleFont: font(for: .input), shadowColor: .clear, cornerRadius: 12, borderWidth: 1, shadowRadius: 0, withBlurEffect: true) - + // MARK: - Shared properties /** A default style configuration that defines the appearance of the text field, including its background color, border color, text color, corner radius, border width and the placeholder foreground color. It is used for input text fields on Payment Review Screen. */ - public lazy var defaultStyleInputFieldConfiguration = TextFieldConfiguration(backgroundColor: GiniColor(lightModeColor: UIColor.GiniHealthColors.dark6, - darkModeColor: UIColor.GiniHealthColors.light6).uiColor(), - borderColor: GiniColor(lightModeColor: UIColor.GiniHealthColors.dark5, - darkModeColor: UIColor.GiniHealthColors.light5).uiColor(), - textColor: GiniColor(lightModeColor: UIColor.GiniHealthColors.dark1, - darkModeColor: UIColor.GiniHealthColors.light1).uiColor(), + public lazy var defaultStyleInputFieldConfiguration = TextFieldConfiguration(backgroundColor: GiniColor.standard6.uiColor(), + borderColor: GiniColor.standard5.uiColor(), + textColor: GiniColor.standard1.uiColor(), + textFont: font(for: .captions2), cornerRadius: 12.0, borderWidth: 1.0, - placeholderForegroundColor: GiniColor(lightModeColor: UIColor.GiniHealthColors.dark4, - darkModeColor: UIColor.GiniHealthColors.light4).uiColor()) + placeholderForegroundColor: GiniColor.standard4.uiColor()) /** A error style configuration that defines the appearance of the text field, including its background color, border color, text color, corner radius, border width and the placeholder foreground color. It is used for input text fields on Payment Review Screen. */ - public lazy var errorStyleInputFieldConfiguration = TextFieldConfiguration(backgroundColor: GiniColor(lightModeColor: UIColor.GiniHealthColors.dark6, - darkModeColor: UIColor.GiniHealthColors.light6).uiColor(), - borderColor: GiniColor(lightModeColor: UIColor.GiniHealthColors.feedback1, - darkModeColor: UIColor.GiniHealthColors.feedback1).uiColor(), - textColor: GiniColor(lightModeColor: UIColor.GiniHealthColors.dark1, - darkModeColor: UIColor.GiniHealthColors.light1).uiColor(), - cornerRadius: 12.0, - borderWidth: 1.0, - placeholderForegroundColor: GiniColor(lightModeColor: UIColor.GiniHealthColors.dark4, - darkModeColor: UIColor.GiniHealthColors.light4).uiColor()) + public lazy var errorStyleInputFieldConfiguration = TextFieldConfiguration(backgroundColor: GiniColor.standard6.uiColor(), + borderColor: GiniColor(lightModeColorName: .feedback1, darkModeColorName: .feedback1).uiColor(), + textColor: GiniColor.standard1.uiColor(), + textFont: font(for: .captions2), + cornerRadius: 12.0, + borderWidth: 1.0, + placeholderForegroundColor: GiniColor.standard4.uiColor()) /** A selection style configuration that defines the appearance of the text field, including its background color, border color, text color, corner radius, border width and the placeholder foreground color. It is used for input text fields on Payment Review Screen. */ - public lazy var selectionStyleInputFieldConfiguration = TextFieldConfiguration(backgroundColor: GiniColor(lightModeColor: UIColor.GiniHealthColors.dark6, - darkModeColor: UIColor.GiniHealthColors.light6).uiColor(), - borderColor: GiniColor(lightModeColor: UIColor.GiniHealthColors.accent1, - darkModeColor: UIColor.GiniHealthColors.accent1).uiColor(), - textColor: GiniColor(lightModeColor: UIColor.GiniHealthColors.dark1, - darkModeColor: UIColor.GiniHealthColors.light1).uiColor(), - cornerRadius: 12.0, - borderWidth: 1.0, - placeholderForegroundColor: GiniColor(lightModeColor: UIColor.GiniHealthColors.dark4, - darkModeColor: UIColor.GiniHealthColors.light4).uiColor()) - - /** - Custom localization configuration for localizable strings. - */ - public var customLocalization: GiniLocalization? - + public lazy var selectionStyleInputFieldConfiguration = TextFieldConfiguration(backgroundColor: GiniColor.standard6.uiColor(), + borderColor: GiniColor.accent1.uiColor(), + textColor: GiniColor.standard1.uiColor(), + textFont: font(for: .captions2), + cornerRadius: 12.0, + borderWidth: 1.0, + placeholderForegroundColor: GiniColor.standard4.uiColor()) + // MARK: - Update to custom font /** Allows setting a custom font for specific text styles. The change will affect all screens where a specific text style was used. @@ -138,26 +130,25 @@ public final class GiniHealthConfiguration: NSObject { - parameter textStyle: Constants that describe the preferred styles for fonts. Please, find additional information [here](https://developer.apple.com/documentation/uikit/uifont/textstyle) */ public func updateFont(_ font: UIFont, for textStyle: UIFont.TextStyle) { - textStyleFonts[textStyle] = font + fontProvider.updateFont(font, for: textStyle) } - + + public func font(for textStyle: UIFont.TextStyle) -> UIFont { + return fontProvider.font(for: textStyle) + } + + // We will switch this option internally to stil handle documents with extractions on GiniHealthSDK and still handle invoices without document on GiniHealthSDK + public var useInvoiceWithoutDocument: Bool = true + /** - Set dictionary of fonts for available text styles. Used internally. + Custom localization configuration for localizable strings. + */ + public var customLocalization: GiniLocalization? + + /** + Sets the status bar style on the payment review screen. Only if `View controller-based status bar appearance` = `YES` in info.plist. */ - var textStyleFonts: [UIFont.TextStyle: UIFont] = [ - .headline1: UIFontMetrics(forTextStyle: .headline1).scaledFont(for: UIFont.systemFont(ofSize: 26, weight: .regular)), - .headline2: UIFontMetrics(forTextStyle: .headline2).scaledFont(for: UIFont.systemFont(ofSize: 20, weight: .bold)), - .headline3: UIFontMetrics(forTextStyle: .headline3).scaledFont(for: UIFont.systemFont(ofSize: 18, weight: .bold)), - .caption1: UIFontMetrics(forTextStyle: .caption1).scaledFont(for: UIFont.systemFont(ofSize: 13, weight: .regular)), - .caption2: UIFontMetrics(forTextStyle: .caption2).scaledFont(for: UIFont.systemFont(ofSize: 12, weight: .regular)), - .linkBold: UIFontMetrics(forTextStyle: .linkBold).scaledFont(for: UIFont.systemFont(ofSize: 14, weight: .bold)), - .subtitle1: UIFontMetrics(forTextStyle: .subtitle1).scaledFont(for: UIFont.systemFont(ofSize: 16, weight: .bold)), - .subtitle2: UIFontMetrics(forTextStyle: .subtitle2).scaledFont(for: UIFont.systemFont(ofSize: 14, weight: .medium)), - .input: UIFontMetrics(forTextStyle: .input).scaledFont(for: UIFont.systemFont(ofSize: 16, weight: .medium)), - .button: UIFontMetrics(forTextStyle: .button).scaledFont(for: UIFont.systemFont(ofSize: 16, weight: .bold)), - .body1: UIFontMetrics(forTextStyle: .body1).scaledFont(for: UIFont.systemFont(ofSize: 16, weight: .regular)), - .body2: UIFontMetrics(forTextStyle: .body2).scaledFont(for: UIFont.systemFont(ofSize: 14, weight: .regular)), - ] + @objc public var paymentReviewStatusBarStyle: UIStatusBarStyle = .default } extension GiniHealthConfiguration { diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealthImage.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealthImage.swift new file mode 100644 index 000000000..bf3151847 --- /dev/null +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealthImage.swift @@ -0,0 +1,51 @@ +// +// GiniHealthImage.swift +// GiniHealthSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + +import UIKit + +/** + The GiniHealthImage enumeration provides a convenient way to manage image assets within the Gini SDK, supporting customization for both light and dark modes. Each case in the enumeration represents a specific image asset used by the SDK. + + - Note: The raw values for each case correspond to the image asset names in the asset catalog. + */ + +public enum GiniHealthImage: String { + case logo = "gh.giniLogo" + case info = "gh.infoCircle" + case close = "gh.close" + case more = "gh.more" + case plus = "gh.plus" + case minus = "gh.minus" + case appStore = "gh.appStoreIcon" + case chevronDown = "gh.iconChevronDown" + case selectionIndicator = "gh.selectionIndicator" + case paymentReviewClose = "gh.paymentReviewClose" + case lock = "gh.iconInputLock" + + /** + Retrieves an image corresponding to the enumeration case, prioritizing the client's bundle. If the image is not found in the client's bundle, it attempts to load the image from the Gini Health SDK bundle. + + - Returns: An UIImage instance corresponding to the enumeration case. If the image cannot be found in either the client's bundle or the Gini Health SDK bundle, the method triggers a runtime error. + */ + public func preferredUIImage() -> UIImage { + return UIImage(named: self.rawValue) ?? defaultImage() + } +} + +//MARK: - Private +private extension GiniHealthImage { + func defaultImage() -> UIImage { + guard let image = UIImage(named: self.rawValue, in: giniHealthBundle(), compatibleWith: nil) else { + fatalError("Health SDK: Image \(self.rawValue) not found") + } + return image + } +} + + + diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealthUtils.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealthUtils.swift index f44a6398c..14c107485 100644 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealthUtils.swift +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/GiniHealthUtils.swift @@ -1,19 +1,27 @@ // // GiniHealthUtils.swift -// GiniHealth +// GiniHealthSDK // // Copyright © 2024 Gini GmbH. All rights reserved. // import UIKit +import GiniUtilites +/** + Returns the GiniHealth bundle. + */ +public func giniHealthBundle() -> Bundle { + Bundle.module +} /** Returns an optional `UIImage` instance with the given `name` preferably from the client's bundle. - + - parameter name: The name of the image file without file extension. - + - returns: Image if found with name. */ + func UIImageNamedPreferred(named name: String) -> UIImage? { if let clientImage = UIImage(named: name) { return clientImage @@ -37,80 +45,18 @@ func decimal(from inputFieldString: String) -> Decimal? { } /** - A help price structure with decimal value and currency code, used in amout inpur field. - */ + Returns a localized string resource preferably from the client's bundle. -public struct Price { - // Decimal value - var value: Decimal - // Currency code - let currencyCode: String - - /** - Returns a price structure with decimal value and currency code from extraction string - - - parameter extractionString: extracted string - */ - - init(value: Decimal, currencyCode: String) { - self.value = value - self.currencyCode = currencyCode - } - - /** - Returns a price structure with decimal value and currency code from extraction string - - - parameter extractionString: extracted string - */ - - public init?(extractionString: String) { - - let components = extractionString.components(separatedBy: ":") - - guard components.count == 2 else { return nil } - - guard let decimal = Decimal(string: components.first ?? "", locale: Locale(identifier: "en")), - let currencyCode = components.last?.lowercased() else { - return nil - } - - self.value = decimal - self.currencyCode = currencyCode - } - - // Formatted string with currency code for sending to the Gini Health Api - var extractionString: String { - return "\(value):\(currencyCode.uppercased())" - } - - // Currency symbol - var currencySymbol: String? { - return (Locale.current as NSLocale).displayName(forKey: NSLocale.Key.currencySymbol, - value: currencyCode.uppercased()) - } - - // Formatted string with currency symbol - public var string: String? { - - let result = (Price.stringWithoutSymbol(from: value) ?? "") + " " + (currencySymbol ?? "") - - if result.isEmpty { return nil } - - return result - } - // Formatted string without currency symbol - var stringWithoutSymbol: String? { - return Price.stringWithoutSymbol(from: value) - } - - static func stringWithoutSymbol(from value: Decimal) -> String? { - let formatter = NumberFormatter() - formatter.numberStyle = .currency - formatter.currencySymbol = "" - let formattedString = formatter.string(from: NSDecimalNumber(decimal: value)) - let trimmedFormattedStringWithoutCurrency = formattedString?.trimmingCharacters(in: .whitespaces) - return trimmedFormattedStringWithoutCurrency - } + - parameter key: The key to search for in the strings file. + - parameter comment: The corresponding comment. + + - returns: String resource for the given key. + */ +func NSLocalizedStringPreferredFormat(_ key: String, + fallbackKey: String = "", + comment: String, + isCustomizable: Bool = true) -> String { + GiniLocalized.string(key, fallbackKey: fallbackKey, comment: comment, locale: GiniHealthConfiguration.shared.customLocalization?.rawValue, bundle: giniHealthBundle()) } /** diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/IBANValidator.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/IBANValidator.swift deleted file mode 100644 index 75f8d0355..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/IBANValidator.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// IBANValidator.swift -// GiniHealth -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -import Foundation -/** - Helper class for IBAN validation. - - */ -final class IBANValidator { - private var countryIbanDictionary: [String: Int] { - return [ - "AL": 28, "AD": 24, "AT": 20, "AZ": 28, "BH": 22, "BE": 16, - "BA": 20, "BR": 29, "BG": 22, "CR": 21, "HR": 21, "CY": 28, - "CZ": 24, "DK": 18, "DO": 28, "EE": 20, "FO": 18, "FI": 18, - "FR": 27, "GE": 22, "DE": 22, "GI": 23, "GB": 22, "GR": 27, - "GL": 18, "GT": 28, "HU": 28, "IS": 26, "IE": 22, "IL": 23, - "IT": 27, "KZ": 20, "KW": 30, "LV": 21, "LB": 28, "LT": 20, - "LU": 20, "MK": 19, "MT": 31, "MR": 27, "MU": 30, "MD": 24, - "MC": 27, "ME": 22, "NL": 18, "NO": 15, "PK": 24, "PS": 29, - "PL": 28, "PT": 25, "RO": 24, "SM": 27, "SA": 24, "RS": 22, - "SK": 24, "SI": 19, "ES": 24, "SE": 24, "TN": 24, "TR": 26, - "AE": 23, "VG": 24, "CH": 21 - ] - } - - private var validationSet: CharacterSet { - return CharacterSet(charactersIn: "01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ").inverted - } - - func isValid(iban: String) -> Bool { - let iban = iban.replacingOccurrences(of: " ", with: "") - let ibanLength = iban.count - guard let minValues = countryIbanDictionary.values.min(), ibanLength >= minValues else { - return false - } - - if iban.rangeOfCharacter(from: validationSet) != nil { - return false - } - - let countryCode = String(iban[.. UInt32 { - var checkSum = UInt32(0) - var letterNumberMapping: [Character: Int] { - var dict = [Character: Int]() - "ABCDEFGHIJKLMNOPQRSTUVWXYZ".forEach { dict[$0] = Int($0.unicodeScalarCodePoint() - 55) } - return dict - } - - for char in iban { - let value = UInt32(letterNumberMapping[char] ?? Int(String(char)) ?? 0) - if value < 10 { - checkSum = (10 * checkSum) + value - } else { - checkSum = (100 * checkSum) + value - } - if checkSum >= UInt32(UINT32_MAX) / 100 { - checkSum = checkSum % 97 - } - } - return checkSum % 97 - } - - func validateMod97(iban: String) -> Bool { - return checkSum(iban: iban) == 1 - } -} - -extension Character { - func unicodeScalarCodePoint() -> UInt32 { - let scalars = String(self).unicodeScalars - return scalars[scalars.startIndex].value - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PageCollectionViewCell.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PageCollectionViewCell.swift deleted file mode 100644 index 12ba624fd..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PageCollectionViewCell.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// PageCollectionViewCell.swift -// GiniHealth -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -import UIKit - -class PageCollectionViewCell: UICollectionViewCell { - - var pageImageView: ZoomedImageView = { - let iv = ZoomedImageView() - iv.setup() - iv.clipsToBounds = true - return iv - }() - - fileprivate func addImageView() { - contentView.addSubview(pageImageView) - } - - override init(frame: CGRect) { - super.init(frame: .zero) - addImageView() - } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - addImageView() - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/BankSelectionTableViewCell.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/BankSelectionTableViewCell.swift deleted file mode 100644 index d64598500..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/BankSelectionTableViewCell.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// BankSelectionTableViewCell.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit - -class BankSelectionTableViewCell: UITableViewCell { - static let identifier = "BankSelectionTableViewCell" - - var cellViewModel: BankSelectionTableViewCellModel? { - didSet { - guard let cellViewModel else { return } - cellView.backgroundColor = cellViewModel.backgroundColor - bankImageView.image = cellViewModel.bankImageIcon - bankImageView.layer.cornerRadius = Constants.bankIconCornerRadius - bankImageView.layer.borderWidth = Constants.bankIconBorderWidth - bankImageView.layer.borderColor = cellViewModel.bankIconBorderColor.cgColor - bankNameLabel.text = cellViewModel.bankName - bankNameLabel.font = cellViewModel.bankNameLabelFont - bankNameLabel.textColor = cellViewModel.bankNameLabelAccentColor - - setBorder(isSelected: cellViewModel.shouldShowSelectionIcon, - selectedBorderColor: cellViewModel.selectedBankBorderColor, - notSelectedBorderColor: cellViewModel.notSelectedBankBorderColor) - - selectionIndicatorImageView.image = cellViewModel.selectionIndicatorImage - selectionIndicatorImageView.isHidden = !cellViewModel.shouldShowSelectionIcon - } - } - - @IBOutlet private weak var cellView: UIView! - @IBOutlet private weak var bankImageView: UIImageView! - @IBOutlet private weak var bankNameLabel: UILabel! - @IBOutlet private weak var selectionIndicatorImageView: UIImageView! - - override func awakeFromNib() { - super.awakeFromNib() - selectionStyle = .none - } - - private func setBorder(isSelected: Bool, selectedBorderColor: UIColor, notSelectedBorderColor: UIColor) { - cellView.roundCorners(corners: .allCorners, radius: Constants.viewCornerRadius) - if isSelected { - cellView.layer.borderColor = selectedBorderColor.cgColor - cellView.layer.borderWidth = Constants.selectedBorderWidth - } else { - cellView.layer.borderColor = notSelectedBorderColor.cgColor - cellView.layer.borderWidth = Constants.notSelectedBorderWidth - } - } -} - -extension BankSelectionTableViewCell { - private enum Constants { - static let viewCornerRadius = 8.0 - static let selectedBorderWidth = 3.0 - static let notSelectedBorderWidth = 1.0 - static let bankIconBorderWidth = 1.0 - static let bankIconCornerRadius = 6.0 - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/BankSelectionTableViewCellModel.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/BankSelectionTableViewCellModel.swift deleted file mode 100644 index d4acfd1f9..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/BankSelectionTableViewCellModel.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// BankSelectionTableViewCellModel.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit -import GiniHealthAPILibrary - -final class BankSelectionTableViewCellModel { - - private var isSelected: Bool = false - - var shouldShowSelectionIcon: Bool { - isSelected - } - - let backgroundColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark7, - darkModeColor: UIColor.GiniHealthColors.light7).uiColor() - - private var bankImageIconData: Data? - var bankImageIcon: UIImage { - if let bankImageIconData { - return UIImage(data: bankImageIconData) ?? UIImage() - } - return UIImage() - } - var bankIconBorderColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark5, - darkModeColor: UIColor.GiniHealthColors.light5).uiColor() - - var bankName: String - var bankNameLabelFont: UIFont - let bankNameLabelAccentColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark1, - darkModeColor: UIColor.GiniHealthColors.light1).uiColor() - - let selectedBankBorderColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.accent1, - darkModeColor: UIColor.GiniHealthColors.accent1).uiColor() - let notSelectedBankBorderColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark5, - darkModeColor: UIColor.GiniHealthColors.light5).uiColor() - - let selectionIndicatorImage = UIImageNamedPreferred(named: "selectionIndicator") - - init(paymentProvider: PaymentProviderAdditionalInfo) { - self.isSelected = paymentProvider.isSelected - self.bankImageIconData = paymentProvider.paymentProvider.iconData - self.bankName = paymentProvider.paymentProvider.name - - let defaultRegularFont: UIFont = UIFont.systemFont(ofSize: 16, weight: .regular) - self.bankNameLabelFont = GiniHealthConfiguration.shared.textStyleFonts[.body1] ?? defaultRegularFont - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/BanksBottomView.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/BanksBottomView.swift deleted file mode 100644 index 64a487382..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/BanksBottomView.swift +++ /dev/null @@ -1,236 +0,0 @@ -// -// PaymentProvidersBottomView.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit - -class BanksBottomView: BottomSheetViewController { - - var viewModel: BanksBottomViewModel - - private lazy var contentStackView: UIStackView = { - EmptyStackView(orientation: .vertical) - }() - - private lazy var titleView: UIView = { - let view = EmptyView() - view.frame = CGRect(x: 0, y: 0, width: .greatestFiniteMagnitude, height: Constants.heightTitleView) - return view - }() - - private lazy var titleLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.text = viewModel.selectBankTitleText - label.textColor = viewModel.selectBankLabelAccentColor - label.font = viewModel.selectBankLabelFont - label.numberOfLines = 1 - label.lineBreakMode = .byTruncatingTail - return label - }() - - private lazy var closeTitleIconImageView: UIImageView = { - let imageView = UIImageView(image: viewModel.closeTitleIcon.withRenderingMode(.alwaysTemplate)) - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.tintColor = viewModel.closeIconAccentColor - imageView.isUserInteractionEnabled = true - imageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapOnCloseIcon))) - return imageView - }() - - private lazy var descriptionView: UIView = { - EmptyView() - }() - - private lazy var descriptionLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.text = viewModel.descriptionText - label.textColor = viewModel.descriptionLabelAccentColor - label.font = viewModel.descriptionLabelFont - label.numberOfLines = 0 - return label - }() - - private lazy var paymentProvidersView: UIView = { - EmptyView() - }() - - private lazy var paymentProvidersTableView: UITableView = { - let tableView = UITableView(frame: .zero, style: .plain) - tableView.translatesAutoresizingMaskIntoConstraints = false - tableView.delegate = self - tableView.dataSource = self - tableView.register(UINib(nibName: BankSelectionTableViewCell.identifier, - bundle: Bundle.resource), - forCellReuseIdentifier: BankSelectionTableViewCell.identifier) - tableView.estimatedRowHeight = viewModel.rowHeight - tableView.rowHeight = viewModel.rowHeight - tableView.separatorStyle = .none - tableView.tableFooterView = UIView() - tableView.backgroundColor = .clear - tableView.showsVerticalScrollIndicator = false - return tableView - }() - - private lazy var bottomView: UIView = { - EmptyView() - }() - - private lazy var bottomStackView: UIStackView = { - EmptyStackView(orientation: .horizontal) - }() - - private lazy var moreInformationView: MoreInformationView = { - let view = MoreInformationView() - let viewModel = MoreInformationViewModel() - viewModel.delegate = self - view.viewModel = viewModel - return view - }() - - private lazy var poweredByGiniView: PoweredByGiniView = { - let view = PoweredByGiniView() - view.viewModel = PoweredByGiniViewModel() - return view - }() - - override func viewDidLoad() { - super.viewDidLoad() - setupView() - } - - init(viewModel: BanksBottomViewModel) { - self.viewModel = viewModel - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupView() { - setupViewHierarchy() - setupViewAttributes() - setupLayout() - } - - private func setupViewHierarchy() { - titleView.addSubview(titleLabel) - titleView.addSubview(closeTitleIconImageView) - contentStackView.addArrangedSubview(titleView) - descriptionView.addSubview(descriptionLabel) - contentStackView.addArrangedSubview(descriptionView) - paymentProvidersView.addSubview(paymentProvidersTableView) - contentStackView.addArrangedSubview(paymentProvidersView) - bottomStackView.addArrangedSubview(moreInformationView) - bottomStackView.addArrangedSubview(UIView()) - bottomStackView.addArrangedSubview(poweredByGiniView) - bottomView.addSubview(bottomStackView) - contentStackView.addArrangedSubview(bottomView) - self.setContent(content: contentStackView) - } - - private func setupViewAttributes() { - let isFullScreen = viewModel.bottomViewHeight >= viewModel.maximumViewHeight - paymentProvidersTableView.isScrollEnabled = isFullScreen - } - - private func setupLayout() { - setupTitleViewConstraints() - setupDescriptionConstraints() - setupTableViewConstraints() - setupPoweredByGiniConstraints() - } - - private func setupTitleViewConstraints() { - NSLayoutConstraint.activate([ - titleView.heightAnchor.constraint(equalToConstant: Constants.heightTitleView), - titleLabel.leadingAnchor.constraint(equalTo: titleView.leadingAnchor, constant: Constants.viewPaddingConstraint), - titleLabel.centerYAnchor.constraint(equalTo: titleView.centerYAnchor), - closeTitleIconImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), - closeTitleIconImageView.heightAnchor.constraint(equalToConstant: Constants.closeIconSize), - closeTitleIconImageView.widthAnchor.constraint(equalToConstant: Constants.closeIconSize), - closeTitleIconImageView.trailingAnchor.constraint(equalTo: titleView.trailingAnchor, constant: -Constants.viewPaddingConstraint), - closeTitleIconImageView.leadingAnchor.constraint(greaterThanOrEqualTo: titleLabel.trailingAnchor, constant: Constants.titleViewTitleIconSpacing) - ]) - } - - private func setupDescriptionConstraints() { - NSLayoutConstraint.activate([ - descriptionLabel.topAnchor.constraint(equalTo: descriptionView.topAnchor), - descriptionLabel.leadingAnchor.constraint(equalTo: descriptionView.leadingAnchor, constant: Constants.viewPaddingConstraint), - descriptionLabel.trailingAnchor.constraint(equalTo: descriptionView.trailingAnchor, constant: -Constants.viewPaddingConstraint), - descriptionLabel.bottomAnchor.constraint(equalTo: descriptionView.bottomAnchor, constant: -Constants.viewPaddingConstraint) - ]) - } - - private func setupTableViewConstraints() { - NSLayoutConstraint.activate([ - paymentProvidersTableView.topAnchor.constraint(equalTo: paymentProvidersView.topAnchor), - paymentProvidersTableView.leadingAnchor.constraint(equalTo: paymentProvidersView.leadingAnchor, constant: Constants.viewPaddingConstraint), - paymentProvidersTableView.trailingAnchor.constraint(equalTo: paymentProvidersView.trailingAnchor, constant: -Constants.viewPaddingConstraint), - paymentProvidersTableView.bottomAnchor.constraint(equalTo: paymentProvidersView.bottomAnchor), - paymentProvidersTableView.heightAnchor.constraint(equalToConstant: viewModel.heightTableView) - ]) - } - - private func setupPoweredByGiniConstraints() { - NSLayoutConstraint.activate([ - bottomStackView.leadingAnchor.constraint(equalTo: bottomView.leadingAnchor, constant: Constants.viewPaddingConstraint), - bottomStackView.trailingAnchor.constraint(equalTo: bottomView.trailingAnchor, constant: -Constants.viewPaddingConstraint), - bottomStackView.topAnchor.constraint(equalTo: bottomView.topAnchor, constant: Constants.topAnchorPoweredByGiniConstraint), - bottomStackView.bottomAnchor.constraint(equalTo: bottomView.bottomAnchor) - ]) - } - - @objc - private func tapOnCloseIcon() { - viewModel.didTapOnClose() - } - -} - -extension BanksBottomView { - enum Constants { - static let heightTitleView = 48.0 - static let viewPaddingConstraint = 16.0 - static let topAnchorTitleView = 32.0 - static let closeIconSize = 24.0 - static let titleViewTitleIconSpacing = 10.0 - static let topAnchorPoweredByGiniConstraint = 5.0 - } -} - -extension BanksBottomView: MoreInformationViewProtocol { - func didTapOnMoreInformation() { - viewModel.didTapOnMoreInformation() - } -} - -extension BanksBottomView: UITableViewDataSource, UITableViewDelegate { - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - viewModel.paymentProviders.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: BankSelectionTableViewCell.identifier, - for: indexPath) as? BankSelectionTableViewCell else { - return UITableViewCell() - } - let invoiceTableViewCellModel = viewModel.paymentProvidersViewModel(paymentProvider: viewModel.paymentProviders[indexPath.row]) - cell.cellViewModel = invoiceTableViewCellModel - return cell - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - viewModel.rowHeight - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - viewModel.viewDelegate?.didSelectPaymentProvider(paymentProvider: viewModel.paymentProviders[indexPath.row].paymentProvider) - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/BanksBottomViewModel.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/BanksBottomViewModel.swift deleted file mode 100644 index ca3b18b5a..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/BanksBottomViewModel.swift +++ /dev/null @@ -1,120 +0,0 @@ -// -// BanksBottomViewModel.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit -import GiniHealthAPILibrary - -public protocol PaymentProvidersBottomViewProtocol: AnyObject { - func didSelectPaymentProvider(paymentProvider: PaymentProvider) - func didTapOnClose() - func didTapOnMoreInformation() -} - -struct PaymentProviderAdditionalInfo { - var isSelected: Bool - var isInstalled: Bool - let paymentProvider: PaymentProvider -} - -final class BanksBottomViewModel { - - weak var viewDelegate: PaymentProvidersBottomViewProtocol? - - var paymentProviders: [PaymentProviderAdditionalInfo] = [] - private var selectedPaymentProvider: PaymentProvider? - - let maximumViewHeight: CGFloat = UIScreen.main.bounds.height - Constants.topPaddingView - let rowHeight: CGFloat = Constants.cellSizeHeight - var bottomViewHeight: CGFloat = 0 - var heightTableView: CGFloat = 0 - - let backgroundColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark7, - darkModeColor: UIColor.GiniHealthColors.light7).uiColor() - let rectangleColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark5, - darkModeColor: UIColor.GiniHealthColors.light5).uiColor() - let dimmingBackgroundColor: UIColor = GiniColor(lightModeColor: UIColor.black, - darkModeColor: UIColor.white).uiColor().withAlphaComponent(0.4) - - let selectBankTitleText: String = GiniLocalized.string("ginihealth.paymentcomponent.selectBank.label", - comment: "Select bank text from the top label on payment providers bottom sheet") - let selectBankLabelAccentColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark2, - darkModeColor: UIColor.GiniHealthColors.light2).uiColor() - var selectBankLabelFont: UIFont - - let closeTitleIcon: UIImage = UIImageNamedPreferred(named: "ic_close") ?? UIImage() - let closeIconAccentColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark2, - darkModeColor: UIColor.GiniHealthColors.light2).uiColor() - - let descriptionText: String = GiniLocalized.string("ginihealth.paymentcomponent.paymentproviderslist.description", - comment: "Top description text on payment providers bottom sheet") - let descriptionLabelAccentColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark3, - darkModeColor: UIColor.GiniHealthColors.light3).uiColor() - var descriptionLabelFont: UIFont - - private var urlOpener: URLOpener - - init(paymentProviders: PaymentProviders, selectedPaymentProvider: PaymentProvider?, urlOpener: URLOpener = URLOpener(UIApplication.shared)) { - self.selectedPaymentProvider = selectedPaymentProvider - self.urlOpener = urlOpener - - let defaultRegularFont: UIFont = UIFont.systemFont(ofSize: 14, weight: .regular) - let defaultBoldFont: UIFont = UIFont.systemFont(ofSize: 14, weight: .bold) - - let giniHealthConfiguration = GiniHealthConfiguration.shared - - self.selectBankLabelFont = giniHealthConfiguration.textStyleFonts[.subtitle1] ?? defaultBoldFont - self.descriptionLabelFont = giniHealthConfiguration.textStyleFonts[.caption1] ?? defaultRegularFont - - self.paymentProviders = paymentProviders - .map({ PaymentProviderAdditionalInfo(isSelected: $0.id == selectedPaymentProvider?.id, - isInstalled: isPaymentProviderInstalled(paymentProvider: $0), - paymentProvider: $0)}) - .filter { $0.paymentProvider.gpcSupportedPlatforms.contains(.ios) || $0.paymentProvider.openWithSupportedPlatforms.contains(.ios) } - .sorted(by: { ($0.paymentProvider.index ?? 0 < $1.paymentProvider.index ?? 0) }) - .sorted(by: { ($0.isInstalled && !$1.isInstalled) }) - self.calculateHeights() - } - - private func calculateHeights() { - let totalTableViewHeight = CGFloat(paymentProviders.count) * Constants.cellSizeHeight - let totalBottomViewHeight = Constants.blankBottomViewHeight + totalTableViewHeight - if totalBottomViewHeight > maximumViewHeight { - self.heightTableView = maximumViewHeight - Constants.blankBottomViewHeight - self.bottomViewHeight = maximumViewHeight - } else { - self.heightTableView = totalTableViewHeight - self.bottomViewHeight = totalTableViewHeight + Constants.blankBottomViewHeight - } - } - - func paymentProvidersViewModel(paymentProvider: PaymentProviderAdditionalInfo) -> BankSelectionTableViewCellModel { - BankSelectionTableViewCellModel(paymentProvider: paymentProvider) - } - - func didTapOnClose() { - viewDelegate?.didTapOnClose() - } - - func didTapOnMoreInformation() { - viewDelegate?.didTapOnMoreInformation() - } - - private func isPaymentProviderInstalled(paymentProvider: PaymentProvider) -> Bool { - if let urlAppScheme = URL(string: paymentProvider.appSchemeIOS) { - return urlOpener.canOpenLink(url: urlAppScheme) - } - return false - } -} - -extension BanksBottomViewModel { - enum Constants { - static let blankBottomViewHeight: CGFloat = 200.0 - static let cellSizeHeight: CGFloat = 64.0 - static let topPaddingView: CGFloat = 100.0 - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/BottomSheetViewController.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/BottomSheetViewController.swift deleted file mode 100644 index 8e2bf0439..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/BottomSheetViewController.swift +++ /dev/null @@ -1,227 +0,0 @@ -// -// BottomSheetViewController.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit - -class BottomSheetViewController: UIViewController { - // MARK: - UI - /// Main bottom sheet container view - private lazy var mainContainerView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = backgroundColor - view.roundCorners(corners: [.topLeft, .topRight], radius: Constants.cornerRadiusView) - view.layer.cornerRadius = Constants.cornerRadiusView - view.clipsToBounds = true - return view - }() - - /// View to hold dynamic content - private lazy var contentView: UIView = { - EmptyView() - }() - - /// Top bar view that draggable to dismiss - private lazy var topBarView: UIView = { - EmptyView() - }() - - /// Top view bar - private lazy var barLineView: UIView = { - let view = UIView() - view.backgroundColor = rectangleColor - view.layer.cornerRadius = Constants.cornerRadiusTopRectangle - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - /// Dimmed background view - private lazy var dimmedView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = dimmingBackgroundColor - view.alpha = 0 - return view - }() - - let backgroundColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark7, - darkModeColor: UIColor.GiniHealthColors.light7).uiColor() - let rectangleColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark5, - darkModeColor: UIColor.GiniHealthColors.light5).uiColor() - let dimmingBackgroundColor: UIColor = GiniColor(lightModeColor: UIColor.black, - darkModeColor: UIColor.white).uiColor().withAlphaComponent(0.4) - var minHeight: CGFloat = 0 - - // MARK: - View Setup - override func viewDidLoad() { - super.viewDidLoad() - setupViews() - setupGestures() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - animatePresent() - } - - private func setupViews() { - view.backgroundColor = .clear - view.addSubview(dimmedView) - NSLayoutConstraint.activate([ - // Set dimmedView edges to superview - dimmedView.topAnchor.constraint(equalTo: view.topAnchor), - dimmedView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - dimmedView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - dimmedView.trailingAnchor.constraint(equalTo: view.trailingAnchor) - ]) - - // Container View - view.addSubview(mainContainerView) - NSLayoutConstraint.activate([ - mainContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - mainContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - mainContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor) - ]) - if minHeight > 0 { - mainContainerView.topAnchor.constraint(lessThanOrEqualTo: view.topAnchor, constant: obtainTopAnchorMinHeightConstraint()).isActive = true - } else { - mainContainerView.topAnchor.constraint(greaterThanOrEqualTo: view.topAnchor, constant: Constants.minTopSpacing).isActive = true - } - - // Top draggable bar view - mainContainerView.addSubview(topBarView) - NSLayoutConstraint.activate([ - topBarView.topAnchor.constraint(equalTo: mainContainerView.topAnchor), - topBarView.leadingAnchor.constraint(equalTo: mainContainerView.leadingAnchor), - topBarView.trailingAnchor.constraint(equalTo: mainContainerView.trailingAnchor), - topBarView.heightAnchor.constraint(equalToConstant: Constants.heightTopBarView) - ]) - topBarView.addSubview(barLineView) - NSLayoutConstraint.activate([ - barLineView.centerXAnchor.constraint(equalTo: topBarView.centerXAnchor), - barLineView.topAnchor.constraint(equalTo: topBarView.topAnchor, constant: Constants.topAnchorTopRectangle), - barLineView.widthAnchor.constraint(equalToConstant: Constants.widthTopRectangle), - barLineView.heightAnchor.constraint(equalToConstant: Constants.heightTopRectangle) - ]) - - // Content View - mainContainerView.addSubview(contentView) - contentView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - contentView.leadingAnchor.constraint(equalTo: mainContainerView.leadingAnchor), - contentView.trailingAnchor.constraint(equalTo: mainContainerView.trailingAnchor), - contentView.topAnchor.constraint(equalTo: topBarView.bottomAnchor), - contentView.bottomAnchor.constraint(equalTo: mainContainerView.bottomAnchor, constant: -Constants.bottomPaddingConstraint) - ]) - } - - private func obtainTopAnchorMinHeightConstraint() -> CGFloat { - let extraBottomSafeAreaConstant = UIApplication.shared.keyWindow?.safeAreaInsets.bottom == 0 ? Constants.safeAreaBottomPadding : 0 // fix for small devices - let topAnchorWithMinHeightConstant = view.frame.height - minHeight + extraBottomSafeAreaConstant - return topAnchorWithMinHeightConstant - } - - private func setupGestures() { - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapDimmedView)) - dimmedView.addGestureRecognizer(tapGesture) - - let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) - panGesture.delaysTouchesBegan = false - panGesture.delaysTouchesEnded = false - topBarView.addGestureRecognizer(panGesture) - } - - @objc private func handleTapDimmedView() { - dismissBottomSheet() - } - - @objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) { - let translation = gesture.translation(in: view) - // get drag direction - let isDraggingDown = translation.y > 0 - guard isDraggingDown else { return } - let pannedHeight = translation.y - let currentY = self.view.frame.height - self.mainContainerView.frame.height - // handle gesture state - switch gesture.state { - case .changed: - // This state will occur when user is dragging - self.mainContainerView.frame.origin.y = currentY + pannedHeight - case .ended: - // When user stop dragging - // if fulfil the condition dismiss it, else move to original position - if pannedHeight >= Constants.minDismissiblePanHeight { - dismissBottomSheet() - } else { - self.mainContainerView.frame.origin.y = currentY - } - default: - break - } - } - - private func animatePresent() { - dimmedView.alpha = 0 - mainContainerView.transform = CGAffineTransform(translationX: 0, y: view.frame.height) - UIView.animate(withDuration: 0.2) { [weak self] in - self?.mainContainerView.transform = .identity - } - // add more animation duration for smoothness - UIView.animate(withDuration: 0.4) { [weak self] in - self?.dimmedView.alpha = Constants.maxDimmedAlpha - } - } - - func dismissBottomSheet() { - UIView.animate(withDuration: 0.2, animations: { [weak self] in - guard let self = self else { return } - self.dimmedView.alpha = Constants.maxDimmedAlpha - self.mainContainerView.frame.origin.y = self.view.frame.height - }, completion: { [weak self] _ in - self?.dismiss(animated: false) - }) - } - - // sub-view controller will call this function to set content - func setContent(content: UIView) { - contentView.addSubview(content) - NSLayoutConstraint.activate([ - content.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - content.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - content.topAnchor.constraint(equalTo: contentView.topAnchor), - content.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) - ]) - view.layoutIfNeeded() - } -} - -extension BottomSheetViewController { - enum Constants { - /// Maximum alpha for dimmed view - static let maxDimmedAlpha: CGFloat = 0.8 - /// Minimum drag vertically that enable bottom sheet to dismiss - static let minDismissiblePanHeight: CGFloat = 20 - /// Minimum spacing between the top edge and bottom sheet - static var minTopSpacing: CGFloat = 80 - /// Minimum bottom sheet height - static let heightTopBarView = 32.0 - static let cornerRadiusTopRectangle = 2.0 - static let cornerRadiusView = 12.0 - static let topAnchorTopRectangle = 16.0 - static let widthTopRectangle = 48.0 - static let heightTopRectangle = 4.0 - static let bottomPaddingConstraint = 34.0 - static let safeAreaBottomPadding = 32.0 - } -} - -extension UIViewController { - func presentBottomSheet(viewController: BottomSheetViewController) { - viewController.modalPresentationStyle = .overFullScreen - present(viewController, animated: false, completion: nil) - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/HelpingViews/EmptyStackView.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/HelpingViews/EmptyStackView.swift deleted file mode 100644 index c203f6dd0..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/HelpingViews/EmptyStackView.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// EmptyStackView.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit - -class EmptyStackView: UIStackView { - init(orientation: NSLayoutConstraint.Axis) { - super.init(frame: .zero) - self.axis = orientation - self.translatesAutoresizingMaskIntoConstraints = false - self.backgroundColor = .clear - } - - required init(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/HelpingViews/EmptyView.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/HelpingViews/EmptyView.swift deleted file mode 100644 index 8a44a6149..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/HelpingViews/EmptyView.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// EmptyView.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit - -class EmptyView: UIView { - override init(frame: CGRect) { - super.init(frame: frame) - self.translatesAutoresizingMaskIntoConstraints = false - self.backgroundColor = .clear - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/InstallAppBottomView.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/InstallAppBottomView.swift deleted file mode 100644 index a5174222b..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/InstallAppBottomView.swift +++ /dev/null @@ -1,282 +0,0 @@ -// -// InstallAppBottomView.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit - -class InstallAppBottomView: BottomSheetViewController { - - var viewModel: InstallAppBottomViewModel - - private lazy var contentStackView: UIStackView = { - EmptyStackView(orientation: .vertical) - }() - - private lazy var titleView: UIView = { - EmptyView() - }() - - private lazy var titleLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.text = viewModel.titleText - label.textColor = viewModel.titleLabelAccentColor - label.font = viewModel.titleLabelFont - label.numberOfLines = 0 - label.lineBreakMode = .byTruncatingTail - return label - }() - - private lazy var bankView: UIView = { - EmptyView() - }() - - private lazy var bankIconImageView: UIImageView = { - let imageView = UIImageView(image: viewModel.bankImageIcon) - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.roundCorners(corners: .allCorners, radius: Constants.bankIconCornerRadius) - imageView.layer.borderWidth = Constants.bankIconBorderWidth - imageView.layer.borderColor = viewModel.bankIconBorderColor.cgColor - return imageView - }() - - private lazy var moreInformationView: UIView = { - EmptyView() - }() - - private lazy var moreInformationStackView: UIStackView = { - let stackView = EmptyStackView(orientation: .horizontal) - stackView.spacing = Constants.viewPaddingConstraint - stackView.distribution = .fillProportionally - return stackView - }() - - private lazy var moreInformationLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = viewModel.moreInformationLabelTextColor - label.font = viewModel.moreInformationLabelFont - label.numberOfLines = 0 - label.text = viewModel.moreInformationLabelText - return label - }() - - private lazy var moreInformationButton: UIButton = { - let button = UIButton(type: .system) - button.translatesAutoresizingMaskIntoConstraints = false - let image = UIImageNamedPreferred(named: viewModel.moreInformationIconName) - button.setImage(image, for: .normal) - button.tintColor = viewModel.moreInformationAccentColor - button.isUserInteractionEnabled = false - return button - }() - - private lazy var continueButton: PaymentPrimaryButton = { - let button = PaymentPrimaryButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.configure(with: viewModel.giniHealthConfiguration.primaryButtonConfiguration) - button.customConfigure(paymentProviderColors: viewModel.paymentProviderColors, - text: viewModel.continueLabelText) - return button - }() - - private lazy var appStoreImageView: UIButton = { - let button = UIButton(type: .custom) - button.translatesAutoresizingMaskIntoConstraints = false - let image = UIImageNamedPreferred(named: viewModel.appStoreImageIconName) - button.setImage(image, for: .normal) - button.imageView?.contentMode = .scaleAspectFit - button.addTarget(self, action: #selector(tapOnAppStoreButton), for: .touchUpInside) - return button - }() - - private lazy var buttonsView: UIView = { - EmptyView() - }() - - private lazy var bottomView: UIView = { - EmptyView() - }() - - private lazy var bottomStackView: UIStackView = { - EmptyStackView(orientation: .horizontal) - }() - - private lazy var poweredByGiniView: PoweredByGiniView = { - let view = PoweredByGiniView() - view.viewModel = PoweredByGiniViewModel() - return view - }() - - override func viewDidLoad() { - super.viewDidLoad() - setupView() - } - - init(viewModel: InstallAppBottomViewModel) { - self.viewModel = viewModel - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupView() { - setupViewHierarchy() - setupLayout() - setupListeners() - setButtonsState() - } - - private func setupViewHierarchy() { - titleView.addSubview(titleLabel) - contentStackView.addArrangedSubview(titleView) - bankView.addSubview(bankIconImageView) - contentStackView.addArrangedSubview(bankView) - moreInformationStackView.addArrangedSubview(moreInformationButton) - moreInformationStackView.addArrangedSubview(moreInformationLabel) - moreInformationView.addSubview(moreInformationStackView) - contentStackView.addArrangedSubview(moreInformationView) - buttonsView.addSubview(continueButton) - buttonsView.addSubview(appStoreImageView) - contentStackView.addArrangedSubview(buttonsView) - contentStackView.addArrangedSubview(UIView()) - bottomStackView.addArrangedSubview(UIView()) - bottomStackView.addArrangedSubview(poweredByGiniView) - bottomView.addSubview(bottomStackView) - contentStackView.addArrangedSubview(bottomView) - self.setContent(content: contentStackView) - } - - private func setupLayout() { - setupTitleViewConstraints() - setupBankImageConstraints() - setupMoreInformationConstraints() - setupContinueButtonConstraints() - setupAppStoreButtonConstraints() - setupPoweredByGiniConstraints() - } - - private func setupListeners() { - NotificationCenter.default.addObserver(self, - selector: #selector(willEnterForeground), - name: UIApplication.willEnterForegroundNotification, - object: nil) - } - - @objc private func willEnterForeground() { - setButtonsState() - } - - private func setButtonsState() { - appStoreImageView.isHidden = viewModel.isBankInstalled - continueButton.isHidden = !viewModel.isBankInstalled - moreInformationLabel.text = viewModel.moreInformationLabelText - - continueButton.didTapButton = { [weak self] in - self?.tapOnContinueButton() - } - } - - private func setupTitleViewConstraints() { - NSLayoutConstraint.activate([ - titleLabel.leadingAnchor.constraint(equalTo: titleView.leadingAnchor, constant: Constants.viewPaddingConstraint), - titleLabel.trailingAnchor.constraint(equalTo: titleView.trailingAnchor, constant: -Constants.viewPaddingConstraint), - titleLabel.topAnchor.constraint(equalTo: titleView.topAnchor, constant: Constants.topBottomPaddingConstraint), - titleLabel.bottomAnchor.constraint(equalTo: titleView.bottomAnchor, constant: -Constants.topBottomPaddingConstraint) - ]) - } - - private func setupBankImageConstraints() { - NSLayoutConstraint.activate([ - bankIconImageView.heightAnchor.constraint(equalToConstant: Constants.bankIconSize), - bankIconImageView.widthAnchor.constraint(equalToConstant: Constants.bankIconSize), - bankIconImageView.topAnchor.constraint(equalTo: bankView.topAnchor), - bankIconImageView.bottomAnchor.constraint(equalTo: bankView.bottomAnchor), - bankIconImageView.centerXAnchor.constraint(equalTo: bankView.centerXAnchor), - - ]) - } - - private func setupMoreInformationConstraints() { - NSLayoutConstraint.activate([ - moreInformationStackView.leadingAnchor.constraint(equalTo: moreInformationView.leadingAnchor, constant: Constants.viewPaddingConstraint), - moreInformationStackView.trailingAnchor.constraint(equalTo: moreInformationView.trailingAnchor, constant: -Constants.viewPaddingConstraint), - moreInformationStackView.topAnchor.constraint(equalTo: moreInformationView.topAnchor, constant: Constants.viewPaddingConstraint), - moreInformationStackView.bottomAnchor.constraint(equalTo: moreInformationView.bottomAnchor, constant: Constants.moreInformationBottomAnchorConstraint), - moreInformationButton.widthAnchor.constraint(equalToConstant: Constants.infoIconSize) - ]) - } - - private func setupContinueButtonConstraints() { - NSLayoutConstraint.activate([ - continueButton.leadingAnchor.constraint(equalTo: buttonsView.leadingAnchor, constant: Constants.viewPaddingConstraint), - continueButton.trailingAnchor.constraint(equalTo: buttonsView.trailingAnchor, constant: -Constants.viewPaddingConstraint), - continueButton.heightAnchor.constraint(equalToConstant: Constants.continueButtonViewHeight), - continueButton.topAnchor.constraint(equalTo: buttonsView.topAnchor, constant: Constants.continueButtonTopAnchor), - continueButton.bottomAnchor.constraint(equalTo: buttonsView.bottomAnchor, constant: -Constants.continueButtonBottomAnchor) - ]) - } - - private func setupAppStoreButtonConstraints() { - NSLayoutConstraint.activate([ - appStoreImageView.leadingAnchor.constraint(equalTo: buttonsView.leadingAnchor, constant: Constants.viewPaddingConstraint), - appStoreImageView.trailingAnchor.constraint(equalTo: buttonsView.trailingAnchor, constant: -Constants.viewPaddingConstraint), - appStoreImageView.heightAnchor.constraint(equalToConstant: Constants.appStoreImageViewHeight), - appStoreImageView.topAnchor.constraint(equalTo: buttonsView.topAnchor, constant: Constants.continueButtonTopAnchor), - appStoreImageView.centerXAnchor.constraint(equalTo: buttonsView.centerXAnchor), - appStoreImageView.bottomAnchor.constraint(equalTo: buttonsView.bottomAnchor, constant: -Constants.appStoreBottomAnchor) - ]) - } - - private func setupPoweredByGiniConstraints() { - NSLayoutConstraint.activate([ - bottomStackView.leadingAnchor.constraint(equalTo: bottomView.leadingAnchor, constant: Constants.viewPaddingConstraint), - bottomStackView.trailingAnchor.constraint(equalTo: bottomView.trailingAnchor, constant: -Constants.viewPaddingConstraint), - bottomStackView.topAnchor.constraint(equalTo: bottomView.topAnchor, constant: Constants.topAnchorPoweredByGiniConstraint), - bottomStackView.bottomAnchor.constraint(equalTo: bottomView.bottomAnchor) - ]) - } - - @objc - private func tapOnContinueButton() { - viewModel.didTapOnContinue() - } - - @objc - private func tapOnAppStoreButton() { - openPaymentProvidersAppStoreLink(urlString: viewModel.selectedPaymentProvider?.appStoreUrlIOS) - } - - private func openPaymentProvidersAppStoreLink(urlString: String?) { - guard let urlString = urlString else { - print("AppStore link unavailable for this payment provider") - return - } - if let url = URL(string: urlString), UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url) - } - } -} - -extension InstallAppBottomView { - enum Constants { - static let viewPaddingConstraint = 16.0 - static let bankIconSize = 36.0 - static let bankIconCornerRadius = 6.0 - static let bankIconBorderWidth = 1.0 - static let continueButtonViewHeight = 56.0 - static let continueButtonTopAnchor = 16.0 - static let continueButtonBottomAnchor = 4.0 - static let appStoreBottomAnchor = 16.0 - static let appStoreImageViewHeight = 44.0 - static let topBottomPaddingConstraint = 10.0 - static let topAnchorPoweredByGiniConstraint = 5.0 - static let moreInformationBottomAnchorConstraint = 8.0 - static let infoIconSize = 24.0 - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/InstallAppBottomViewModel.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/InstallAppBottomViewModel.swift deleted file mode 100644 index 4f35cb70b..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/InstallAppBottomViewModel.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// InstallAppBottomViewModel.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit -import GiniHealthAPILibrary - -protocol InstallAppBottomViewProtocol: AnyObject { - func didTapOnContinue() -} - -final class InstallAppBottomViewModel { - - let giniHealthConfiguration = GiniHealthConfiguration.shared - - var selectedPaymentProvider: PaymentProvider? - // Payment provider colors - var paymentProviderColors: ProviderColors? - - weak var viewDelegate: InstallAppBottomViewProtocol? - - let backgroundColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark7, - darkModeColor: UIColor.GiniHealthColors.light7).uiColor() - let rectangleColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark5, - darkModeColor: UIColor.GiniHealthColors.light5).uiColor() - let dimmingBackgroundColor: UIColor = GiniColor(lightModeColor: UIColor.black, - darkModeColor: UIColor.white).uiColor().withAlphaComponent(0.4) - - var titleText: String = GiniLocalized.string("ginihealth.paymentcomponent.installAppBottomSheet.title", - comment: "Install App Bottom sheet title") - let titleLabelAccentColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark2, - darkModeColor: UIColor.GiniHealthColors.light2).uiColor() - var titleLabelFont: UIFont - - private var bankImageIconData: Data? - var bankImageIcon: UIImage { - if let bankImageIconData { - return UIImage(data: bankImageIconData) ?? UIImage() - } - return UIImage() - } - var bankIconBorderColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark5, - darkModeColor: UIColor.GiniHealthColors.light5).uiColor() - - // More information part - let moreInformationLabelTextColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark3, - darkModeColor: UIColor.GiniHealthColors.light3).uiColor() - let moreInformationAccentColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark3, - darkModeColor: UIColor.GiniHealthColors.light3).uiColor() - var moreInformationLabelText: String { - isBankInstalled ? - GiniLocalized.string("ginihealth.paymentcomponent.installAppBottomSheet.tip.description", - comment: "Text for tip information label").replacingOccurrences(of: bankToReplaceString, - with: selectedPaymentProvider?.name ?? "") : - GiniLocalized.string("ginihealth.paymentcomponent.installAppBottomSheet.notes.description", - comment: "Text for notes information label").replacingOccurrences(of: bankToReplaceString, - with: selectedPaymentProvider?.name ?? "") - } - - - var moreInformationLabelFont: UIFont - let moreInformationIconName = "info.circle" - - // Pay invoice label - let continueLabelText: String = GiniLocalized.string("ginihealth.paymentcomponent.installAppBottomSheet.continue.button.text", - comment: "Title label used for the Continue button") - - var appStoreImageIconName = "appStoreIcon" - let bankToReplaceString = "[BANK]" - - var isBankInstalled: Bool { - selectedPaymentProvider?.appSchemeIOS.canOpenURLString() ?? false - } - - init(selectedPaymentProvider: PaymentProvider?) { - self.selectedPaymentProvider = selectedPaymentProvider - self.bankImageIconData = selectedPaymentProvider?.iconData - self.paymentProviderColors = selectedPaymentProvider?.colors - - titleText = titleText.replacingOccurrences(of: bankToReplaceString, with: selectedPaymentProvider?.name ?? "") - - let defaultRegularFont: UIFont = UIFont.systemFont(ofSize: 14, weight: .regular) - let defaultBoldFont: UIFont = UIFont.systemFont(ofSize: 14, weight: .bold) - - self.titleLabelFont = giniHealthConfiguration.textStyleFonts[.subtitle1] ?? defaultBoldFont - self.moreInformationLabelFont = giniHealthConfiguration.textStyleFonts[.caption1] ?? defaultRegularFont - } - - func didTapOnContinue() { - viewDelegate?.didTapOnContinue() - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/MoreInformationView.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/MoreInformationView.swift deleted file mode 100644 index 120047a93..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/MoreInformationView.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// MoreInformationView.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit - -final class MoreInformationView: UIView { - var viewModel: MoreInformationViewModel! { - didSet { - setupView() - } - } - - private lazy var moreInformationView: UIView = { - EmptyView() - }() - - private lazy var moreInformationLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.numberOfLines = 0 - - let attributes: [NSAttributedString.Key: Any] = [ - .foregroundColor: viewModel.moreInformationLabelTextColor, - .underlineStyle: NSUnderlineStyle.single.rawValue, - .font: viewModel.moreInformationLabelLinkFont - ] - let moreInformationActionableAttributtedString = NSMutableAttributedString(string: viewModel.moreInformationActionablePartText, attributes: attributes) - label.attributedText = moreInformationActionableAttributtedString - - let tapOnMoreInformation = UITapGestureRecognizer(target: self, - action: #selector(tapOnMoreInformationLabelAction(gesture:))) - label.isUserInteractionEnabled = true - label.addGestureRecognizer(tapOnMoreInformation) - - label.attributedText = moreInformationActionableAttributtedString - return label - }() - - private lazy var moreInformationButton: UIButton = { - let button = UIButton(type: .system) - button.translatesAutoresizingMaskIntoConstraints = false - let image = UIImageNamedPreferred(named: viewModel.moreInformationIconName) - button.setImage(image, for: .normal) - button.tintColor = viewModel.moreInformationAccentColor - button.addTarget(self, action: #selector(tapOnMoreInformationButtonAction), for: .touchUpInside) - return button - }() - - override init(frame: CGRect) { - super.init(frame: frame) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupView() { - self.translatesAutoresizingMaskIntoConstraints = false - - moreInformationView.addSubview(moreInformationButton) - moreInformationView.addSubview(moreInformationLabel) - self.addSubview(moreInformationView) - - setupConstraints() - } - - private func setupConstraints() { - NSLayoutConstraint.activate([ - moreInformationButton.leadingAnchor.constraint(equalTo: moreInformationView.leadingAnchor), - moreInformationButton.centerYAnchor.constraint(equalTo: moreInformationView.centerYAnchor), - moreInformationButton.widthAnchor.constraint(equalToConstant: Constants.infoIconSize), - moreInformationButton.heightAnchor.constraint(equalToConstant: Constants.infoIconSize), - moreInformationLabel.leadingAnchor.constraint(equalTo: moreInformationButton.trailingAnchor, constant: Constants.spacingPadding), - moreInformationLabel.centerYAnchor.constraint(equalTo: moreInformationButton.centerYAnchor), - moreInformationLabel.trailingAnchor.constraint(greaterThanOrEqualTo: moreInformationView.trailingAnchor), - moreInformationView.leadingAnchor.constraint(equalTo: leadingAnchor), - moreInformationView.trailingAnchor.constraint(equalTo: trailingAnchor), - moreInformationView.topAnchor.constraint(equalTo: topAnchor), - moreInformationView.bottomAnchor.constraint(equalTo: bottomAnchor), - moreInformationView.heightAnchor.constraint(equalToConstant: Constants.infoIconSize), - moreInformationView.centerYAnchor.constraint(equalTo: self.centerYAnchor) - ]) - } - - @objc - private func tapOnMoreInformationLabelAction(gesture: UITapGestureRecognizer) { - if gesture.didTapAttributedTextInLabel(label: moreInformationLabel, - targetText: viewModel.moreInformationActionablePartText) { - viewModel.tapOnMoreInformation() - } - } - - @objc - private func tapOnMoreInformationButtonAction(gesture: UITapGestureRecognizer) { - viewModel.tapOnMoreInformation() - } -} - -extension MoreInformationView { - private enum Constants { - static let buttonPadding = 10.0 - static let spacingPadding = 8.0 - static let infoIconSize = 24.0 - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/MoreInformationViewModel.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/MoreInformationViewModel.swift deleted file mode 100644 index c9b2e02ad..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/MoreInformationViewModel.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// MoreInformationViewModel.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit - -protocol MoreInformationViewProtocol: AnyObject { - func didTapOnMoreInformation() -} - -final class MoreInformationViewModel { - - weak var delegate: MoreInformationViewProtocol? - // More information part - let moreInformationAccentColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark2, - darkModeColor: UIColor.GiniHealthColors.light2).uiColor() - let moreInformationLabelTextColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark4, - darkModeColor: UIColor.GiniHealthColors.light4).uiColor() - let moreInformationActionablePartText = GiniLocalized.string("ginihealth.paymentcomponent.moreInformation.underlined.part", - comment: "Text for more information actionable part from the label") - var moreInformationLabelLinkFont: UIFont - let moreInformationIconName = "info.circle" - - init() { - let defaultBoldFont: UIFont = UIFont.systemFont(ofSize: 12, weight: .regular) - self.moreInformationLabelLinkFont = GiniHealthConfiguration.shared.textStyleFonts[.caption2] ?? defaultBoldFont - } - - func tapOnMoreInformation() { - delegate?.didTapOnMoreInformation() - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/OnboardingShareInvoiceScreenCount.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/OnboardingShareInvoiceScreenCount.swift deleted file mode 100644 index 4f6e08f14..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/OnboardingShareInvoiceScreenCount.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// OnboardingShareInvoiceScreenCount.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import Foundation - -struct OnboardingShareInvoiceScreenCount: Codable { - var providerCounts: [String: Int] // Dictionary to store count for each provider -} - -extension OnboardingShareInvoiceScreenCount { - // UserDefaults key for storing onboarding presentation counts - private static let onboardingShareScreenCountKey = "OnboardingShareInvoiceScreenCount" - - // Load onboarding presentation counts from UserDefaults - static func load() -> OnboardingShareInvoiceScreenCount { - if let data = UserDefaults.standard.data(forKey: onboardingShareScreenCountKey), - let counts = try? JSONDecoder().decode(OnboardingShareInvoiceScreenCount.self, from: data) { - return counts - } - return OnboardingShareInvoiceScreenCount(providerCounts: [:]) - } - - // Save onboarding presentation counts to UserDefaults - func save() { - if let data = try? JSONEncoder().encode(self) { - UserDefaults.standard.set(data, forKey: OnboardingShareInvoiceScreenCount.onboardingShareScreenCountKey) - } - } - - // Get presentation count for a specific provider - func presentationCount(forProvider providerID: String) -> Int { - return providerCounts[providerID] ?? 0 - } - - // Increment presentation count for a specific provider - mutating func incrementPresentationCount(forProvider providerID: String) { - if let count = providerCounts[providerID] { - providerCounts[providerID] = count + 1 - } else { - providerCounts[providerID] = 1 - } - save() // Save updated counts to UserDefaults - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentComponentConfiguration.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentComponentConfiguration.swift deleted file mode 100644 index 4b0b8c349..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentComponentConfiguration.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// PaymentComponentConfiguration.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import Foundation - -public struct PaymentComponentConfiguration { - /** - * Please contact a Gini representative before changing this configuration option. - */ - public var isPaymentComponentBranded: Bool = true - - public init(isPaymentComponentBranded: Bool = true) { - self.isPaymentComponentBranded = isPaymentComponentBranded - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentComponentView.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentComponentView.swift deleted file mode 100644 index 4f5723791..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentComponentView.swift +++ /dev/null @@ -1,212 +0,0 @@ -// -// PaymentComponentView.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit - -final class PaymentComponentView: UIView { - - var viewModel: PaymentComponentViewModel! { - didSet { - setupView() - } - } - - private lazy var contentStackView: UIStackView = { - EmptyStackView(orientation: .vertical) - }() - - private lazy var selectYourBankView: UIView = { - EmptyView() - }() - - private lazy var selectYourBankLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.text = viewModel.selectYourBankLabelText - label.textColor = viewModel.selectYourBankAccentColor - label.font = viewModel.selectYourBankLabelFont - label.numberOfLines = 0 - return label - }() - - private lazy var buttonsView: UIView = { - EmptyView() - }() - - private lazy var buttonsStackView: UIStackView = { - let stackView = EmptyStackView(orientation: .horizontal) - stackView.spacing = Constants.buttonsSpacing - return stackView - }() - - private lazy var selectBankButton: PaymentSecondaryButton = { - let button = PaymentSecondaryButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.configure(with: viewModel.giniHealthConfiguration.secondaryButtonConfiguration) - return button - }() - - private lazy var payInvoiceButton: PaymentPrimaryButton = { - let button = PaymentPrimaryButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.configure(with: viewModel.giniHealthConfiguration.primaryButtonConfiguration) - button.customConfigure(paymentProviderColors: viewModel.paymentProviderColors, - text: viewModel.payInvoiceLabelText) - return button - }() - - private lazy var bottomView: UIView = { - EmptyView() - }() - - private lazy var bottomStackView: UIStackView = { - EmptyStackView(orientation: .horizontal) - }() - - private lazy var moreInformationView: MoreInformationView = { - let view = MoreInformationView() - let viewModel = MoreInformationViewModel() - viewModel.delegate = self - view.viewModel = viewModel - return view - }() - - private lazy var poweredByGiniView: PoweredByGiniView = { - let view = PoweredByGiniView() - view.viewModel = PoweredByGiniViewModel() - return view - }() - - override init(frame: CGRect) { - super.init(frame: frame) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupView() { - self.translatesAutoresizingMaskIntoConstraints = false - self.backgroundColor = viewModel.backgroundColor - - selectYourBankView.addSubview(selectYourBankLabel) - contentStackView.addArrangedSubview(selectYourBankView) - - buttonsStackView.addArrangedSubview(selectBankButton) - buttonsStackView.addArrangedSubview(payInvoiceButton) - buttonsView.addSubview(buttonsStackView) - contentStackView.addArrangedSubview(buttonsView) - - if !viewModel.isPaymentComponentUsed() { - bottomStackView.addArrangedSubview(moreInformationView) - } - bottomStackView.addArrangedSubview(UIView()) - if viewModel.shouldShowBrandedView { - bottomStackView.addArrangedSubview(poweredByGiniView) - } - bottomView.addSubview(bottomStackView) - contentStackView.addArrangedSubview(bottomView) - - self.addSubview(contentStackView) - activateAllConstraints() - updateAvailableViews() - updateButtonsViews() - setupGestures() - } - - private func activateAllConstraints() { - activateContentStackViewConstraints() - activateSelectYourBankButtonConstraints() - activateButtonsConstraints() - activateBottomViewConstraints() - } - - private func setupGestures() { - payInvoiceButton.didTapButton = { [weak self] in - self?.tapOnPayInvoiceView() - } - selectBankButton.didTapButton = { [weak self] in - self?.tapOnBankPicker() - } - } - - private func updateAvailableViews() { - let isPaymentComponentUsed = viewModel.isPaymentComponentUsed() - selectYourBankView.isHidden = isPaymentComponentUsed - } - - private func updateButtonsViews() { - selectBankButton.customConfigure(labelText: viewModel.placeholderBankNameText, - leftImageIcon: viewModel.bankImageIcon, - rightImageIcon: viewModel.chevronDownIconName, - rightImageTintColor: viewModel.chevronDownIconColor, - shouldShowLabel: !viewModel.hasBankSelected) - payInvoiceButton.isHidden = !viewModel.hasBankSelected - } - - private func activateContentStackViewConstraints() { - NSLayoutConstraint.activate([ - contentStackView.leadingAnchor.constraint(equalTo: leadingAnchor), - contentStackView.trailingAnchor.constraint(equalTo: trailingAnchor), - contentStackView.topAnchor.constraint(equalTo: topAnchor, constant: Constants.contentTopPadding), - contentStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Constants.contentBottomPadding) - ]) - } - - private func activateSelectYourBankButtonConstraints() { - NSLayoutConstraint.activate([ - selectYourBankLabel.leadingAnchor.constraint(equalTo: selectYourBankView.leadingAnchor), - selectYourBankLabel.trailingAnchor.constraint(equalTo: selectYourBankView.trailingAnchor), - selectYourBankLabel.topAnchor.constraint(equalTo: selectYourBankView.topAnchor), - selectYourBankLabel.bottomAnchor.constraint(equalTo: selectYourBankView.bottomAnchor) - ]) - } - - private func activateButtonsConstraints() { - NSLayoutConstraint.activate([ - buttonsStackView.leadingAnchor.constraint(equalTo: buttonsView.leadingAnchor), - buttonsStackView.trailingAnchor.constraint(equalTo: buttonsView.trailingAnchor), - buttonsStackView.topAnchor.constraint(equalTo: buttonsView.topAnchor, constant: Constants.buttonsTopBottomSpacing), - buttonsStackView.bottomAnchor.constraint(equalTo: buttonsView.bottomAnchor, constant: -Constants.buttonsTopBottomSpacing), - buttonsStackView.heightAnchor.constraint(equalToConstant: viewModel.minimumButtonsHeight) - ]) - } - - private func activateBottomViewConstraints() { - NSLayoutConstraint.activate([ - bottomStackView.leadingAnchor.constraint(equalTo: bottomView.leadingAnchor), - bottomStackView.trailingAnchor.constraint(equalTo: bottomView.trailingAnchor), - bottomStackView.topAnchor.constraint(equalTo: bottomView.topAnchor), - bottomStackView.bottomAnchor.constraint(equalTo: bottomView.bottomAnchor) - ]) - } - - @objc - private func tapOnBankPicker() { - viewModel.tapOnBankPicker() - } - - @objc - private func tapOnPayInvoiceView() { - viewModel.tapOnPayInvoiceView() - } -} - -extension PaymentComponentView: MoreInformationViewProtocol { - func didTapOnMoreInformation() { - viewModel.tapOnMoreInformation() - } -} - -extension PaymentComponentView { - private enum Constants { - static let contentTopPadding = 8.0 - static let contentBottomPadding: CGFloat = 4 - static let buttonsSpacing = 8.0 - static let buttonsTopBottomSpacing = 4.0 - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentComponentViewModel.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentComponentViewModel.swift deleted file mode 100644 index 176ece5f2..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentComponentViewModel.swift +++ /dev/null @@ -1,167 +0,0 @@ -// -// PaymentComponentViewModel.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit -import GiniHealthAPILibrary - -/** - Delegate to inform about the actions happened of the custom payment component view. - You may find out when the user tapped on more information area, on the payment provider picker or on the pay invoice button - - */ -public protocol PaymentComponentViewProtocol: AnyObject { - /** - Called when the user tapped on the more information actionable label or the information icon - - - parameter documentId: Id of document - */ - func didTapOnMoreInformation(documentId: String?) - - /** - Called when the user tapped on payment provider picker to change the selected payment provider or install it - - - parameter documentId: Id of document - */ - func didTapOnBankPicker(documentId: String?) - - /** - Called when the user tapped on the pay the invoice button to pay the invoice/document - - parameter documentId: Id of document - */ - func didTapOnPayInvoice(documentId: String?) -} - -/** - Helping extension for using the PaymentComponentViewProtocol methods without the document ID. This should be kept by the document view model and passed hierarchically from there. - - */ -extension PaymentComponentViewProtocol { - public func didTapOnMoreInformation() { - didTapOnMoreInformation(documentId: nil) - } - public func didTapOnBankPicker() { - didTapOnBankPicker(documentId: nil) - } - public func didTapOnPayInvoice() { - didTapOnPayInvoice(documentId: nil) - } -} - -final class PaymentComponentViewModel { - let giniHealthConfiguration: GiniHealthConfiguration - - let backgroundColor: UIColor = UIColor.from(giniColor: GiniColor(lightModeColor: .clear, - darkModeColor: .clear)) - - // More information part - let moreInformationAccentColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark2, - darkModeColor: UIColor.GiniHealthColors.light2).uiColor() - let moreInformationLabelTextColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark4, - darkModeColor: UIColor.GiniHealthColors.light4).uiColor() - let moreInformationLabelText = GiniLocalized.string("ginihealth.paymentcomponent.moreInformation.label", - comment: "Text for more information label") - let moreInformationActionablePartText = GiniLocalized.string("ginihealth.paymentcomponent.moreInformation.underlined.part", - comment: "Text for more information actionable part from the label") - var moreInformationLabelFont: UIFont - var moreInformationLabelLinkFont: UIFont - let moreInformationIconName = "info.circle" - - // Select bank label - let selectYourBankLabelText = GiniLocalized.string("ginihealth.paymentcomponent.selectYourBank.label", - comment: "Text for the select your bank label that's above the payment provider picker") - let selectYourBankLabelFont: UIFont - let selectYourBankAccentColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark1, - darkModeColor: UIColor.GiniHealthColors.light1).uiColor() - - // Bank image icon - private var bankImageIconData: Data? - var bankImageIcon: UIImage? { - guard let bankImageIconData else { return nil } - return UIImage(data: bankImageIconData) - } - - // Primary button - let notInstalledBankTextColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark4, - darkModeColor: UIColor.GiniHealthColors.light4).uiColor() - let placeholderBankNameText: String = GiniLocalized.string("ginihealth.paymentcomponent.selectBank.label", - comment: "Placeholder text used when there isn't a payment provider app installed") - - let chevronDownIconName: String = "iconChevronDown" - let chevronDownIconColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.light7, - darkModeColor: UIColor.GiniHealthColors.light1).uiColor() - - // Payment provider colors - var paymentProviderColors: ProviderColors? - - // Pay invoice label - let payInvoiceLabelText: String = GiniLocalized.string("ginihealth.paymentcomponent.payInvoice.label", - comment: "Title label used for the pay invoice button") - - private var paymentProviderScheme: String? - - weak var delegate: PaymentComponentViewProtocol? - - var documentId: String? - - var minimumButtonsHeight: CGFloat - - var hasBankSelected: Bool - - var paymentComponentConfiguration: PaymentComponentConfiguration? - - var shouldShowBrandedView: Bool { - paymentComponentConfiguration?.isPaymentComponentBranded ?? true - } - - init(paymentProvider: PaymentProvider?, giniHealthConfiguration: GiniHealthConfiguration, paymentComponentConfiguration: PaymentComponentConfiguration?) { - self.giniHealthConfiguration = giniHealthConfiguration - let defaultRegularFont: UIFont = UIFont.systemFont(ofSize: 13, weight: .regular) - let defaultBoldFont: UIFont = UIFont.systemFont(ofSize: 14, weight: .bold) - let defaultMediumFont: UIFont = UIFont.systemFont(ofSize: 14, weight: .medium) - self.moreInformationLabelFont = giniHealthConfiguration.textStyleFonts[.caption1] ?? defaultRegularFont - self.moreInformationLabelLinkFont = giniHealthConfiguration.textStyleFonts[.linkBold] ?? defaultBoldFont - self.selectYourBankLabelFont = giniHealthConfiguration.textStyleFonts[.subtitle2] ?? defaultMediumFont - - self.hasBankSelected = paymentProvider != nil - self.bankImageIconData = paymentProvider?.iconData - self.paymentProviderColors = paymentProvider?.colors - self.paymentProviderScheme = paymentProvider?.appSchemeIOS - - self.minimumButtonsHeight = giniHealthConfiguration.paymentComponentButtonsHeight - - self.paymentComponentConfiguration = paymentComponentConfiguration - } - - func tapOnMoreInformation() { - delegate?.didTapOnMoreInformation(documentId: documentId) - } - - func tapOnBankPicker() { - delegate?.didTapOnBankPicker(documentId: documentId) - } - - func tapOnPayInvoiceView() { - savePaymentComponentViewUsageStatus() - delegate?.didTapOnPayInvoice(documentId: documentId) - } - - // Function to check if Payment was used at least once - func isPaymentComponentUsed() -> Bool { - return UserDefaults.standard.bool(forKey: Constants.paymentComponentViewUsedKey) - } - - // Function to save the boolean value indicating whether Payment was used - private func savePaymentComponentViewUsageStatus() { - UserDefaults.standard.set(true, forKey: Constants.paymentComponentViewUsedKey) - } -} - -extension PaymentComponentViewModel { - private enum Constants { - static let paymentComponentViewUsedKey = "kPaymentComponentViewUsed" - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentComponentsController.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentComponentsController.swift deleted file mode 100644 index 5ff6f49bb..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentComponentsController.swift +++ /dev/null @@ -1,239 +0,0 @@ -// -// PaymentComponentController.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit -import GiniHealthAPILibrary -/** - Protocol used to provide updates on the current status of the Payment Components Controller. - Uses a callback mechanism to handle payment provider requests. - */ -public protocol PaymentComponentsControllerProtocol: AnyObject { - func isLoadingStateChanged(isLoading: Bool) // Because we can't use Combine - func didFetchedPaymentProviders() -} - -protocol PaymentComponentsProtocol { - var isLoading: Bool { get set } - var selectedPaymentProvider: PaymentProvider? { get set } - func loadPaymentProviders() - func checkIfDocumentIsPayable(docId: String, completion: @escaping (Result) -> Void) - func checkIfDocumentContainsMultipleInvoices(docId: String, completion: @escaping (Result) -> Void) - func paymentView(documentId: String) -> UIView - func bankSelectionBottomSheet() -> UIViewController - func loadPaymentReviewScreenFor(documentID: String, trackingDelegate: GiniHealthTrackingDelegate?, completion: @escaping (UIViewController?, GiniHealthError?) -> Void) - func paymentInfoViewController() -> UIViewController -} - -/** - The `PaymentComponentsController` class allows control over the payment components. - */ -public final class PaymentComponentsController: PaymentComponentsProtocol { - /// handling the Payment Component Controller delegate - public weak var delegate: PaymentComponentsControllerProtocol? - /// handling the Payment Component view delegate - public weak var viewDelegate: PaymentComponentViewProtocol? - /// handling the Payment Bottom view delegate - public weak var bottomViewDelegate: PaymentProvidersBottomViewProtocol? - - private var giniHealth: GiniHealth - private let giniHealthConfiguration = GiniHealthConfiguration.shared - private var paymentProviders: PaymentProviders = [] - - /// storing the current selected payment provider - public var selectedPaymentProvider: PaymentProvider? - - /// Payment Component View Configuration - public var paymentComponentConfiguration: PaymentComponentConfiguration? - - /// reponsible for storing the loading state of the controller and passing it to the delegate listeners - var isLoading: Bool = false { - didSet { - delegate?.isLoadingStateChanged(isLoading: isLoading) - } - } - - var paymentComponentView: PaymentComponentView! - - /** - Initializer of the Payment Component Controller class. - - - Parameters: - - giniHealth: An instance of GiniHealth initialized with GiniHealthAPI. - - Returns: - - instance of the payment component controller class - */ - public init(giniHealth: GiniHealth) { - self.giniHealth = giniHealth - } - - /** - Retrieves the default installed payment provider, if available. - - Returns: a Payment Provider object. - */ - private func defaultInstalledPaymentProvider() -> PaymentProvider? { - savedPaymentProvider() - } - - /** - Loads the payment providers list and stores them. - - note: Also triggers a function that checks if the payment providers are installed. - */ - public func loadPaymentProviders() { - self.isLoading = true - self.giniHealth.fetchBankingApps { [weak self] result in - self?.isLoading = false - switch result { - case let .success(paymentProviders): - self?.paymentProviders = paymentProviders - self?.selectedPaymentProvider = self?.defaultInstalledPaymentProvider() - self?.delegate?.didFetchedPaymentProviders() - case let .failure(error): - print("Couldn't load payment providers: \(error.localizedDescription)") - } - } - } - - private func storeDefaultPaymentProvider(paymentProvider: PaymentProvider) { - do { - let encoder = JSONEncoder() - let data = try encoder.encode(paymentProvider) - UserDefaults.standard.set(data, forKey: Constants.kDefaultPaymentProvider) - } catch { - print("Unable to encode payment provider: (\(error))") - } - } - - private func savedPaymentProvider() -> PaymentProvider? { - if let data = UserDefaults.standard.data(forKey: Constants.kDefaultPaymentProvider) { - do { - let decoder = JSONDecoder() - let paymentProvider = try decoder.decode(PaymentProvider.self, from: data) - if self.paymentProviders.contains(where: { $0.id == paymentProvider.id }) { - return paymentProvider - } - } catch { - print("Unable to decode payment provider: (\(error))") - } - } - return nil - } - - /** - Checks if the document is payable by extracting the IBAN. - - Parameters: - - docId: The ID of the uploaded document. - - completion: A closure for processing asynchronous data received from the service. It has a Result type parameter, representing either success or failure. The completion block is called on the main thread. - In the case of success, it includes a boolean value indicating whether the IBAN was extracted successfully. - In case of failure, it returns an error from the server side. - */ - public func checkIfDocumentIsPayable(docId: String, completion: @escaping (Result) -> Void) { - giniHealth.checkIfDocumentIsPayable(docId: docId, completion: completion) - } - - /** - Checks if the document uploaded is having multiple invoices in it. - - Parameters: - - docId: The ID of the uploaded document. - - completion: A closure for processing asynchronous data received from the service. It has a Result type parameter, representing either success or failure. The completion block is called on the main thread. - In the case of success, it includes a boolean value indicating if the upload contains multiple documents - In case of failure, it returns an error from the server side. - */ - public func checkIfDocumentContainsMultipleInvoices(docId: String, completion: @escaping (Result) -> Void) { - giniHealth.checkIfDocumentContainsMultipleInvoices(docId: docId, completion: completion) - } - /** - Provides a custom Gini view that contains more information, bank selection if available and a tappable button to pay the document/invoice - - - Parameters: - - Returns: a custom view - */ - public func paymentView(documentId: String) -> UIView { - paymentComponentView = PaymentComponentView() - let paymentComponentViewModel = PaymentComponentViewModel(paymentProvider: selectedPaymentProvider, giniHealthConfiguration: giniHealthConfiguration, paymentComponentConfiguration: paymentComponentConfiguration) - paymentComponentViewModel.delegate = viewDelegate - paymentComponentViewModel.documentId = documentId - paymentComponentView.viewModel = paymentComponentViewModel - return paymentComponentView - } - - public func bankSelectionBottomSheet() -> UIViewController { - let paymentProvidersBottomViewModel = BanksBottomViewModel(paymentProviders: paymentProviders, - selectedPaymentProvider: selectedPaymentProvider) - let paymentProvidersBottomView = BanksBottomView(viewModel: paymentProvidersBottomViewModel) - paymentProvidersBottomViewModel.viewDelegate = self - paymentProvidersBottomView.viewModel = paymentProvidersBottomViewModel - return paymentProvidersBottomView - } - - public func loadPaymentReviewScreenFor(documentID: String, trackingDelegate: GiniHealthTrackingDelegate?, completion: @escaping (UIViewController?, GiniHealthError?) -> Void) { - self.isLoading = true - self.giniHealth.fetchDataForReview(documentId: documentID) { [weak self] result in - self?.isLoading = false - switch result { - case .success(let data): - guard let self else { - completion(nil, nil) - return - } - guard let selectedPaymentProvider else { - completion(nil, nil) - return - } - let vc = PaymentReviewViewController.instantiate(with: self.giniHealth, - data: data, - selectedPaymentProvider: selectedPaymentProvider, - trackingDelegate: trackingDelegate) - completion(vc, nil) - case .failure(let error): - completion(nil, error) - } - } - } - - public func paymentInfoViewController() -> UIViewController { - let paymentInfoViewController = PaymentInfoViewController() - let paymentInfoViewModel = PaymentInfoViewModel(paymentProviders: paymentProviders) - paymentInfoViewController.viewModel = paymentInfoViewModel - return paymentInfoViewController - } -} - -extension PaymentComponentsController: PaymentComponentViewProtocol { - public func didTapOnMoreInformation(documentId: String?) { - viewDelegate?.didTapOnMoreInformation() - } - - public func didTapOnBankPicker(documentId: String?) { - viewDelegate?.didTapOnBankPicker() - } - - public func didTapOnPayInvoice(documentId: String?) { - viewDelegate?.didTapOnPayInvoice() - } -} - -extension PaymentComponentsController: PaymentProvidersBottomViewProtocol { - public func didSelectPaymentProvider(paymentProvider: PaymentProvider) { - selectedPaymentProvider = paymentProvider - storeDefaultPaymentProvider(paymentProvider: paymentProvider) - bottomViewDelegate?.didSelectPaymentProvider(paymentProvider: paymentProvider) - } - - public func didTapOnClose() { - bottomViewDelegate?.didTapOnClose() - } - - public func didTapOnMoreInformation() { - viewDelegate?.didTapOnMoreInformation() - } -} - -extension PaymentComponentsController { - private enum Constants { - static let kDefaultPaymentProvider = "defaultPaymentProvider" - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentInfoAnswerTableViewCell.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentInfoAnswerTableViewCell.swift deleted file mode 100644 index dd030a2fc..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentInfoAnswerTableViewCell.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// PaymentInfoAnswerTableViewCell.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit - -final class PaymentInfoAnswerTableViewCell: UITableViewCell { - static let identifier = "PaymentInfoAnswerTableViewCell" - - private lazy var textView: UITextView = { - let textView = UITextView() - textView.translatesAutoresizingMaskIntoConstraints = false - textView.isScrollEnabled = false - textView.isEditable = false - textView.textContainerInset = .zero - textView.textContainer.lineFragmentPadding = 0 - textView.isUserInteractionEnabled = true - textView.backgroundColor = .clear - return textView - }() - - var cellViewModel: PaymentInfoAnswerTableViewModel? { - didSet { - configure() - } - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - self.backgroundColor = .clear - contentView.addSubview(textView) - setupConstraints() - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupConstraints() { - NSLayoutConstraint.activate([ - textView.topAnchor.constraint(equalTo: contentView.topAnchor), - textView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - textView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - textView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -Constants.bottomPadding) - ]) - } - - private func configure() { - guard let cellViewModel = cellViewModel else { return } - textView.attributedText = cellViewModel.answerAttributedText - textView.textColor = cellViewModel.answerTextColor - textView.linkTextAttributes = cellViewModel.answerLinkAttributes - textView.layoutIfNeeded() - } -} - -struct PaymentInfoAnswerTableViewModel { - let answerAttributedText: NSAttributedString - let answerTextColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark1, - darkModeColor: UIColor.GiniHealthColors.light1).uiColor() - let answerLinkColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.accent1, - darkModeColor: UIColor.GiniHealthColors.accent1).uiColor() - let answerLinkAttributes: [NSAttributedString.Key: Any] - - init(answerAttributedText: NSAttributedString) { - self.answerAttributedText = answerAttributedText - self.answerLinkAttributes = [.foregroundColor: answerLinkColor] - } -} - -extension PaymentInfoAnswerTableViewCell { - private enum Constants { - static let bottomPadding = 16.0 - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentInfoBankCollectionViewCell.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentInfoBankCollectionViewCell.swift deleted file mode 100644 index 76051538f..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentInfoBankCollectionViewCell.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// PaymentInfoBankCollectionViewCell.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit - -final class PaymentInfoBankCollectionViewCell: UICollectionViewCell { - - static let identifier = "PaymentInfoBankCollectionViewCell" - - var cellViewModel: PaymentInfoBankCollectionViewCellModel? { - didSet { - guard let cellViewModel else { return } - bankIconImageView.image = cellViewModel.bankImageIcon - bankIconImageView.layer.borderColor = cellViewModel.borderColor.cgColor - } - } - - private lazy var bankIconImageView: UIImageView = { - let imageView = UIImageView() - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.contentMode = .scaleAspectFit - imageView.clipsToBounds = true - imageView.backgroundColor = .clear - imageView.roundCorners(corners: .allCorners, radius: Constants.bankIconCornerRadius) - imageView.layer.borderWidth = Constants.bankIconBorderWidth - return imageView - }() - - override init(frame: CGRect) { - super.init(frame: frame) - contentView.addSubview(bankIconImageView) - setupConstraints() - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(frame:) has not been implemented") - } - - private func setupConstraints() { - NSLayoutConstraint.activate([ - bankIconImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - bankIconImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - bankIconImageView.topAnchor.constraint(equalTo: contentView.topAnchor), - bankIconImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - ]) - } -} - -final class PaymentInfoBankCollectionViewCellModel { - private var bankImageIconData: Data? - var bankImageIcon: UIImage { - if let bankImageIconData { - return UIImage(data: bankImageIconData) ?? UIImage() - } - return UIImage() - } - - var borderColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark5, - darkModeColor: UIColor.GiniHealthColors.light5).uiColor() - - init(bankImageIconData: Data?) { - self.bankImageIconData = bankImageIconData - } -} - -extension PaymentInfoBankCollectionViewCell { - private enum Constants { - static let bankIconCornerRadius = 6.0 - static let bankIconBorderWidth = 1.0 - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentInfoQuestionHeaderViewCell.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentInfoQuestionHeaderViewCell.swift deleted file mode 100644 index ffaf2f582..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentInfoQuestionHeaderViewCell.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// PaymentInfoQuestionHeaderViewCell.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit - -final class PaymentInfoQuestionHeaderViewCell: UIView { - var didTapSelectButton: (() -> Void) = {} - - var headerViewModel: PaymentInfoQuestionHeaderViewModel? { - didSet { - guard let headerViewModel else { return } - configureView(viewModel: headerViewModel) - } - } - - private lazy var titleLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.lineBreakMode = .byWordWrapping - label.numberOfLines = 0 - label.textAlignment = .left - return label - }() - - private lazy var extendedImageView: UIImageView = { - let imageView = UIImageView() - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.contentMode = .scaleAspectFit - imageView.clipsToBounds = true - imageView.backgroundColor = .clear - imageView.frame = CGRect(x: 0, y: 0, width: Constants.imageSize, height: Constants.imageSize) - return imageView - }() - - override init(frame: CGRect) { - super.init(frame: frame) - addSubview(titleLabel) - addSubview(extendedImageView) - setupConstraints() - addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tappedOnView))) - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - } - - private func configureView(viewModel: PaymentInfoQuestionHeaderViewModel) { - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineHeightMultiple = Constants.titleLineHeight - titleLabel.attributedText = NSMutableAttributedString(string: viewModel.titleText, - attributes: [NSAttributedString.Key.paragraphStyle: paragraphStyle]) - titleLabel.textColor = viewModel.titleTextColor - titleLabel.font = viewModel.titleFont - extendedImageView.image = UIImageNamedPreferred(named: viewModel.extendedIconName) - } - - private func setupConstraints() { - NSLayoutConstraint.activate([ - titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor), - titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor), - titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Constants.titleRightPadding), - extendedImageView.widthAnchor.constraint(equalToConstant: extendedImageView.frame.width), - extendedImageView.heightAnchor.constraint(equalToConstant: extendedImageView.frame.height), - extendedImageView.trailingAnchor.constraint(equalTo: trailingAnchor), - extendedImageView.centerYAnchor.constraint(equalTo: centerYAnchor) - ]) - } - - @objc private func tappedOnView() { - didTapSelectButton() - } -} - -final class PaymentInfoQuestionHeaderViewModel { - var titleText: String - var titleFont: UIFont - let titleTextColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark1, - darkModeColor: UIColor.GiniHealthColors.light1).uiColor() - var extendedIconName: String - private let plusIcon = "ic_plus" - private let minusIcon = "ic_minus" - - init(title: String, isExtended: Bool) { - self.titleText = title - let giniConfiguration = GiniHealthConfiguration.shared - self.titleFont = giniConfiguration.textStyleFonts[.body1] ?? UIFont.systemFont(ofSize: 16) - self.extendedIconName = isExtended ? minusIcon : plusIcon - } -} - -extension PaymentInfoQuestionHeaderViewCell { - private enum Constants { - static let titleLineHeight = 1.15 - static let titleRightPadding = 85.0 - static let imageSize = 24.0 - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentInfoViewController.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentInfoViewController.swift deleted file mode 100644 index 82b5a43b0..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentInfoViewController.swift +++ /dev/null @@ -1,357 +0,0 @@ -// -// PaymentInfoViewController.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit - -class PaymentInfoViewController: UIViewController { - - var viewModel: PaymentInfoViewModel! { - didSet { - setupView() - } - } - - private lazy var scrollView: UIScrollView = { - let scrollView = UIScrollView() - scrollView.showsVerticalScrollIndicator = false - scrollView.showsHorizontalScrollIndicator = false - scrollView.isScrollEnabled = true - scrollView.translatesAutoresizingMaskIntoConstraints = false - return scrollView - }() - - private lazy var contentView: UIView = { - let contentView = UIView() - contentView.translatesAutoresizingMaskIntoConstraints = false - return contentView - }() - - private lazy var bankIconsCollectionView: UICollectionView = { - let collectionLayout = UICollectionViewFlowLayout() - collectionLayout.scrollDirection = .horizontal - collectionLayout.minimumInteritemSpacing = Constants.bankIconsSpacing - - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionLayout) - collectionView.translatesAutoresizingMaskIntoConstraints = false - collectionView.frame = CGRect(x: 0, y: 0, width: .greatestFiniteMagnitude, height: Constants.bankIconsWidth) - collectionView.dataSource = self - collectionView.delegate = self - collectionView.backgroundColor = .clear - collectionView.allowsSelection = false - collectionView.showsHorizontalScrollIndicator = false - collectionView.isScrollEnabled = true - collectionView.register(PaymentInfoBankCollectionViewCell.self, - forCellWithReuseIdentifier: PaymentInfoBankCollectionViewCell.identifier) - return collectionView - }() - - private lazy var poweredByGiniView: PoweredByGiniView = { - let view = PoweredByGiniView() - view.viewModel = PoweredByGiniViewModel() - return view - }() - - private lazy var payBillsTitleLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.font = viewModel.payBillsTitleFont - label.textColor = viewModel.payBillsTitleTextColor - label.lineBreakMode = .byWordWrapping - label.numberOfLines = 0 - label.textAlignment = .left - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineHeightMultiple = Constants.payBillsTitleLineHeight - label.attributedText = NSMutableAttributedString(string: viewModel.payBillsTitleText, - attributes: [NSAttributedString.Key.paragraphStyle: paragraphStyle]) - return label - }() - - private lazy var payBillsDescriptionTextView: UITextView = { - let textView = UITextView() - textView.translatesAutoresizingMaskIntoConstraints = false - textView.isScrollEnabled = false - textView.isEditable = false - textView.textContainerInset = .zero - textView.textContainer.lineFragmentPadding = 0 - textView.isUserInteractionEnabled = true - textView.backgroundColor = .clear - textView.attributedText = viewModel.payBillsDescriptionAttributedText - textView.linkTextAttributes = viewModel.payBillsDescriptionLinkAttributes - return textView - }() - - private lazy var questionsTitleLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.font = viewModel.questionsTitleFont - label.textColor = viewModel.questionsTitleTextColor - label.lineBreakMode = .byWordWrapping - label.numberOfLines = 0 - label.textAlignment = .left - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineHeightMultiple = Constants.questionsTitleLineHeight - label.attributedText = NSMutableAttributedString(string: viewModel.questionsTitleText, - attributes: [NSAttributedString.Key.paragraphStyle: paragraphStyle]) - return label - }() - - private lazy var questionsTableView: UITableView = { - let tableView = UITableView() - tableView.delegate = self - tableView.dataSource = self - tableView.register(PaymentInfoAnswerTableViewCell.self, - forCellReuseIdentifier: PaymentInfoAnswerTableViewCell.identifier) - tableView.separatorStyle = .singleLine - tableView.backgroundColor = .clear - tableView.showsVerticalScrollIndicator = false - tableView.translatesAutoresizingMaskIntoConstraints = false - tableView.isScrollEnabled = false - tableView.rowHeight = UITableView.automaticDimension - tableView.estimatedRowHeight = Constants.questionTitleHeight - tableView.estimatedSectionHeaderHeight = Constants.questionTitleHeight - tableView.estimatedSectionFooterHeight = 1.0 - if #available(iOS 15.0, *) { - tableView.sectionHeaderTopPadding = 0 - } - return tableView - }() - - private var heightsQuestionsTableView: [NSLayoutConstraint] = [] - - override func viewDidLoad() { - super.viewDidLoad() - self.title = viewModel.titleText - } - - private func setupView() { - setupViewHierarchy() - setupViewAttributes() - setupViewConstraints() - } - - private func setupViewHierarchy() { - view.addSubview(scrollView) - scrollView.addSubview(contentView) - contentView.addSubview(bankIconsCollectionView) - contentView.addSubview(poweredByGiniView) - contentView.addSubview(payBillsTitleLabel) - contentView.addSubview(payBillsDescriptionTextView) - contentView.addSubview(questionsTitleLabel) - contentView.addSubview(questionsTableView) - } - - private func setupViewAttributes() { - view.backgroundColor = viewModel.backgroundColor - } - - private func setupViewConstraints() { - setupContentViewConstraints() - setupBankIconsCollectionViewConstraints() - setupPoweredByGiniConstraints() - setupPayBillsConstraints() - setupQuestionsConstraints() - } - - private func setupContentViewConstraints() { - NSLayoutConstraint.activate([ - scrollView.topAnchor.constraint(equalTo: view.topAnchor), - scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), - - contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), - contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), - contentView.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor), - contentView.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor), - ]) - } - - private func setupBankIconsCollectionViewConstraints() { - NSLayoutConstraint.activate([ - bankIconsCollectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - bankIconsCollectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - bankIconsCollectionView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: Constants.bankIconsTopSpacing), - bankIconsCollectionView.heightAnchor.constraint(equalToConstant: bankIconsCollectionView.frame.height) - ]) - } - - private func setupPoweredByGiniConstraints() { - NSLayoutConstraint.activate([ - poweredByGiniView.topAnchor.constraint(equalTo: bankIconsCollectionView.bottomAnchor, constant: Constants.poweredByGiniTopPadding), - poweredByGiniView.centerXAnchor.constraint(equalTo: view.centerXAnchor) - ]) - } - - private func setupPayBillsConstraints() { - NSLayoutConstraint.activate([ - payBillsTitleLabel.topAnchor.constraint(equalTo: poweredByGiniView.bottomAnchor, constant: Constants.payBillsTitleTopPadding), - payBillsTitleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.leftRightPadding), - payBillsTitleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.leftRightPadding), - payBillsTitleLabel.heightAnchor.constraint(lessThanOrEqualToConstant: Constants.maxPayBillsTitleHeight), - payBillsDescriptionTextView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.leftRightPadding), - payBillsDescriptionTextView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.payBillsDescriptionRightPadding), - payBillsDescriptionTextView.topAnchor.constraint(equalTo: payBillsTitleLabel.bottomAnchor, constant: Constants.payBillsDescriptionTopPadding), - payBillsDescriptionTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: Constants.minPayBillsDescriptionHeight), - ]) - } - - private func setupQuestionsConstraints() { - NSLayoutConstraint.activate([ - questionsTitleLabel.topAnchor.constraint(equalTo: payBillsDescriptionTextView.bottomAnchor, constant: Constants.questionsTitleTopPadding), - questionsTitleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.leftRightPadding), - questionsTitleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.leftRightPadding), - questionsTableView.topAnchor.constraint(equalTo: questionsTitleLabel.bottomAnchor), - questionsTableView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.leftRightPadding), - questionsTableView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.leftRightPadding), - questionsTableView.heightAnchor.constraint(greaterThanOrEqualToConstant: Double(viewModel.questions.count) * Constants.questionTitleHeight), - questionsTableView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -Constants.leftRightPadding) - ]) - } - - private func extended(section: Int) { - let isExtended = viewModel.questions[section].isExtended - viewModel.questions[section].isExtended = !isExtended - questionsTableView.reloadData() - questionsTableView.layoutIfNeeded() - // Small hack needed to satisfy automatic dimension table view inside scrollView - NSLayoutConstraint.deactivate(heightsQuestionsTableView) - heightsQuestionsTableView = [questionsTableView.heightAnchor.constraint(greaterThanOrEqualToConstant: questionsTableView.contentSize.height)] - NSLayoutConstraint.activate(heightsQuestionsTableView) - } -} - -extension PaymentInfoViewController: UICollectionViewDataSource { - func collectionView(_ collectionView: UICollectionView, - cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PaymentInfoBankCollectionViewCell.identifier, - for: indexPath) as? PaymentInfoBankCollectionViewCell else { - return UICollectionViewCell() - } - cell.cellViewModel = PaymentInfoBankCollectionViewCellModel(bankImageIconData: viewModel.paymentProviders[indexPath.row].iconData) - return cell - } - - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return viewModel.paymentProviders.count - } - - func numberOfSections(in collectionView: UICollectionView) -> Int { - return 1 - } -} - -extension PaymentInfoViewController: UICollectionViewDelegateFlowLayout { - func collectionView(_ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - sizeForItemAt indexPath: IndexPath) -> CGSize { - return CGSize(width: Constants.bankIconsWidth, height: Constants.bankIconsHeight) - } - - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { - let cellCount = Double(viewModel.paymentProviders.count) - if cellCount > 0 { - let cellWidth = Constants.bankIconsWidth - - let totalCellWidth = cellWidth * cellCount + Constants.bankIconsSpacing * (cellCount - 1) - let contentWidth = collectionView.frame.size.width - (2 * Constants.leftRightPadding) - - if totalCellWidth < contentWidth { - let padding = (contentWidth - totalCellWidth) / 2.0 - return UIEdgeInsets(top: 0, left: padding, bottom: 0, right: padding) - } else { - return UIEdgeInsets(top: 0, left: Constants.leftRightPadding, bottom: 0, right: Constants.leftRightPadding) - } - } - return UIEdgeInsets.zero - } -} - -extension PaymentInfoViewController: UITableViewDelegate, UITableViewDataSource { - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if viewModel.questions[section].isExtended { - return 1 - } - return 0 - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: PaymentInfoAnswerTableViewCell.identifier, - for: indexPath) as? PaymentInfoAnswerTableViewCell else { - return UITableViewCell() - } - let answerTableViewCellModel = PaymentInfoAnswerTableViewModel(answerAttributedText: viewModel.questions[indexPath.section].description) - cell.cellViewModel = answerTableViewCellModel - return cell - } - - func numberOfSections(in tableView: UITableView) -> Int { - viewModel.questions.count - } - - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let viewHeader = PaymentInfoQuestionHeaderViewCell(frame: CGRect(x: 0, y: 0, width: .greatestFiniteMagnitude, height: Constants.questionTitleHeight)) - viewHeader.headerViewModel = PaymentInfoQuestionHeaderViewModel(title: viewModel.questions[section].title, isExtended: viewModel.questions[section].isExtended) - viewHeader.didTapSelectButton = { [weak self] in - self?.extended(section: section) - } - return viewHeader - } - - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - Constants.questionTitleHeight - } - - func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { - guard section < viewModel.questions.count - 1 else { return UIView() } - let separatorView = UIView(frame: CGRect(x: 0, y: 0, width: .greatestFiniteMagnitude, height: Constants.questionSectionSeparatorHeight)) - separatorView.backgroundColor = viewModel.separatorColor - return separatorView - } - - func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { - Constants.questionSectionSeparatorHeight - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - UITableView.automaticDimension - } - - func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { - Constants.estimatedAnswerHeight - } -} - -extension PaymentInfoViewController { - private enum Constants { - static let paragraphSpacing = 10.0 - - static let leftRightPadding = 16.0 - - static let bankIconsSpacing = 5.0 - static let bankIconsTopSpacing = 15.0 - static let bankIconsWidth = 36.0 - static let bankIconsHeight = 36.0 - - static let poweredByGiniTopPadding = 16.0 - - static let payBillsTitleTopPadding = 16.0 - static let payBillsTitleLineHeight = 1.26 - static let maxPayBillsTitleHeight = 100.0 - static let payBillsDescriptionTopPadding = 8.0 - static let payBillsDescriptionRightPadding = 31.0 - static let minPayBillsDescriptionHeight = 100.0 - - static let questionsTitleTopPadding = 24.0 - static let questionsTitleLineHeight = 1.28 - - static let questionTitleHeight = 72.0 - static let questionSectionSeparatorHeight = 1.0 - - static let estimatedAnswerHeight = 250.0 - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentInfoViewModel.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentInfoViewModel.swift deleted file mode 100644 index 20100a919..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentInfoViewModel.swift +++ /dev/null @@ -1,149 +0,0 @@ -// -// PaymentInfoViewModel.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit -import GiniHealthAPILibrary - -struct FAQSection { - let title: String - var description: NSAttributedString - var isExtended: Bool -} - -final class PaymentInfoViewModel { - - var paymentProviders: PaymentProviders - - let backgroundColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark7, - darkModeColor: UIColor.GiniHealthColors.light7).uiColor() - - let titleText: String = GiniLocalized.string("ginihealth.paymentcomponent.paymentinfo.title.label", - comment: "Payment Info title label text") - - let payBillsTitleText: String = GiniLocalized.string("ginihealth.paymentcomponent.paymentinfo.payBills.title.label", - comment: "Payment Info pay bills title label text") - let payBillsTitleFont: UIFont - let payBillsTitleTextColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark1, - darkModeColor: UIColor.GiniHealthColors.light1).uiColor() - - private let payBillsDescriptionText: String = GiniLocalized.string("ginihealth.paymentcomponent.paymentinfo.payBills.description.label", - comment: "Payment Info pay bills description text") - var payBillsDescriptionAttributedText: NSMutableAttributedString = NSMutableAttributedString() - var payBillsDescriptionLinkAttributes: [NSAttributedString.Key: Any] - private let payBillsDescriptionFont: UIFont - private let payBillsDescriptionTextColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark1, - darkModeColor: UIColor.GiniHealthColors.light1).uiColor() - private let giniWebsiteText = GiniLocalized.string("ginihealth.paymentcomponent.paymentinfo.payBills.description.clickable.text", - comment: "Word range that's clickable in pay bills description") - private let giniFont: UIFont - private let giniURLText = GiniLocalized.string("ginihealth.paymentcomponent.paymentinfo.gini.link", - comment: "Gini website link url") - - let questionsTitleText: String = GiniLocalized.string("ginihealth.paymentcomponent.paymentinfo.questions.title.label", - comment: "Payment Info questions title label text") - let questionsTitleFont: UIFont - let questionsTitleTextColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark1, - darkModeColor: UIColor.GiniHealthColors.light1).uiColor() - - private var answersFont: UIFont - private let answerPrivacyPolicyText = GiniLocalized.string("ginihealth.paymentcomponent.paymentinfo.questions.answer.clickable.text", - comment: "Payment info answers clickable privacy policy") - private let privacyPolicyURLText = GiniLocalized.string("ginihealth.paymentcomponent.paymentinfo.gini.privacypolicy.link", - comment: "Gini privacy policy link url") - private var linksFont: UIFont - private let linksTextColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.accent1, - darkModeColor: UIColor.GiniHealthColors.accent1).uiColor() - - let separatorColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark5, - darkModeColor: UIColor.GiniHealthColors.light5).uiColor() - - var questions: [FAQSection] = [] - - init(paymentProviders: PaymentProviders) { - self.paymentProviders = paymentProviders - - let giniHealthConfiguration = GiniHealthConfiguration.shared - - let defaultRegularFont: UIFont = UIFont.systemFont(ofSize: 13, weight: .regular) - let defaultBoldFont: UIFont = UIFont.systemFont(ofSize: 13, weight: .bold) - - payBillsTitleFont = giniHealthConfiguration.textStyleFonts[.subtitle1] ?? defaultBoldFont - payBillsDescriptionFont = giniHealthConfiguration.textStyleFonts[.body2] ?? defaultRegularFont - questionsTitleFont = giniHealthConfiguration.textStyleFonts[.subtitle1] ?? defaultBoldFont - giniFont = giniHealthConfiguration.textStyleFonts[.button] ?? defaultBoldFont - answersFont = giniHealthConfiguration.textStyleFonts[.body2] ?? defaultRegularFont - linksFont = giniHealthConfiguration.textStyleFonts[.linkBold] ?? defaultBoldFont - - payBillsDescriptionLinkAttributes = [.foregroundColor: linksTextColor] - - configurePayBillsGiniLink() - setupQuestions() - } - - private func setupQuestions() { - for index in 1 ... Constants.numberOfQuestions { - let answerAttributedString = answerWithAttributes(answer: GiniLocalized.string("ginihealth.paymentcomponent.paymentinfo.questions.answer.\(index)", - comment: "Answers description")) - let questionSection = FAQSection(title: GiniLocalized.string("ginihealth.paymentcomponent.paymentinfo.questions.question.\(index)", - comment: "Questions titles"), - description: textWithLinks(linkFont: linksFont, - attributedString: answerAttributedString), - isExtended: false) - questions.append(questionSection) - } - } - - private func configurePayBillsGiniLink() { - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineHeightMultiple = Constants.payBillsDescriptionLineHeight - paragraphStyle.paragraphSpacing = Constants.payBillsParagraphSpacing - payBillsDescriptionAttributedText = NSMutableAttributedString(string: payBillsDescriptionText, - attributes: [.paragraphStyle: paragraphStyle, - .font: payBillsDescriptionFont, - .foregroundColor: payBillsTitleTextColor]) - payBillsDescriptionAttributedText = textWithLinks(linkFont: giniFont, - attributedString: payBillsDescriptionAttributedText) - } - - private func answerWithAttributes(answer: String) -> NSMutableAttributedString { - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineHeightMultiple = Constants.answersLineHeight - paragraphStyle.paragraphSpacing = Constants.answersParagraphSpacing - let answerAttributedText = NSMutableAttributedString(string: answer, - attributes: [.font: answersFont, .paragraphStyle: paragraphStyle]) - return answerAttributedText - } - - private func textWithLinks(linkFont: UIFont, attributedString: NSMutableAttributedString) -> NSMutableAttributedString { - let attributedString = attributedString - let giniRange = (attributedString.string as NSString).range(of: giniWebsiteText) - attributedString.addLinkToRange(link: giniURLText, - range: giniRange, - linkFont: linkFont, - textToRemove: Constants.linkTextToRemove) - let privacyPolicyRange = (attributedString.string as NSString).range(of: answerPrivacyPolicyText) - attributedString.addLinkToRange(link: privacyPolicyURLText, - range: privacyPolicyRange, - linkFont: linkFont, - textToRemove: Constants.linkTextToRemove) - return attributedString - } -} - -extension PaymentInfoViewModel { - private enum Constants { - static let numberOfQuestions = 6 - - static let payBillsDescriptionLineHeight = 1.32 - static let payBillsParagraphSpacing = 10.0 - - static let answersLineHeight = 1.32 - static let answersParagraphSpacing = 10.0 - - static let linkTextToRemove = "[LINK]" - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentPrimaryButton.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentPrimaryButton.swift deleted file mode 100644 index 485a3eda9..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentPrimaryButton.swift +++ /dev/null @@ -1,114 +0,0 @@ -// -// PaymentPrimaryButton.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit -import GiniHealthAPILibrary - -final class PaymentPrimaryButton: UIView { - - private let giniHealthConfiguration = GiniHealthConfiguration.shared - - var didTapButton: (() -> Void)? - - private lazy var contentView: UIView = { - let view = EmptyView() - view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapOnPayInvoiceView))) - return view - }() - - private lazy var titleLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.numberOfLines = 1 - label.adjustsFontSizeToFitWidth = true - label.textAlignment = .center - return label - }() - - private lazy var leftImageView: UIImageView = { - let imageView = UIImageView() - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.frame = CGRect(x: 0, y: 0, width: Constants.bankIconSize, height: Constants.bankIconSize) - return imageView - }() - - init() { - super.init(frame: .zero) - addSubview(contentView) - contentView.addSubview(titleLabel) - setupConstraints() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupConstraints() { - NSLayoutConstraint.activate([ - contentView.leadingAnchor.constraint(equalTo: leadingAnchor), - contentView.trailingAnchor.constraint(equalTo: trailingAnchor), - contentView.topAnchor.constraint(equalTo: topAnchor), - contentView.bottomAnchor.constraint(equalTo: bottomAnchor), - contentView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), - contentView.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), - contentView.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor) - ]) - } - - private func setupLeftImageConstraints() { - leftImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.contentLeadingPadding).isActive = true - leftImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true - leftImageView.widthAnchor.constraint(equalToConstant: leftImageView.frame.width).isActive = true - leftImageView.heightAnchor.constraint(equalToConstant: leftImageView.frame.height).isActive = true - } - - @objc private func tapOnPayInvoiceView() { - didTapButton?() - } -} - -extension PaymentPrimaryButton { - func configure(with configuration: ButtonConfiguration) { - self.contentView.backgroundColor = configuration.backgroundColor - self.contentView.layer.cornerRadius = configuration.cornerRadius - self.contentView.layer.borderColor = configuration.borderColor.cgColor - self.contentView.layer.shadowColor = configuration.shadowColor.cgColor - - self.titleLabel.textColor = configuration.titleColor - - if let buttonFont = giniHealthConfiguration.textStyleFonts[.button] { - self.titleLabel.font = buttonFont - } - } - - func customConfigure(paymentProviderColors: ProviderColors?, text: String, leftImageData: Data? = nil) { - if let backgroundHexColor = paymentProviderColors?.background.toColor() { - contentView.backgroundColor = backgroundHexColor - } - contentView.isUserInteractionEnabled = true - - titleLabel.text = text - if let textHexColor = paymentProviderColors?.text.toColor() { - titleLabel.textColor = textHexColor - } - // Left image appears only on Payment Review Screen - if let leftImageData { - contentView.addSubview(leftImageView) - setupLeftImageConstraints() - leftImageView.roundCorners(corners: .allCorners, radius: Constants.bankIconCornerRadius) - leftImageView.image = UIImage(data: leftImageData) - } - } -} - -extension PaymentPrimaryButton { - private enum Constants { - static let bankIconSize: CGFloat = 36 - static let bankIconCornerRadius: CGFloat = 8 - static let contentLeadingPadding: CGFloat = 19 - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentSecondaryButton.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentSecondaryButton.swift deleted file mode 100644 index 1a79da063..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PaymentSecondaryButton.swift +++ /dev/null @@ -1,146 +0,0 @@ -// -// PaymentSecondaryButton.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit - -final class PaymentSecondaryButton: UIView { - - private let giniHealthConfiguration = GiniHealthConfiguration.shared - - var didTapButton: (() -> Void)? - - private lazy var contentView: UIView = { - let view = EmptyView() - view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapOnBankPicker))) - return view - }() - - private lazy var leftImageView: UIImageView = { - let imageView = UIImageView() - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.frame = CGRect(x: 0, y: 0, width: Constants.bankIconSize, height: Constants.bankIconSize) - return imageView - }() - - private lazy var titleLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.numberOfLines = 1 - label.lineBreakMode = .byTruncatingTail - return label - }() - - private lazy var rightImageView: UIImageView = { - let imageView = UIImageView() - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.frame = CGRect(x: 0, y: 0, width: Constants.chevronIconSize, height: Constants.chevronIconSize) - return imageView - }() - - init() { - super.init(frame: .zero) - addSubview(contentView) - contentView.addSubview(leftImageView) - contentView.addSubview(titleLabel) - contentView.addSubview(rightImageView) - setupConstraints() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupConstraints() { - NSLayoutConstraint.activate([ - contentView.leadingAnchor.constraint(equalTo: leadingAnchor), - contentView.trailingAnchor.constraint(equalTo: trailingAnchor), - contentView.topAnchor.constraint(equalTo: topAnchor), - contentView.bottomAnchor.constraint(equalTo: bottomAnchor), - rightImageView.widthAnchor.constraint(equalToConstant: rightImageView.frame.width), - rightImageView.heightAnchor.constraint(equalToConstant: rightImageView.frame.height), - contentView.trailingAnchor.constraint(equalTo: rightImageView.trailingAnchor, constant: Constants.contentTrailingPadding), - rightImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - ]) - } - - private func activateImagesViewConstraints() { - if !leftImageView.isHidden { - leftImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.contentPadding).isActive = true - leftImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true - leftImageView.widthAnchor.constraint(equalToConstant: leftImageView.frame.width).isActive = true - leftImageView.heightAnchor.constraint(equalToConstant: leftImageView.frame.height).isActive = true - - titleLabel.leadingAnchor.constraint(equalTo: leftImageView.trailingAnchor, constant: Constants.contentPadding).isActive = true - leftImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor).isActive = true - } else { - titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.contentPadding).isActive = true - titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true - } - if titleLabel.isHidden { - rightImageView.leadingAnchor.constraint(equalTo: leftImageView.trailingAnchor, constant: Constants.bankIconChevronIconPadding).isActive = true - } else { - rightImageView.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor).isActive = true - } - } - - @objc - private func tapOnBankPicker() { - didTapButton?() - } -} - -extension PaymentSecondaryButton { - func configure(with configuration: ButtonConfiguration) { - contentView.layer.cornerRadius = configuration.cornerRadius - contentView.layer.borderWidth = configuration.borderWidth - contentView.layer.borderColor = configuration.borderColor.cgColor - contentView.backgroundColor = configuration.backgroundColor - - leftImageView.layer.borderColor = configuration.borderColor.cgColor - leftImageView.layer.borderWidth = configuration.borderWidth - leftImageView.roundCorners(corners: .allCorners, radius: Constants.bankIconCornerRadius) - - titleLabel.textColor = configuration.titleColor - if let inputFont = giniHealthConfiguration.textStyleFonts[.input] { - titleLabel.font = inputFont - } - } - - func customConfigure(labelText: String, leftImageIcon: UIImage?, rightImageIcon: String?, rightImageTintColor: UIColor, shouldShowLabel: Bool) { - if let leftImageIcon { - leftImageView.image = leftImageIcon - leftImageView.isHidden = false - } else { - leftImageView.isHidden = true - } - if let rightImageIcon { - rightImageView.image = UIImageNamedPreferred(named: rightImageIcon)?.withRenderingMode(.alwaysTemplate) - rightImageView.tintColor = rightImageTintColor - rightImageView.isHidden = false - } else { - rightImageView.isHidden = true - } - if shouldShowLabel { - titleLabel.text = labelText - titleLabel.isHidden = false - } else { - titleLabel.isHidden = true - } - activateImagesViewConstraints() - } -} - -extension PaymentSecondaryButton { - enum Constants { - static let bankIconSize: CGFloat = 32 - static let bankIconCornerRadius: CGFloat = 6 - static let chevronIconSize: CGFloat = 24 - static let contentTrailingPadding: CGFloat = 16 - static let bankIconChevronIconPadding: CGFloat = 12 - static let contentPadding: CGFloat = 12 - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PoweredByGiniView.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PoweredByGiniView.swift deleted file mode 100644 index c98fc93ee..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PoweredByGiniView.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// PoweredByGiniView.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit - -final class PoweredByGiniView: UIView { - - var viewModel: PoweredByGiniViewModel! { - didSet { - setupView() - } - } - - private lazy var poweredByGiniView: UIView = { - EmptyView() - }() - - private lazy var poweredByGiniLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.text = viewModel.poweredByGiniLabelText - label.textColor = viewModel.poweredByGiniLabelAccentColor - label.font = viewModel.poweredByGiniLabelFont - label.numberOfLines = Constants.textNumberOfLines - label.adjustsFontSizeToFitWidth = true - return label - }() - - private lazy var giniImageView: UIImageView = { - let image = UIImageNamedPreferred(named: viewModel.giniIconName) - let imageView = UIImageView(image: image) - imageView.frame = CGRect(x: 0, y: 0, width: Constants.widthGiniLogo, height: Constants.heightGiniLogo) - imageView.translatesAutoresizingMaskIntoConstraints = false - return imageView - }() - - override init(frame: CGRect) { - super.init(frame: frame) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupView() { - self.translatesAutoresizingMaskIntoConstraints = false - - poweredByGiniView.addSubview(poweredByGiniLabel) - poweredByGiniView.addSubview(giniImageView) - self.addSubview(poweredByGiniView) - - NSLayoutConstraint.activate([ - poweredByGiniView.trailingAnchor.constraint(equalTo: trailingAnchor), - poweredByGiniView.leadingAnchor.constraint(equalTo: leadingAnchor), - poweredByGiniView.topAnchor.constraint(equalTo: topAnchor), - poweredByGiniView.bottomAnchor.constraint(equalTo: bottomAnchor), - poweredByGiniView.trailingAnchor.constraint(equalTo: giniImageView.trailingAnchor), - giniImageView.leadingAnchor.constraint(equalTo: poweredByGiniLabel.trailingAnchor, constant: Constants.spacingImageText), - poweredByGiniLabel.centerYAnchor.constraint(equalTo: giniImageView.centerYAnchor), - poweredByGiniLabel.leadingAnchor.constraint(equalTo: poweredByGiniView.leadingAnchor), - poweredByGiniLabel.topAnchor.constraint(equalTo: poweredByGiniView.topAnchor), - poweredByGiniLabel.bottomAnchor.constraint(equalTo: poweredByGiniView.bottomAnchor), - giniImageView.heightAnchor.constraint(equalToConstant: giniImageView.frame.height), - giniImageView.widthAnchor.constraint(equalToConstant: giniImageView.frame.width), - giniImageView.topAnchor.constraint(equalTo: poweredByGiniView.topAnchor, constant: Constants.imageTopBottomPadding), - giniImageView.bottomAnchor.constraint(equalTo: poweredByGiniView.bottomAnchor, constant: -Constants.imageTopBottomPadding) - ]) - } -} - -extension PoweredByGiniView { - private enum Constants { - static let imageTopBottomPadding = 3.0 - static let spacingImageText = 4.0 - static let widthGiniLogo = 28.0 - static let heightGiniLogo = 18.0 - static let textNumberOfLines = 1 - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PoweredByGiniViewModel.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PoweredByGiniViewModel.swift deleted file mode 100644 index d785928da..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/PoweredByGiniViewModel.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// PoweredByGiniViewModel.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit - -final class PoweredByGiniViewModel { - - // powered by Gini view - let poweredByGiniLabelText: String = GiniLocalized.string("ginihealth.paymentcomponent.poweredByGini.label", comment: "") - let poweredByGiniLabelFont: UIFont - let poweredByGiniLabelAccentColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark4, - darkModeColor: UIColor.GiniHealthColors.light4).uiColor() - let giniIconName: String = "giniLogo" - - init() { - self.poweredByGiniLabelFont = GiniHealthConfiguration.shared.textStyleFonts[.caption2] ?? UIFont.systemFont(ofSize: 12, weight: .regular) - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/ShareInvoiceBottomView.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/ShareInvoiceBottomView.swift deleted file mode 100644 index e6bdd3a85..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/ShareInvoiceBottomView.swift +++ /dev/null @@ -1,320 +0,0 @@ -// -// ShareInvoiceBottomView.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit - -class ShareInvoiceBottomView: BottomSheetViewController { - - var viewModel: ShareInvoiceBottomViewModel - - private lazy var contentStackView: UIStackView = { - EmptyStackView(orientation: .vertical) - }() - - private lazy var titleView: UIView = { - EmptyView() - }() - - private lazy var titleLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.text = viewModel.titleText - label.textColor = viewModel.titleLabelAccentColor - label.font = viewModel.titleLabelFont - label.numberOfLines = 0 - label.lineBreakMode = .byTruncatingTail - return label - }() - - private lazy var descriptionView: UIView = { - EmptyView() - }() - - private lazy var descriptionLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.text = viewModel.descriptionLabelText - label.textColor = viewModel.descriptionAccentColor - label.font = viewModel.descriptionLabelFont - label.numberOfLines = 0 - label.lineBreakMode = .byTruncatingTail - return label - }() - - private lazy var appsView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = viewModel.appsBackgroundColor - return view - }() - - private lazy var appsStackView: UIStackView = { - let stackView = EmptyStackView(orientation: .horizontal) - stackView.distribution = .fillEqually - stackView.spacing = Constants.appsViewSpacing - return stackView - }() - - private lazy var bankIconImageView: UIImageView = { - let imageView = UIImageView(image: viewModel.bankImageIcon) - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.frame = CGRect(x: 0, y: 0, width: Constants.bankIconSize, height: Constants.bankIconSize) - imageView.roundCorners(corners: .allCorners, radius: Constants.bankIconCornerRadius) - imageView.layer.borderWidth = Constants.bankIconBorderWidth - imageView.layer.borderColor = viewModel.bankIconBorderColor.cgColor - return imageView - }() - - private lazy var tipView: UIView = { - EmptyView() - }() - - private lazy var tipStackView: UIStackView = { - let stackView = EmptyStackView(orientation: .horizontal) - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.spacing = Constants.viewPaddingConstraint - stackView.distribution = .fillProportionally - return stackView - }() - - private lazy var tipLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = viewModel.tipAccentColor - label.font = viewModel.tipLabelFont - label.numberOfLines = 0 - label.text = viewModel.tipLabelText - - let tipActionableAttributtedString = NSMutableAttributedString(string: viewModel.tipLabelText) - let tipPartString = (viewModel.tipLabelText as NSString).range(of: viewModel.tipActionablePartText) - tipActionableAttributtedString.addAttribute(.foregroundColor, - value: viewModel.tipAccentColor, - range: tipPartString) - tipActionableAttributtedString.addAttribute(NSAttributedString.Key.underlineStyle, - value: NSUnderlineStyle.single.rawValue, - range: tipPartString) - tipActionableAttributtedString.addAttribute(NSAttributedString.Key.font, - value: viewModel.tipLabelLinkFont, - range: tipPartString) - let tapOnMoreInformation = UITapGestureRecognizer(target: self, - action: #selector(tapOnLabelAction(gesture:))) - label.isUserInteractionEnabled = true - label.addGestureRecognizer(tapOnMoreInformation) - label.attributedText = tipActionableAttributtedString - return label - }() - - private lazy var tipButton: UIButton = { - let button = UIButton(type: .system) - button.translatesAutoresizingMaskIntoConstraints = false - let image = UIImageNamedPreferred(named: viewModel.tipIconName) - button.setImage(image, for: .normal) - button.tintColor = viewModel.tipAccentColor - button.isUserInteractionEnabled = false - button.imageView?.contentMode = .scaleAspectFit - return button - }() - - private lazy var continueView: UIView = { - EmptyView() - }() - - private lazy var continueButton: PaymentPrimaryButton = { - let button = PaymentPrimaryButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.configure(with: viewModel.giniHealthConfiguration.primaryButtonConfiguration) - button.customConfigure(paymentProviderColors: viewModel.paymentProviderColors, - text: viewModel.continueLabelText) - return button - }() - - private lazy var bottomView: UIView = { - EmptyView() - }() - - private lazy var bottomStackView: UIStackView = { - EmptyStackView(orientation: .horizontal) - }() - - private lazy var poweredByGiniView: PoweredByGiniView = { - let view = PoweredByGiniView() - view.viewModel = PoweredByGiniViewModel() - return view - }() - - override func viewDidLoad() { - super.viewDidLoad() - setupView() - } - - init(viewModel: ShareInvoiceBottomViewModel) { - self.viewModel = viewModel - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupView() { - setupViewHierarchy() - setupLayout() - setButtonsState() - } - - private func setupViewHierarchy() { - titleView.addSubview(titleLabel) - contentStackView.addArrangedSubview(titleView) - descriptionView.addSubview(descriptionLabel) - contentStackView.addArrangedSubview(descriptionView) - generateAppViews().forEach { appView in - appsStackView.addArrangedSubview(appView) - } - appsView.addSubview(appsStackView) - contentStackView.addArrangedSubview(appsView) - tipStackView.addArrangedSubview(tipButton) - tipStackView.addArrangedSubview(tipLabel) - tipView.addSubview(tipStackView) - contentStackView.addArrangedSubview(tipView) - continueView.addSubview(continueButton) - contentStackView.addArrangedSubview(continueView) - bottomStackView.addArrangedSubview(UIView()) - bottomStackView.addArrangedSubview(poweredByGiniView) - bottomView.addSubview(bottomStackView) - contentStackView.addArrangedSubview(bottomView) - self.setContent(content: contentStackView) - } - - private func setupLayout() { - setupTitleViewConstraints() - setupDescriptionViewConstraints() - setupAppsView() - setupTipViewConstraints() - setupContinueButtonConstraints() - setupPoweredByGiniConstraints() - } - - private func setButtonsState() { - continueButton.didTapButton = { [weak self] in - self?.tapOnContinueButton() - } - } - - private func setupTitleViewConstraints() { - NSLayoutConstraint.activate([ - titleLabel.leadingAnchor.constraint(equalTo: titleView.leadingAnchor, constant: Constants.viewPaddingConstraint), - titleLabel.trailingAnchor.constraint(equalTo: titleView.trailingAnchor, constant: -Constants.viewPaddingConstraint), - titleLabel.topAnchor.constraint(equalTo: titleView.topAnchor, constant: Constants.topBottomPaddingConstraint), - titleLabel.bottomAnchor.constraint(equalTo: titleView.bottomAnchor, constant: -Constants.topBottomPaddingConstraint) - ]) - } - - private func setupDescriptionViewConstraints() { - NSLayoutConstraint.activate([ - descriptionLabel.leadingAnchor.constraint(equalTo: descriptionView.leadingAnchor, constant: Constants.viewPaddingConstraint), - descriptionLabel.trailingAnchor.constraint(equalTo: descriptionView.trailingAnchor, constant: -Constants.viewPaddingConstraint), - descriptionLabel.topAnchor.constraint(equalTo: descriptionView.topAnchor, constant: Constants.topBottomPaddingConstraint), - descriptionLabel.bottomAnchor.constraint(equalTo: descriptionView.bottomAnchor, constant: -Constants.bottomDescriptionConstraint) - ]) - } - - private func setupAppsView() { - NSLayoutConstraint.activate([ - appsView.heightAnchor.constraint(equalToConstant: Constants.appsViewHeight), - appsStackView.leadingAnchor.constraint(equalTo: appsView.leadingAnchor), - appsStackView.topAnchor.constraint(equalTo: appsView.topAnchor, constant: Constants.topAnchorAppsViewConstraint), - appsStackView.bottomAnchor.constraint(equalTo: appsView.bottomAnchor, constant: -Constants.viewPaddingConstraint), - appsStackView.trailingAnchor.constraint(equalTo: appsView.trailingAnchor, constant: Constants.trailingAppsViewConstraint) - ]) - } - - private func setupTipViewConstraints() { - NSLayoutConstraint.activate([ - tipStackView.leadingAnchor.constraint(equalTo: tipView.leadingAnchor, constant: Constants.viewPaddingConstraint), - tipStackView.trailingAnchor.constraint(equalTo: tipView.trailingAnchor, constant: -Constants.viewPaddingConstraint), - tipStackView.topAnchor.constraint(equalTo: tipView.topAnchor, constant: Constants.topAnchorTipViewConstraint), - tipStackView.bottomAnchor.constraint(equalTo: tipView.bottomAnchor, constant: -Constants.topBottomPaddingConstraint), - tipButton.widthAnchor.constraint(equalToConstant: Constants.tipIconSize) - ]) - } - - private func setupContinueButtonConstraints() { - NSLayoutConstraint.activate([ - continueButton.leadingAnchor.constraint(equalTo: continueView.leadingAnchor, constant: Constants.viewPaddingConstraint), - continueButton.trailingAnchor.constraint(equalTo: continueView.trailingAnchor, constant: -Constants.viewPaddingConstraint), - continueButton.heightAnchor.constraint(equalToConstant: Constants.continueButtonViewHeight), - continueButton.topAnchor.constraint(equalTo: continueView.topAnchor, constant: Constants.topBottomPaddingConstraint), - continueButton.bottomAnchor.constraint(equalTo: continueView.bottomAnchor) - ]) - } - - private func setupPoweredByGiniConstraints() { - NSLayoutConstraint.activate([ - bottomStackView.leadingAnchor.constraint(equalTo: bottomView.leadingAnchor, constant: Constants.viewPaddingConstraint), - bottomStackView.trailingAnchor.constraint(equalTo: bottomView.trailingAnchor, constant: -Constants.viewPaddingConstraint), - bottomStackView.topAnchor.constraint(equalTo: bottomView.topAnchor, constant: Constants.topAnchorPoweredByGiniConstraint), - bottomStackView.bottomAnchor.constraint(equalTo: bottomView.bottomAnchor) - ]) - } - - @objc - private func tapOnContinueButton() { - viewModel.didTapOnContinue() - } - - @objc - private func tapOnAppStoreButton() { - openPaymentProvidersAppStoreLink(urlString: viewModel.selectedPaymentProvider?.appStoreUrlIOS) - } - - @objc - private func tapOnLabelAction(gesture: UITapGestureRecognizer) { - if gesture.didTapAttributedTextInLabel(label: tipLabel, - targetText: viewModel.tipActionablePartText) { - tapOnAppStoreButton() - } - } - - private func openPaymentProvidersAppStoreLink(urlString: String?) { - guard let urlString = urlString else { - print("AppStore link unavailable for this payment provider") - return - } - if let url = URL(string: urlString), UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url) - } - } - - private func generateAppViews() -> [ShareInvoiceSingleAppView] { - var viewsToReturn: [ShareInvoiceSingleAppView] = [] - viewModel.appsMocked.forEach { singleApp in - let view = ShareInvoiceSingleAppView() - view.configure(image: singleApp.image, title: singleApp.title, isMoreButton: singleApp.isMoreButton) - viewsToReturn.append(view) - } - return viewsToReturn - } -} - -extension ShareInvoiceBottomView { - enum Constants { - static let viewPaddingConstraint = 16.0 - static let topBottomPaddingConstraint = 10.0 - static let bottomDescriptionConstraint = 20.0 - static let bankIconSize = 36 - static let bankIconCornerRadius = 6.0 - static let bankIconBorderWidth = 1.0 - static let continueButtonViewHeight = 56.0 - static let appsViewSpacing: CGFloat = -20 - static let appsViewHeight: CGFloat = 112.0 - static let topAnchorAppsViewConstraint = 20.0 - static let trailingAppsViewConstraint = 40.0 - static let topAnchorTipViewConstraint = 5.0 - static let topAnchorPoweredByGiniConstraint = 5.0 - static let tipIconSize = 24.0 - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/ShareInvoiceBottomViewModel.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/ShareInvoiceBottomViewModel.swift deleted file mode 100644 index 00674b89b..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/ShareInvoiceBottomViewModel.swift +++ /dev/null @@ -1,125 +0,0 @@ -// -// ShareInvoiceBottomViewModel.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit -import GiniHealthAPILibrary - -protocol ShareInvoiceBottomViewProtocol: AnyObject { - func didTapOnContinueToShareInvoice() -} - -struct SingleApp { - var title: String - var image: UIImage? - var isMoreButton: Bool -} - -final class ShareInvoiceBottomViewModel { - - var giniHealthConfiguration = GiniHealthConfiguration.shared - - var selectedPaymentProvider: PaymentProvider? - // Payment provider colors - var paymentProviderColors: ProviderColors? - - weak var viewDelegate: ShareInvoiceBottomViewProtocol? - - let backgroundColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark7, - darkModeColor: UIColor.GiniHealthColors.light7).uiColor() - let rectangleColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark5, - darkModeColor: UIColor.GiniHealthColors.light5).uiColor() - let dimmingBackgroundColor: UIColor = GiniColor(lightModeColor: UIColor.black, - darkModeColor: UIColor.white).uiColor().withAlphaComponent(0.4) - let appRectangleBackgroundColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark6, - darkModeColor: UIColor.GiniHealthColors.light6).uiColor() - - // Title label - var titleText: String = GiniLocalized.string("ginihealth.paymentcomponent.shareInvoiceBottomSheet.title", - comment: "Share Invoice Bottom sheet title") - let titleLabelAccentColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark2, - darkModeColor: UIColor.GiniHealthColors.light2).uiColor() - var titleLabelFont: UIFont - - private var bankImageIconData: Data? - var bankImageIcon: UIImage { - if let bankImageIconData { - return UIImage(data: bankImageIconData) ?? UIImage() - } - return UIImage() - } - var bankIconBorderColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark5, - darkModeColor: UIColor.GiniHealthColors.light5).uiColor() - - // Description label - let descriptionLabelTextColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark3, - darkModeColor: UIColor.GiniHealthColors.light3).uiColor() - let descriptionAccentColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark3, - darkModeColor: UIColor.GiniHealthColors.light3).uiColor() - var descriptionLabelText: String = GiniLocalized.string("ginihealth.paymentcomponent.shareInvoiceBottomSheet.description", - comment: "Text description for share bottom sheet") - var descriptionLabelFont: UIFont - - // Apps View - let appsBackgroundColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark6, - darkModeColor: UIColor.GiniHealthColors.light6).uiColor() - let moreIconName: String = "more_vertical" - - // Tip label - let tipAccentColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark2, - darkModeColor: UIColor.GiniHealthColors.light2).uiColor() - let tipLabelTextColor: UIColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark4, - darkModeColor: UIColor.GiniHealthColors.light4).uiColor() - var tipLabelText = GiniLocalized.string("ginihealth.paymentcomponent.shareInvoiceBottomSheet.tip.description", - comment: "Text for tip label") - let tipActionablePartText = GiniLocalized.string("ginihealth.paymentcomponent.shareInvoiceBottomSheet.tip.underlined.part", - comment: "Text for tip actionable part from the label") - var tipLabelFont: UIFont - var tipLabelLinkFont: UIFont - let tipIconName = "info.circle" - - // Continue label - let continueLabelText: String = GiniLocalized.string("ginihealth.paymentcomponent.shareInvoiceBottomSheet.continue.button.text", - comment: "Title label used for the Continue button") - - let bankToReplaceString = "[BANK]" - - var appsMocked: [SingleApp] = [] - - init(selectedPaymentProvider: PaymentProvider?) { - self.selectedPaymentProvider = selectedPaymentProvider - self.bankImageIconData = selectedPaymentProvider?.iconData - self.paymentProviderColors = selectedPaymentProvider?.colors - - titleText = titleText.replacingOccurrences(of: bankToReplaceString, with: selectedPaymentProvider?.name ?? "") - descriptionLabelText = descriptionLabelText.replacingOccurrences(of: bankToReplaceString, with: selectedPaymentProvider?.name ?? "") - tipLabelText = tipLabelText.replacingOccurrences(of: bankToReplaceString, with: selectedPaymentProvider?.name ?? "") - - let defaultRegularFont: UIFont = UIFont.systemFont(ofSize: 14, weight: .regular) - let defaultBoldFont: UIFont = UIFont.systemFont(ofSize: 14, weight: .bold) - - self.titleLabelFont = giniHealthConfiguration.textStyleFonts[.subtitle1] ?? defaultBoldFont - self.descriptionLabelFont = giniHealthConfiguration.textStyleFonts[.caption1] ?? defaultRegularFont - self.tipLabelFont = giniHealthConfiguration.textStyleFonts[.caption1] ?? defaultRegularFont - self.tipLabelLinkFont = giniHealthConfiguration.textStyleFonts[.linkBold] ?? defaultBoldFont - - self.generateAppMockedElements() - } - - private func generateAppMockedElements() { - for _ in 0..<2 { - self.appsMocked.append(SingleApp(title: GiniLocalized.string("ginihealth.paymentcomponent.shareInvoiceBottomSheet.app", comment: ""), isMoreButton: false)) - } - self.appsMocked.append(SingleApp(title: selectedPaymentProvider?.name ?? "", image: bankImageIcon, isMoreButton: false)) - self.appsMocked.append(SingleApp(title: GiniLocalized.string("ginihealth.paymentcomponent.shareInvoiceBottomSheet.app", comment: ""), isMoreButton: false)) - self.appsMocked.append(SingleApp(title: GiniLocalized.string("ginihealth.paymentcomponent.shareInvoiceBottomSheet.more", comment: ""), image: UIImageNamedPreferred(named: moreIconName), isMoreButton: true)) - - } - - func didTapOnContinue() { - viewDelegate?.didTapOnContinueToShareInvoice() - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/ShareInvoiceSingleAppView.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/ShareInvoiceSingleAppView.swift deleted file mode 100644 index 5fa22b027..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponent/ShareInvoiceSingleAppView.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// ShareInvoiceSingleAppView.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit - -class ShareInvoiceSingleAppView: UIView { - // Subviews - private let imageView: UIImageView = { - let imageView = UIImageView() - imageView.roundCorners(corners: .allCorners, radius: Constants.imageViewCornerRardius) - imageView.translatesAutoresizingMaskIntoConstraints = false - return imageView - }() - - private let titleLabel: UILabel = { - let label = UILabel() - label.textAlignment = .center - label.textColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark3, - darkModeColor: UIColor.GiniHealthColors.light3).uiColor() - label.font = GiniHealthConfiguration.shared.textStyleFonts[.caption2] ?? UIFont.systemFont(ofSize: 14, weight: .regular) - label.numberOfLines = 0 - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - // Initializer - override init(frame: CGRect) { - super.init(frame: frame) - setupViews() - } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setupViews() - } - - // Setup views and constraints - private func setupViews() { - addSubview(imageView) - addSubview(titleLabel) - - NSLayoutConstraint.activate([ - imageView.topAnchor.constraint(equalTo: topAnchor), - imageView.heightAnchor.constraint(equalToConstant: Constants.imageViewHeight), - imageView.widthAnchor.constraint(equalToConstant: Constants.imageViewHeight), - imageView.centerXAnchor.constraint(equalTo: centerXAnchor), - - titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: Constants.topAnchorTitleLabelConstraint), - titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor), - titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor), - titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor) - ]) - } - - // Function to configure view - func configure(image: UIImage?, title: String?, isMoreButton: Bool) { - imageView.image = image - titleLabel.text = title - imageView.layer.borderColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark3, - darkModeColor: UIColor.GiniHealthColors.light3).uiColor().cgColor - imageView.layer.borderWidth = isMoreButton ? 1 : 0 - imageView.backgroundColor = isMoreButton ? .clear : GiniColor(lightModeColor: .white, - darkModeColor: UIColor.GiniHealthColors.light3).uiColor() - imageView.contentMode = isMoreButton ? .center : .scaleAspectFit - } -} - -extension ShareInvoiceSingleAppView { - enum Constants { - static let imageViewHeight = 36.0 - static let topAnchorTitleLabelConstraint = 8.0 - static let imageViewCornerRardius = 6.0 - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponentsController+Helpers.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponentsController+Helpers.swift new file mode 100644 index 000000000..81cac9024 --- /dev/null +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponentsController+Helpers.swift @@ -0,0 +1,842 @@ +// +// PaymentComponentsController+Helpers.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import GiniHealthAPILibrary +import GiniInternalPaymentSDK +import GiniUtilites +import UIKit + +/// A protocol for handling user actions in the Payment Providers bottom views. +protocol PaymentProvidersBottomViewProtocol: AnyObject { + func didSelectPaymentProvider(paymentProvider: PaymentProvider) + func didTapOnClose() + func didTapOnMoreInformation() + func didTapOnContinueOnShareBottomSheet() + func didTapForwardOnInstallBottomSheet() + func didTapOnPayButton() +} + +extension PaymentComponentsController { + // MARK: - Payment Provider Selection + + /** + Loads the payment providers list and stores them. + - note: Also triggers a function that checks if the payment providers are installed. + */ + func loadPaymentProviders() { + self.isLoading = true + self.giniSDK.fetchBankingApps { [weak self] result in + self?.isLoading = false + switch result { + case let .success(paymentProviders): + self?.paymentProviders = paymentProviders.map{ $0.toHealthPaymentProvider() } + self?.sortPaymentProviders() + self?.selectedPaymentProvider = self?.defaultInstalledPaymentProvider() + self?.delegate?.didFetchedPaymentProviders() + case let .failure(error): + GiniUtilites.Log("Couldn't load payment providers: \(error.localizedDescription)", event: .error) + } + } + } + + /** + Retrieves the default installed payment provider, if available. + - Returns: a Payment Provider object. + */ + func defaultInstalledPaymentProvider() -> PaymentProvider? { + savedPaymentProvider() + } + + func storeDefaultPaymentProvider(paymentProvider: PaymentProvider) { + do { + let encoder = JSONEncoder() + let data = try encoder.encode(paymentProvider) + UserDefaults.standard.set(data, forKey: Constants.kDefaultPaymentProvider) + } catch { + GiniUtilites.Log("Unable to encode payment provider: (\(error))", event: .error) + } + } + + func savedPaymentProvider() -> PaymentProvider? { + if let data = UserDefaults.standard.data(forKey: Constants.kDefaultPaymentProvider) { + do { + let decoder = JSONDecoder() + let paymentProvider = try decoder.decode(PaymentProvider.self, from: data) + if self.paymentProviders.contains(where: { $0.id == paymentProvider.id }) { + return paymentProvider + } + } catch { + GiniUtilites.Log("Unable to decode payment provider: (\(error))", event: .error) + } + } + return nil + } + + func sortPaymentProviders() { + guard !paymentProviders.isEmpty else { return } + self.paymentProviders = paymentProviders + .filter { $0.gpcSupportedPlatforms.contains(.ios) || $0.openWithSupportedPlatforms.contains(.ios) } + .sorted { + // First sort by whether the app scheme can be opened + if $0.appSchemeIOS.canOpenURLString() != $1.appSchemeIOS.canOpenURLString() { + return $0.appSchemeIOS.canOpenURLString() && !$1.appSchemeIOS.canOpenURLString() + } + // Then sort by the index if the app scheme condition is the same + return ($0.index ?? 0) < ($1.index ?? 0) + } + } + + // MARK: - Bottom sheets + /** + Provides a custom Gini view for the payment view that is going to be presented as a bottom sheet. + + - Parameter documentId: An optional identifier for the document associated id with the payment. + - Returns: A configured `UIViewController` for displaying the payment bottom view. + */ + public func paymentViewBottomSheet(documentId: String?) -> UIViewController { + previousPresentedViews = [.paymentComponent] + let paymentComponentBottomView = PaymentComponentBottomView(paymentView: paymentView(), bottomSheetConfiguration: configurationProvider.bottomSheetConfiguration) + return paymentComponentBottomView + } + + /** + Provides a custom Gini payment view. + This method creates and returns a custom view for handling payments. The view includes: + - Additional information + - A bank selection option (if available). + - A tappable button for initiating the payment process. + - Returns: A UIView instance representing the payment component. + */ + func paymentView() -> UIView { + let paymentComponentViewModel = PaymentComponentViewModel( + paymentProvider: healthSelectedPaymentProvider, + primaryButtonConfiguration: configurationProvider.primaryButtonConfiguration, + secondaryButtonConfiguration: configurationProvider.secondaryButtonConfiguration, + configuration: configurationProvider.paymentComponentsConfiguration, + strings: stringsProvider.paymentComponentsStrings, + poweredByGiniConfiguration: configurationProvider.poweredByGiniConfiguration, + poweredByGiniStrings: stringsProvider.poweredByGiniStrings, + moreInformationConfiguration: configurationProvider.moreInformationConfiguration, + moreInformationStrings: stringsProvider.moreInformationStrings, + minimumButtonsHeight: configurationProvider.paymentComponentButtonsHeight, + paymentComponentConfiguration: configurationProvider.paymentComponentConfiguration + ) + paymentComponentViewModel.delegate = self + paymentComponentViewModel.documentId = documentId + return PaymentComponentView(viewModel: paymentComponentViewModel) + } + + func presentPaymentViewBottomSheet() { + let paymentViewBottomSheet = paymentViewBottomSheet(documentId: documentId ?? "") + paymentViewBottomSheet.modalPresentationStyle = .overFullScreen + self.dismissAndPresent(viewController: paymentViewBottomSheet, animated: false) + } + + private func dismissAndPresent(viewController: UIViewController, animated: Bool) { + if let presentedViewController = navigationControllerProvided?.presentedViewController { + presentedViewController.dismiss(animated: true) { + self.navigationControllerProvided?.present(viewController, animated: animated) + } + } else { + navigationControllerProvided?.present(viewController, animated: animated) + } + } + + /** + Provides a custom Gini view for the bank selection bottom sheet. + + - Returns: A configured `UIViewController` for displaying the bank selection options. + */ + public func bankSelectionBottomSheet() -> UIViewController { + if previousPresentedViews.count > 0, previousPresentedViews.first != .paymentReview { + previousPresentedViews.removeAll() + } + previousPresentedViews.insert(.bankPicker) + let paymentProvidersBottomViewModel = BanksBottomViewModel(paymentProviders: paymentProviders, + selectedPaymentProvider: healthSelectedPaymentProvider, + configuration: configurationProvider.bankSelectionConfiguration, + strings: stringsProvider.banksBottomStrings, + poweredByGiniConfiguration: configurationProvider.poweredByGiniConfiguration, + poweredByGiniStrings: stringsProvider.poweredByGiniStrings, + moreInformationConfiguration: configurationProvider.moreInformationConfiguration, + moreInformationStrings: stringsProvider.moreInformationStrings) + paymentProvidersBottomViewModel.viewDelegate = self + paymentProvidersBottomViewModel.documentId = documentId + return BanksBottomView(viewModel: paymentProvidersBottomViewModel, bottomSheetConfiguration: configurationProvider.bottomSheetConfiguration) + } + + /** + Loads the payment review screen for the specified document or for the provided payment information + + This method fetches data for review based on the provided document ID. If the configuration + allows for invoice handling without a document, it directly loads the payment review screen + using the provided payment information provided. + + - Parameters: + - trackingDelegate: An optional delegate for tracking events related to Gini Health. + - completion: A closure that is called with the resulting `UIViewController` and an optional + `GiniHealthError` once the loading process is complete. + */ + func loadPaymentReviewScreenFor(trackingDelegate: GiniHealthTrackingDelegate?, + completion: @escaping (UIViewController?, GiniHealthError?) -> Void) { + previousPresentedViews.insert(.paymentReview) + if !GiniHealthConfiguration.shared.useInvoiceWithoutDocument { + guard let documentId else { + completion(nil, nil) + return + } + self.isLoading = true + self.giniSDK.fetchDataForReview(documentId: documentId) { [weak self] result in + self?.isLoading = false + switch result { + case .success(let data): + guard let self else { + completion(nil, nil) + return + } + self.preparePaymentReviewViewController(data: data, paymentInfo: nil, completion: completion) + case .failure(let error): + completion(nil, error) + } + } + } else { + loadPaymentReviewScreenWithoutDocument(paymentInfo: paymentInfo, trackingDelegate: trackingDelegate, completion: completion) + } + } + + private func loadPaymentReviewScreenWithoutDocument(paymentInfo: GiniInternalPaymentSDK.PaymentInfo?, + trackingDelegate: GiniHealthTrackingDelegate?, + completion: @escaping (UIViewController?, GiniHealthError?) -> Void) { + preparePaymentReviewViewController(data: nil, + paymentInfo: paymentInfo, + completion: completion) + } + + private func preparePaymentReviewViewController(data: DataForReview?, + paymentInfo: GiniInternalPaymentSDK.PaymentInfo?, + completion: @escaping (UIViewController?, GiniHealthError?) -> Void) { + guard let healthSelectedPaymentProvider else { + completion(nil, nil) + return + } + let viewModel = PaymentReviewModel(delegate: self, + bottomSheetsProvider: self, + document: data?.document.toHealthDocument(), + extractions: data?.extractions.map { $0.toHealthExtraction() }, + paymentInfo: paymentInfo, + selectedPaymentProvider: healthSelectedPaymentProvider, + configuration: configurationProvider.paymentReviewConfiguration, + strings: stringsProvider.paymentReviewStrings, + containerConfiguration: configurationProvider.paymentReviewContainerConfiguration, + containerStrings: stringsProvider.paymentReviewContainerStrings, + defaultStyleInputFieldConfiguration: configurationProvider.defaultStyleInputFieldConfiguration, + errorStyleInputFieldConfiguration: configurationProvider.errorStyleInputFieldConfiguration, + selectionStyleInputFieldConfiguration: configurationProvider.selectionStyleInputFieldConfiguration, + primaryButtonConfiguration: configurationProvider.primaryButtonConfiguration, + secondaryButtonConfiguration: configurationProvider.secondaryButtonConfiguration, + poweredByGiniConfiguration: configurationProvider.poweredByGiniConfiguration, + poweredByGiniStrings: stringsProvider.poweredByGiniStrings, + bottomSheetConfiguration: configurationProvider.bottomSheetConfiguration, + showPaymentReviewCloseButton: configurationProvider.showPaymentReviewCloseButton) + + let vc = PaymentReviewViewController.instantiate(viewModel: viewModel, + selectedPaymentProvider: healthSelectedPaymentProvider) + + completion(vc, nil) + } + + /** + Provides a custom Gini view for displaying payment more information view. + + This method initializes a `PaymentInfoViewModel` with the necessary configurations and + localized strings, then returns a `PaymentInfoViewController` with the view model. + + - Returns: A configured `UIViewController` for displaying payment information. + */ + func paymentInfoViewController() -> UIViewController { + let paymentInfoViewModel = PaymentInfoViewModel(paymentProviders: paymentProviders, + configuration: configurationProvider.paymentInfoConfiguration, + strings: stringsProvider.paymentInfoStrings, + poweredByGiniConfiguration: configurationProvider.poweredByGiniConfiguration, + poweredByGiniStrings: stringsProvider.poweredByGiniStrings) + return PaymentInfoViewController(viewModel: paymentInfoViewModel) + } + + /** + Provides a custom Gini view for installing the app if not present. + + This method initializes an `InstallAppBottomViewModel` with the necessary configurations and + localized strings, and returns an `InstallAppBottomView` configured with the view model. + + - Returns: A configured `BottomSheetViewController` for the app installation process. + */ + public func installAppBottomSheet() -> BottomSheetViewController { + previousPresentedViews.removeAll() + let installAppBottomViewModel = InstallAppBottomViewModel(selectedPaymentProvider: healthSelectedPaymentProvider, + installAppConfiguration: configurationProvider.installAppConfiguration, + strings: stringsProvider.installAppStrings, + primaryButtonConfiguration: configurationProvider.primaryButtonConfiguration, + poweredByGiniConfiguration: configurationProvider.poweredByGiniConfiguration, + poweredByGiniStrings: stringsProvider.poweredByGiniStrings) + installAppBottomViewModel.viewDelegate = self + let installAppBottomView = InstallAppBottomView(viewModel: installAppBottomViewModel, bottomSheetConfiguration: configurationProvider.bottomSheetConfiguration) + return installAppBottomView + } + + /** + Provides a custom Gini view to onboard the user about the sharing invoices flow. + + This method initializes a `ShareInvoiceBottomViewModel` with the necessary configurations and + localized strings, and returns a `ShareInvoiceBottomView` configured with the view model. + It also increments the onboarding count for the selected payment provider. + + - Parameter qrCodeData: A qrCode data information for the document associated payment request generated by the payment details. + - Returns: A configured `BottomSheetViewController` for sharing invoices. + */ + public func shareInvoiceBottomSheet(qrCodeData: Data) -> BottomSheetViewController { + previousPresentedViews.removeAll() + let shareInvoiceBottomViewModel = ShareInvoiceBottomViewModel(selectedPaymentProvider: healthSelectedPaymentProvider, + configuration: configurationProvider.shareInvoiceConfiguration, + strings: stringsProvider.shareInvoiceStrings, + primaryButtonConfiguration: configurationProvider.primaryButtonConfiguration, + poweredByGiniConfiguration: configurationProvider.poweredByGiniConfiguration, + poweredByGiniStrings: stringsProvider.poweredByGiniStrings, + qrCodeData: qrCodeData, + paymentInfo: paymentInfo) + shareInvoiceBottomViewModel.viewDelegate = self + shareInvoiceBottomViewModel.documentId = documentId + let shareInvoiceBottomView = ShareInvoiceBottomView(viewModel: shareInvoiceBottomViewModel, bottomSheetConfiguration: configurationProvider.bottomSheetConfiguration) + return shareInvoiceBottomView + } + + /** + Updates the selected payment provider with the given payment provider. This method is used when updating the payment provider from Payment Review Screen + + - Parameters: + - paymentProvider: The new payment provider to be set. + */ + public func updatedPaymentProvider(_ paymentProvider: GiniHealthAPILibrary.PaymentProvider) { + self.selectedPaymentProvider = PaymentProvider(healthPaymentProvider: paymentProvider) + if let provider = selectedPaymentProvider { + storeDefaultPaymentProvider(paymentProvider: provider) + } + } + + /** + Opens the more information view controller by notifying the view delegate. This method is used when opening the More Information screen inside the bank selection bottom sheet that's presented in the Payment Review Screen. + + This method triggers the delegate's action for displaying more information. + */ + public func openMoreInformationViewController() { + didTapOnMoreInformation(documentId: documentId) + } + + // MARK: - Other helpers + func setupObservers() { + NotificationCenter.default.addObserver(self, selector: #selector(paymentInfoDissapeared), name: .paymentInfoDissapeared, object: nil) + } + + @objc + private func paymentInfoDissapeared() { + switch previousPresentedViews.first { + case .bankPicker: + previousPresentedViews.removeAll() + didTapOnBankPicker(documentId: documentId) + case .paymentComponent: + previousPresentedViews.removeAll() + presentPaymentViewBottomSheet() + case .paymentReview: + didTapOnPayInvoice() + default: + break + } + + } + + /// Checks if the payment provider app can be opened based on the selected payment provider and GPC(Gini Pay Connect) support. + public func canOpenPaymentProviderApp() -> Bool { + guard supportsGPC() else { return false } + guard let appScheme = healthSelectedPaymentProvider?.appSchemeIOS else { return false } + return appScheme.canOpenURLString() + } + + /// Checks if the selected payment provider supports the "Open With" feature on iOS. + public func supportsOpenWith() -> Bool { + healthSelectedPaymentProvider?.openWithSupportedPlatforms.contains(.ios) == true + } + + /// Checks if the selected payment provider supports GPC(Gini Pay Connect) on iOS. + public func supportsGPC() -> Bool { + healthSelectedPaymentProvider?.gpcSupportedPlatforms.contains(.ios) == true + } + + /** + Creates a payment request and obtains the PDF URL using the provided payment information. + + - Parameter paymentInfo: The payment information for the request. + - Parameter viewController: The view controller used to present any necessary UI related to the request. + */ + public func obtainPDFURLFromPaymentRequest(paymentInfo: GiniInternalPaymentSDK.PaymentInfo, viewController: UIViewController) { + createPaymentRequest(paymentInfo: paymentInfo) { [weak self] result in + switch result { + case .success(let paymentRequestId): + self?.loadPDFData(paymentRequestId: paymentRequestId, viewController: viewController) + case .failure: + break + } + } + } + + private func loadPDFData(paymentRequestId: String, viewController: UIViewController) { + self.loadPDF(paymentRequestId: paymentRequestId, completion: { [weak self] pdfData in + let pdfPath = self?.writePDFDataToFile(data: pdfData, fileName: paymentRequestId) + + guard let pdfPath else { + GiniUtilites.Log("Couldn't retrieve pdf URL", event: .warning) + return + } + + self?.sharePDF(pdfURL: pdfPath, paymentRequestId: paymentRequestId, viewController: viewController) { [weak self] (activity, _, _, _) in + guard activity != nil else { + return + } + + // Publish the payment request id only after a user has picked an activity (app) + self?.giniSDK.delegate?.didCreatePaymentRequest(paymentRequestId: paymentRequestId) + } + }) + } + + private func loadPDF(paymentRequestId: String, completion: @escaping (Data) -> ()) { + isLoading = true + giniSDK.paymentService.pdfWithQRCode(paymentRequestId: paymentRequestId) { [weak self] result in + self?.isLoading = false + switch result { + case .success(let data): + completion(data) + case .failure: + break + } + } + } + + private func writePDFDataToFile(data: Data, fileName: String) -> URL? { + do { + let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + guard let docDirectoryPath = paths.first else { return nil} + let pdfFileName = fileName + Constants.pdfExtension + let pdfPath = docDirectoryPath.appendingPathComponent(pdfFileName) + try data.write(to: pdfPath) + return pdfPath + } catch { + GiniUtilites.Log("Error while write pdf file to location: \(error.localizedDescription)", event: .error) + return nil + } + } + + private func sharePDF(pdfURL: URL, paymentRequestId: String, viewController: UIViewController, + completionWithItemsHandler: @escaping UIActivityViewController.CompletionWithItemsHandler) { + // Create UIActivityViewController with the PDF file + let activityViewController = UIActivityViewController(activityItems: [pdfURL], applicationActivities: nil) + activityViewController.completionWithItemsHandler = completionWithItemsHandler + + // Exclude some activities if needed + activityViewController.excludedActivityTypes = [ + .addToReadingList, + .assignToContact, + .airDrop, + .mail, + .message, + .postToFacebook, + .postToVimeo, + .postToWeibo, + .postToFlickr, + .postToTwitter, + .postToTencentWeibo, + .copyToPasteboard, + .markupAsPDF, + .openInIBooks, + .print, + .saveToCameraRoll + ] + + // Present the UIActivityViewController + DispatchQueue.main.async { + if let popoverController = activityViewController.popoverPresentationController { + popoverController.sourceView = viewController.view + popoverController.sourceRect = CGRect(x: viewController.view.bounds.midX, y: viewController.view.bounds.midY, width: 0, height: 0) + popoverController.permittedArrowDirections = [] + } + + if (viewController.presentedViewController != nil) { + viewController.presentedViewController?.dismiss(animated: true, completion: { + viewController.present(activityViewController, animated: true, completion: nil) + }) + } else { + viewController.present(activityViewController, animated: true, completion: nil) + } + } + } + + // MARK: - Payment Review Screen functions + /** + Creates a payment request using the provided payment information. + + - Parameter paymentInfo: The payment information to be used for the request. + - Parameter completion: A closure to be executed once the request is completed, containing the result of the operation. + */ + public func createPaymentRequest(paymentInfo: GiniInternalPaymentSDK.PaymentInfo, completion: @escaping (Result) -> Void) { + giniSDK.createPaymentRequest(paymentInfo: paymentInfo, completion: { result in + switch result { + case .success(let paymentRequestId): + completion(.success(paymentRequestId)) + case .failure(let error): + let healthError = GiniHealthAPILibrary.GiniError.unknown(response: error.response, data: error.data) + completion(.failure(healthError)) + } + }) + } + + /** + Submits feedback for the specified document and its updated extractions. Method used to update the information extracted from a document. + + - Parameters: + - document: The document for which feedback is being submitted. + - updatedExtractions: The updated extractions related to the document. + - completion: An optional closure to be executed upon completion, containing the result of the submission. + */ + public func submitFeedback(for document: GiniHealthAPILibrary.Document, updatedExtractions: [GiniHealthAPILibrary.Extraction], completion: ((Result) -> Void)?) { + let newDocument = Document(healthDocument: document) + let extractions = updatedExtractions.map { Extraction(healthExtraction: $0) } + giniSDK.documentService.submitFeedback(for: newDocument, with: [], and: ["payment": [extractions]]) { result in + switch result { + case .success(let result): + completion?(.success(result)) + case .failure(let error): + let healthError = GiniHealthAPILibrary.GiniError.unknown(response: error.response, data: error.data) + completion?(.failure(healthError)) + } + } + } + + /** + Retrieves a preview for the specified document and page number. + + - Parameters: + - documentId: The ID of the document to preview. + - pageNumber: The page number of the document to retrieve. + - completion: A closure that gets called with the result containing either the preview data or an error. + */ + public func preview(for documentId: String, pageNumber: Int, completion: @escaping (Result) -> Void) { + giniSDK.documentService.preview(for: documentId, pageNumber: pageNumber) { result in + switch result { + case .success(let data): + completion(.success(data)) + case .failure(let error): + let healthError = GiniHealthAPILibrary.GiniError.unknown(response: error.response, data: error.data) + completion(.failure(healthError)) + } + } + } + + /** + Opens the payment provider app using the specified request ID and universal link. + + - Parameters: + - requestId: The ID of the payment request. + - universalLink: The universal link to open the payment provider app. + */ + public func openPaymentProviderApp(requestId: String, universalLink: String) { + giniSDK.openPaymentProviderApp(requestID: requestId, universalLink: universalLink) + } + + /** + Retrieves a payment request using the provided payment request ID. + + - Parameter id: The ID of the payment request to retrieve. + - Parameter completion: A closure to be executed once the retrieval is completed, containing the result of the operation as a `PaymentRequest` object on success or a `GiniError` on failure. + */ + public func getPaymentRequest(by id: String, completion: @escaping (Result) -> Void) { + giniSDK.getPaymentRequest(by: id) { result in + switch result { + case .success(let paymentRequest): + completion(.success(paymentRequest)) + case .failure(let error): + let healthError = GiniHealthAPILibrary.GiniError.unknown(response: error.response, data: error.data) + completion(.failure(healthError)) + } + } + } +} + +extension PaymentComponentsController: BanksSelectionProtocol { + /// Updates the selected payment provider and notifies the delegate with the provider and optional document ID. + public func didSelectPaymentProvider(paymentProvider: GiniHealthAPILibrary.PaymentProvider) { + selectedPaymentProvider = PaymentProvider(healthPaymentProvider: paymentProvider) + if let provider = selectedPaymentProvider { + storeDefaultPaymentProvider(paymentProvider: provider) + self.presentPaymentViewBottomSheet() + } + } + + /// Handles the action when the continue button is tapped on the share bottom sheet. + public func didTapOnContinueOnShareBottomSheet() { + GiniUtilites.Log("Tapped Continue on Share Bottom Sheet", event: .success) + } + + /// Handles the action when the forward button is tapped on the install bottom sheet. + public func didTapForwardOnInstallBottomSheet() { + GiniUtilites.Log("Tapped Forward on Install Bottom Sheet", event: .success) + } + + /// Handles the action when the pay button is tapped on install bottom sheet. + public func didTapOnPayButton() {} +} + +extension PaymentComponentsController: PaymentComponentViewProtocol { + /// Handles the action when the more information button is tapped on the payment component view, using the provided document ID. + public func didTapOnMoreInformation(documentId: String?) { + let paymentInfoVC = paymentInfoViewController() + pushOrDismissAndPush(paymentInfoVC) + } + + private func pushOrDismissAndPush(_ viewController: UIViewController) { + if let doublePresentedVC = navigationControllerProvided?.presentedViewController?.presentedViewController { + doublePresentedVC.dismiss(animated: true) { [weak self] in + if let presentedVC = self?.navigationControllerProvided?.presentedViewController { + presentedVC.dismiss(animated: true) { [weak self] in + self?.navigationControllerProvided?.pushViewController(viewController, animated: true) + } + } + } + } else if let presentedVC = navigationControllerProvided?.presentedViewController { + presentedVC.dismiss(animated: true) { [weak self] in + if self?.navigationControllerProvided?.viewControllers.last is PaymentReviewViewController { + self?.navigationControllerProvided?.popViewController(animated: true, completion: { + self?.navigationControllerProvided?.pushViewController(viewController, animated: true) + }) + } else { + self?.navigationControllerProvided?.pushViewController(viewController, animated: true) + } + } + } else { + navigationControllerProvided?.pushViewController(viewController, animated: true) + } + } + + /// Handles the action when the bank picker button is tapped on the payment component view, using the provided document ID. + public func didTapOnBankPicker(documentId: String?) { + GiniUtilites.Log("Tapped on Bank Picker on :\(documentId ?? "")", event: .success) + if GiniHealthConfiguration.shared.useBottomPaymentComponentView { + let bankSelectionBottomSheet = bankSelectionBottomSheet() + bankSelectionBottomSheet.modalPresentationStyle = .overFullScreen + dismissAndPresent(viewController: bankSelectionBottomSheet, animated: false) + } + } + + /// Handles the action when the pay invoice button is tapped on the payment component view, using the provided document ID. + public func didTapOnPayInvoice(documentId: String?) { + GiniUtilites.Log("Tapped on Pay Invoice on :\(documentId ?? "")", event: .success) + if GiniHealthConfiguration.shared.showPaymentReviewScreen || !GiniHealthConfiguration.shared.useInvoiceWithoutDocument { + loadPaymentReviewScreenFor(trackingDelegate: self) { [weak self] viewController, error in + if let error = error { + self?.handleError(error) + } else if let viewController = viewController { + self?.presentOrPushPaymentReviewScreen(viewController) + } + } + } else { + if supportsOpenWith() { + guard let paymentInfo else { return } + createPaymentRequest(paymentInfo: paymentInfo) { [weak self] result in + self?.handlePaymentRequestResult(result) + } + } else if supportsGPC() { + if canOpenPaymentProviderApp() { + guard let paymentInfo else { return } + processPaymentRequest(paymentInfo: paymentInfo) + } else { + presentInstallAppBottomSheet() + } + } + } + } + + private func presentOrPushPaymentReviewScreen(_ viewController: UIViewController) { + viewController.modalTransitionStyle = .coverVertical + viewController.modalPresentationStyle = .overCurrentContext + + let presentOrPush: () -> Void = { [weak self] in + guard let self = self else { return } + if self.documentId != nil { + self.navigationControllerProvided?.pushViewController(viewController, animated: true) + } else { + self.navigationControllerProvided?.present(viewController, animated: true) + } + } + + if let presentedVC = navigationControllerProvided?.presentedViewController { + presentedVC.dismiss(animated: true, completion: presentOrPush) + } else { + presentOrPush() + } + } + + public func presentShareInvoiceBottomSheet(paymentRequestId: String, paymentInfo: GiniInternalPaymentSDK.PaymentInfo) { + self.paymentInfo = paymentInfo + giniSDK.paymentService.qrCodeImage(paymentRequestId: paymentRequestId) { [weak self] result in + switch result { + case .success(let image): + DispatchQueue.main.async { + let shareInvoiceBottomSheet = self?.shareInvoiceBottomSheet(qrCodeData: image) + shareInvoiceBottomSheet?.modalPresentationStyle = .overFullScreen + guard let shareInvoiceBottomSheet else { return } + self?.dismissAndPresent(viewController: shareInvoiceBottomSheet, animated: false) + } + case .failure(let error): + self?.handleError(error) + } + } + } + + private func handleDismissalAndPDFURL(paymentInfo: GiniInternalPaymentSDK.PaymentInfo) { + if let presentedVC = self.navigationControllerProvided?.presentedViewController { + presentedVC.dismiss(animated: true) { [weak self] in + guard let self = self, let navController = self.navigationControllerProvided else { return } + self.obtainPDFURLFromPaymentRequest(paymentInfo: paymentInfo, viewController: navController) + } + } else { + guard let presentedVC = self.navigationControllerProvided?.presentedViewController else { return } + self.obtainPDFURLFromPaymentRequest(paymentInfo: paymentInfo, viewController: presentedVC) + } + } + + private func processPaymentRequest(paymentInfo: GiniInternalPaymentSDK.PaymentInfo) { + createPaymentRequest(paymentInfo: paymentInfo) { [weak self] result in + switch result { + case .success(let paymentRequestID): + self?.handleSuccessfulPaymentRequest(paymentRequestID: paymentRequestID) + case .failure(let error): + self?.handleError(error) + } + } + } + + private func handleSuccessfulPaymentRequest(paymentRequestID: String) { + if let presentedVC = navigationControllerProvided?.presentedViewController { + presentedVC.dismiss(animated: true) { [weak self] in + self?.openPaymentProviderApp(requestId: paymentRequestID) + } + } else { + openPaymentProviderApp(requestId: paymentRequestID) + } + } + + private func openPaymentProviderApp(requestId: String) { + let universalLink = selectedPaymentProvider?.universalLinkIOS ?? "" + openPaymentProviderApp(requestId: requestId, universalLink: universalLink) + } + + private func presentInstallAppBottomSheet() { + let installAppBottomSheet = installAppBottomSheet() + installAppBottomSheet.modalPresentationStyle = .overFullScreen + self.dismissAndPresent(viewController: installAppBottomSheet, animated: false) + } + + private func showErrorsIfAny() { + if !errors.isEmpty { + let uniqueErrorMessages = Array(Set(errors)) + DispatchQueue.main.async { + self.delegate?.isLoadingStateChanged(isLoading: false) + self.showErrorAlertView(error: uniqueErrorMessages.joined(separator: ", ")) + } + errors = [] + } + } + + func showErrorAlertView(error: String) { + if navigationControllerProvided?.presentedViewController != nil { + self.navigationControllerProvided?.presentedViewController?.dismiss(animated: true, completion: { + self.presentAlertViewController(error: error) + }) + } else { + presentAlertViewController(error: error) + } + } + + private func presentAlertViewController(error: String) { + let alertController = UIAlertController(title: NSLocalizedStringPreferredFormat("gini.health.errors.default", comment: ""), + message: "", + preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: "Ok", style: .default)) + self.navigationControllerProvided?.present(alertController, animated: true) + } + + private func handlePaymentRequestResult(_ result: Result) { + switch result { + case .success(let paymentRequestId): + fetchQRCodeImage(for: paymentRequestId) + case .failure(let error): + handleError(error) + } + } + + private func fetchQRCodeImage(for paymentRequestId: String) { + giniSDK.paymentService.qrCodeImage(paymentRequestId: paymentRequestId) { [weak self] result in + switch result { + case .success(let image): + self?.presentShareInvoiceBottomSheet(with: image) + case .failure(let error): + self?.handleError(error) + } + } + } + + private func presentShareInvoiceBottomSheet(with qrCodeData: Data) { + DispatchQueue.main.async { [weak self] in + let shareInvoiceBottomSheet = self?.shareInvoiceBottomSheet(qrCodeData: qrCodeData) + shareInvoiceBottomSheet?.modalPresentationStyle = .overFullScreen + guard let shareInvoiceBottomSheet else { return } + self?.dismissAndPresent(viewController: shareInvoiceBottomSheet, animated: false) + } + } + + private func handleError(_ error: Error) { + errors.append(error.localizedDescription) + showErrorsIfAny() + } +} + +extension PaymentComponentsController: PaymentProvidersBottomViewProtocol { + /// Updates the selected payment provider from the bank selection bottom view and notifies the delegate with the selected provider and document ID. + public func didSelectPaymentProvider(paymentProvider: PaymentProvider) { + selectedPaymentProvider = paymentProvider + storeDefaultPaymentProvider(paymentProvider: paymentProvider) + } + + /// Notifies the delegate when the close button is tapped on bank selection bottom view + public func didTapOnClose() {} + + /// Notifies the delegate when the more information button is tapped on the bank selection bottom view + public func didTapOnMoreInformation() { + openMoreInformationViewController() + } +} + +extension PaymentComponentsController: ShareInvoiceBottomViewProtocol { + /// Notifies the delegate to continue sharing the invoice with the provided document ID. + public func didTapOnContinueToShareInvoice() { + guard let navigationControllerProvided, let paymentInfo else { return } + obtainPDFURLFromPaymentRequest(paymentInfo: paymentInfo, viewController: navigationControllerProvided) + } +} + +extension PaymentComponentsController: InstallAppBottomViewProtocol { + // Notifies the delegate to proceed when the continue button is tapped in the install app bottom view. This happens after the user installed the app from AppStore + public func didTapOnContinue() { + didTapForwardOnInstallBottomSheet() + } +} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponentsController.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponentsController.swift new file mode 100644 index 000000000..fdf4768fa --- /dev/null +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentComponentsController.swift @@ -0,0 +1,268 @@ +// +// PaymentComponentController.swift +// GiniHealthSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + +import UIKit +import GiniHealthAPILibrary +import GiniInternalPaymentSDK +import GiniUtilites + +/** + Protocol used to provide updates on the current status of the Payment Components Controller. + Uses a callback mechanism to handle payment provider requests. + */ +public protocol PaymentComponentsControllerProtocol: AnyObject { + func isLoadingStateChanged(isLoading: Bool) // Because we can't use Combine + func didFetchedPaymentProviders() +} + +protocol PaymentComponentsProtocol { + var isLoading: Bool { get set } + var selectedPaymentProvider: PaymentProvider? { get set } + func loadPaymentProviders() + func checkIfDocumentIsPayable(docId: String, completion: @escaping (Result) -> Void) + func paymentView() -> UIView + func bankSelectionBottomSheet() -> UIViewController + func loadPaymentReviewScreenFor(trackingDelegate: GiniHealthTrackingDelegate?, completion: @escaping (UIViewController?, GiniHealthError?) -> Void) + func paymentInfoViewController() -> UIViewController + func paymentViewBottomSheet() -> UIViewController +} + +/// A protocol that provides configuration settings for various payment components. +public protocol PaymentComponentsConfigurationProvider { + var paymentReviewContainerConfiguration: PaymentReviewContainerConfiguration { get } + var installAppConfiguration: InstallAppConfiguration { get } + var bottomSheetConfiguration: BottomSheetConfiguration { get } + var shareInvoiceConfiguration: ShareInvoiceConfiguration { get } + var paymentInfoConfiguration: PaymentInfoConfiguration { get } + var bankSelectionConfiguration: BankSelectionConfiguration { get } + var paymentComponentsConfiguration: PaymentComponentsConfiguration { get } + var paymentReviewConfiguration: PaymentReviewConfiguration { get } + var poweredByGiniConfiguration: PoweredByGiniConfiguration { get } + var moreInformationConfiguration: MoreInformationConfiguration { get } + var paymentComponentConfiguration: PaymentComponentConfiguration { get set } + + var primaryButtonConfiguration: ButtonConfiguration { get } + var secondaryButtonConfiguration: ButtonConfiguration { get } + var defaultStyleInputFieldConfiguration: TextFieldConfiguration { get } + var errorStyleInputFieldConfiguration: TextFieldConfiguration { get } + var selectionStyleInputFieldConfiguration: TextFieldConfiguration { get } + + var showPaymentReviewCloseButton: Bool { get } + var paymentComponentButtonsHeight: CGFloat { get } +} + +/// A protocol that provides localized string resources for various payment components. +public protocol PaymentComponentsStringsProvider { + var paymentReviewContainerStrings: PaymentReviewContainerStrings { get } + var paymentComponentsStrings: PaymentComponentsStrings { get } + var installAppStrings: InstallAppStrings { get } + var shareInvoiceStrings: ShareInvoiceStrings { get } + var paymentInfoStrings: PaymentInfoStrings { get } + var banksBottomStrings: BanksBottomStrings { get } + var paymentReviewStrings: PaymentReviewStrings { get } + var poweredByGiniStrings: PoweredByGiniStrings { get } + var moreInformationStrings: MoreInformationStrings { get } +} + +/** + The `PaymentComponentsController` class allows control over the payment components. + */ +public final class PaymentComponentsController: BottomSheetsProviderProtocol, GiniHealthTrackingDelegate { + /// handling the Payment Component Controller delegate + public weak var delegate: PaymentComponentsControllerProtocol? + + let giniSDK: GiniHealth + private var trackingDelegate: GiniHealthTrackingDelegate? + + var paymentProviders: GiniHealthAPILibrary.PaymentProviders = [] + + let configurationProvider: PaymentComponentsConfigurationProvider + let stringsProvider: PaymentComponentsStringsProvider + + /// storing the current selected payment provider + public var selectedPaymentProvider: PaymentProvider? + var healthSelectedPaymentProvider: GiniHealthAPILibrary.PaymentProvider? { + selectedPaymentProvider?.toHealthPaymentProvider() + } + + /// reponsible for storing the loading state of the controller and passing it to the delegate listeners + var isLoading: Bool = false { + didSet { + delegate?.isLoadingStateChanged(isLoading: isLoading) + } + } + + /// Previous presented view + var previousPresentedViews: Set = [] + // Client's navigation controller provided in order to handle all HealthSDK flows + weak var navigationControllerProvided: UINavigationController? + // Payment Information from the invoice that contains a document or not + var paymentInfo: GiniInternalPaymentSDK.PaymentInfo? + // Document Id if present for invocies with document + var documentId: String? + // Errors stack received from API. We will show them for the clients + var errors: [String] = [] + + /** + Initializer of the Payment Component Controller class. + + - Parameters: + - giniHealth: An instance of GiniHealth initialized with GiniHealthAPI. + - Returns: + - instance of the payment component controller class + */ + public init(giniHealth: GiniHealth & PaymentComponentsConfigurationProvider & PaymentComponentsStringsProvider) { + self.giniSDK = giniHealth + self.configurationProvider = giniHealth + self.stringsProvider = giniHealth + setupObservers() + loadPaymentProviders() + } + + /** + Initiates the payment flow for a specified document and payment information. + + - Parameters: + - documentId: An optional identifier for the document associated id with the payment flow. + - paymentInfo: An optional `PaymentInfo` object containing the payment details. + - navigationController: The `UINavigationController` used to present subsequent view controllers in the payment flow. + + This method sets up the payment flow by storing the provided document ID, payment information, and navigation controller. + If a `selectedPaymentProvider` is available, it either presents the payment review screen or the payment view bottom sheet, + depending on the configuration. If no payment provider is selected, it directly presents the payment view bottom sheet. + */ + public func startPaymentFlow(documentId: String?, paymentInfo: GiniHealthSDK.PaymentInfo?, navigationController: UINavigationController, trackingDelegate: GiniHealthTrackingDelegate?) { + self.navigationControllerProvided = navigationController + if let paymentInfo { + self.paymentInfo = GiniInternalPaymentSDK.PaymentInfo(paymentConponentsInfo: paymentInfo) + } + self.documentId = documentId + self.trackingDelegate = trackingDelegate + guard let _ = selectedPaymentProvider else { + presentPaymentViewBottomSheet() + return + } + if GiniHealthConfiguration.shared.useInvoiceWithoutDocument { + if GiniHealthConfiguration.shared.showPaymentReviewScreen { + didTapOnPayInvoice(documentId: documentId) + } else { + presentPaymentViewBottomSheet() + } + } else { + didTapOnPayInvoice(documentId: documentId) + } + } + + /** + Checks if the document is payable by extracting the IBAN. + - Parameters: + - docId: The ID of the uploaded document. + - completion: A closure for processing asynchronous data received from the service. It has a Result type parameter, representing either success or failure. The completion block is called on the main thread. + In the case of success, it includes a boolean value indicating whether the IBAN was extracted successfully. + In case of failure, it returns an error from the server side. + */ + public func checkIfDocumentIsPayable(docId: String, completion: @escaping (Result) -> Void) { + giniSDK.checkIfDocumentIsPayable(docId: docId, completion: completion) + } +} + +extension PaymentComponentsController: PaymentReviewProtocol { + /** + Submits feedback for the specified document and its updated extractions. Method used to update the information extracted from a document. + + - Parameters: + - document: The document for which feedback is being submitted. + - updatedExtractions: The updated extractions related to the document. + - completion: An optional closure to be executed upon completion, containing the result of the submission. + */ + public func submitFeedback(for documentId: String, updatedExtractions: [GiniHealthAPILibrary.Extraction], completion: ((Result) -> Void)?) { + let extractions = updatedExtractions.map { Extraction(healthExtraction: $0) } + giniSDK.documentService.submitFeedback(for: documentId, with: [], and: ["payment": [extractions]]) { result in + switch result { + case .success(let result): + completion?(.success(result)) + case .failure(let error): + let healthError = GiniHealthAPILibrary.GiniError.unknown(response: error.response, data: error.data) + completion?(.failure(healthError)) + } + } + } + + /** + Determines if the specified error should be handled internally by the SDK. + + - Parameter error: The Gini error to evaluate. + - Returns: A Boolean value indicating whether the error should be handled internally. + */ + public func shouldHandleErrorInternally(error: GiniHealthAPILibrary.GiniError) -> Bool { + let healthError = GiniHealthError.apiError(GiniError.decorator(error)) + return giniSDK.delegate?.shouldHandleErrorInternally(error: healthError) == true + } + + /** + Tracks the event when the keyboard is closed on the payment review screen. + + This method informs the tracking delegate about the keyboard close event. + */ + public func trackOnPaymentReviewCloseKeyboardClicked() { + trackingDelegate?.onPaymentReviewScreenEvent(event: TrackingEvent.init(type: .onCloseKeyboardButtonClicked)) + } + + /** + Tracks the event when the close button is clicked on the payment review screen. + + This method notifies the tracking delegate about the close button click event. + */ + public func trackOnPaymentReviewCloseButtonClicked() { + // Not anymore tracked on HealthSDK + } + + /** + Tracks the event when the bank button is clicked on the payment review screen. + + - Parameters: + - providerName: The name of the payment provider associated with the button click. + */ + public func trackOnPaymentReviewBankButtonClicked(providerName: String) { + var event = TrackingEvent.init(type: PaymentReviewScreenEventType.onToTheBankButtonClicked) + event.info = ["paymentProvider": providerName] + trackingDelegate?.onPaymentReviewScreenEvent(event: event) + } + + /** + Notifies the tracking delegate of an event occurring on the payment review screen. + + - Parameter event: A `TrackingEvent` of type `PaymentReviewScreenEventType` that describes the specific event + that occurred on the payment review screen. + + This method forwards the event to the `trackingDelegate`, which can handle it based on the event type and any associated data. + */ + public func onPaymentReviewScreenEvent(event: TrackingEvent) { + trackingDelegate?.onPaymentReviewScreenEvent(event: event) + } + + /** + Fetches bank logos for the available payment providers. + + - Returns: A tuple containing an array of logo data and the count of additional banks, if any. + */ + func fetchBankLogos() -> (logos: [Data]?, additionalBankCount: Int?) { + guard !paymentProviders.isEmpty else { return ([], nil)} + let maxShownProviders = min(paymentProviders.count, 2) + let additionalBankCount = paymentProviders.count > 2 ? paymentProviders.count - 2 : nil + return (paymentProviders.prefix(maxShownProviders).map { $0.iconData }, additionalBankCount) + } +} + +extension PaymentComponentsController { + enum Constants { + static let kDefaultPaymentProvider = "defaultPaymentProvider" + static let pdfExtension = ".pdf" + static let numberOfTimesOnboardingShareScreenShouldAppear = 3 + } +} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentInfo.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentInfo.swift deleted file mode 100644 index b6f0aa53d..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentInfo.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// PaymentInfo.swift -// GiniHealth -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -import Foundation -/** - Model object for payment information - */ - -public struct PaymentInfo { - public var recipient, iban: String - public var bic: String - public var amount, purpose: String - public var paymentUniversalLink: String - public var paymentProviderId: String - -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentReviewModel.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentReviewModel.swift deleted file mode 100644 index 14188ccbe..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentReviewModel.swift +++ /dev/null @@ -1,241 +0,0 @@ -// -// PaymentReviewModer.swift -// GiniHealth -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -import GiniHealthAPILibrary -import UIKit - -protocol PaymentReviewViewModelDelegate: AnyObject { - func presentInstallAppBottomSheet(bottomSheet: BottomSheetViewController) - func presentShareInvoiceBottomSheet(bottomSheet: BottomSheetViewController) - func createPaymentRequestAndOpenBankApp() - func obtainPDFFromPaymentRequest() -} - -/** - View model class for review screen - */ -public class PaymentReviewModel: NSObject { - var onDocumentUpdated: () -> Void = {} - - var onExtractionFetched: () -> Void = {} - var onExtractionUpdated: () -> Void = {} - var onPreviewImagesFetched: () -> Void = {} - var reloadCollectionViewClosure: () -> Void = {} - var updateLoadingStatus: () -> Void = {} - var updateImagesLoadingStatus: () -> Void = {} - - var onErrorHandling: (_ error: GiniHealthError) -> Void = { _ in } - - var onNoAppsErrorHandling: (_ error: GiniHealthError) -> Void = { _ in } - - var onCreatePaymentRequestErrorHandling: () -> Void = {} - - var onBankSelection: (_ provider: PaymentProvider) -> Void = { _ in } - - weak var viewModelDelegate: PaymentReviewViewModelDelegate? - - public var document: Document { - didSet { - self.onDocumentUpdated() - } - } - - public var extractions: [Extraction] { - didSet { - self.onExtractionFetched() - } - } - - public var documentId: String - var healthSDK: GiniHealth - private var selectedPaymentProvider: PaymentProvider? - - private var cellViewModels: [PageCollectionCellViewModel] = [PageCollectionCellViewModel]() { - didSet { - self.reloadCollectionViewClosure() - } - } - - var numberOfCells: Int { - return cellViewModels.count - } - - var isLoading: Bool = false { - didSet { - self.updateLoadingStatus() - } - } - - var isImagesLoading: Bool = false { - didSet { - self.updateImagesLoadingStatus() - } - } - - // Pay invoice label - let payInvoiceLabelText: String = GiniLocalized.string("ginihealth.reviewscreen.banking.app.button.label", - comment: "Title label used for the pay invoice button") - - public init(with giniHealth: GiniHealth, document: Document, extractions: [Extraction], selectedPaymentProvider: PaymentProvider?) { - self.healthSDK = giniHealth - self.documentId = document.id - self.document = document - self.extractions = extractions - self.selectedPaymentProvider = selectedPaymentProvider - } - - func getCellViewModel(at indexPath: IndexPath) -> PageCollectionCellViewModel { - return cellViewModels[indexPath.section] - } - - private func createCellViewModel(previewImage: UIImage) -> PageCollectionCellViewModel { - return PageCollectionCellViewModel(preview: previewImage) - } - - func sendFeedback(updatedExtractions: [Extraction]) { - healthSDK.documentService.submitFeedback(for: document, with: [], and: ["payment": [updatedExtractions]]){ result in - switch result { - case .success: break - case .failure: break - } - } - } - - func createPaymentRequest(paymentInfo: PaymentInfo, completion: ((_ paymentRequestID: String) -> ())? = nil) { - isLoading = true - healthSDK.createPaymentRequest(paymentInfo: paymentInfo) {[weak self] result in - self?.isLoading = false - switch result { - case let .success(requestId): - completion?(requestId) - case let .failure(error): - if let delegate = self?.healthSDK.delegate, delegate.shouldHandleErrorInternally(error: error) { - self?.onCreatePaymentRequestErrorHandling() - } - } - } - } - - func openInstallAppBottomSheet() { - let installAppBottomSheet = installAppBottomSheet() - installAppBottomSheet.modalPresentationStyle = .overFullScreen - viewModelDelegate?.presentInstallAppBottomSheet(bottomSheet: installAppBottomSheet) - } - - func openOnboardingShareInvoiceBottomSheet() { - let shareInvoiceBottomSheet = shareInvoiceBottomSheet() - shareInvoiceBottomSheet.modalPresentationStyle = .overFullScreen - viewModelDelegate?.presentShareInvoiceBottomSheet(bottomSheet: shareInvoiceBottomSheet) - } - - func openPaymentProviderApp(requestId: String, universalLink: String) { - healthSDK.openPaymentProviderApp(requestID: requestId, universalLink: universalLink) - } - - func shouldShowOnboardingScreenFor(paymentProvider: PaymentProvider) -> Bool { - let onboardingCounts = OnboardingShareInvoiceScreenCount.load() - let count = onboardingCounts.presentationCount(forProvider: paymentProvider.name) - return count < Constants.numberOfTimesOnboardingShareScreenShouldAppear - } - - func incrementOnboardingCountFor(paymentProvider: PaymentProvider) { - var onboardingCounts = OnboardingShareInvoiceScreenCount.load() - onboardingCounts.incrementPresentationCount(forProvider: paymentProvider.name) - } - - func fetchImages() { - self.isImagesLoading = true - let dispatchGroup = DispatchGroup() - let dispatchQueue = DispatchQueue(label: "imagesQueue") - let dispatchSemaphore = DispatchSemaphore(value: 0) - var vms = [PageCollectionCellViewModel]() - dispatchQueue.async { - for page in 1 ... self.document.pageCount { - dispatchGroup.enter() - - self.healthSDK.documentService.preview(for: self.documentId, pageNumber: page) {[weak self] result in - switch result { - case let .success(dataImage): - if let image = UIImage(data: dataImage), let cellModel = self?.createCellViewModel(previewImage: image) { - vms.append(cellModel) - } - case let .failure(error): - if let delegate = self?.healthSDK.delegate, delegate.shouldHandleErrorInternally(error: .apiError(error)) { - self?.onErrorHandling(.apiError(error)) - } - } - dispatchSemaphore.signal() - dispatchGroup.leave() - } - dispatchSemaphore.wait() - } - - dispatchGroup.notify(queue: dispatchQueue) { - DispatchQueue.main.async { - self.isImagesLoading = false - self.cellViewModels.append(contentsOf: vms) - self.onPreviewImagesFetched() - } - } - } - } - - func installAppBottomSheet() -> BottomSheetViewController { - let installAppBottomViewModel = InstallAppBottomViewModel(selectedPaymentProvider: selectedPaymentProvider) - installAppBottomViewModel.viewDelegate = self - let installAppBottomView = InstallAppBottomView(viewModel: installAppBottomViewModel) - return installAppBottomView - } - - func shareInvoiceBottomSheet() -> BottomSheetViewController { - let shareInvoiceBottomViewModel = ShareInvoiceBottomViewModel(selectedPaymentProvider: selectedPaymentProvider) - shareInvoiceBottomViewModel.viewDelegate = self - let shareInvoiceBottomView = ShareInvoiceBottomView(viewModel: shareInvoiceBottomViewModel) - return shareInvoiceBottomView - } - - func loadPDF(paymentRequestID: String, completion: @escaping (Data) -> ()) { - isLoading = true - healthSDK.paymentService.pdfWithQRCode(paymentRequestId: paymentRequestID) { [weak self] result in - self?.isLoading = false - switch result { - case .success(let data): - completion(data) - case let .failure(error): - if let delegate = self?.healthSDK.delegate, delegate.shouldHandleErrorInternally(error: .apiError(error)) { - self?.onCreatePaymentRequestErrorHandling() - } - } - } - } -} - -extension PaymentReviewModel: InstallAppBottomViewProtocol { - func didTapOnContinue() { - viewModelDelegate?.createPaymentRequestAndOpenBankApp() - } -} - -extension PaymentReviewModel: ShareInvoiceBottomViewProtocol { - func didTapOnContinueToShareInvoice() { - viewModelDelegate?.obtainPDFFromPaymentRequest() - } -} - -/** - View model class for collection view cell - - */ -public struct PageCollectionCellViewModel { - let preview: UIImage -} - -extension PaymentReviewModel { - private enum Constants { - static let numberOfTimesOnboardingShareScreenShouldAppear = 3 - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentReviewViewController+PaymentReviewViewModelDelegate.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentReviewViewController+PaymentReviewViewModelDelegate.swift deleted file mode 100644 index fb446a42c..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentReviewViewController+PaymentReviewViewModelDelegate.swift +++ /dev/null @@ -1,111 +0,0 @@ -// -// PaymentReviewViewController+PaymentReviewViewModelDelegate.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -import UIKit - -extension PaymentReviewViewController: PaymentReviewViewModelDelegate { - func presentInstallAppBottomSheet(bottomSheet: BottomSheetViewController) { - bottomSheet.minHeight = inputContainer.frame.height - presentBottomSheet(viewController: bottomSheet) - } - - func createPaymentRequestAndOpenBankApp() { - self.presentedViewController?.dismiss(animated: true) - if inputFieldsHaveNoErrors() { - createPaymentRequest() - } - } - - func presentShareInvoiceBottomSheet(bottomSheet: BottomSheetViewController) { - bottomSheet.minHeight = inputContainer.frame.height - presentBottomSheet(viewController: bottomSheet) - model?.incrementOnboardingCountFor(paymentProvider: selectedPaymentProvider) - } - - func obtainPDFFromPaymentRequest() { - model?.createPaymentRequest(paymentInfo: obtainPaymentInfo(), completion: { [weak self] paymentRequestID in - self?.loadPDFData(paymentRequestID: paymentRequestID) - }) - } - - private func loadPDFData(paymentRequestID: String) { - self.model?.loadPDF(paymentRequestID: paymentRequestID, completion: { [weak self] pdfData in - let pdfPath = self?.writePDFDataToFile(data: pdfData, fileName: paymentRequestID) - - guard let pdfPath else { - print("Error while write pdf file to location: missing pdf path") - return - } - - self?.sharePDF(pdfURL: pdfPath, paymentRequestID: paymentRequestID) { [weak self] (activity, _, _, _) in - guard activity != nil else { - return - } - - // Publish the payment request id only after a user has picked an activity (app) - self?.model?.healthSDK.delegate?.didCreatePaymentRequest(paymentRequestID: paymentRequestID) - } - }) - } - - private func writePDFDataToFile(data: Data, fileName: String) -> URL? { - do { - let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) - guard let docDirectoryPath = paths.first else { return nil } - let pdfFileName = fileName + Constants.pdfExtension - let pdfPath = docDirectoryPath.appendingPathComponent(pdfFileName) - try data.write(to: pdfPath) - return pdfPath - } catch { - print("Error while write pdf file to location: \(error.localizedDescription)") - return nil - } - } - - private func sharePDF(pdfURL: URL, paymentRequestID: String, - completionWithItemsHandler: @escaping UIActivityViewController.CompletionWithItemsHandler) { - // Create UIActivityViewController with the PDF file - let activityViewController = UIActivityViewController(activityItems: [pdfURL], applicationActivities: nil) - activityViewController.completionWithItemsHandler = completionWithItemsHandler - - // Exclude some activities if needed - activityViewController.excludedActivityTypes = [ - .addToReadingList, - .assignToContact, - .airDrop, - .mail, - .message, - .postToFacebook, - .postToVimeo, - .postToWeibo, - .postToFlickr, - .postToTwitter, - .postToTencentWeibo, - .copyToPasteboard, - .markupAsPDF, - .openInIBooks, - .print, - .saveToCameraRoll - ] - - // Present the UIActivityViewController - DispatchQueue.main.async { - if let popoverController = activityViewController.popoverPresentationController { - popoverController.sourceView = self.view - popoverController.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0) - popoverController.permittedArrowDirections = [] - } - - if (self.presentedViewController != nil) { - self.presentedViewController?.dismiss(animated: true, completion: { - self.present(activityViewController, animated: true, completion: nil) - }) - } else { - self.present(activityViewController, animated: true, completion: nil) - } - } - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentReviewViewController+UICollection.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentReviewViewController+UICollection.swift deleted file mode 100644 index f7a22f28c..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentReviewViewController+UICollection.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// PaymentReviewViewController+UICollection.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -import UIKit - -// MARK: - UICollectionViewDelegate, UICollectionViewDataSource - -extension PaymentReviewViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { - public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 1 } - - public func numberOfSections(in collectionView: UICollectionView) -> Int { - model?.numberOfCells ?? 1 - } - - public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "pageCellIdentifier", for: indexPath) as! PageCollectionViewCell - cell.pageImageView.frame = CGRect(x: 0, y: 0, width: collectionView.frame.width, height: collectionView.frame.height) - cell.pageImageView.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: Constants.bottomPaddingPageImageView, right: 0.0) - let cellModel = model?.getCellViewModel(at: indexPath) - cell.pageImageView.display(image: cellModel?.preview ?? UIImage()) - return cell - } - - // MARK: - UICollectionViewDelegateFlowLayout - - public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - let height = collectionView.frame.height - let width = collectionView.frame.width - return CGSize(width: width, height: height) - } - - // MARK: - For Display the page number in page controll of collection view Cell - - public func scrollViewDidScroll(_ scrollView: UIScrollView) { - pageControl.currentPage = Int(scrollView.contentOffset.x) / Int(scrollView.frame.width) - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentReviewViewController+UITextFieldDelegate.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentReviewViewController+UITextFieldDelegate.swift deleted file mode 100644 index c05527b45..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentReviewViewController+UITextFieldDelegate.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// PaymentReviewViewController+UITextFieldDelegate.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -import UIKit - -// MARK: - UITextFieldDelegate - -extension PaymentReviewViewController: UITextFieldDelegate { - /** - Dissmiss the keyboard when return key pressed - */ - public func textFieldShouldReturn(_ textField: UITextField) -> Bool { - textField.resignFirstResponder() - return true - } - - /** - Updates amoutToPay, formated string with a currency and removes "0.00" value - */ - func updateAmoutToPayWithCurrencyFormat() { - if amountTextFieldView.textField.hasText, let amountFieldText = amountTextFieldView.text { - if let priceValue = decimal(from: amountFieldText ) { - amountToPay.value = priceValue - if priceValue > 0 { - let amountToPayText = amountToPay.string - amountTextFieldView.text = amountToPayText - } else { - amountTextFieldView.text = "" - } - } - } - } - public func textFieldDidBeginEditing(_ textField: UITextField) { - applySelectionStyle(textFieldViewWithTag(tag: textField.tag)) - - // remove currency symbol and whitespaces for edit mode - if let fieldIdentifier = TextFieldType(rawValue: textField.tag) { - hideErrorLabel(textFieldTag: fieldIdentifier) - - if fieldIdentifier == .amountFieldTag { - let amountToPayText = amountToPay.stringWithoutSymbol - amountTextFieldView.text = amountToPayText - } - } - } - - public func textFieldDidEndEditing(_ textField: UITextField) { - // add currency format when edit is finished - if TextFieldType(rawValue: textField.tag) == .amountFieldTag { - updateAmoutToPayWithCurrencyFormat() - } - validateTextField(textField.tag) - if TextFieldType(rawValue: textField.tag) == .ibanFieldTag { - if textField.text == lastValidatedIBAN { - showIBANValidationErrorIfNeeded() - } - } - disablePayButtonIfNeeded() - } - - public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - if TextFieldType(rawValue: textField.tag) == .amountFieldTag, - let text = textField.text, - let textRange = Range(range, in: text) { - let updatedText = text.replacingCharacters(in: textRange, with: string) - - // Limit length to 7 digits - let onlyDigits = String(updatedText - .trimmingCharacters(in: .whitespaces) - .filter { c in c != "," && c != "."} - .prefix(7)) - - if let decimal = Decimal(string: onlyDigits) { - let decimalWithFraction = decimal / 100 - - if let newAmount = Price.stringWithoutSymbol(from: decimalWithFraction)?.trimmingCharacters(in: .whitespaces) { - // Save the selected text range to restore the cursor position after replacing the text - let selectedRange = textField.selectedTextRange - - textField.text = newAmount - amountToPay.value = decimalWithFraction - - // Move the cursor position after the inserted character - if let selectedRange = selectedRange { - let countDelta = newAmount.count - text.count - let offset = countDelta == 0 ? 1 : countDelta - textField.moveSelectedTextRange(from: selectedRange.start, to: offset) - } - } - } - disablePayButtonIfNeeded() - return false - } - return true - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentReviewViewController.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentReviewViewController.swift deleted file mode 100644 index 7dd815d90..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/PaymentReviewViewController.swift +++ /dev/null @@ -1,732 +0,0 @@ -// -// PaymentReviewViewController.swift -// GiniHealth -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -import UIKit -import GiniHealthAPILibrary - -public final class PaymentReviewViewController: UIViewController, UIGestureRecognizerDelegate { - @IBOutlet var pageControl: UIPageControl! - @IBOutlet weak var pageControlHeightConstraint: NSLayoutConstraint! - @IBOutlet weak var recipientTextFieldView: TextFieldWithLabelView! - @IBOutlet weak var ibanTextFieldView: TextFieldWithLabelView! - @IBOutlet weak var amountTextFieldView: TextFieldWithLabelView! - @IBOutlet weak var usageTextFieldView: TextFieldWithLabelView! - @IBOutlet weak var payButtonStackView: UIStackView! - @IBOutlet var paymentInputFieldsErrorLabels: [UILabel]! - @IBOutlet var usageErrorLabel: UILabel! - @IBOutlet var amountErrorLabel: UILabel! - @IBOutlet var ibanErrorLabel: UILabel! - @IBOutlet var recipientErrorLabel: UILabel! - @IBOutlet var paymentInputFields: [TextFieldWithLabelView]! - @IBOutlet weak var mainView: UIView! - @IBOutlet var inputContainer: UIView! - @IBOutlet var containerCollectionView: UIView! - @IBOutlet var paymentInfoStackView: UIStackView! - @IBOutlet var collectionView: UICollectionView! - @IBOutlet weak var closeButton: UIButton! - @IBOutlet weak var infoBar: UIView! - @IBOutlet weak var infoBarLabel: UILabel! - @IBOutlet weak var bottomView: UIView! - var model: PaymentReviewModel? - var amountToPay = Price(value: 0, currencyCode: "€") - var lastValidatedIBAN = "" - private var showInfoBarOnce = true - - private lazy var payInvoiceButton: PaymentPrimaryButton = { - let button = PaymentPrimaryButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.frame = CGRect(x: 0, y: 0, width: .greatestFiniteMagnitude, height: Constants.buttonViewHeight) - return button - }() - - private lazy var poweredByGiniView: PoweredByGiniView = { - let view = PoweredByGiniView() - view.viewModel = PoweredByGiniViewModel() - return view - }() - - var selectedPaymentProvider: PaymentProvider! - - public weak var trackingDelegate: GiniHealthTrackingDelegate? - - enum TextFieldType: Int { - case recipientFieldTag = 1 - case ibanFieldTag - case amountFieldTag - case usageFieldTag - } - - public static func instantiate(with giniHealth: GiniHealth, document: Document, extractions: [Extraction], selectedPaymentProvider: PaymentProvider, trackingDelegate: GiniHealthTrackingDelegate? = nil) -> PaymentReviewViewController { - let viewController = (UIStoryboard(name: "PaymentReview", bundle: giniHealthBundleResource()) - .instantiateViewController(withIdentifier: "paymentReviewViewController") as? PaymentReviewViewController)! - let viewModel = PaymentReviewModel(with: giniHealth, - document: document, - extractions: extractions, - selectedPaymentProvider: selectedPaymentProvider) - viewController.model = viewModel - viewController.trackingDelegate = trackingDelegate - viewController.selectedPaymentProvider = selectedPaymentProvider - return viewController - } - - public static func instantiate(with giniHealth: GiniHealth, data: DataForReview, selectedPaymentProvider: PaymentProvider, trackingDelegate: GiniHealthTrackingDelegate? = nil) -> PaymentReviewViewController { - let viewController = (UIStoryboard(name: "PaymentReview", bundle: giniHealthBundleResource()) - .instantiateViewController(withIdentifier: "paymentReviewViewController") as? PaymentReviewViewController)! - let viewModel = PaymentReviewModel(with: giniHealth, - document: data.document, - extractions: data.extractions, - selectedPaymentProvider: selectedPaymentProvider) - viewController.model = viewModel - viewController.trackingDelegate = trackingDelegate - viewController.selectedPaymentProvider = selectedPaymentProvider - return viewController - } - - let giniHealthConfiguration = GiniHealthConfiguration.shared - - override public func viewDidLoad() { - super.viewDidLoad() - subscribeOnNotifications() - dismissKeyboardOnTap() - configureUI() - setupViewModel() - } - - public override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - if showInfoBarOnce { - showInfoBar() - showInfoBarOnce = false - } - } - - fileprivate func setupViewModel() { - model?.onExtractionFetched = { [weak self] () in - DispatchQueue.main.async { - self?.fillInInputFields() - } - } - - model?.updateImagesLoadingStatus = { [weak self] () in - DispatchQueue.main.async { [weak self] in - let isLoading = self?.model?.isImagesLoading ?? false - if isLoading { - self?.collectionView.showLoading(style: .whiteLarge, - color: UIColor.GiniHealthColors.accent1, - scale: Constants.loadingIndicatorScale) - } else { - self?.collectionView.stopLoading() - } - } - } - - model?.updateLoadingStatus = { [weak self] () in - DispatchQueue.main.async { [weak self] in - let isLoading = self?.model?.isLoading ?? false - if isLoading { - if #available(iOS 13.0, *) { - self?.view.showLoading(style: Constants.loadingIndicatorStyle, - color: UIColor.GiniHealthColors.accent1, - scale: Constants.loadingIndicatorScale) - } else { - self?.view.showLoading(style: .whiteLarge, - color: UIColor.GiniHealthColors.accent1, - scale: Constants.loadingIndicatorScale) - } - } else { - self?.view.stopLoading() - } - } - } - - model?.reloadCollectionViewClosure = { [weak self] () in - DispatchQueue.main.async { - self?.collectionView.reloadData() - } - } - - model?.onPreviewImagesFetched = { [weak self] () in - DispatchQueue.main.async { - self?.collectionView.reloadData() - } - } - - model?.onErrorHandling = {[weak self] error in - DispatchQueue.main.async { - self?.showError(message: GiniLocalized.string("ginihealth.errors.default", - comment: "default error message") ) - } - } - - model?.onCreatePaymentRequestErrorHandling = {[weak self] () in - DispatchQueue.main.async { - self?.showError(message: GiniLocalized.string("ginihealth.errors.failed.payment.request.creation", - comment: "error for creating payment request")) - } - } - - model?.fetchImages() - - model?.viewModelDelegate = self - } - - override public func viewDidDisappear(_ animated: Bool) { - unsubscribeFromNotifications() - } - - override public func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - inputContainer.roundCorners(corners: [.topLeft, .topRight], radius: Constants.cornerRadiusInputContainer) - } - - public override var preferredStatusBarStyle: UIStatusBarStyle { - return giniHealthConfiguration.paymentReviewStatusBarStyle - } - - // MARK: - congifureUI - - fileprivate func configureUI() { - configureScreenBackgroundColor() - configureCollectionView() - configurePaymentInputFields() - configurePageControl() - configureCloseButton() - configurePayButtonInitialState() - configurePoweredByGiniView() - hideErrorLabels() - fillInInputFields() - addDoneButtonForNumPad(amountTextFieldView) - } - - // MARK: - Info bar - - fileprivate func configureInfoBar() { - infoBar.roundCorners(corners: [.topLeft, .topRight], radius: Constants.cornerRadiusInfoBar) - infoBar.backgroundColor = UIColor.GiniHealthColors.success1 - infoBarLabel.textColor = UIColor.GiniHealthColors.dark7 - infoBarLabel.font = giniHealthConfiguration.textStyleFonts[.caption1] - infoBarLabel.adjustsFontForContentSizeCategory = true - infoBarLabel.text = GiniLocalized.string("ginihealth.reviewscreen.infobar.message", - comment: "info bar message") - } - - fileprivate func showInfoBar() { - configureInfoBar() - infoBar.isHidden = false - let screenSize = UIScreen.main.bounds.size - UIView.animate(withDuration: Constants.animationDuration, - delay: 0, usingSpringWithDamping: 1.0, - initialSpringVelocity: 1.0, - options: [], animations: { - self.infoBar.frame = CGRect(x: 0, y: self.inputContainer.frame.minY + Constants.moveHeightInfoBar - self.infoBar.frame.height, width: screenSize.width, height: self.infoBar.frame.height) - }, completion: nil) - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - self.animateSlideDownInfoBar() - } - } - - fileprivate func animateSlideDownInfoBar() { - let screenSize = UIScreen.main.bounds.size - UIView.animate(withDuration: Constants.animationDuration, - delay: 0, usingSpringWithDamping: 1.0, - initialSpringVelocity: 1.0, - options: [], animations: { - self.infoBar.frame = CGRect(x: 0, y: self.inputContainer.frame.minY, width: screenSize.width, height: self.infoBar.frame.height) - }, completion: { _ in - self.infoBar.isHidden = true - }) - } - - fileprivate func configurePayButtonInitialState() { - payButtonStackView.addArrangedSubview(payInvoiceButton) - guard let model else { return } - payInvoiceButton.configure(with: giniHealthConfiguration.primaryButtonConfiguration) - payInvoiceButton.customConfigure(paymentProviderColors: selectedPaymentProvider.colors, - text: model.payInvoiceLabelText, - leftImageData: selectedPaymentProvider.iconData) - disablePayButtonIfNeeded() - payInvoiceButton.didTapButton = { [weak self] in - self?.payButtonClicked() - } - } - - fileprivate func configurePaymentInputFields() { - for field in paymentInputFields { - applyDefaultStyle(field) - } - } - - fileprivate func configureCollectionView() { - collectionView.delegate = self - collectionView.dataSource = self - } - - fileprivate func configurePageControl() { - pageControl.layer.zPosition = Constants.zPositionPageControl - pageControl.pageIndicatorTintColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark4, - darkModeColor: UIColor.GiniHealthColors.light4).uiColor() - pageControl.currentPageIndicatorTintColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark2, - darkModeColor: UIColor.GiniHealthColors.light5).uiColor() - pageControl.hidesForSinglePage = true - pageControl.numberOfPages = model?.document.pageCount ?? 1 - if pageControl.numberOfPages == 1 { - pageControlHeightConstraint.constant = 0 - } else { - pageControlHeightConstraint.constant = Constants.heightPageControl - } - } - - fileprivate func configureCloseButton() { - closeButton.isHidden = !giniHealthConfiguration.showPaymentReviewCloseButton - closeButton.setImage(UIImageNamedPreferred(named: "paymentReviewCloseButton"), for: .normal) - } - - fileprivate func configureScreenBackgroundColor() { - let screenBackgroundColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.light7, - darkModeColor: UIColor.GiniHealthColors.light7).uiColor() - mainView.backgroundColor = screenBackgroundColor - collectionView.backgroundColor = screenBackgroundColor - pageControl.backgroundColor = screenBackgroundColor - inputContainer.backgroundColor = GiniColor(lightModeColor: UIColor.GiniHealthColors.dark7, - darkModeColor: UIColor.GiniHealthColors.light7).uiColor() - } - - fileprivate func configurePoweredByGiniView() { - bottomView.addSubview(poweredByGiniView) - setupPoweredByGiniConstraints() - } - - private func setupPoweredByGiniConstraints() { - NSLayoutConstraint.activate([ - poweredByGiniView.topAnchor.constraint(equalTo: bottomView.topAnchor), - poweredByGiniView.trailingAnchor.constraint(equalTo: bottomView.trailingAnchor), - poweredByGiniView.bottomAnchor.constraint(equalTo: poweredByGiniView.bottomAnchor) - ]) - } - - // MARK: - Input fields configuration - - fileprivate func applyDefaultStyle(_ textFieldView: TextFieldWithLabelView) { - textFieldView.configure(configuration: giniHealthConfiguration.defaultStyleInputFieldConfiguration) - textFieldView.customConfigure(labelTitle: inputFieldPlaceholderText(textFieldView)) - textFieldView.textField.delegate = self - textFieldView.textField.tag = textFieldView.tag - textFieldView.layer.masksToBounds = true - } - - fileprivate func applyErrorStyle(_ textFieldView: TextFieldWithLabelView) { - UIView.animate(withDuration: Constants.animationDuration) { - textFieldView.configure(configuration: self.giniHealthConfiguration.errorStyleInputFieldConfiguration) - textFieldView.layer.masksToBounds = true - } - } - - func applySelectionStyle(_ textFieldView: TextFieldWithLabelView) { - UIView.animate(withDuration: Constants.animationDuration) { [self] in - textFieldView.configure(configuration: self.giniHealthConfiguration.selectionStyleInputFieldConfiguration) - textFieldView.layer.masksToBounds = true - } - } - - @objc fileprivate func doneWithAmountInputButtonTapped() { - amountTextFieldView.textField.endEditing(true) - amountTextFieldView.textField.resignFirstResponder() - - if amountTextFieldView.textField.hasText && !amountTextFieldView.textField.isReallyEmpty { - updateAmoutToPayWithCurrencyFormat() - } - } - - func addDoneButtonForNumPad(_ textFieldView: TextFieldWithLabelView) { - let toolbarDone = UIToolbar(frame:CGRect(x: 0, y: 0, width: view.frame.width, height: Constants.heightToolbar)) - toolbarDone.sizeToFit() - let flexBarButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) - let barBtnDone = UIBarButtonItem.init(barButtonSystemItem: UIBarButtonItem.SystemItem.done, - target: self, - action: #selector(PaymentReviewViewController.doneWithAmountInputButtonTapped)) - - toolbarDone.items = [flexBarButton, barBtnDone] - textFieldView.textField.inputAccessoryView = toolbarDone - } - - fileprivate func inputFieldPlaceholderText(_ textFieldView: TextFieldWithLabelView) -> String { - if let fieldIdentifier = TextFieldType(rawValue: textFieldView.tag) { - switch fieldIdentifier { - case .recipientFieldTag: - return GiniLocalized.string("ginihealth.reviewscreen.recipient.placeholder", - comment: "placeholder text for recipient input field") - case .ibanFieldTag: - return GiniLocalized.string("ginihealth.reviewscreen.iban.placeholder", - comment: "placeholder text for iban input field") - case .amountFieldTag: - return GiniLocalized.string("ginihealth.reviewscreen.amount.placeholder", - comment: "placeholder text for amount input field") - case .usageFieldTag: - return GiniLocalized.string("ginihealth.reviewscreen.usage.placeholder", - comment: "placeholder text for usage input field") - } - } - return "" - } - - // MARK: - Input fields validation - - @IBAction func textFieldChanged(_ sender: UITextField) { - disablePayButtonIfNeeded() - } - - func validateTextField(_ textFieldViewTag: Int) { - let textFieldView = textFieldViewWithTag(tag: textFieldViewTag) - if let fieldIdentifier = TextFieldType(rawValue: textFieldViewTag) { - switch fieldIdentifier { - case .amountFieldTag: - if amountTextFieldView.textField.hasText && !amountTextFieldView.textField.isReallyEmpty { - let decimalPart = amountToPay.value - if decimalPart > 0 { - applyDefaultStyle(textFieldView) - hideErrorLabel(textFieldTag: fieldIdentifier) - } else { - amountTextFieldView.text = "" - applyErrorStyle(textFieldView) - showErrorLabel(textFieldTag: fieldIdentifier) - } - } else { - applyErrorStyle(textFieldView) - showErrorLabel(textFieldTag: fieldIdentifier) - } - case .ibanFieldTag, .recipientFieldTag, .usageFieldTag: - if textFieldView.textField.hasText && !textFieldView.textField.isReallyEmpty { - applyDefaultStyle(textFieldView) - hideErrorLabel(textFieldTag: fieldIdentifier) - } else { - applyErrorStyle(textFieldView) - showErrorLabel(textFieldTag: fieldIdentifier) - } - } - } - } - - func textFieldViewWithTag(tag: Int) -> TextFieldWithLabelView { - paymentInputFields.first(where: { $0.tag == tag }) ?? TextFieldWithLabelView() - } - - fileprivate func validateIBANTextField(){ - if let ibanText = ibanTextFieldView.textField.text, ibanTextFieldView.textField.hasText { - if IBANValidator().isValid(iban: ibanText) { - applyDefaultStyle(ibanTextFieldView) - hideErrorLabel(textFieldTag: .ibanFieldTag) - } else { - applyErrorStyle(ibanTextFieldView) - showValidationErrorLabel(textFieldTag: .ibanFieldTag) - } - } else { - applyErrorStyle(ibanTextFieldView) - showErrorLabel(textFieldTag: .ibanFieldTag) - } - } - - func showIBANValidationErrorIfNeeded(){ - if IBANValidator().isValid(iban: lastValidatedIBAN) { - applyDefaultStyle(ibanTextFieldView) - hideErrorLabel(textFieldTag: .ibanFieldTag) - } else { - applyErrorStyle(ibanTextFieldView) - showValidationErrorLabel(textFieldTag: .ibanFieldTag) - } - } - - fileprivate func validateAllInputFields() { - for textField in paymentInputFields { - validateTextField(textField.tag) - } - } - - fileprivate func hideErrorLabels() { - for errorLabel in paymentInputFieldsErrorLabels { - errorLabel.isHidden = true - } - } - - fileprivate func fillInInputFields() { - guard let model else { return } - recipientTextFieldView.text = model.extractions.first(where: {$0.name == ExtractionType.doctorName.rawValue})?.value ?? model.extractions.first(where: {$0.name == ExtractionType.paymentRecipient.rawValue})?.value - ibanTextFieldView.text = model.extractions.first(where: {$0.name == ExtractionType.iban.rawValue})?.value - usageTextFieldView.text = model.extractions.first(where: {$0.name == ExtractionType.paymentPurpose.rawValue})?.value - if let amountString = model.extractions.first(where: {$0.name == ExtractionType.amountToPay.rawValue})?.value, let amountToPay = Price(extractionString: amountString) { - self.amountToPay = amountToPay - let amountToPayText = amountToPay.string - amountTextFieldView.text = amountToPayText - } - validateAllInputFields() - disablePayButtonIfNeeded() - } - - func disablePayButtonIfNeeded() { - payInvoiceButton.superview?.alpha = paymentInputFields.allSatisfy({ !$0.textField.isReallyEmpty }) && amountToPay.value > 0 ? 1 : 0.4 - } - - - fileprivate func showErrorLabel(textFieldTag: TextFieldType) { - var errorLabel = UILabel() - var errorMessage = "" - switch textFieldTag { - case .recipientFieldTag: - errorLabel = recipientErrorLabel - errorMessage = GiniLocalized.string("ginihealth.errors.failed.recipient.non.empty.check", - comment: " recipient failed non empty check") - case .ibanFieldTag: - errorLabel = ibanErrorLabel - errorMessage = GiniLocalized.string("ginihealth.errors.failed.iban.non.empty.check", - comment: "iban failed non empty check") - case .amountFieldTag: - errorLabel = amountErrorLabel - errorMessage = GiniLocalized.string("ginihealth.errors.failed.amount.non.empty.check", - comment: "amount failed non empty check") - case .usageFieldTag: - errorLabel = usageErrorLabel - errorMessage = GiniLocalized.string("ginihealth.errors.failed.purpose.non.empty.check", - comment: "purpose failed non empty check") - } - if errorLabel.isHidden { - errorLabel.isHidden = false - errorLabel.textColor = UIColor.GiniHealthColors.feedback1 - errorLabel.text = errorMessage - } - } - - fileprivate func showValidationErrorLabel(textFieldTag: TextFieldType) { - var errorLabel = UILabel() - var errorMessage = GiniLocalized.string("ginihealth.errors.failed.default.textfield.validation.check", - comment: "the field failed non empty check") - switch textFieldTag { - case .recipientFieldTag: - errorLabel = recipientErrorLabel - case .ibanFieldTag: - errorLabel = ibanErrorLabel - errorMessage = GiniLocalized.string("ginihealth.errors.failed.iban.validation.check", - comment: "iban failed validation check") - case .amountFieldTag: - errorLabel = amountErrorLabel - case .usageFieldTag: - errorLabel = usageErrorLabel - } - if errorLabel.isHidden { - errorLabel.isHidden = false - errorLabel.textColor = UIColor.GiniHealthColors.feedback1 - errorLabel.text = errorMessage - } - } - - func hideErrorLabel(textFieldTag: TextFieldType) { - var errorLabel = UILabel() - switch textFieldTag { - case .recipientFieldTag: - errorLabel = recipientErrorLabel - case .ibanFieldTag: - errorLabel = ibanErrorLabel - case .amountFieldTag: - errorLabel = amountErrorLabel - case .usageFieldTag: - errorLabel = usageErrorLabel - } - if !errorLabel.isHidden { - errorLabel.isHidden = true - } - disablePayButtonIfNeeded() - } - - // MARK: - Pay Button Action - func payButtonClicked() { - var event = TrackingEvent.init(type: PaymentReviewScreenEventType.onToTheBankButtonClicked) - event.info = ["paymentProvider": selectedPaymentProvider.name] - trackingDelegate?.onPaymentReviewScreenEvent(event: event) - view.endEditing(true) - validateAllInputFields() - validateIBANTextField() - if let iban = ibanTextFieldView.text { - lastValidatedIBAN = iban - } - guard inputFieldsHaveNoErrors() else { return } - if selectedPaymentProvider.gpcSupportedPlatforms.contains(.ios) { - guard selectedPaymentProvider.appSchemeIOS.canOpenURLString() else { - model?.openInstallAppBottomSheet() - return - } - createPaymentRequest() - } else if selectedPaymentProvider.openWithSupportedPlatforms.contains(.ios) { - if model?.shouldShowOnboardingScreenFor(paymentProvider: selectedPaymentProvider) ?? false { - model?.openOnboardingShareInvoiceBottomSheet() - } else { - obtainPDFFromPaymentRequest() - } - } - } - - func inputFieldsHaveNoErrors() -> Bool { - paymentInputFieldsErrorLabels.allSatisfy { $0.isHidden } - } - - func createPaymentRequest() { - if !amountTextFieldView.textField.isReallyEmpty { - let paymentInfo = obtainPaymentInfo() - model?.createPaymentRequest(paymentInfo: paymentInfo, completion: { [weak self] requestId in - // Publish the payment request id before launching the payment provider app - self?.model?.healthSDK.delegate?.didCreatePaymentRequest(paymentRequestID: requestId) - - self?.model?.openPaymentProviderApp(requestId: requestId, universalLink: paymentInfo.paymentUniversalLink) - }) - sendFeedback(paymentInfo: paymentInfo) - } - } - - func obtainPaymentInfo() -> PaymentInfo { - let amountText = amountToPay.extractionString - let paymentInfo = PaymentInfo(recipient: recipientTextFieldView.text ?? "", - iban: ibanTextFieldView.text ?? "", - bic: "", amount: amountText, - purpose: usageTextFieldView.text ?? "", - paymentUniversalLink: selectedPaymentProvider.universalLinkIOS, - paymentProviderId: selectedPaymentProvider.id) - return paymentInfo - } - - private func sendFeedback(paymentInfo: PaymentInfo) { - let paymentRecipientExtraction = Extraction(box: nil, - candidates: "", - entity: "text", - value: recipientTextFieldView.text ?? "", - name: "payment_recipient") - let ibanExtraction = Extraction(box: nil, - candidates: "", - entity: "iban", - value: paymentInfo.iban, - name: "iban") - let referenceExtraction = Extraction(box: nil, - candidates: "", - entity: "text", - value: paymentInfo.purpose, - name: "payment_purpose") - let amoutToPayExtraction = Extraction(box: nil, - candidates: "", - entity: "amount", - value: paymentInfo.amount, - name: "amount_to_pay") - let updatedExtractions = [paymentRecipientExtraction, ibanExtraction, referenceExtraction, amoutToPayExtraction] - model?.sendFeedback(updatedExtractions: updatedExtractions) - } - - @IBAction func closeButtonClicked(_ sender: UIButton) { - if (keyboardWillShowCalled) { - trackingDelegate?.onPaymentReviewScreenEvent(event: TrackingEvent.init(type: .onCloseKeyboardButtonClicked)) - view.endEditing(true) - } else { - trackingDelegate?.onPaymentReviewScreenEvent(event: TrackingEvent.init(type: .onCloseButtonClicked)) - dismiss(animated: true, completion: nil) - } - } - - // MARK: - Keyboard handling - - private var keyboardWillShowCalled = false - - @objc func keyboardWillShow(notification: NSNotification) { - guard let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { - /** - If keyboard size is not available for some reason, dont do anything - */ - return - } - /** - Moves the root view up by the distance of keyboard height taking in account safeAreaInsets.bottom - */ - mainView.bounds.origin.y = keyboardSize.height - view.safeAreaInsets.bottom - - keyboardWillShowCalled = true - } - - @objc func keyboardWillHide(notification: NSNotification) { - let animationDuration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double ?? Constants.animationDuration - let animationCurve = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt ?? UInt(UIView.AnimationCurve.easeOut.rawValue) - - self.keyboardWillShowCalled = false - - /** - Moves back the root view origin to zero. Schedules it on the main dispatch queue to prevent - the view jumping if another keyboard is shown right after this one is hidden. - */ - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in - guard let self = self else { return } - - if !self.keyboardWillShowCalled { - UIView.animate(withDuration: animationDuration, delay: 0.0, options: UIView.AnimationOptions(rawValue: animationCurve), animations: { - self.mainView.bounds.origin.y = 0 - }, completion: nil) - } - } - } - - func subscribeOnNotifications() { - subscribeOnKeyboardNotifications() - } - - func subscribeOnKeyboardNotifications() { - /** - Calls the 'keyboardWillShow' function when the view controller receive the notification that a keyboard is going to be shown - */ - NotificationCenter.default.addObserver(self, selector: #selector(PaymentReviewViewController.keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) - - /** - Calls the 'keyboardWillHide' function when the view controlelr receive notification that keyboard is going to be hidden - */ - NotificationCenter.default.addObserver(self, selector: #selector(PaymentReviewViewController.keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) - } - - fileprivate func unsubscribeFromKeyboardNotifications() { - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) - } - - fileprivate func unsubscribeFromNotifications() { - unsubscribeFromKeyboardNotifications() - } - - fileprivate func dismissKeyboardOnTap() { - let tap = UITapGestureRecognizer(target: view, action: #selector(UIView.endEditing)) - tap.cancelsTouchesInView = false - mainView.addGestureRecognizer(tap) - } -} - -extension PaymentReviewViewController { - func showError(_ title: String? = nil, message: String) { - let alertController = UIAlertController(title: title, - message: message, - preferredStyle: .alert) - let action = UIAlertAction(title: GiniLocalized.string("ginihealth.alert.ok.title", - comment: "ok title for action"), style: .default, handler: nil) - alertController.addAction(action) - present(alertController, animated: true, completion: nil) - } -} - -extension PaymentReviewViewController { - enum Constants { - static let buttonViewHeight: CGFloat = 56 - static let animationDuration: CGFloat = 0.3 - static let cornerRadiusInputContainer = 12.0 - static let cornerRadiusInfoBar = 12.0 - static let moveHeightInfoBar = 32.0 - static let zPositionPageControl = 10.0 - static let heightPageControl = 20.0 - static let heightToolbar = 40.0 - static let bottomPaddingPageImageView = 20.0 - static let loadingIndicatorScale = 1.0 - @available(iOS 13.0, *) - static let loadingIndicatorStyle = UIActivityIndicatorView.Style.large - static let pdfExtension = ".pdf" - } -} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/SSLPinning/GiniSessionDelegate.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/SSLPinning/GiniSessionDelegate.swift similarity index 98% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/SSLPinning/GiniSessionDelegate.swift rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/SSLPinning/GiniSessionDelegate.swift index 90d2f8f26..880b210ee 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/SSLPinning/GiniSessionDelegate.swift +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/SSLPinning/GiniSessionDelegate.swift @@ -9,11 +9,11 @@ import Foundation class GiniSessionDelegate: NSObject, URLSessionDelegate { private let pinningManager: SSLPinningManager - + internal init(pinningConfig: [String: [String]]) { self.pinningManager = SSLPinningManager(pinningConfig: pinningConfig) } - + func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/SSLPinning/SSLPinningManager.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/SSLPinning/SSLPinningManager.swift similarity index 99% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/SSLPinning/SSLPinningManager.swift rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/SSLPinning/SSLPinningManager.swift index 17a224dd3..b0b29c911 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/SSLPinning/SSLPinningManager.swift +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/SSLPinning/SSLPinningManager.swift @@ -17,7 +17,7 @@ struct SSLPinningManager { case failedToGetDataFromPublicKey case receivedWrongCertificate } - + // ASN.1 header for RSA 2048-bit keys. The same for all keys private static let rsa2048ASN1Header: [UInt8] = [ 0x30, 0x82, 0x01, 0x22, 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, @@ -30,7 +30,7 @@ struct SSLPinningManager { init(pinningConfig: [String: [String]]) { self.pinningConfig = pinningConfig } - + // Function to validate the server's certificate func validate(challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { @@ -54,7 +54,7 @@ private extension SSLPinningManager { !trustCertificateChain.isEmpty else { throw PinningError.noCertificatesFromServer } - + // Step 2: Get the domain from the challenge and check if it has a pinning configuration guard let domain = challenge.protectionSpace.host.lowercased() as String? else { throw PinningError.receivedWrongCertificate @@ -77,7 +77,7 @@ private extension SSLPinningManager { } throw PinningError.receivedWrongCertificate } - + // Extract the public key from the server's certificate func getPublicKey(for certificate: SecCertificate) throws -> SecKey { let policy = SecPolicyCreateBasicX509() @@ -87,10 +87,10 @@ private extension SSLPinningManager { guard let trust, trustCreationStatus == errSecSuccess, let publicKey = trustCopyPublicKey(trust) else { throw PinningError.failedToGetPublicKey } - + return publicKey } - + // Generate a SHA-256 hash of the public key func getKeyHash(of publicKey: SecKey) throws -> String { guard let publicKeyCFData = SecKeyCopyExternalRepresentation(publicKey, nil) else { @@ -115,7 +115,7 @@ private extension SSLPinningManager { return SecTrustCopyPublicKey(trust) } } - + func trustCopyCertificateChain(_ trust: SecTrust) -> [SecCertificate]? { if #available(iOS 15, macOS 11, tvOS 14, watchOS 7, visionOS 1, *) { return (SecTrustCopyCertificateChain(trust) as? [SecCertificate]) diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/TextFieldConfiguration.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/TextFieldConfiguration.swift deleted file mode 100644 index 3abb80675..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/TextFieldConfiguration.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// TextFieldConfiguration.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit -public struct TextFieldConfiguration { - let backgroundColor: UIColor - let borderColor: UIColor - let textColor: UIColor - let cornerRadius: CGFloat - let borderWidth: CGFloat - let placeholderForegroundColor: UIColor - - - /// Text Field configuration initalizer - /// - Parameters: - /// - backgroundColor: the textField's background color - /// - borderColor: the textField's border color - /// - textColor: the textField's text color - /// - cornerRadius: the textField's corner radius - /// - borderWidth: the textField's border width - /// - placeholderForegroundColor:the textField's placeholder foreground color - - public init(backgroundColor: UIColor, - borderColor: UIColor, - textColor: UIColor, - cornerRadius: CGFloat, - borderWidth: CGFloat, - placeholderForegroundColor: UIColor) { - self.backgroundColor = backgroundColor - self.borderColor = borderColor - self.textColor = textColor - self.cornerRadius = cornerRadius - self.borderWidth = borderWidth - self.placeholderForegroundColor = placeholderForegroundColor - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/TextFieldWithLabelView.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/TextFieldWithLabelView.swift deleted file mode 100644 index e9ed9a325..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/TextFieldWithLabelView.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// TextFieldWithLabelView.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -import UIKit - -final class TextFieldWithLabelView: UIView { - private lazy var configuration = GiniHealthConfiguration.shared - - var text: String? { - get { - return textField.text - } - set { - textField.text = newValue - textField.accessibilityValue = newValue - } - } - - private lazy var titleLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.font = configuration.textStyleFonts[.caption2] - label.adjustsFontForContentSizeCategory = true - return label - }() - - lazy var textField: UITextField = { - let textField = UITextField() - textField.translatesAutoresizingMaskIntoConstraints = false - textField.adjustsFontForContentSizeCategory = true - return textField - }() - - override init(frame: CGRect) { - super.init(frame: frame) - setupView() - setupConstraints() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setupView() - setupConstraints() - } - - private func setupView() { - addSubview(titleLabel) - addSubview(textField) - } - - func configure(configuration: TextFieldConfiguration) { - self.layer.cornerRadius = configuration.cornerRadius - self.layer.borderWidth = configuration.borderWidth - self.layer.borderColor = configuration.borderColor.cgColor - self.backgroundColor = configuration.backgroundColor - self.textField.textColor = configuration.textColor - self.textField.attributedPlaceholder = NSAttributedString(string: "", - attributes: [.foregroundColor: configuration.placeholderForegroundColor]) - self.titleLabel.textColor = configuration.placeholderForegroundColor - } - - func customConfigure(labelTitle: String) { - titleLabel.text = labelTitle - titleLabel.accessibilityValue = labelTitle - } - - private func setupConstraints() { - NSLayoutConstraint.activate([ - titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: Constants.topBottomPadding), - titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Constants.leftRightPadding), - titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -Constants.leftRightPadding), - - textField.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: Constants.textFieldTopPadding), - textField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Constants.leftRightPadding), - textField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Constants.leftRightPadding), - textField.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Constants.topBottomPadding) - ]) - } -} - -private extension TextFieldWithLabelView { - enum Constants { - static let leftRightPadding: CGFloat = 12 - static let topBottomPadding: CGFloat = 8 - static let textFieldTopPadding: CGFloat = 0 - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Tracking/GiniHealthTrackingDelegate.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Tracking/GiniHealthTrackingDelegate.swift index a0c6cf503..e56165009 100644 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Tracking/GiniHealthTrackingDelegate.swift +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/Tracking/GiniHealthTrackingDelegate.swift @@ -24,8 +24,6 @@ Event types relating to the payment review screen. public enum PaymentReviewScreenEventType: String { /// User tapped "To the banking app" button and ready to be redirected to the banking app case onToTheBankButtonClicked - /// User tapped "close" button and closed the screen - case onCloseButtonClicked /// User tapped "close" button and keyboard will be hidden case onCloseKeyboardButtonClicked } diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/URLOpener.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/URLOpener.swift deleted file mode 100644 index 7ef0b97e0..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/URLOpener.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// URLOpener.swift -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -import UIKit - -#if compiler(>=6.0) -public typealias GiniOpenLinkCompletionBlock = @MainActor @Sendable (Bool) -> Void -#else -public typealias GiniOpenLinkCompletionBlock = (Bool) -> Void -#endif - -// URLOpener helper structure for better testing of the open AppStore links functionality -public struct URLOpener { - private let application: URLOpenerProtocol - - public init(_ application: URLOpenerProtocol) { - self.application = application - } - - /// Opens AppStore with the provided URL - /// - /// - Parameters: - /// - url: link that will be opened - /// - completion: called after opening is completed - /// param is true if website was opened successfully - /// param is false if opening failed - - func openLink(url: URL, completion: GiniOpenLinkCompletionBlock?) { - if application.canOpenURL(url) { - application.open(url, options: [:], completionHandler: completion) - } else { - if #available(iOS 13, *) { - Task { @MainActor in - completion?(false) - } - } else { - DispatchQueue.main.async { - completion?(false) - } - } - } - } - - func canOpenLink(url: URL) -> Bool { - application.canOpenURL(url) - } -} - -public protocol URLOpenerProtocol { - func canOpenURL(_ url: URL) -> Bool - func open(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey : Any], completionHandler completion: GiniOpenLinkCompletionBlock?) -} - -extension UIApplication: URLOpenerProtocol {} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/ZoomedImageView.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/ZoomedImageView.swift deleted file mode 100644 index 63d245e89..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Core/ZoomedImageView.swift +++ /dev/null @@ -1,352 +0,0 @@ -// -// ZoomedImageView.swift -// GiniHealth -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -import UIKit - -@objc public protocol ImageScrollViewDelegate: UIScrollViewDelegate { - func imageScrollViewDidChangeOrientation(imageScrollView: ZoomedImageView) -} - -open class ZoomedImageView: UIScrollView { - - @objc public enum ScaleMode: Int { - case aspectFill - case aspectFit - case widthFill - case heightFill - } - - @objc public enum Offset: Int { - case begining - case center - } - - static let kZoomInFactorFromMinWhenDoubleTap: CGFloat = 2 - - @objc open var imageContentMode: ScaleMode = .aspectFit - @objc open var initialOffset: Offset = .begining - - @objc public private(set) var zoomView: UIImageView? = nil - - @objc open weak var imageScrollViewDelegate: ImageScrollViewDelegate? - - var imageSize: CGSize = CGSize.zero - private var pointToCenterAfterResize: CGPoint = CGPoint.zero - private var scaleToRestoreAfterResize: CGFloat = 1.0 - open var maxScaleFromMinScale: CGFloat = 3.0 - - override open var frame: CGRect { - willSet { - if frame.equalTo(newValue) == false && newValue.equalTo(CGRect.zero) == false && imageSize.equalTo(CGSize.zero) == false { - prepareToResize() - } - } - - didSet { - if frame.equalTo(oldValue) == false && frame.equalTo(CGRect.zero) == false && imageSize.equalTo(CGSize.zero) == false { - recoverFromResizing() - } - } - } - - override public init(frame: CGRect) { - super.init(frame: frame) - - initialize() - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - - initialize() - } - - deinit { - NotificationCenter.default.removeObserver(self) - } - - private func initialize() { - showsVerticalScrollIndicator = false - showsHorizontalScrollIndicator = false - bouncesZoom = true - decelerationRate = UIScrollView.DecelerationRate.fast - delegate = self - - NotificationCenter.default.addObserver(self, selector: #selector(ZoomedImageView.changeOrientationNotification), name: UIDevice.orientationDidChangeNotification, object: nil) - } - - @objc public func adjustFrameToCenter() { - - guard let unwrappedZoomView = zoomView else { - return - } - - var frameToCenter = unwrappedZoomView.frame - - // center horizontally - if frameToCenter.size.width < bounds.width { - frameToCenter.origin.x = (bounds.width - frameToCenter.size.width) / 2 - } - else { - frameToCenter.origin.x = 0 - } - - // center vertically - if frameToCenter.size.height < bounds.height { - frameToCenter.origin.y = (bounds.height - frameToCenter.size.height) / 2 - } - else { - frameToCenter.origin.y = 0 - } - - unwrappedZoomView.frame = frameToCenter - } - - private func prepareToResize() { - let boundsCenter = CGPoint(x: bounds.midX, y: bounds.midY) - pointToCenterAfterResize = convert(boundsCenter, to: zoomView) - - scaleToRestoreAfterResize = zoomScale - - // If we're at the minimum zoom scale, preserve that by returning 0, which will be converted to the minimum - // allowable scale when the scale is restored. - if scaleToRestoreAfterResize <= minimumZoomScale + CGFloat(Float.ulpOfOne) { - scaleToRestoreAfterResize = 0 - } - } - - private func recoverFromResizing() { - setMaxMinZoomScalesForCurrentBounds() - - // restore zoom scale, first making sure it is within the allowable range. - let maxZoomScale = max(minimumZoomScale, scaleToRestoreAfterResize) - zoomScale = min(maximumZoomScale, maxZoomScale) - - // restore center point, first making sure it is within the allowable range. - - // convert our desired center point back to our own coordinate space - let boundsCenter = convert(pointToCenterAfterResize, to: zoomView) - - // calculate the content offset that would yield that center point - var offset = CGPoint(x: boundsCenter.x - bounds.size.width/2.0, y: boundsCenter.y - bounds.size.height/2.0) - - // restore offset, adjusted to be within the allowable range - let maxOffset = maximumContentOffset() - let minOffset = minimumContentOffset() - - var realMaxOffset = min(maxOffset.x, offset.x) - offset.x = max(minOffset.x, realMaxOffset) - - realMaxOffset = min(maxOffset.y, offset.y) - offset.y = max(minOffset.y, realMaxOffset) - - contentOffset = offset - } - - private func maximumContentOffset() -> CGPoint { - return CGPoint(x: contentSize.width - bounds.width,y:contentSize.height - bounds.height) - } - - private func minimumContentOffset() -> CGPoint { - return CGPoint.zero - } - - // MARK: - Set up - - open func setup() { - var topSupperView = superview - - while topSupperView?.superview != nil { - topSupperView = topSupperView?.superview - } - - // Make sure views have already layout with precise frame - topSupperView?.layoutIfNeeded() - - DispatchQueue.main.async { - self.refresh() - } - } - - // MARK: - Display image - - @objc open func display(image: UIImage) { - - if let zoomView = zoomView { - zoomView.removeFromSuperview() - } - - zoomView = UIImageView(image: image) - zoomView!.isUserInteractionEnabled = true - addSubview(zoomView!) - - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(ZoomedImageView.doubleTapGestureRecognizer(_:))) - tapGesture.numberOfTapsRequired = 2 - zoomView!.addGestureRecognizer(tapGesture) - - configureImageForSize(image.size) - } - - private func configureImageForSize(_ size: CGSize) { - imageSize = size - contentSize = imageSize - setMaxMinZoomScalesForCurrentBounds() - zoomScale = minimumZoomScale - - switch initialOffset { - case .begining: - contentOffset = CGPoint.zero - case .center: - let xOffset = contentSize.width < bounds.width ? 0 : (contentSize.width - bounds.width)/2 - let yOffset = contentSize.height < bounds.height ? 0 : (contentSize.height - bounds.height)/2 - - switch imageContentMode { - case .aspectFit: - contentOffset = CGPoint.zero - case .aspectFill: - contentOffset = CGPoint(x: xOffset, y: yOffset) - case .heightFill: - contentOffset = CGPoint(x: xOffset, y: 0) - case .widthFill: - contentOffset = CGPoint(x: 0, y: yOffset) - } - } - } - - private func setMaxMinZoomScalesForCurrentBounds() { - // calculate min/max zoomscale - let xScale = bounds.width / imageSize.width // the scale needed to perfectly fit the image width-wise - let yScale = bounds.height / imageSize.height // the scale needed to perfectly fit the image height-wise - - var minScale: CGFloat = 1 - - switch imageContentMode { - case .aspectFill: - minScale = max(xScale, yScale) - case .aspectFit: - minScale = min(xScale, yScale) - case .widthFill: - minScale = xScale - case .heightFill: - minScale = yScale - } - - - let maxScale = maxScaleFromMinScale*minScale - - // don't let minScale exceed maxScale. (If the image is smaller than the screen, we don't want to force it to be zoomed.) - if minScale > maxScale { - minScale = maxScale - } - - maximumZoomScale = maxScale - minimumZoomScale = minScale * 0.999 // the multiply factor to prevent user cannot scroll page while they use this control in UIPageViewController - } - - // MARK: - Gesture - - @objc func doubleTapGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) { - // zoom out if it bigger than middle scale point. Else, zoom in - if zoomScale >= maximumZoomScale / 2.0 { - setZoomScale(minimumZoomScale, animated: true) - } - else { - let center = gestureRecognizer.location(in: gestureRecognizer.view) - let zoomRect = zoomRectForScale(ZoomedImageView.kZoomInFactorFromMinWhenDoubleTap * minimumZoomScale, center: center) - zoom(to: zoomRect, animated: true) - } - } - - private func zoomRectForScale(_ scale: CGFloat, center: CGPoint) -> CGRect { - var zoomRect = CGRect.zero - - // the zoom rect is in the content view's coordinates. - // at a zoom scale of 1.0, it would be the size of the imageScrollView's bounds. - // as the zoom scale decreases, so more content is visible, the size of the rect grows. - zoomRect.size.height = frame.size.height / scale - zoomRect.size.width = frame.size.width / scale - - // choose an origin so as to get the right center. - zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0) - zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0) - - return zoomRect - } - - open func refresh() { - if let image = zoomView?.image { - display(image: image) - } - } - - // MARK: - Actions - - @objc func changeOrientationNotification() { - // A weird bug that frames are not update right after orientation changed. Need delay a little bit with async. - DispatchQueue.main.async { - self.configureImageForSize(self.imageSize) - self.imageScrollViewDelegate?.imageScrollViewDidChangeOrientation(imageScrollView: self) - } - } -} - -extension ZoomedImageView: UIScrollViewDelegate { - - public func scrollViewDidScroll(_ scrollView: UIScrollView) { - imageScrollViewDelegate?.scrollViewDidScroll?(scrollView) - } - - public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - imageScrollViewDelegate?.scrollViewWillBeginDragging?(scrollView) - } - - public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { - imageScrollViewDelegate?.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset) - } - - public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - imageScrollViewDelegate?.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate) - } - - public func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) { - imageScrollViewDelegate?.scrollViewWillBeginDecelerating?(scrollView) - } - - public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { - imageScrollViewDelegate?.scrollViewDidEndDecelerating?(scrollView) - } - - public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { - imageScrollViewDelegate?.scrollViewDidEndScrollingAnimation?(scrollView) - } - - public func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { - imageScrollViewDelegate?.scrollViewWillBeginZooming?(scrollView, with: view) - } - - public func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { - imageScrollViewDelegate?.scrollViewDidEndZooming?(scrollView, with: view, atScale: scale) - } - - public func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { - return false - } - - public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) { - imageScrollViewDelegate?.scrollViewDidChangeAdjustedContentInset?(scrollView) - } - - public func viewForZooming(in scrollView: UIScrollView) -> UIView? { - return zoomView - } - - public func scrollViewDidZoom(_ scrollView: UIScrollView) { - adjustFrameToCenter() - imageScrollViewDelegate?.scrollViewDidZoom?(scrollView) - } - -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/GiniHealthSDKVersion.swift b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/GiniHealthSDKVersion.swift index 42471ccc5..9057455fc 100644 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/GiniHealthSDKVersion.swift +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/GiniHealthSDKVersion.swift @@ -5,4 +5,4 @@ // Copyright © 2024 Gini GmbH. All rights reserved. // -public let GiniHealthSDKVersion = "4.3.0" +public let GiniHealthSDKVersion = "5.0.0" diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/BankSelection.storyboard b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/BankSelection.storyboard deleted file mode 100644 index 14c27c298..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/BankSelection.storyboard +++ /dev/null @@ -1,177 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/BankSelectionTableViewCell.xib b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/BankSelectionTableViewCell.xib deleted file mode 100644 index e37a6dc96..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/BankSelectionTableViewCell.xib +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/bank.imageset/bankIcon.svg b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/bank.imageset/bankIcon.svg deleted file mode 100644 index 1eaab8ad3..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/bank.imageset/bankIcon.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/editIcon.imageset/Contents.json b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/editIcon.imageset/Contents.json deleted file mode 100644 index a53ca28f2..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/editIcon.imageset/Contents.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "images" : [ - { - "filename" : "Vector 430-2.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "Vector 430-1.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "Vector 430.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/editIcon.imageset/Vector 430-1.png b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/editIcon.imageset/Vector 430-1.png deleted file mode 100644 index 72de90deb..000000000 Binary files a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/editIcon.imageset/Vector 430-1.png and /dev/null differ diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/editIcon.imageset/Vector 430-2.png b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/editIcon.imageset/Vector 430-2.png deleted file mode 100644 index 72de90deb..000000000 Binary files a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/editIcon.imageset/Vector 430-2.png and /dev/null differ diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/editIcon.imageset/Vector 430.png b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/editIcon.imageset/Vector 430.png deleted file mode 100644 index e7e5073d3..000000000 Binary files a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/editIcon.imageset/Vector 430.png and /dev/null differ diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/appStoreIcon.imageset/Contents.json b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.appStoreIcon.imageset/Contents.json similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/appStoreIcon.imageset/Contents.json rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.appStoreIcon.imageset/Contents.json diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/appStoreIcon.imageset/Download_on_the_App_Store_Badge_DE_RGB_wht_092917 1.pdf b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.appStoreIcon.imageset/Download_on_the_App_Store_Badge_DE_RGB_wht_092917 1.pdf similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/appStoreIcon.imageset/Download_on_the_App_Store_Badge_DE_RGB_wht_092917 1.pdf rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.appStoreIcon.imageset/Download_on_the_App_Store_Badge_DE_RGB_wht_092917 1.pdf diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/appStoreIcon.imageset/Download_on_the_App_Store_Badge_US-UK_RGB_wht_092917 1.pdf b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.appStoreIcon.imageset/Download_on_the_App_Store_Badge_US-UK_RGB_wht_092917 1.pdf similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/appStoreIcon.imageset/Download_on_the_App_Store_Badge_US-UK_RGB_wht_092917 1.pdf rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.appStoreIcon.imageset/Download_on_the_App_Store_Badge_US-UK_RGB_wht_092917 1.pdf diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/appStoreIcon.imageset/Download_on_the_App_Store_Badge_US-UK_RGB_wht_092917.pdf b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.appStoreIcon.imageset/Download_on_the_App_Store_Badge_US-UK_RGB_wht_092917.pdf similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/appStoreIcon.imageset/Download_on_the_App_Store_Badge_US-UK_RGB_wht_092917.pdf rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.appStoreIcon.imageset/Download_on_the_App_Store_Badge_US-UK_RGB_wht_092917.pdf diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/appStoreIcon.imageset/img_button_app_store_english 1.pdf b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.appStoreIcon.imageset/img_button_app_store_english 1.pdf similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/appStoreIcon.imageset/img_button_app_store_english 1.pdf rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.appStoreIcon.imageset/img_button_app_store_english 1.pdf diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/appStoreIcon.imageset/img_button_app_store_english.pdf b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.appStoreIcon.imageset/img_button_app_store_english.pdf similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/appStoreIcon.imageset/img_button_app_store_english.pdf rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.appStoreIcon.imageset/img_button_app_store_english.pdf diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/appStoreIcon.imageset/img_button_app_store_german.pdf b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.appStoreIcon.imageset/img_button_app_store_german.pdf similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/appStoreIcon.imageset/img_button_app_store_german.pdf rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.appStoreIcon.imageset/img_button_app_store_german.pdf diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/ic_close.imageset/Contents.json b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.close.imageset/Contents.json similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/ic_close.imageset/Contents.json rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.close.imageset/Contents.json diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/ic_close.imageset/ic_close.pdf b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.close.imageset/ic_close.pdf similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/ic_close.imageset/ic_close.pdf rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.close.imageset/ic_close.pdf diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/giniLogo.imageset/Contents.json b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.giniLogo.imageset/Contents.json similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/giniLogo.imageset/Contents.json rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.giniLogo.imageset/Contents.json diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/giniLogo.imageset/giniLogo.pdf b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.giniLogo.imageset/giniLogo.pdf similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/giniLogo.imageset/giniLogo.pdf rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.giniLogo.imageset/giniLogo.pdf diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/iconChevronDown.imageset/Contents.json b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.iconChevronDown.imageset/Contents.json similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/iconChevronDown.imageset/Contents.json rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.iconChevronDown.imageset/Contents.json diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/iconChevronDown.imageset/ic_chevron-down.pdf b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.iconChevronDown.imageset/ic_chevron-down.pdf similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/iconChevronDown.imageset/ic_chevron-down.pdf rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.iconChevronDown.imageset/ic_chevron-down.pdf diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/bank.imageset/Contents.json b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.iconInputLock.imageset/Contents.json similarity index 74% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/bank.imageset/Contents.json rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.iconInputLock.imageset/Contents.json index bbcb678ea..9354a9c1b 100644 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/bank.imageset/Contents.json +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.iconInputLock.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "bankIcon.svg", + "filename" : "gm.iconInputLock.pdf", "idiom" : "universal" } ], diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.iconInputLock.imageset/gm.iconInputLock.pdf b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.iconInputLock.imageset/gm.iconInputLock.pdf new file mode 100644 index 000000000..0142d44e6 Binary files /dev/null and b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.iconInputLock.imageset/gm.iconInputLock.pdf differ diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/info.circle.imageset/Contents.json b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.infoCircle.imageset/Contents.json similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/info.circle.imageset/Contents.json rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.infoCircle.imageset/Contents.json diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/info.circle.imageset/Group 188.pdf b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.infoCircle.imageset/Group 188.pdf similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/info.circle.imageset/Group 188.pdf rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.infoCircle.imageset/Group 188.pdf diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/ic_minus.imageset/Contents.json b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.minus.imageset/Contents.json similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/ic_minus.imageset/Contents.json rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.minus.imageset/Contents.json diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/ic_minus.imageset/ic_minus.pdf b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.minus.imageset/ic_minus.pdf similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/ic_minus.imageset/ic_minus.pdf rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.minus.imageset/ic_minus.pdf diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/more_vertical.imageset/Contents.json b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.more.imageset/Contents.json similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/more_vertical.imageset/Contents.json rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.more.imageset/Contents.json diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/more_vertical.imageset/more-vertical (1).pdf b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.more.imageset/more-vertical (1).pdf similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/more_vertical.imageset/more-vertical (1).pdf rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.more.imageset/more-vertical (1).pdf diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/more_vertical.imageset/more-vertical.pdf b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.more.imageset/more-vertical.pdf similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/more_vertical.imageset/more-vertical.pdf rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.more.imageset/more-vertical.pdf diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/paymentReviewCloseButton.imageset/Contents.json b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.paymentReviewClose.imageset/Contents.json similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/paymentReviewCloseButton.imageset/Contents.json rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.paymentReviewClose.imageset/Contents.json diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/paymentReviewCloseButton.imageset/paymentReviewCloseButton.pdf b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.paymentReviewClose.imageset/paymentReviewCloseButton.pdf similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/paymentReviewCloseButton.imageset/paymentReviewCloseButton.pdf rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.paymentReviewClose.imageset/paymentReviewCloseButton.pdf diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/ic_plus.imageset/Contents.json b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.plus.imageset/Contents.json similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/ic_plus.imageset/Contents.json rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.plus.imageset/Contents.json diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/ic_plus.imageset/ic_plus.pdf b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.plus.imageset/ic_plus.pdf similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/ic_plus.imageset/ic_plus.pdf rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.plus.imageset/ic_plus.pdf diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/selectionIndicator.imageset/Contents.json b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.selectionIndicator.imageset/Contents.json similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/selectionIndicator.imageset/Contents.json rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.selectionIndicator.imageset/Contents.json diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/selectionIndicator.imageset/ic_check_selected.pdf b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.selectionIndicator.imageset/ic_check_selected.pdf similarity index 100% rename from HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/selectionIndicator.imageset/ic_check_selected.pdf rename to HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/GiniImages.xcassets/gh.selectionIndicator.imageset/ic_check_selected.pdf diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/PaymentReview.storyboard b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/PaymentReview.storyboard deleted file mode 100644 index e42a1a288..000000000 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/PaymentReview.storyboard +++ /dev/null @@ -1,308 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/de.lproj/Localizable.strings b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/de.lproj/Localizable.strings index ffdc48b89..7db732937 100644 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/de.lproj/Localizable.strings +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/de.lproj/Localizable.strings @@ -1,62 +1,61 @@ -/* - File.strings +/* + Localizable.strings Pods // Copyright © 2024 Gini GmbH. All rights reserved. */ -"ginihealth.reviewscreen.recipient.placeholder" = "Empfänger"; -"ginihealth.reviewscreen.iban.placeholder" = "IBAN"; -"ginihealth.reviewscreen.amount.placeholder" = "Betrag"; -"ginihealth.reviewscreen.usage.placeholder" = "Verwendungszweck"; -"ginihealth.reviewscreen.infobar.message" = "Bitte prüfen Sie die vorausgefüllten Daten."; -"ginihealth.reviewscreen.banking.app.button.label" = "Zur Banking App"; -"ginihealth.errors.default" = "Oh da ist was schief gelaufen. Probiere es noch einmal."; -"ginihealth.errors.failed.payment.request.creation" = "Oh da ist was schief gelaufen. Probiere es noch einmal."; -"ginihealth.errors.failed.recipient.non.empty.check" = "Empfänger ist notwendig."; -"ginihealth.errors.failed.iban.non.empty.check" = "IBAN ist notwendig."; -"ginihealth.errors.failed.iban.validation.check" = "IBAN ist ungültig."; -"ginihealth.errors.failed.amount.non.empty.check" = "Ungültig."; -"ginihealth.errors.failed.purpose.non.empty.check" = "Verwendungszweck ist notwendig."; -"ginihealth.errors.failed.default.textfield.validation.check" = "Das Feld ist ungültig."; +"gini.health.reviewscreen.recipient.placeholder" = "Empfänger"; +"gini.health.reviewscreen.iban.placeholder" = "IBAN"; +"gini.health.reviewscreen.amount.placeholder" = "Betrag"; +"gini.health.reviewscreen.usage.placeholder" = "Verwendungszweck"; +"gini.health.reviewscreen.infobar.message" = "Bitte prüfen Sie die vorausgefüllten Daten."; +"gini.health.reviewscreen.banking.app.button.label" = "Zur Banking App"; +"gini.health.errors.default" = "Oh da ist was schief gelaufen. Probiere es noch einmal."; +"gini.health.errors.failed.payment.request.creation" = "Oh da ist was schief gelaufen. Probiere es noch einmal."; +"gini.health.errors.failed.recipient.non.empty.check" = "Empfänger ist notwendig."; +"gini.health.errors.failed.iban.non.empty.check" = "IBAN ist notwendig."; +"gini.health.errors.failed.iban.validation.check" = "IBAN ist ungültig."; +"gini.health.errors.failed.amount.non.empty.check" = "Ungültig."; +"gini.health.errors.failed.purpose.non.empty.check" = "Verwendungszweck ist notwendig."; +"gini.health.errors.failed.default.textfield.validation.check" = "Das Feld ist ungültig."; -"ginihealth.alert.ok.title" = "OK"; +"gini.health.alert.ok.title" = "OK"; -"ginihealth.paymentcomponent.moreInformation.label" = "Bezahldaten an Banking-App übergeben und dort direkt bezahlen. Mehr Informationen."; -"ginihealth.paymentcomponent.moreInformation.underlined.part" = "Mehr Informationen."; -"ginihealth.paymentcomponent.selectYourBank.label" = "Bank auswählen und bezahlen"; -"ginihealth.paymentcomponent.payInvoice.label" = "Rechnung bezahlen"; -"ginihealth.paymentcomponent.poweredByGini.label" = "Powered by"; -"ginihealth.paymentcomponent.selectBank.label" = "Ihre Bank"; -"ginihealth.paymentcomponent.paymentproviderslist.description" = "Sie können die Rechnung nur bezahlen, wenn Sie ein Konto bei einer der unten aufgeführten Banken haben."; -"ginihealth.paymentcomponent.paymentinfo.title.label" = "Mehr Informationen"; -"ginihealth.paymentcomponent.paymentinfo.payBills.title.label" = "Rechnungen ganz einfach mit der Banking-App bezahlen."; -"ginihealth.paymentcomponent.paymentinfo.payBills.description.label" = "Arztrechnungen und andere eingereichte Belege können jetzt ganz einfach bezahlt werden.\nDie Bezahldaten wie IBAN, Betrag, Empfänger und Verwendungszweck werden nahtlos in die Banking-App übergeben und dort nur noch bestätigt.\nSie können die Rechnung auch parken und innerhalb von 3 Monaten nach Upload begleichen.\nDie für die Zahlung notwendigen Daten werden verschlüsselt und sicher an Ihre Banking-App übertragen. Es gelten die Datenschutzbestimmungen Ihrer Bank.\nUnterstützt von den größten Bankinstituten. Integration durch Gini[LINK]."; -"ginihealth.paymentcomponent.paymentinfo.payBills.description.clickable.text" = "Gini[LINK]"; -"ginihealth.paymentcomponent.paymentinfo.questions.title.label" = "Häufig gestellte Fragen"; -"ginihealth.paymentcomponent.paymentinfo.questions.question.1" = "Kann ich Rechnungen einreichen und später bezahlen?"; -"ginihealth.paymentcomponent.paymentinfo.questions.question.2" = "Ist der Service kostenlos?"; -"ginihealth.paymentcomponent.paymentinfo.questions.question.3" = "Sind meine Daten sicher?"; -"ginihealth.paymentcomponent.paymentinfo.questions.question.4" = "Wer oder Was ist Gini?"; -"ginihealth.paymentcomponent.paymentinfo.questions.question.5" = "Welches Format muss die eingereichte Rechnung haben?"; -"ginihealth.paymentcomponent.paymentinfo.questions.question.6" = "Wie erkenne ich, welche Banken unterstützt werden?"; -"ginihealth.paymentcomponent.paymentinfo.questions.answer.1" = "Ja, das ist möglich. So können insbesondere größere Beträge erst nach der erfolgten Erstattung bezahlt werden."; -"ginihealth.paymentcomponent.paymentinfo.questions.answer.2" = "Ja, die Übertragung der Bezahldaten in die gewählte Banking-App ist kostenlos. Für die eigentliche Überweisung können je nach Kontenmodell Gebühren anfallen – wenden Sie sich bitte für weitere Details an Ihre Bank."; -"ginihealth.paymentcomponent.paymentinfo.questions.answer.3" = "Ja! Die Übertragung der Daten an die Banking-App erfolgt verschlüsselt über einen Server von Gini. Gini nimmt die Zahlungsdaten entgegen und leitet sie in die Banking-App weiter. Dort besteht stets die Möglichkeit, die Zahlungsdaten zu überprüfen, bevor die Überweisung ausgeführt wird. Gini hat hierfür sowohl mit der Versicherung als auch mit den Banken Verträge geschlossen, und wir lassen uns regelmäßig von beiden auditieren."; -"ginihealth.paymentcomponent.paymentinfo.questions.answer.4" = "Gini macht das Bezahlen automagisch einfach. Das Münchner Unternehmen, das hinter der Fotoüberweisung steht, arbeitet mit den führenden deutschen Banken und Versicherungen zusammen, um die direkte Zahlung mit der Hausbank zu ermöglichen.\nGini ist für maximale Datensicherheit ISO 27001 zertifiziert und betreibt eigene Rechner in einem ISO 27001 zertifizierten Rechenzentrum in Deutschland. Weitere Informationen finden Sie in der Datenschutzerklärung[LINK] sowie auf der Website von Gini[LINK]."; -"ginihealth.paymentcomponent.paymentinfo.questions.answer.5" = "Ob Foto einer Rechnung, Screenshot, oder digitales PDF – jedes Format ist geeignet. Bitte achten Sie lediglich darauf, dass alle Bezahlinformationen, wie IBAN, Empfänger, Verwendungszweck und Betrag sichtbar und nicht abgeschnitten sind."; -"ginihealth.paymentcomponent.paymentinfo.questions.answer.6" = "Im Bank-Auswahlmenü werden die Banken angezeigt, die die Gini-Bezahlfunktion unterstützen. Voraussetzung für die Nutzung ist, dass Sie die mobile Banking-App ihrer Bank auf demselben Smartphone oder Tablet installiert haben, auf dem Sie die Versicherungs-App nutzen. Sollten Sie keine der Banking-Apps installiert haben, können Sie diese aus dem Apple App- oder Google Playstore herunterladen. Eine anschließende Aktivierung der Banking-App kann nötig sein."; -"ginihealth.paymentcomponent.paymentinfo.questions.answer.clickable.text" = "Datenschutzerklärung[LINK]"; -"ginihealth.paymentcomponent.paymentinfo.gini.link" = "https://gini.net/"; -"ginihealth.paymentcomponent.paymentinfo.gini.privacypolicy.link" = ""; -"ginihealth.paymentcomponent.installAppBottomSheet.title" = "Rechnungsdaten bereit zum Teilen mit der [BANK]"; -"ginihealth.paymentcomponent.installAppBottomSheet.notes.description" = "Hinweis: Bitte aktualisieren oder installieren Sie zunächst die [BANK]-App aus dem App-Store."; -"ginihealth.paymentcomponent.installAppBottomSheet.tip.description" = "Tipp: Tippen Sie auf 'Weiter', um die Zahlung in der [BANK]-App abzuschließen."; -"ginihealth.paymentcomponent.installAppBottomSheet.continue.button.text" = "Weiter"; -"ginihealth.paymentcomponent.shareInvoiceBottomSheet.title" = "Rechnungsdaten bereit zum Teilen mit der [BANK]"; -"ginihealth.paymentcomponent.shareInvoiceBottomSheet.description" = "Im nächsten Schritt wählen Sie die [BANK]-App auf der Seite aus, um Ihre Zahlung in der [BANK]-App abzuschließen."; -"ginihealth.paymentcomponent.shareInvoiceBottomSheet.app" = "App"; -"ginihealth.paymentcomponent.shareInvoiceBottomSheet.more" = "Mehr"; -"ginihealth.paymentcomponent.shareInvoiceBottomSheet.tip.description" = "Tipp: Wenn Sie die App nicht sehen, scrollen Sie oder tippen Sie auf \"Mehr\", bis Sie die [BANK]-App finden. Wenn Sie die [BANK]-App noch nicht installiert haben, laden Sie sie im Store herunter."; -"ginihealth.paymentcomponent.shareInvoiceBottomSheet.tip.underlined.part" = "laden Sie sie im Store herunter"; -"ginihealth.paymentcomponent.shareInvoiceBottomSheet.continue.button.text" = "Weiter"; +"gini.health.paymentcomponent.more.information.label" = "Bezahldaten an Banking-App übergeben und dort direkt bezahlen. Mehr Informationen."; +"gini.health.paymentcomponent.more.information.underlined.part" = "Mehr Informationen."; +"gini.health.paymentcomponent.select.bank.label" = "Bank auswählen"; +"gini.health.paymentcomponent.select.your.bank.label" = "Bank auswählen und bezahlen"; +"gini.health.paymentcomponent.pay.invoice.label" = "Rechnung bezahlen"; +"gini.health.paymentcomponent.to.banking.app.label" = "Zur Banking App"; +"gini.health.paymentcomponent.continue.to.overview.label" = "Weiter zur Übersicht"; +"gini.health.paymentcomponent.powered.by.gini.label" = "Powered by"; +"gini.health.paymentcomponent.your.bank.label" = "Ihre Bank"; +"gini.health.paymentcomponent.payment.providers.list.description" = "Sie können die Rechnung nur bezahlen, wenn Sie ein Konto bei einer der unten aufgeführten Banken haben."; +"gini.health.paymentcomponent.payment.info.title.label" = "Mehr Informationen"; +"gini.health.paymentcomponent.payment.info.pay.bills.title.label" = "Rechnungen ganz einfach mit der Banking-App bezahlen."; +"gini.health.paymentcomponent.payment.info.pay.bills.description.label" = "Arztrechnungen und andere eingereichte Belege können jetzt ganz einfach bezahlt werden.\nDie Bezahldaten wie IBAN, Betrag, Empfänger und Verwendungszweck werden nahtlos in die Banking-App übergeben und dort nur noch bestätigt.\nSie können die Rechnung auch parken und innerhalb von 3 Monaten nach Upload begleichen.\nDie für die Zahlung notwendigen Daten werden verschlüsselt und sicher an Ihre Banking-App übertragen. Es gelten die Datenschutzbestimmungen Ihrer Bank.\nUnterstützt von den größten Bankinstituten. Integration durch Gini[LINK]."; +"gini.health.paymentcomponent.payment.info.pay.bills.description.clickable.text" = "Gini[LINK]"; +"gini.health.paymentcomponent.payment.info.questions.title.label" = "Häufig gestellte Fragen"; +"gini.health.paymentcomponent.payment.info.questions.question.1" = "Kann ich Rechnungen einreichen und später bezahlen?"; +"gini.health.paymentcomponent.payment.info.questions.question.2" = "Ist der Service kostenlos?"; +"gini.health.paymentcomponent.payment.info.questions.question.3" = "Sind meine Daten sicher?"; +"gini.health.paymentcomponent.payment.info.questions.question.4" = "Wer oder Was ist Gini?"; +"gini.health.paymentcomponent.payment.info.questions.question.5" = "Welches Format muss die eingereichte Rechnung haben?"; +"gini.health.paymentcomponent.payment.info.questions.question.6" = "Wie erkenne ich, welche Banken unterstützt werden?"; +"gini.health.paymentcomponent.payment.info.questions.answer.1" = "Ja, das ist möglich. So können insbesondere größere Beträge erst nach der erfolgten Erstattung bezahlt werden."; +"gini.health.paymentcomponent.payment.info.questions.answer.2" = "Ja, die Übertragung der Bezahldaten in die gewählte Banking-App ist kostenlos. Für die eigentliche Überweisung können je nach Kontenmodell Gebühren anfallen – wenden Sie sich bitte für weitere Details an Ihre Bank."; +"gini.health.paymentcomponent.payment.info.questions.answer.3" = "Ja! Die Übertragung der Daten an die Banking-App erfolgt verschlüsselt über einen Server von Gini. Gini nimmt die Zahlungsdaten entgegen und leitet sie in die Banking-App weiter. Dort besteht stets die Möglichkeit, die Zahlungsdaten zu überprüfen, bevor die Überweisung ausgeführt wird. Gini hat hierfür sowohl mit der Versicherung als auch mit den Banken Verträge geschlossen, und wir lassen uns regelmäßig von beiden auditieren."; +"gini.health.paymentcomponent.payment.info.questions.answer.4" = "Gini macht das Bezahlen automagisch einfach. Das Münchner Unternehmen, das hinter der Fotoüberweisung steht, arbeitet mit den führenden deutschen Banken und Versicherungen zusammen, um die direkte Zahlung mit der Hausbank zu ermöglichen.\nGini ist für maximale Datensicherheit ISO 27001 zertifiziert und betreibt eigene Rechner in einem ISO 27001 zertifizierten Rechenzentrum in Deutschland. Weitere Informationen finden Sie in der Datenschutzerklärung[LINK] sowie auf der Website von Gini[LINK]."; +"gini.health.paymentcomponent.payment.info.questions.answer.5" = "Ob Foto einer Rechnung, Screenshot, oder digitales PDF – jedes Format ist geeignet. Bitte achten Sie lediglich darauf, dass alle Bezahlinformationen, wie IBAN, Empfänger, Verwendungszweck und Betrag sichtbar und nicht abgeschnitten sind."; +"gini.health.paymentcomponent.payment.info.questions.answer.6" = "Im Bank-Auswahlmenü werden die Banken angezeigt, die die Gini-Bezahlfunktion unterstützen. Voraussetzung für die Nutzung ist, dass Sie die mobile Banking-App ihrer Bank auf demselben Smartphone oder Tablet installiert haben, auf dem Sie die Versicherungs-App nutzen. Sollten Sie keine der Banking-Apps installiert haben, können Sie diese aus dem Apple App- oder Google Playstore herunterladen. Eine anschließende Aktivierung der Banking-App kann nötig sein."; +"gini.health.paymentcomponent.payment.info.questions.answer.clickable.text" = "Datenschutzerklärung[LINK]"; +"gini.health.paymentcomponent.payment.info.gini.link" = "https://gini.net/"; +"gini.health.paymentcomponent.payment.info.gini.privacypolicy.link" = ""; +"gini.health.paymentcomponent.install.app.bottom.sheet.title" = "Rechnungsdaten bereit zum Teilen mit der [BANK]"; +"gini.health.paymentcomponent.install.app.bottom.sheet.notes.description" = "Hinweis: Bitte aktualisieren oder installieren Sie zunächst die [BANK]-App aus dem App-Store."; +"gini.health.paymentcomponent.install.app.bottom.sheet.tip.description" = "Tipp: Tippen Sie auf 'Weiter', um die Zahlung in der [BANK]-App abzuschließen."; +"gini.health.paymentcomponent.install.app.bottom.sheet.continue.button.text" = "Weiter"; +"gini.health.paymentcomponent.share.invoice.bottom.sheet.title" = "Teilen Sie diesen QR-Zahlungscode mit Ihrer Banking-App"; +"gini.health.paymentcomponent.share.invoice.bottom.sheet.description" = "Alternativ können Sin ein Screenshot von dieser Seite machen mit der Fotoüberweisung Funktion der [BANK] öffnen."; +"gini.health.paymentcomponent.share.invoice.bottom.sheet.continue.button.text" = "Mit der [BANK]-App teilen"; diff --git a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/en.lproj/Localizable.strings b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/en.lproj/Localizable.strings index 7596ed14a..769087a97 100644 --- a/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/en.lproj/Localizable.strings +++ b/HealthSDK/GiniHealthSDK/Sources/GiniHealthSDK/Resources/en.lproj/Localizable.strings @@ -1,62 +1,61 @@ -/* - File.strings +/* + Localizable.strings Pods // Copyright © 2024 Gini GmbH. All rights reserved. */ -"ginihealth.reviewscreen.recipient.placeholder" = "Recipient"; -"ginihealth.reviewscreen.iban.placeholder" = "IBAN"; -"ginihealth.reviewscreen.amount.placeholder" = "Amount"; -"ginihealth.reviewscreen.usage.placeholder" = "Reference number"; -"ginihealth.reviewscreen.banking.app.button.label" = "To the banking app"; -"ginihealth.reviewscreen.infobar.message" = "Please check the pre-filled data."; -"ginihealth.errors.default" = "Oops something went wrong. Please try again."; -"ginihealth.errors.failed.payment.request.creation" = "Oops something went wrong. Please try again."; -"ginihealth.errors.failed.recipient.non.empty.check" = "Recipient is required."; -"ginihealth.errors.failed.iban.non.empty.check" = "IBAN is required."; -"ginihealth.errors.failed.iban.validation.check" = "IBAN is not valid."; -"ginihealth.errors.failed.amount.non.empty.check" = "Invalid."; -"ginihealth.errors.failed.purpose.non.empty.check" = "Purpose is required."; -"ginihealth.errors.failed.default.textfield.validation.check" = "The field is not valid."; +"gini.health.reviewscreen.recipient.placeholder" = "Recipient"; +"gini.health.reviewscreen.iban.placeholder" = "IBAN"; +"gini.health.reviewscreen.amount.placeholder" = "Amount"; +"gini.health.reviewscreen.usage.placeholder" = "Reference number"; +"gini.health.reviewscreen.banking.app.button.label" = "To the banking app"; +"gini.health.reviewscreen.infobar.message" = "Please check the pre-filled data."; +"gini.health.errors.default" = "Oops something went wrong. Please try again."; +"gini.health.errors.failed.payment.request.creation" = "Oops something went wrong. Please try again."; +"gini.health.errors.failed.recipient.non.empty.check" = "Recipient is required."; +"gini.health.errors.failed.iban.non.empty.check" = "IBAN is required."; +"gini.health.errors.failed.iban.validation.check" = "IBAN is not valid."; +"gini.health.errors.failed.amount.non.empty.check" = "Invalid."; +"gini.health.errors.failed.purpose.non.empty.check" = "Purpose is required."; +"gini.health.errors.failed.default.textfield.validation.check" = "The field is not valid."; -"ginihealth.alert.ok.title" = "OK"; +"gini.health.alert.ok.title" = "OK"; -"ginihealth.paymentcomponent.moreInformation.label" = "Transfer payment data to the banking app and pay there directly. More information."; -"ginihealth.paymentcomponent.moreInformation.underlined.part" = "More information."; -"ginihealth.paymentcomponent.selectYourBank.label" = "Select your bank to pay"; -"ginihealth.paymentcomponent.payInvoice.label" = "Pay the invoice"; -"ginihealth.paymentcomponent.poweredByGini.label" = "Powered by"; -"ginihealth.paymentcomponent.selectBank.label" = "Your bank"; -"ginihealth.paymentcomponent.paymentproviderslist.description" = "You can only pay the bill if you have an account with one of the banks listed below."; -"ginihealth.paymentcomponent.paymentinfo.title.label" = "More information"; -"ginihealth.paymentcomponent.paymentinfo.payBills.title.label" = "Pay bills easily with the banking app."; -"ginihealth.paymentcomponent.paymentinfo.payBills.description.label" = "Medical bills and other submitted receipts can now be paid very easily.\nThe payment data such as IBAN, amount, recipient and purpose are seamlessly transferred to the banking app, and the payment only needs to be confirmed there.\nYou can also park the invoice and pay it within 3 months after uploading.\nThe data is encrypted and transferred securely to your banking app. The data protection regulations of your bank apply.\nSupported by the largest banks. Integration by Gini[LINK]."; -"ginihealth.paymentcomponent.paymentinfo.payBills.description.clickable.text" = "Gini[LINK]"; -"ginihealth.paymentcomponent.paymentinfo.questions.title.label" = "Frequently asked questions"; -"ginihealth.paymentcomponent.paymentinfo.questions.question.1" = "Can I submit invoices and pay them later?"; -"ginihealth.paymentcomponent.paymentinfo.questions.question.2" = "Is the service free of charge?"; -"ginihealth.paymentcomponent.paymentinfo.questions.question.3" = "Is my data secure?"; -"ginihealth.paymentcomponent.paymentinfo.questions.question.4" = "Who or what is Gini?"; -"ginihealth.paymentcomponent.paymentinfo.questions.question.5" = "What format must the submitted invoice have?"; -"ginihealth.paymentcomponent.paymentinfo.questions.question.6" = "How do I know which banks are supported?"; -"ginihealth.paymentcomponent.paymentinfo.questions.answer.1" = "Yes, this is possible. Larger amounts in particular can be paid after the reimbursement has been made."; -"ginihealth.paymentcomponent.paymentinfo.questions.answer.2" = "Yes, transferring the payment data to the selected banking app is free of charge. Charges may apply for the actual transfer, depending on the account model - please contact your bank for further details."; -"ginihealth.paymentcomponent.paymentinfo.questions.answer.3" = "Yes, the data is transferred to the banking app in encrypted form via a Gini server. Gini receives the payment data and forwards it to the banking app. There you always have the option of checking the payment data before the transfer is executed. Gini has concluded contracts with the insurance company and the banks for this purpose, and both regularly audit us."; -"ginihealth.paymentcomponent.paymentinfo.questions.answer.4" = "Gini makes simplifies payments. The Munich-based company behind the photo payment works with the leading German banks and insurance companies to enable direct payment with your bank.\nGini is ISO 27001 certified for maximum data security and operates its own server machines in an ISO 27001 certified data center in Germany. Further information can be found in the privacy policy[LINK] and on the Gini[LINK] website."; -"ginihealth.paymentcomponent.paymentinfo.questions.answer.5" = "Whether a photo of an invoice, screenshot or digital PDF - any format is suitable. Please just make sure that all payment information such as IBAN, recipient, purpose and amount are visible and not cut off."; -"ginihealth.paymentcomponent.paymentinfo.questions.answer.6" = "The banks that support the Gini payment function are displayed in the bank selection menu. To use it, you must have installed your bank's mobile banking app on the same smartphone or tablet on which you use the insurance app. If you have not installed any of the banking apps, you can download them from the Apple App Store or Google Playstore. Subsequent activation of the banking app may be necessary."; -"ginihealth.paymentcomponent.paymentinfo.questions.answer.clickable.text" = "privacy policy[LINK]"; -"ginihealth.paymentcomponent.paymentinfo.gini.link" = "https://gini.net/en/"; -"ginihealth.paymentcomponent.paymentinfo.gini.privacypolicy.link" = ""; -"ginihealth.paymentcomponent.installAppBottomSheet.title" = "Invoice data ready to share with the [BANK] bank"; -"ginihealth.paymentcomponent.installAppBottomSheet.notes.description" = "Note: You must first update or install the [BANK] app from the App store"; -"ginihealth.paymentcomponent.installAppBottomSheet.tip.description" = "Tip: Tap 'Forward' to complete the payment in the [BANK] app."; -"ginihealth.paymentcomponent.installAppBottomSheet.continue.button.text" = "Forward"; -"ginihealth.paymentcomponent.shareInvoiceBottomSheet.title" = "Invoice data ready to share"; -"ginihealth.paymentcomponent.shareInvoiceBottomSheet.description" = "In the next step, select the [BANK] app to open and complete your payment in the banking app."; -"ginihealth.paymentcomponent.shareInvoiceBottomSheet.app" = "App"; -"ginihealth.paymentcomponent.shareInvoiceBottomSheet.more" = "More"; -"ginihealth.paymentcomponent.shareInvoiceBottomSheet.tip.description" = "Tip: If you don't see the app, scroll or tap \"More\" until you find the [BANK] app. If you haven't installed the [BANK] app yet, download it from the store."; -"ginihealth.paymentcomponent.shareInvoiceBottomSheet.tip.underlined.part" = "download it from the store"; -"ginihealth.paymentcomponent.shareInvoiceBottomSheet.continue.button.text" = "Forward"; +"gini.health.paymentcomponent.more.information.label" = "Transfer payment data to the banking app and pay there directly. More information."; +"gini.health.paymentcomponent.more.information.underlined.part" = "More information."; +"gini.health.paymentcomponent.select.bank.label" = "Select bank"; +"gini.health.paymentcomponent.select.your.bank.label" = "Select your bank to pay"; +"gini.health.paymentcomponent.pay.invoice.label" = "Pay the invoice"; +"gini.health.paymentcomponent.to.banking.app.label" = "To the banking app"; +"gini.health.paymentcomponent.continue.to.overview.label" = "Continue to overview"; +"gini.health.paymentcomponent.powered.by.gini.label" = "Powered by"; +"gini.health.paymentcomponent.your.bank.label" = "Your bank"; +"gini.health.paymentcomponent.payment.providers.list.description" = "You can only pay the bill if you have an account with one of the banks listed below."; +"gini.health.paymentcomponent.payment.info.title.label" = "More information"; +"gini.health.paymentcomponent.payment.info.pay.bills.title.label" = "Pay bills easily with the banking app."; +"gini.health.paymentcomponent.payment.info.pay.bills.description.label" = "Medical bills and other submitted receipts can now be paid very easily.\nThe payment data such as IBAN, amount, recipient and purpose are seamlessly transferred to the banking app, and the payment only needs to be confirmed there.\nYou can also park the invoice and pay it within 3 months after uploading.\nThe data is encrypted and transferred securely to your banking app. The data protection regulations of your bank apply.\nSupported by the largest banks. Integration by Gini[LINK]."; +"gini.health.paymentcomponent.payment.info.pay.bills.description.clickable.text" = "Gini[LINK]"; +"gini.health.paymentcomponent.paymentinfo.questions.title.label" = "Frequently asked questions"; +"gini.health.paymentcomponent.payment.info.questions.question.1" = "Can I submit invoices and pay them later?"; +"gini.health.paymentcomponent.payment.info.questions.question.2" = "Is the service free of charge?"; +"gini.health.paymentcomponent.payment.info.questions.question.3" = "Is my data secure?"; +"gini.health.paymentcomponent.payment.info.questions.question.4" = "Who or what is Gini?"; +"gini.health.paymentcomponent.payment.info.questions.question.5" = "What format must the submitted invoice have?"; +"gini.health.paymentcomponent.payment.info.questions.question.6" = "How do I know which banks are supported?"; +"gini.health.paymentcomponent.payment.info.questions.answer.1" = "Yes, this is possible. Larger amounts in particular can be paid after the reimbursement has been made."; +"gini.health.paymentcomponent.payment.info.questions.answer.2" = "Yes, transferring the payment data to the selected banking app is free of charge. Charges may apply for the actual transfer, depending on the account model - please contact your bank for further details."; +"gini.health.paymentcomponent.payment.info.questions.answer.3" = "Yes, the data is transferred to the banking app in encrypted form via a Gini server. Gini receives the payment data and forwards it to the banking app. There you always have the option of checking the payment data before the transfer is executed. Gini has concluded contracts with the insurance company and the banks for this purpose, and both regularly audit us."; +"gini.health.paymentcomponent.payment.info.questions.answer.4" = "Gini makes simplifies payments. The Munich-based company behind the photo payment works with the leading German banks and insurance companies to enable direct payment with your bank.\nGini is ISO 27001 certified for maximum data security and operates its own server machines in an ISO 27001 certified data center in Germany. Further information can be found in the privacy policy[LINK] and on the Gini[LINK] website."; +"gini.health.paymentcomponent.payment.info.questions.answer.5" = "Whether a photo of an invoice, screenshot or digital PDF - any format is suitable. Please just make sure that all payment information such as IBAN, recipient, purpose and amount are visible and not cut off."; +"gini.health.paymentcomponent.payment.info.questions.answer.6" = "The banks that support the Gini payment function are displayed in the bank selection menu. To use it, you must have installed your bank's mobile banking app on the same smartphone or tablet on which you use the insurance app. If you have not installed any of the banking apps, you can download them from the Apple App Store or Google Playstore. Subsequent activation of the banking app may be necessary."; +"gini.health.paymentcomponent.payment.info.questions.answer.clickable.text" = "privacy policy[LINK]"; +"gini.health.paymentcomponent.payment.info.gini.link" = "https://gini.net/en/"; +"gini.health.paymentcomponent.payment.info.gini.privacypolicy.link" = ""; +"gini.health.paymentcomponent.install.app.bottom.sheet.title" = "Invoice data ready to share with the [BANK] bank"; +"gini.health.paymentcomponent.install.app.bottom.sheet.notes.description" = "Note: You must first update or install the [BANK] app from the App store"; +"gini.health.paymentcomponent.install.app.bottom.sheet.tip.description" = "Tip: Tap 'Forward' to complete the payment in the [BANK] app."; +"gini.health.paymentcomponent.install.app.bottom.sheet.continue.button.text" = "Forward"; +"gini.health.paymentcomponent.share.invoice.bottom.sheet.title" = "Share this QR payment code with your banking app"; +"gini.health.paymentcomponent.share.invoice.bottom.sheet.description" = "Or take a screenshot and upload it directly in your [BANK] app using the Photo Payment functionality"; +"gini.health.paymentcomponent.share.invoice.bottom.sheet.continue.button.text" = "Share with [BANK] app"; diff --git a/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/GiniHealthTests.swift b/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/GiniHealthTests.swift index e4ba75913..6b7512056 100644 --- a/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/GiniHealthTests.swift +++ b/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/GiniHealthTests.swift @@ -1,6 +1,8 @@ import XCTest @testable import GiniHealthSDK @testable import GiniHealthAPILibrary +@testable import GiniInternalPaymentSDK +@testable import GiniUtilites final class GiniHealthTests: XCTestCase { @@ -13,7 +15,7 @@ final class GiniHealthTests: XCTestCase { let documentService = DefaultDocumentService(sessionManager: sessionManagerMock, apiVersion: versionAPI) let paymentService = PaymentService(sessionManager: sessionManagerMock, apiVersion: versionAPI) giniHealthAPI = GiniHealthAPI(documentService: documentService, paymentService: paymentService) - giniHealth = GiniHealth(with: giniHealthAPI) + giniHealth = GiniHealth(giniApiLib: giniHealthAPI) } override func tearDown() { @@ -34,11 +36,11 @@ final class GiniHealthTests: XCTestCase { func testFetchBankingApps_Success() { // Given - let expectedProviders: [PaymentProvider]? = loadProviders(fileName: "providers") - + let expectedProviders: [GiniHealthSDK.PaymentProvider]? = loadProviders(fileName: "providers") + // When let expectation = self.expectation(description: "Fetching banking apps") - var receivedProviders: [PaymentProvider]? + var receivedProviders: [GiniHealthSDK.PaymentProvider]? giniHealth.fetchBankingApps { result in switch result { case .success(let providers): @@ -59,13 +61,13 @@ final class GiniHealthTests: XCTestCase { func testDocumentIsPayable() { // When let fileName = "extractionResultWithIBAN" - let extractions: ExtractionsContainer? = GiniHealthSDKTests.load(fromFile: fileName) + let extractions: GiniHealthSDK.ExtractionsContainer? = GiniHealthSDKTests.load(fromFile: fileName) guard let extractions else { XCTFail("Error loading file: `\(fileName).json`") return } let extractionsResult = ExtractionResult(extractionsContainer: extractions) - let isPayable = extractionsResult.extractions.first(where: { $0.name == ExtractionType.paymentState.rawValue })?.value == PaymentState.payable.rawValue + let isPayable = extractionsResult.extractions.first(where: { $0.name == ExtractionType.paymentState.rawValue })?.value == GiniHealthSDK.PaymentState.payable.rawValue // Then XCTAssertEqual(isPayable, true) } @@ -73,13 +75,13 @@ final class GiniHealthTests: XCTestCase { func testDocumentIsNotPayable_Success() { // When let fileName = "extractionResultWithoutIBAN" - let extractions: ExtractionsContainer? = GiniHealthSDKTests.load(fromFile: fileName) + let extractions: GiniHealthSDK.ExtractionsContainer? = GiniHealthSDKTests.load(fromFile: fileName) guard let extractions else { XCTFail("Error loading file: `\(fileName).json`") return } let extractionsResult = ExtractionResult(extractionsContainer: extractions) - let isPayable = extractionsResult.extractions.first(where: { $0.name == ExtractionType.paymentState.rawValue })?.value == PaymentState.payable.rawValue + let isPayable = extractionsResult.extractions.first(where: { $0.name == ExtractionType.paymentState.rawValue })?.value == GiniHealthSDK.PaymentState.payable.rawValue // Then XCTAssertEqual(isPayable, false) } @@ -87,12 +89,12 @@ final class GiniHealthTests: XCTestCase { func testCheckIfDocumentIsPayable_Success() { // Given let fileName = "extractionResultWithIBAN" - let expectedExtractions: ExtractionsContainer? = GiniHealthSDKTests.load(fromFile: fileName) + let expectedExtractions: GiniHealthSDK.ExtractionsContainer? = GiniHealthSDKTests.load(fromFile: fileName) guard let expectedExtractions else { XCTFail("Error loading file: `\(fileName).json`") return } - let expectedExtractionsResult = ExtractionResult(extractionsContainer: expectedExtractions) + let expectedExtractionsResult = GiniHealthSDK.ExtractionResult(extractionsContainer: expectedExtractions) let expectedIsPayable = expectedExtractionsResult.extractions.first(where: { $0.name == "iban" })?.value.isNotEmpty // When @@ -116,7 +118,7 @@ final class GiniHealthTests: XCTestCase { func testCheckIfDocumentIsNotPayable_Success() { // Given let fileName = "extractionResultWithIBAN" - let expectedExtractions: ExtractionsContainer? = GiniHealthSDKTests.load(fromFile: fileName) + let expectedExtractions: GiniHealthSDK.ExtractionsContainer? = GiniHealthSDKTests.load(fromFile: fileName) guard let expectedExtractions else { XCTFail("Error loading file: `\(fileName).json`") return @@ -220,11 +222,12 @@ final class GiniHealthTests: XCTestCase { func testPollDocument_Success() { // Given - let expectedDocument: Document? = GiniHealthSDKTests.load(fromFile: "document1") + let healthDocument: GiniHealthAPILibrary.Document = GiniHealthSDKTests.load(fromFile: "document1")! + let expectedDocument: GiniHealthSDK.Document? = GiniHealthSDK.Document(healthDocument: healthDocument) // When let expectation = self.expectation(description: "Polling document") - var receivedDocument: Document? + var receivedDocument: GiniHealthSDK.Document? giniHealth.pollDocument(docId: MockSessionManager.payableDocumentID) { result in switch result { case .success(let document): @@ -244,7 +247,7 @@ final class GiniHealthTests: XCTestCase { func testPollDocument_Failure() { // When let expectation = self.expectation(description: "Polling failure document") - var receivedDocument: Document? + var receivedDocument: GiniHealthSDK.Document? giniHealth.pollDocument(docId: MockSessionManager.missingDocumentID) { result in switch result { case .success(let document): @@ -263,16 +266,16 @@ final class GiniHealthTests: XCTestCase { func testGetExtractions_Success() { // Given let fileName = "extractionsWithPayment" - let expectedExtractionContainer: ExtractionsContainer? = GiniHealthSDKTests.load(fromFile: fileName) + let expectedExtractionContainer: GiniHealthSDK.ExtractionsContainer? = GiniHealthSDKTests.load(fromFile: fileName) guard let expectedExtractionContainer else { XCTFail("Error loading file: `\(fileName).json`") return } - let expectedExtractions: [Extraction] = ExtractionResult(extractionsContainer: expectedExtractionContainer).payment?.first ?? [] + let expectedExtractions: [GiniHealthSDK.Extraction] = ExtractionResult(extractionsContainer: expectedExtractionContainer).payment?.first ?? [] // When let expectation = self.expectation(description: "Getting extractions") - var receivedExtractions: [Extraction]? + var receivedExtractions: [GiniHealthSDK.Extraction]? giniHealth.getExtractions(docId: MockSessionManager.extractionsWithPaymentDocumentID) { result in switch result { case .success(let extractions): @@ -292,7 +295,7 @@ final class GiniHealthTests: XCTestCase { func testGetExtractions_Failure() { // When let expectation = self.expectation(description: "Extraction failure") - var receivedExtractions: [Extraction]? + var receivedExtractions: [GiniHealthSDK.Extraction]? giniHealth.getExtractions(docId: MockSessionManager.failurePayableDocumentID) { result in switch result { case .success(let extractions): @@ -315,8 +318,8 @@ final class GiniHealthTests: XCTestCase { // When let expectation = self.expectation(description: "Creating payment request") var receivedRequestId: String? - let paymentInfo = PaymentInfo(recipient: "Uno Flüchtlingshilfe", iban: "DE78370501980020008850", bic: "COLSDE33", amount: "1.00:EUR", purpose: "ReNr 12345", paymentUniversalLink: "ginipay-test://paymentRequester", paymentProviderId: "b09ef70a-490f-11eb-952e-9bc6f4646c57") - giniHealth.createPaymentRequest(paymentInfo: paymentInfo, completion: { result in + let paymentInfo = GiniHealthSDK.PaymentInfo(recipient: "Uno Flüchtlingshilfe", iban: "DE78370501980020008850", bic: "COLSDE33", amount: "1.00:EUR", purpose: "ReNr 12345", paymentUniversalLink: "ginipay-test://paymentRequester", paymentProviderId: "b09ef70a-490f-11eb-952e-9bc6f4646c57") + giniHealth.createPaymentRequest(paymentInfo: PaymentInfo(paymentConponentsInfo: paymentInfo), completion: { result in switch result { case .success(let requestId): receivedRequestId = requestId @@ -361,16 +364,16 @@ final class GiniHealthTests: XCTestCase { func testSetDocumentForReview_Success() { // Given let fileName = "extractionsWithPayment" - let expectedExtractionContainer: ExtractionsContainer? = GiniHealthSDKTests.load(fromFile: fileName) + let expectedExtractionContainer: GiniHealthSDK.ExtractionsContainer? = GiniHealthSDKTests.load(fromFile: fileName) guard let expectedExtractionContainer else { XCTFail("Error loading file: `\(fileName).json`") return } - let expectedExtractions: [Extraction] = ExtractionResult(extractionsContainer: expectedExtractionContainer).payment?.first ?? [] + let expectedExtractions: [GiniHealthSDK.Extraction] = ExtractionResult(extractionsContainer: expectedExtractionContainer).payment?.first ?? [] // When let expectation = self.expectation(description: "Setting document for review") - var receivedExtractions: [Extraction]? + var receivedExtractions: [GiniHealthSDK.Extraction]? giniHealth.setDocumentForReview(documentId: MockSessionManager.extractionsWithPaymentDocumentID) { result in switch result { case .success(let extractions): @@ -390,14 +393,17 @@ final class GiniHealthTests: XCTestCase { func testFetchDataForReview_Success() { // Given let fileName = "extractionsWithPayment" - let expectedExtractionContainer: ExtractionsContainer? = GiniHealthSDKTests.load(fromFile: fileName) + let expectedExtractionContainer: GiniHealthSDK.ExtractionsContainer? = GiniHealthSDKTests.load(fromFile: fileName) guard let expectedExtractionContainer else { XCTFail("Error loading file: `\(fileName).json`") return } - let expectedExtractions: [Extraction] = ExtractionResult(extractionsContainer: expectedExtractionContainer).payment?.first ?? [] + let expectedExtractions: [GiniHealthSDK.Extraction] = ExtractionResult(extractionsContainer: expectedExtractionContainer).payment?.first ?? [] let documentFileName = "document4" - let expectedDocument: Document? = GiniHealthSDKTests.load(fromFile: documentFileName) + + let healthDocument: GiniHealthAPILibrary.Document = GiniHealthSDKTests.load(fromFile: documentFileName)! + let expectedDocument: GiniHealthSDK.Document? = GiniHealthSDK.Document(healthDocument: healthDocument) + guard let expectedDocument else { XCTFail("Error loading file: `\(documentFileName).json`") return @@ -446,16 +452,16 @@ final class GiniHealthTests: XCTestCase { func testGetAllExtractions_Success() { // Given let fileName = "test_doctorsname" - let expectedExtractionContainer: ExtractionsContainer? = GiniHealthSDKTests.load(fromFile: fileName) + let expectedExtractionContainer: GiniHealthSDK.ExtractionsContainer? = GiniHealthSDKTests.load(fromFile: fileName) guard let expectedExtractionContainer else { XCTFail("Error loading file: `\(fileName).json`") return } - let expectedExtractions: [Extraction] = ExtractionResult(extractionsContainer: expectedExtractionContainer).extractions + let expectedExtractions: [GiniHealthSDK.Extraction] = ExtractionResult(extractionsContainer: expectedExtractionContainer).extractions // When let expectation = self.expectation(description: "Getting all extractions") - var receivedExtractions: [Extraction]? + var receivedExtractions: [GiniHealthSDK.Extraction]? giniHealth.getAllExtractions(docId: MockSessionManager.doctorsNameDocumentID) { result in switch result { case .success(let extractions): @@ -475,7 +481,7 @@ final class GiniHealthTests: XCTestCase { func testGetAlllExtractions_Failure() { // When let expectation = self.expectation(description: "Extraction failure") - var receivedExtractions: [Extraction]? + var receivedExtractions: [GiniHealthSDK.Extraction]? giniHealth.getAllExtractions(docId: MockSessionManager.failurePayableDocumentID) { result in switch result { case .success(let extractions): @@ -497,7 +503,7 @@ final class GiniHealthTests: XCTestCase { // When let expectation = self.expectation(description: "Getting doctor name extractions") - var receivedDoctorExtraction: Extraction? + var receivedDoctorExtraction: GiniHealthSDK.Extraction? giniHealth.getAllExtractions(docId: MockSessionManager.doctorsNameDocumentID) { result in switch result { case .success(let extractions): diff --git a/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/MockHelpers/FileLoader.swift b/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/MockHelpers/FileLoader.swift index bb550fdf1..501062bec 100644 --- a/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/MockHelpers/FileLoader.swift +++ b/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/MockHelpers/FileLoader.swift @@ -6,11 +6,12 @@ import Foundation +import GiniUtilites struct FileLoader { static func loadFile(withName mockFileName: String, ofType fileType: String) -> Data? { guard let filePath = Bundle.module.path(forResource: mockFileName, ofType: fileType) else { - print("File not found.") + GiniUtilites.Log("File not found.", event: .warning) return nil } @@ -19,7 +20,7 @@ struct FileLoader { let data = try Data(contentsOf: fileURL) return data } catch { - print("Error loading file:", error) + GiniUtilites.Log("Error loading file: \(error)", event: .error) return nil } } diff --git a/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/MockHelpers/MockPaymentComponents.swift b/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/MockHelpers/MockPaymentComponents.swift index 42fe33c29..abdde5252 100644 --- a/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/MockHelpers/MockPaymentComponents.swift +++ b/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/MockHelpers/MockPaymentComponents.swift @@ -8,19 +8,28 @@ import UIKit @testable import GiniHealthSDK @testable import GiniHealthAPILibrary +@testable import GiniInternalPaymentSDK class MockPaymentComponents: PaymentComponentsProtocol { var isLoading: Bool = false - var selectedPaymentProvider: PaymentProvider? - + var selectedPaymentProvider: GiniHealthSDK.PaymentProvider? + private var healthSelectedPaymentProvider: GiniHealthAPILibrary.PaymentProvider? { + selectedPaymentProvider?.toHealthPaymentProvider() + } + private var giniHealth: GiniHealth - private var paymentProviders: PaymentProviders = [] - private var installedPaymentProviders: PaymentProviders = [] + private var paymentProviders: GiniHealthSDK.PaymentProviders = [] + private var installedPaymentProviders: GiniHealthSDK.PaymentProviders = [] private let giniHealthConfiguration = GiniHealthConfiguration.shared - - init(giniHealthSDK: GiniHealth) { - self.giniHealth = giniHealthSDK + private let configurationProvider: PaymentComponentsConfigurationProvider + private let stringsProvider: PaymentComponentsStringsProvider + var documentId: String? + + init(giniHealth: GiniHealth & PaymentComponentsConfigurationProvider & PaymentComponentsStringsProvider) { + self.giniHealth = giniHealth + self.configurationProvider = giniHealth + self.stringsProvider = giniHealth } func loadPaymentProviders() { @@ -29,7 +38,9 @@ class MockPaymentComponents: PaymentComponentsProtocol { return } if let iconData = Data(url: URL(string: paymentProviderResponse.iconLocation)) { - selectedPaymentProvider = PaymentProvider(id: paymentProviderResponse.id, name: paymentProviderResponse.name, appSchemeIOS: paymentProviderResponse.appSchemeIOS, minAppVersion: paymentProviderResponse.minAppVersion, colors: paymentProviderResponse.colors, iconData: iconData, appStoreUrlIOS: paymentProviderResponse.appStoreUrlIOS, universalLinkIOS: paymentProviderResponse.universalLinkIOS, index: paymentProviderResponse.index, gpcSupportedPlatforms: paymentProviderResponse.gpcSupportedPlatforms, openWithSupportedPlatforms: paymentProviderResponse.openWithSupportedPlatforms) + let provider = GiniHealthAPILibrary.PaymentProvider(id: paymentProviderResponse.id, name: paymentProviderResponse.name, appSchemeIOS: paymentProviderResponse.appSchemeIOS, minAppVersion: paymentProviderResponse.minAppVersion, colors: paymentProviderResponse.colors, iconData: iconData, appStoreUrlIOS: paymentProviderResponse.appStoreUrlIOS, universalLinkIOS: paymentProviderResponse.universalLinkIOS, index: paymentProviderResponse.index, gpcSupportedPlatforms: paymentProviderResponse.gpcSupportedPlatforms, openWithSupportedPlatforms: paymentProviderResponse.openWithSupportedPlatforms) + selectedPaymentProvider = GiniHealthSDK.PaymentProvider(healthPaymentProvider: provider) + } } @@ -40,7 +51,7 @@ class MockPaymentComponents: PaymentComponentsProtocol { case MockSessionManager.notPayableDocumentID: completion(.success(false)) case MockSessionManager.missingDocumentID: - completion(.failure(.apiError(.noResponse))) + completion(.failure(.apiError(.decorator(.noResponse)))) default: fatalError("Document id not handled in tests") } @@ -53,42 +64,61 @@ class MockPaymentComponents: PaymentComponentsProtocol { case MockSessionManager.notPayableDocumentID: completion(.success(true)) case MockSessionManager.missingDocumentID: - completion(.failure(.apiError(.noResponse))) + completion(.failure(.apiError(.decorator(.noResponse)))) default: fatalError("Document id not handled in tests") } } - func paymentView(documentId: String) -> UIView { - let viewModel = PaymentComponentViewModel(paymentProvider: selectedPaymentProvider, giniHealthConfiguration: giniHealthConfiguration, paymentComponentConfiguration: PaymentComponentConfiguration(isPaymentComponentBranded: true)) - viewModel.documentId = documentId - let view = PaymentComponentView() - view.viewModel = viewModel + func paymentView() -> UIView { + let paymentComponentViewModel = PaymentComponentViewModel( + paymentProvider: healthSelectedPaymentProvider, + primaryButtonConfiguration: configurationProvider.primaryButtonConfiguration, + secondaryButtonConfiguration: configurationProvider.secondaryButtonConfiguration, + configuration: configurationProvider.paymentComponentsConfiguration, + strings: stringsProvider.paymentComponentsStrings, + poweredByGiniConfiguration: configurationProvider.poweredByGiniConfiguration, + poweredByGiniStrings: stringsProvider.poweredByGiniStrings, + moreInformationConfiguration: configurationProvider.moreInformationConfiguration, + moreInformationStrings: stringsProvider.moreInformationStrings, + minimumButtonsHeight: configurationProvider.paymentComponentButtonsHeight, + paymentComponentConfiguration: configurationProvider.paymentComponentConfiguration + ) + paymentComponentViewModel.documentId = MockSessionManager.payableDocumentID + let view = PaymentComponentView(viewModel: paymentComponentViewModel) return view } func bankSelectionBottomSheet() -> UIViewController { - let paymentProvidersBottomViewModel = BanksBottomViewModel(paymentProviders: paymentProviders, - selectedPaymentProvider: selectedPaymentProvider) - let paymentProvidersBottomView = BanksBottomView(viewModel: paymentProvidersBottomViewModel) - return paymentProvidersBottomView + let paymentProvidersBottomViewModel = BanksBottomViewModel(paymentProviders: paymentProviders.map { $0.toHealthPaymentProvider() }, + selectedPaymentProvider: healthSelectedPaymentProvider, + configuration: configurationProvider.bankSelectionConfiguration, + strings: stringsProvider.banksBottomStrings, + poweredByGiniConfiguration: configurationProvider.poweredByGiniConfiguration, + poweredByGiniStrings: stringsProvider.poweredByGiniStrings, + moreInformationConfiguration: configurationProvider.moreInformationConfiguration, + moreInformationStrings: stringsProvider.moreInformationStrings) + return BanksBottomView(viewModel: paymentProvidersBottomViewModel, bottomSheetConfiguration: configurationProvider.bottomSheetConfiguration) } - func loadPaymentReviewScreenFor(documentID: String, trackingDelegate: (any GiniHealthTrackingDelegate)?, completion: @escaping (UIViewController?, GiniHealthError?) -> Void) { - switch documentID { - case MockSessionManager.payableDocumentID: - completion(PaymentReviewViewController(), nil) - case MockSessionManager.missingDocumentID: - completion(nil, .apiError(.noResponse)) - default: - fatalError("Document id not handled in tests") - } + func loadPaymentReviewScreenFor(trackingDelegate: (any GiniHealthTrackingDelegate)?, completion: @escaping (UIViewController?, GiniHealthError?) -> Void) { + completion(UIViewController(), nil) } func paymentInfoViewController() -> UIViewController { - let paymentInfoViewController = PaymentInfoViewController() - let paymentInfoViewModel = PaymentInfoViewModel(paymentProviders: paymentProviders) - paymentInfoViewController.viewModel = paymentInfoViewModel + let paymentInfoViewModel = PaymentInfoViewModel(paymentProviders: paymentProviders.map { $0.toHealthPaymentProvider() }, + configuration: giniHealth.paymentInfoConfiguration, + strings: giniHealth.paymentInfoStrings, + poweredByGiniConfiguration: giniHealth.poweredByGiniConfiguration, + poweredByGiniStrings: giniHealth.poweredByGiniStrings) + let paymentInfoViewController = PaymentInfoViewController(viewModel: paymentInfoViewModel) return paymentInfoViewController } + + func paymentViewBottomSheet() -> UIViewController { + let paymentComponentBottomView = PaymentComponentBottomView(paymentView: paymentView(), + bottomSheetConfiguration: giniHealth.bottomSheetConfiguration) + return paymentComponentBottomView + } + } diff --git a/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/MockHelpers/MockUIApplication.swift b/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/MockHelpers/MockUIApplication.swift index df4fdb5d4..9178049bd 100644 --- a/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/MockHelpers/MockUIApplication.swift +++ b/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/MockHelpers/MockUIApplication.swift @@ -7,6 +7,7 @@ import UIKit @testable import GiniHealthSDK +@testable import GiniUtilites struct MockUIApplication: URLOpenerProtocol { var canOpen: Bool diff --git a/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/MockHelpers/Utils.swift b/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/MockHelpers/Utils.swift index 8fbeb8279..5101388e4 100644 --- a/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/MockHelpers/Utils.swift +++ b/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/MockHelpers/Utils.swift @@ -7,16 +7,31 @@ import UIKit import GiniHealthAPILibrary +import GiniHealthSDK - -func loadProviders(fileName: String) -> PaymentProviders? { - var providers: PaymentProviders = [] +func loadProviders(fileName: String) -> GiniHealthSDK.PaymentProviders? { + var providers: GiniHealthSDK.PaymentProviders = [] let providersResponse: [PaymentProviderResponse]? = load(fromFile: fileName) guard let providersResponse else { return nil } for providerResponse in providersResponse { let imageData = UIImage(named: "Gini-Test-Payment-Provider", in: Bundle.module, compatibleWith: nil)?.pngData() - let provider = PaymentProvider(id: providerResponse.id, name: providerResponse.name, appSchemeIOS: providerResponse.appSchemeIOS, minAppVersion: providerResponse.minAppVersion, colors: providerResponse.colors, iconData: imageData ?? Data(), appStoreUrlIOS: providerResponse.appStoreUrlIOS, universalLinkIOS: providerResponse.universalLinkIOS, index: providerResponse.index, gpcSupportedPlatforms: providerResponse.gpcSupportedPlatforms, openWithSupportedPlatforms: providerResponse.openWithSupportedPlatforms) - providers.append(provider) + let openWithPlatforms = providerResponse.openWithSupportedPlatforms.compactMap { GiniHealthSDK.PlatformSupported(rawValue: $0.rawValue) } + let gpcSupportedPlatforms = providerResponse.gpcSupportedPlatforms.compactMap { GiniHealthSDK.PlatformSupported(rawValue: $0.rawValue) } + let colors = GiniHealthSDK.ProviderColors(background: providerResponse.colors.background, + text: providerResponse.colors.text) + + let provider = GiniHealthSDK.PaymentProvider(id: providerResponse.id, + name: providerResponse.name, + appSchemeIOS: providerResponse.appSchemeIOS, + minAppVersion: nil, + colors: colors, + iconData: imageData ?? Data(), + appStoreUrlIOS: providerResponse.appStoreUrlIOS, + universalLinkIOS: providerResponse.universalLinkIOS, + index: providerResponse.index, + gpcSupportedPlatforms: gpcSupportedPlatforms, + openWithSupportedPlatforms: openWithPlatforms) + providers.append(provider) } return providers } diff --git a/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/PaymentComponentsControllerTests.swift b/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/PaymentComponentsControllerTests.swift index e4efda1c5..73ba27b0c 100644 --- a/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/PaymentComponentsControllerTests.swift +++ b/HealthSDK/GiniHealthSDK/Tests/GiniHealthSDKTests/PaymentComponentsControllerTests.swift @@ -8,9 +8,12 @@ import XCTest @testable import GiniHealthSDK @testable import GiniHealthAPILibrary +@testable import GiniInternalPaymentSDK +@testable import GiniUtilites final class PaymentComponentsControllerTests: XCTestCase { private var giniHealthAPI: GiniHealthAPI! + private var giniHealth: GiniHealth! private var mockPaymentComponentsController: PaymentComponentsProtocol! private let giniHealthConfiguration = GiniHealthConfiguration.shared private let versionAPI = 4 @@ -21,8 +24,8 @@ final class PaymentComponentsControllerTests: XCTestCase { let documentService = DefaultDocumentService(sessionManager: sessionManagerMock, apiVersion: versionAPI) let paymentService = PaymentService(sessionManager: sessionManagerMock, apiVersion: versionAPI) giniHealthAPI = GiniHealthAPI(documentService: documentService, paymentService: paymentService) - let giniHealth = GiniHealth(with: giniHealthAPI) - mockPaymentComponentsController = MockPaymentComponents(giniHealthSDK: giniHealth) + giniHealth = GiniHealth(giniApiLib: giniHealthAPI) + mockPaymentComponentsController = MockPaymentComponents(giniHealth: giniHealth) } override func tearDown() { @@ -65,7 +68,7 @@ final class PaymentComponentsControllerTests: XCTestCase { } func testCheckIfDocumentIsPayable_Failure() { - let expectedResult: Result = .failure(.apiError(.noResponse)) + let expectedResult: Result = .failure(.apiError(.decorator(.noResponse))) // When var receivedResult: Result? mockPaymentComponentsController.checkIfDocumentIsPayable(docId: MockSessionManager.missingDocumentID) { result in @@ -79,12 +82,22 @@ final class PaymentComponentsControllerTests: XCTestCase { func testPaymentView_ReturnsView() { // Given let documentId = "123456" - let expectedViewModel = PaymentComponentViewModel(paymentProvider: nil, giniHealthConfiguration: giniHealthConfiguration, paymentComponentConfiguration: PaymentComponentConfiguration(isPaymentComponentBranded: true)) - let expectedView = PaymentComponentView() - expectedView.viewModel = expectedViewModel - + let expectedViewModel = PaymentComponentViewModel(paymentProvider: nil, + primaryButtonConfiguration: giniHealth.primaryButtonConfiguration, + secondaryButtonConfiguration: giniHealth.secondaryButtonConfiguration, + configuration: giniHealth.paymentComponentsConfiguration, + strings: giniHealth.paymentComponentsStrings, + poweredByGiniConfiguration: giniHealth.poweredByGiniConfiguration, + poweredByGiniStrings: giniHealth.poweredByGiniStrings, + moreInformationConfiguration: giniHealth.moreInformationConfiguration, + moreInformationStrings: giniHealth.moreInformationStrings, + minimumButtonsHeight: giniHealth.paymentComponentButtonsHeight, + paymentComponentConfiguration: giniHealth.paymentComponentConfiguration) + + let expectedView = PaymentComponentView(viewModel: expectedViewModel) + expectedViewModel.documentId = documentId // When - let view = mockPaymentComponentsController.paymentView(documentId: documentId) + let view = mockPaymentComponentsController.paymentView() // Then XCTAssertTrue(view is PaymentComponentView) @@ -92,7 +105,6 @@ final class PaymentComponentsControllerTests: XCTestCase { XCTFail("Error finding correct view.") return } - XCTAssertEqual(view.viewModel?.documentId, documentId) } func testBankSelectionBottomSheet_ReturnsViewController() { @@ -110,12 +122,12 @@ final class PaymentComponentsControllerTests: XCTestCase { func testLoadPaymentReviewScreenFor_Success() { // Given - let documentID = MockSessionManager.payableDocumentID + let documentId = MockSessionManager.payableDocumentID // When var receivedViewController: UIViewController? var receivedError: GiniHealthError? - mockPaymentComponentsController.loadPaymentReviewScreenFor(documentID: documentID, trackingDelegate: nil) { viewController, error in + mockPaymentComponentsController.loadPaymentReviewScreenFor(trackingDelegate: nil) { viewController, error in receivedViewController = viewController receivedError = error } @@ -123,25 +135,6 @@ final class PaymentComponentsControllerTests: XCTestCase { // Then XCTAssertNil(receivedError) XCTAssertNotNil(receivedViewController) - XCTAssertTrue(receivedViewController is PaymentReviewViewController) - } - - func testLoadPaymentReviewScreenFor_Failure() { - // Given - let documentID = MockSessionManager.missingDocumentID - - // When - var receivedViewController: UIViewController? - var receivedError: GiniHealthError? - mockPaymentComponentsController.loadPaymentReviewScreenFor(documentID: documentID, trackingDelegate: nil) { viewController, error in - receivedViewController = viewController - receivedError = error - } - - // Then - XCTAssertNotNil(receivedError) - XCTAssertNil(receivedViewController) - XCTAssertEqual(receivedError, .apiError(.noResponse)) } func testPaymentInfoViewController_ReturnsCorrectViewController() { @@ -155,10 +148,7 @@ final class PaymentComponentsControllerTests: XCTestCase { return } XCTAssertNotNil(paymentInfoVC.viewModel) - guard let paymentInfoViewModel = paymentInfoVC.viewModel else { - XCTFail("Error finding payment info viewModel.") - return - } + let paymentInfoViewModel = paymentInfoVC.viewModel XCTAssertEqual(paymentInfoViewModel.paymentProviders, []) } @@ -168,12 +158,21 @@ final class PaymentComponentsControllerTests: XCTestCase { XCTFail("Error loading file: `\(fileName).json`") return } - + let expectedPaymentProviders = loadProviders(fileName: "sortedBanks") - - let bottomViewModel = BanksBottomViewModel(paymentProviders: givenPaymentProviders, selectedPaymentProvider: nil, urlOpener: URLOpener(MockUIApplication(canOpen: false))) - + + let bottomViewModel = BanksBottomViewModel(paymentProviders: givenPaymentProviders.map { $0.toHealthPaymentProvider() }, + selectedPaymentProvider: nil, + configuration: giniHealth.bankSelectionConfiguration, + strings: giniHealth.banksBottomStrings, + poweredByGiniConfiguration: giniHealth.poweredByGiniConfiguration, + poweredByGiniStrings: giniHealth.poweredByGiniStrings, + moreInformationConfiguration: giniHealth.moreInformationConfiguration, + moreInformationStrings: giniHealth.moreInformationStrings, + urlOpener: URLOpener(MockUIApplication(canOpen: false))) + + XCTAssertEqual(bottomViewModel.paymentProviders.count, 11) - XCTAssertEqual(bottomViewModel.paymentProviders.map { $0.paymentProvider }, expectedPaymentProviders) + XCTAssertEqual(bottomViewModel.paymentProviders.map { PaymentProvider(healthPaymentProvider: $0.paymentProvider) }, expectedPaymentProviders) } } diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample.xcodeproj/project.pbxproj b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample.xcodeproj/project.pbxproj index cf4270ad1..b1524ad7b 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample.xcodeproj/project.pbxproj +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 284A53502A86493100183EA7 /* SelectAPIViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 284A534F2A86493100183EA7 /* SelectAPIViewController.xib */; }; + 504435172C5AE5D300105D0B /* GiniHealthSDKPinningExampleIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4DB6420283546960027681C /* GiniHealthSDKPinningExampleIntegrationTests.swift */; }; + 504435182C5AE5D300105D0B /* GiniHealthSDKPinningExampleWrongCertificatesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4DB642728354B2B0027681C /* GiniHealthSDKPinningExampleWrongCertificatesTests.swift */; }; 50ED3DA52C34A16D004FC25C /* DebugMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50ED3DA42C34A16D004FC25C /* DebugMenuViewController.swift */; }; 7D6161C62BE1327E0091000B /* test_pdf.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 7D6161C52BE1327E0091000B /* test_pdf.pdf */; }; 7DB661832B554622004537AA /* InvoicesListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DB661822B554622004537AA /* InvoicesListCoordinator.swift */; }; @@ -22,6 +24,14 @@ 7DB661992B5687F7004537AA /* health-invoice-5.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 7DB661942B5687F7004537AA /* health-invoice-5.jpg */; }; 7DB6619B2B56AE80004537AA /* InvoicesListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DB6619A2B56AE80004537AA /* InvoicesListViewModel.swift */; }; 7DB6619D2B56C0B7004537AA /* InvoiceTableViewCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DB6619C2B56C0B7004537AA /* InvoiceTableViewCellModel.swift */; }; + 8A142EAF2CC7C38D007B268A /* HardcodedOrders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A142EAE2CC7C38D007B268A /* HardcodedOrders.swift */; }; + 8A142EB22CC7C4A5007B268A /* OrderListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A142EB12CC7C4A5007B268A /* OrderListCoordinator.swift */; }; + 8A142EB42CC7C4C6007B268A /* OrderListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A142EB32CC7C4C6007B268A /* OrderListViewController.swift */; }; + 8A142EB62CC7C4DA007B268A /* OrderListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A142EB52CC7C4DA007B268A /* OrderListViewModel.swift */; }; + 8A142EB82CC7C4F6007B268A /* OrderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A142EB72CC7C4F6007B268A /* OrderTableViewCell.swift */; }; + 8A142EBA2CC7C4FE007B268A /* OrderCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A142EB92CC7C4FE007B268A /* OrderCellViewModel.swift */; }; + 8A142EBC2CC7C50D007B268A /* OrderDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A142EBB2CC7C50D007B268A /* OrderDetailViewController.swift */; }; + 8A142EBE2CC7C528007B268A /* OrderDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A142EBD2CC7C528007B268A /* OrderDetailView.swift */; }; 8A77DA1E2C2BF6130049FE10 /* UploadDocumentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A77DA1D2C2BF6130049FE10 /* UploadDocumentsTests.swift */; }; 8ADEE2F02C2BF9E4006D3526 /* GiniSetupHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8ADEE2EF2C2BF9E4006D3526 /* GiniSetupHelper.swift */; }; 8ADEE2F92C2C4822006D3526 /* FileLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8ADEE2F82C2C4822006D3526 /* FileLoader.swift */; }; @@ -33,16 +43,6 @@ F4206BC22A1D012E00E5E101 /* HealthNetworkingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4206BC12A1D012E00E5E101 /* HealthNetworkingService.swift */; }; F42377262799ABB000FAC3D6 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F42377252799ABB000FAC3D6 /* CloudKit.framework */; }; F464B17627CD2147001735B9 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F464B17827CD2147001735B9 /* Localizable.strings */; }; - F466A5DF270F2F9500FB1364 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F466A5E1270F2F9500FB1364 /* Localizable.strings */; }; - F4C36A8C270C6EBF00CCC69C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C36A8B270C6EBF00CCC69C /* AppDelegate.swift */; }; - F4C36A8E270C6EBF00CCC69C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C36A8D270C6EBF00CCC69C /* SceneDelegate.swift */; }; - F4C36A90270C6EBF00CCC69C /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C36A8F270C6EBF00CCC69C /* ViewController.swift */; }; - F4C36A93270C6EBF00CCC69C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F4C36A91270C6EBF00CCC69C /* Main.storyboard */; }; - F4C36A95270C6EC200CCC69C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F4C36A94270C6EC200CCC69C /* Assets.xcassets */; }; - F4C36A98270C6EC200CCC69C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F4C36A96270C6EC200CCC69C /* LaunchScreen.storyboard */; }; - F4CEC7ED270CB06D004A862F /* GiniHealthSDKPinning in Frameworks */ = {isa = PBXBuildFile; productRef = F4CEC7EC270CB06D004A862F /* GiniHealthSDKPinning */; }; - F4DB6421283546960027681C /* GiniHealthSDKPinningExampleIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4DB6420283546960027681C /* GiniHealthSDKPinningExampleIntegrationTests.swift */; }; - F4DB642828354B2B0027681C /* GiniHealthSDKPinningExampleWrongCertificatesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4DB642728354B2B0027681C /* GiniHealthSDKPinningExampleWrongCertificatesTests.swift */; }; F4EC38C2273C21E4007045DC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4EC38C1273C21E4007045DC /* AppDelegate.swift */; }; F4EC38C4273C21E4007045DC /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4EC38C3273C21E4007045DC /* SceneDelegate.swift */; }; F4EC38C6273C21E4007045DC /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4EC38C5273C21E4007045DC /* ViewController.swift */; }; @@ -50,7 +50,6 @@ F4EC38D9273C21E6007045DC /* GiniHealthSDKExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4EC38D8273C21E6007045DC /* GiniHealthSDKExampleTests.swift */; }; F4EC38F5273C2311007045DC /* UIViewController+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4EC38EF273C2310007045DC /* UIViewController+Utils.swift */; }; F4EC38F8273C2311007045DC /* CredentialsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4EC38F2273C2311007045DC /* CredentialsManager.swift */; }; - F4EC38F9273C2311007045DC /* Credentials.plist in Resources */ = {isa = PBXBuildFile; fileRef = F4EC38F3273C2311007045DC /* Credentials.plist */; }; F4EC38FA273C2311007045DC /* Base.lproj in Resources */ = {isa = PBXBuildFile; fileRef = F4EC38F4273C2311007045DC /* Base.lproj */; }; F4EC3900273C245E007045DC /* GiniHealthSDK in Frameworks */ = {isa = PBXBuildFile; productRef = F4EC38FF273C245E007045DC /* GiniHealthSDK */; }; F4EC390A273C2622007045DC /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4EC3903273C2621007045DC /* AppCoordinator.swift */; }; @@ -62,13 +61,6 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - F4DB6422283546960027681C /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = F4C36A80270C6EBF00CCC69C /* Project object */; - proxyType = 1; - remoteGlobalIDString = F4C36A87270C6EBF00CCC69C; - remoteInfo = GiniHealthSDKPinningExample; - }; F4EC38D5273C21E6007045DC /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F4C36A80270C6EBF00CCC69C /* Project object */; @@ -80,6 +72,7 @@ /* Begin PBXFileReference section */ 284A534F2A86493100183EA7 /* SelectAPIViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SelectAPIViewController.xib; sourceTree = ""; }; + 3AC824B42CA2B16500DE37C0 /* GiniHealthSDKExampleTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = GiniHealthSDKExampleTests.xctestplan; sourceTree = ""; }; 50ED3DA42C34A16D004FC25C /* DebugMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugMenuViewController.swift; sourceTree = ""; }; 7D6161C52BE1327E0091000B /* test_pdf.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = test_pdf.pdf; sourceTree = ""; }; 7DB661822B554622004537AA /* InvoicesListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoicesListCoordinator.swift; sourceTree = ""; }; @@ -94,6 +87,14 @@ 7DB661942B5687F7004537AA /* health-invoice-5.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "health-invoice-5.jpg"; sourceTree = ""; }; 7DB6619A2B56AE80004537AA /* InvoicesListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoicesListViewModel.swift; sourceTree = ""; }; 7DB6619C2B56C0B7004537AA /* InvoiceTableViewCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceTableViewCellModel.swift; sourceTree = ""; }; + 8A142EAE2CC7C38D007B268A /* HardcodedOrders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HardcodedOrders.swift; sourceTree = ""; }; + 8A142EB12CC7C4A5007B268A /* OrderListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderListCoordinator.swift; sourceTree = ""; }; + 8A142EB32CC7C4C6007B268A /* OrderListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderListViewController.swift; sourceTree = ""; }; + 8A142EB52CC7C4DA007B268A /* OrderListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderListViewModel.swift; sourceTree = ""; }; + 8A142EB72CC7C4F6007B268A /* OrderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderTableViewCell.swift; sourceTree = ""; }; + 8A142EB92CC7C4FE007B268A /* OrderCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderCellViewModel.swift; sourceTree = ""; }; + 8A142EBB2CC7C50D007B268A /* OrderDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderDetailViewController.swift; sourceTree = ""; }; + 8A142EBD2CC7C528007B268A /* OrderDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderDetailView.swift; sourceTree = ""; }; 8A77DA1D2C2BF6130049FE10 /* UploadDocumentsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadDocumentsTests.swift; sourceTree = ""; }; 8ADEE2EF2C2BF9E4006D3526 /* GiniSetupHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiniSetupHelper.swift; sourceTree = ""; }; 8ADEE2F22C2C3CCC006D3526 /* invoice-12MB.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "invoice-12MB.png"; sourceTree = ""; }; @@ -106,17 +107,6 @@ F4366E62273D183C0054923F /* GiniHealthSDKExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GiniHealthSDKExample.entitlements; sourceTree = ""; }; F464B17727CD2147001735B9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; F464B17927CD214A001735B9 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; - F466A5E0270F2F9500FB1364 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - F466A5E2270F2FB300FB1364 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; - F4C36A88270C6EBF00CCC69C /* GiniHealthSDKPinningExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GiniHealthSDKPinningExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; - F4C36A8B270C6EBF00CCC69C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - F4C36A8D270C6EBF00CCC69C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - F4C36A8F270C6EBF00CCC69C /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - F4C36A92270C6EBF00CCC69C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - F4C36A94270C6EC200CCC69C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - F4C36A97270C6EC200CCC69C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - F4C36A99270C6EC200CCC69C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - F4DB641E283546960027681C /* GiniHealthSDKPinningExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GiniHealthSDKPinningExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F4DB6420283546960027681C /* GiniHealthSDKPinningExampleIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiniHealthSDKPinningExampleIntegrationTests.swift; sourceTree = ""; }; F4DB642728354B2B0027681C /* GiniHealthSDKPinningExampleWrongCertificatesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiniHealthSDKPinningExampleWrongCertificatesTests.swift; sourceTree = ""; }; F4EC38BF273C21E4007045DC /* GiniHealthSDKExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GiniHealthSDKExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -129,7 +119,6 @@ F4EC38D8273C21E6007045DC /* GiniHealthSDKExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiniHealthSDKExampleTests.swift; sourceTree = ""; }; F4EC38EF273C2310007045DC /* UIViewController+Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+Utils.swift"; sourceTree = ""; }; F4EC38F2273C2311007045DC /* CredentialsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CredentialsManager.swift; sourceTree = ""; }; - F4EC38F3273C2311007045DC /* Credentials.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Credentials.plist; sourceTree = ""; }; F4EC38F4273C2311007045DC /* Base.lproj */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base.lproj; sourceTree = ""; }; F4EC38FB273C240F007045DC /* GiniHealthSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; name = GiniHealthSDK; path = ../GiniHealthSDK; sourceTree = ""; }; F4EC38FC273C2432007045DC /* GiniCaptureSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; name = GiniCaptureSDK; path = ../../CaptureSDK/GiniCaptureSDK; sourceTree = ""; }; @@ -139,25 +128,9 @@ F4EC3907273C2621007045DC /* SelectAPIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectAPIViewController.swift; sourceTree = ""; }; F4EC3908273C2622007045DC /* UIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = ""; }; F4EC3912273C2B41007045DC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - F4F9E5C5270E003600C6FBA3 /* GiniHealthSDKPinning */ = {isa = PBXFileReference; lastKnownFileType = folder; name = GiniHealthSDKPinning; path = ../GiniHealthSDKPinning; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - F4C36A85270C6EBF00CCC69C /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - F4CEC7ED270CB06D004A862F /* GiniHealthSDKPinning in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F4DB641B283546960027681C /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; F4EC38BC273C21E4007045DC /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -206,6 +179,21 @@ path = HardcodedInvoices; sourceTree = ""; }; + 8A142EB02CC7C484007B268A /* OrdersList */ = { + isa = PBXGroup; + children = ( + 8A142EAE2CC7C38D007B268A /* HardcodedOrders.swift */, + 8A142EB12CC7C4A5007B268A /* OrderListCoordinator.swift */, + 8A142EB32CC7C4C6007B268A /* OrderListViewController.swift */, + 8A142EB52CC7C4DA007B268A /* OrderListViewModel.swift */, + 8A142EB72CC7C4F6007B268A /* OrderTableViewCell.swift */, + 8A142EB92CC7C4FE007B268A /* OrderCellViewModel.swift */, + 8A142EBB2CC7C50D007B268A /* OrderDetailViewController.swift */, + 8A142EBD2CC7C528007B268A /* OrderDetailView.swift */, + ); + path = OrdersList; + sourceTree = ""; + }; 8ADEE2F12C2C3C9B006D3526 /* Resources */ = { isa = PBXGroup; children = ( @@ -219,10 +207,8 @@ isa = PBXGroup; children = ( F4C36A9F270C6F6B00CCC69C /* Packages */, - F4C36A8A270C6EBF00CCC69C /* GiniHealthSDKPinningExample */, F4EC38C0273C21E4007045DC /* GiniHealthSDKExample */, F4EC38D7273C21E6007045DC /* GiniHealthSDKExampleTests */, - F4DB641F283546960027681C /* GiniHealthSDKPinningExampleTests */, F4C36A89270C6EBF00CCC69C /* Products */, F4C36AA1270C6F9C00CCC69C /* Frameworks */, ); @@ -231,33 +217,15 @@ F4C36A89270C6EBF00CCC69C /* Products */ = { isa = PBXGroup; children = ( - F4C36A88270C6EBF00CCC69C /* GiniHealthSDKPinningExample.app */, F4EC38BF273C21E4007045DC /* GiniHealthSDKExample.app */, F4EC38D4273C21E6007045DC /* GiniHealthSDKExampleTests.xctest */, - F4DB641E283546960027681C /* GiniHealthSDKPinningExampleTests.xctest */, ); name = Products; sourceTree = ""; }; - F4C36A8A270C6EBF00CCC69C /* GiniHealthSDKPinningExample */ = { - isa = PBXGroup; - children = ( - F4C36A8B270C6EBF00CCC69C /* AppDelegate.swift */, - F4C36A8D270C6EBF00CCC69C /* SceneDelegate.swift */, - F466A5E1270F2F9500FB1364 /* Localizable.strings */, - F4C36A8F270C6EBF00CCC69C /* ViewController.swift */, - F4C36A91270C6EBF00CCC69C /* Main.storyboard */, - F4C36A94270C6EC200CCC69C /* Assets.xcassets */, - F4C36A96270C6EC200CCC69C /* LaunchScreen.storyboard */, - F4C36A99270C6EC200CCC69C /* Info.plist */, - ); - path = GiniHealthSDKPinningExample; - sourceTree = ""; - }; F4C36A9F270C6F6B00CCC69C /* Packages */ = { isa = PBXGroup; children = ( - F4F9E5C5270E003600C6FBA3 /* GiniHealthSDKPinning */, F4EC38FB273C240F007045DC /* GiniHealthSDK */, F4EC38FC273C2432007045DC /* GiniCaptureSDK */, ); @@ -272,15 +240,6 @@ name = Frameworks; sourceTree = ""; }; - F4DB641F283546960027681C /* GiniHealthSDKPinningExampleTests */ = { - isa = PBXGroup; - children = ( - F4DB6420283546960027681C /* GiniHealthSDKPinningExampleIntegrationTests.swift */, - F4DB642728354B2B0027681C /* GiniHealthSDKPinningExampleWrongCertificatesTests.swift */, - ); - path = GiniHealthSDKPinningExampleTests; - sourceTree = ""; - }; F4EC38C0273C21E4007045DC /* GiniHealthSDKExample */ = { isa = PBXGroup; children = ( @@ -297,9 +256,9 @@ 50ED3DA42C34A16D004FC25C /* DebugMenuViewController.swift */, 284A534F2A86493100183EA7 /* SelectAPIViewController.xib */, F4EC3907273C2621007045DC /* SelectAPIViewController.swift */, + 8A142EB02CC7C484007B268A /* OrdersList */, 7DB661882B554A79004537AA /* InvoicesList */, F4EC38F4273C2311007045DC /* Base.lproj */, - F4EC38F3273C2311007045DC /* Credentials.plist */, F4EC38F2273C2311007045DC /* CredentialsManager.swift */, F4EC38EF273C2310007045DC /* UIViewController+Utils.swift */, F4EC38C3273C21E4007045DC /* SceneDelegate.swift */, @@ -315,6 +274,9 @@ F4EC38D7273C21E6007045DC /* GiniHealthSDKExampleTests */ = { isa = PBXGroup; children = ( + 3AC824B42CA2B16500DE37C0 /* GiniHealthSDKExampleTests.xctestplan */, + F4DB6420283546960027681C /* GiniHealthSDKPinningExampleIntegrationTests.swift */, + F4DB642728354B2B0027681C /* GiniHealthSDKPinningExampleWrongCertificatesTests.swift */, F4EC38D8273C21E6007045DC /* GiniHealthSDKExampleTests.swift */, 8A77DA1D2C2BF6130049FE10 /* UploadDocumentsTests.swift */, 8ADEE2F82C2C4822006D3526 /* FileLoader.swift */, @@ -326,44 +288,6 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - F4C36A87270C6EBF00CCC69C /* GiniHealthSDKPinningExample */ = { - isa = PBXNativeTarget; - buildConfigurationList = F4C36A9C270C6EC200CCC69C /* Build configuration list for PBXNativeTarget "GiniHealthSDKPinningExample" */; - buildPhases = ( - F4C36A84270C6EBF00CCC69C /* Sources */, - F4C36A85270C6EBF00CCC69C /* Frameworks */, - F4C36A86270C6EBF00CCC69C /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = GiniHealthSDKPinningExample; - packageProductDependencies = ( - F4CEC7EC270CB06D004A862F /* GiniHealthSDKPinning */, - ); - productName = HealthSDKPinningExample; - productReference = F4C36A88270C6EBF00CCC69C /* GiniHealthSDKPinningExample.app */; - productType = "com.apple.product-type.application"; - }; - F4DB641D283546960027681C /* GiniHealthSDKPinningExampleTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = F4DB6426283546960027681C /* Build configuration list for PBXNativeTarget "GiniHealthSDKPinningExampleTests" */; - buildPhases = ( - F4DB641A283546960027681C /* Sources */, - F4DB641B283546960027681C /* Frameworks */, - F4DB641C283546960027681C /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - F4DB6423283546960027681C /* PBXTargetDependency */, - ); - name = GiniHealthSDKPinningExampleTests; - productName = GiniHealthSDKPinningExampleTests; - productReference = F4DB641E283546960027681C /* GiniHealthSDKPinningExampleTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; F4EC38BE273C21E4007045DC /* GiniHealthSDKExample */ = { isa = PBXNativeTarget; buildConfigurationList = F4EC38EC273C21E6007045DC /* Build configuration list for PBXNativeTarget "GiniHealthSDKExample" */; @@ -413,13 +337,6 @@ LastSwiftUpdateCheck = 1330; LastUpgradeCheck = 1300; TargetAttributes = { - F4C36A87270C6EBF00CCC69C = { - CreatedOnToolsVersion = 13.0; - }; - F4DB641D283546960027681C = { - CreatedOnToolsVersion = 13.3.1; - TestTargetID = F4C36A87270C6EBF00CCC69C; - }; F4EC38BE273C21E4007045DC = { CreatedOnToolsVersion = 13.1; }; @@ -443,33 +360,13 @@ projectDirPath = ""; projectRoot = ""; targets = ( - F4C36A87270C6EBF00CCC69C /* GiniHealthSDKPinningExample */, F4EC38BE273C21E4007045DC /* GiniHealthSDKExample */, F4EC38D3273C21E6007045DC /* GiniHealthSDKExampleTests */, - F4DB641D283546960027681C /* GiniHealthSDKPinningExampleTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - F4C36A86270C6EBF00CCC69C /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F4C36A98270C6EC200CCC69C /* LaunchScreen.storyboard in Resources */, - F466A5DF270F2F9500FB1364 /* Localizable.strings in Resources */, - F4C36A95270C6EC200CCC69C /* Assets.xcassets in Resources */, - F4C36A93270C6EBF00CCC69C /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F4DB641C283546960027681C /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; F4EC38BD273C21E4007045DC /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -480,7 +377,6 @@ F464B17627CD2147001735B9 /* Localizable.strings in Resources */, F4EC38FA273C2311007045DC /* Base.lproj in Resources */, 7DB661992B5687F7004537AA /* health-invoice-5.jpg in Resources */, - F4EC38F9273C2311007045DC /* Credentials.plist in Resources */, 8ADEE2FA2C2CB2E4006D3526 /* invoice-12MB.png in Resources */, 8ADEE2FB2C2CB302006D3526 /* invoice-13MB.pdf in Resources */, 284A53502A86493100183EA7 /* SelectAPIViewController.xib in Resources */, @@ -502,25 +398,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - F4C36A84270C6EBF00CCC69C /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F4C36A90270C6EBF00CCC69C /* ViewController.swift in Sources */, - F4C36A8C270C6EBF00CCC69C /* AppDelegate.swift in Sources */, - F4C36A8E270C6EBF00CCC69C /* SceneDelegate.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F4DB641A283546960027681C /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F4DB642828354B2B0027681C /* GiniHealthSDKPinningExampleWrongCertificatesTests.swift in Sources */, - F4DB6421283546960027681C /* GiniHealthSDKPinningExampleIntegrationTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; F4EC38BB273C21E4007045DC /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -530,20 +407,28 @@ F4206BC22A1D012E00E5E101 /* HealthNetworkingService.swift in Sources */, F4EC390D273C2622007045DC /* Coordinator.swift in Sources */, F4EC38F8273C2311007045DC /* CredentialsManager.swift in Sources */, + 8A142EBA2CC7C4FE007B268A /* OrderCellViewModel.swift in Sources */, 7DB6618B2B554ADF004537AA /* InvoiceTableViewCell.swift in Sources */, F4EC38F5273C2311007045DC /* UIViewController+Utils.swift in Sources */, F4EC38C6273C21E4007045DC /* ViewController.swift in Sources */, 7DB6618E2B5687B4004537AA /* HardcodedInvoices.swift in Sources */, F4EC3910273C2622007045DC /* UIViewController.swift in Sources */, 7DB6619B2B56AE80004537AA /* InvoicesListViewModel.swift in Sources */, + 8A142EBC2CC7C50D007B268A /* OrderDetailViewController.swift in Sources */, F4206BC02A1BB86B00E5E101 /* RootNavigationController.swift in Sources */, + 8A142EAF2CC7C38D007B268A /* HardcodedOrders.swift in Sources */, F4EC38C2273C21E4007045DC /* AppDelegate.swift in Sources */, 7DB661862B554A6F004537AA /* InvoicesListViewController.swift in Sources */, + 8A142EB62CC7C4DA007B268A /* OrderListViewModel.swift in Sources */, + 8A142EB82CC7C4F6007B268A /* OrderTableViewCell.swift in Sources */, F4EC38C4273C21E4007045DC /* SceneDelegate.swift in Sources */, 50ED3DA52C34A16D004FC25C /* DebugMenuViewController.swift in Sources */, + 8A142EB22CC7C4A5007B268A /* OrderListCoordinator.swift in Sources */, F4206BBE2A1BB7CC00E5E101 /* ScreenAPICoordinator.swift in Sources */, F4EC390A273C2622007045DC /* AppCoordinator.swift in Sources */, + 8A142EB42CC7C4C6007B268A /* OrderListViewController.swift in Sources */, F4EC390E273C2622007045DC /* PartialDocument.swift in Sources */, + 8A142EBE2CC7C528007B268A /* OrderDetailView.swift in Sources */, 7DB6619D2B56C0B7004537AA /* InvoiceTableViewCellModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -554,7 +439,9 @@ files = ( 8ADEE2F02C2BF9E4006D3526 /* GiniSetupHelper.swift in Sources */, 8A77DA1E2C2BF6130049FE10 /* UploadDocumentsTests.swift in Sources */, + 504435172C5AE5D300105D0B /* GiniHealthSDKPinningExampleIntegrationTests.swift in Sources */, 8ADEE2F92C2C4822006D3526 /* FileLoader.swift in Sources */, + 504435182C5AE5D300105D0B /* GiniHealthSDKPinningExampleWrongCertificatesTests.swift in Sources */, F4EC38D9273C21E6007045DC /* GiniHealthSDKExampleTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -562,11 +449,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - F4DB6423283546960027681C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = F4C36A87270C6EBF00CCC69C /* GiniHealthSDKPinningExample */; - targetProxy = F4DB6422283546960027681C /* PBXContainerItemProxy */; - }; F4EC38D6273C21E6007045DC /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F4EC38BE273C21E4007045DC /* GiniHealthSDKExample */; @@ -584,31 +466,6 @@ name = Localizable.strings; sourceTree = ""; }; - F466A5E1270F2F9500FB1364 /* Localizable.strings */ = { - isa = PBXVariantGroup; - children = ( - F466A5E0270F2F9500FB1364 /* en */, - F466A5E2270F2FB300FB1364 /* de */, - ); - name = Localizable.strings; - sourceTree = ""; - }; - F4C36A91270C6EBF00CCC69C /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - F4C36A92270C6EBF00CCC69C /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - F4C36A96270C6EC200CCC69C /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - F4C36A97270C6EC200CCC69C /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; F4EC38CC273C21E5007045DC /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -673,7 +530,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -730,7 +587,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -740,118 +597,6 @@ }; name = Release; }; - F4C36A9D270C6EC200CCC69C /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = JA825X8F7Z; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = GiniHealthSDKPinningExample/Info.plist; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.gini.healthsdk.pinning.example; - PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - F4C36A9E270C6EC200CCC69C /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = JA825X8F7Z; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = GiniHealthSDKPinningExample/Info.plist; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.gini.healthsdk.pinning.example; - PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - F4DB6424283546960027681C /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = JA825X8F7Z; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.gini.healthsdk.GiniHealthSDKPinningExampleTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GiniHealthSDKPinningExample.app/GiniHealthSDKPinningExample"; - }; - name = Debug; - }; - F4DB6425283546960027681C /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = JA825X8F7Z; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.gini.healthsdk.GiniHealthSDKPinningExampleTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GiniHealthSDKPinningExample.app/GiniHealthSDKPinningExample"; - }; - name = Release; - }; F4EC38E6273C21E6007045DC /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -876,7 +621,7 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportsDocumentBrowser = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -919,7 +664,7 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportsDocumentBrowser = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1008,24 +753,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - F4C36A9C270C6EC200CCC69C /* Build configuration list for PBXNativeTarget "GiniHealthSDKPinningExample" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F4C36A9D270C6EC200CCC69C /* Debug */, - F4C36A9E270C6EC200CCC69C /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - F4DB6426283546960027681C /* Build configuration list for PBXNativeTarget "GiniHealthSDKPinningExampleTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F4DB6424283546960027681C /* Debug */, - F4DB6425283546960027681C /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; F4EC38EC273C21E6007045DC /* Build configuration list for PBXNativeTarget "GiniHealthSDKExample" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1051,10 +778,6 @@ isa = XCSwiftPackageProductDependency; productName = GiniCaptureSDK; }; - F4CEC7EC270CB06D004A862F /* GiniHealthSDKPinning */ = { - isa = XCSwiftPackageProductDependency; - productName = GiniHealthSDKPinning; - }; F4EC38FF273C245E007045DC /* GiniHealthSDK */ = { isa = XCSwiftPackageProductDependency; productName = GiniHealthSDK; diff --git a/HealthAPILibrary/GiniHealthAPILibraryPinning/.swiftpm/xcode/xcshareddata/xcschemes/GiniHealthAPILibraryPinningTests.xcscheme b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample.xcodeproj/xcshareddata/xcschemes/GiniHealthSDKExampleTests.xcscheme similarity index 59% rename from HealthAPILibrary/GiniHealthAPILibraryPinning/.swiftpm/xcode/xcshareddata/xcschemes/GiniHealthAPILibraryPinningTests.xcscheme rename to HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample.xcodeproj/xcshareddata/xcschemes/GiniHealthSDKExampleTests.xcscheme index c440c0f19..4fd034109 100644 --- a/HealthAPILibrary/GiniHealthAPILibraryPinning/.swiftpm/xcode/xcshareddata/xcschemes/GiniHealthAPILibraryPinningTests.xcscheme +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample.xcodeproj/xcshareddata/xcschemes/GiniHealthSDKExampleTests.xcscheme @@ -1,11 +1,22 @@ + LastUpgradeVersion = "1540" + version = "2.1"> + buildImplicitDependencies = "YES" + buildArchitectures = "Automatic"> + + + + + BlueprintIdentifier = "F4EC38BE273C21E4007045DC" + BuildableName = "GiniHealthSDKExample.app" + BlueprintName = "GiniHealthSDKExample" + ReferencedContainer = "container:GiniHealthSDKExample.xcodeproj"> @@ -27,15 +38,21 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + + BlueprintIdentifier = "F4EC38D3273C21E6007045DC" + BuildableName = "GiniHealthSDKExampleTests.xctest" + BlueprintName = "GiniHealthSDKExampleTests" + ReferencedContainer = "container:GiniHealthSDKExample.xcodeproj"> @@ -57,15 +74,6 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES"> - - - - diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample.xcodeproj/xcshareddata/xcschemes/GiniHealthSDKPinningExample.xcscheme b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample.xcodeproj/xcshareddata/xcschemes/GiniHealthSDKPinningExample.xcscheme deleted file mode 100644 index 80bb5c01d..000000000 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample.xcodeproj/xcshareddata/xcschemes/GiniHealthSDKPinningExample.xcscheme +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample.xcodeproj/xcshareddata/xcschemes/GiniHealthSDKPinningExampleTests.xcscheme b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample.xcodeproj/xcshareddata/xcschemes/GiniHealthSDKPinningExampleTests.xcscheme deleted file mode 100644 index f9565b640..000000000 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample.xcodeproj/xcshareddata/xcschemes/GiniHealthSDKPinningExampleTests.xcscheme +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/AppCoordinator.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/AppCoordinator.swift index 015303032..b42d571f3 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/AppCoordinator.swift +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/AppCoordinator.swift @@ -10,6 +10,8 @@ import UIKit import GiniCaptureSDK import GiniHealthAPILibrary import GiniHealthSDK +import GiniInternalPaymentSDK +import GiniUtilites final class AppCoordinator: Coordinator { @@ -23,7 +25,7 @@ final class AppCoordinator: Coordinator { let selectAPIViewController = SelectAPIViewController() selectAPIViewController.delegate = self selectAPIViewController.debugMenuPresenter = self - selectAPIViewController.clientId = self.client.id + selectAPIViewController.clientId = clientID return selectAPIViewController }() @@ -46,22 +48,19 @@ final class AppCoordinator: Coordinator { } return giniConfiguration }() - - private lazy var client: GiniHealthAPILibrary.Client = CredentialsManager.fetchClientFromBundle() - private lazy var apiLib = GiniHealthAPI.Builder(client: client, logLevel: .debug).build() - private lazy var health = GiniHealth(with: apiLib) - private lazy var paymentComponentsController = PaymentComponentsController(giniHealth: health) + + private lazy var health = GiniHealth(id: clientID, secret: clientPassword, domain: clientDomain) + private lazy var giniHealthConfiguration: GiniHealthConfiguration = { let configuration = GiniHealthConfiguration() // Show the close button to dismiss the payment review screen - configuration.showPaymentReviewCloseButton = true configuration.paymentReviewStatusBarStyle = .lightContent return configuration }() var isBrandedPaymentComponent = true - private var documentMetadata: GiniHealthAPILibrary.Document.Metadata? + private var documentMetadata: GiniHealthSDK.Document.Metadata? private let documentMetadataBranchId = "GiniHealthExampleIOS" private let documentMetadataAppFlowKey = "AppFlow" @@ -69,16 +68,13 @@ final class AppCoordinator: Coordinator { self.window = window print("------------------------------------\n\n", "📸 Gini Capture SDK for iOS (\(GiniCapture.versionString))\n\n", - " - Client id: \(client.id)\n", - " - Client email domain: \(client.domain)", + " - Client id: \(clientID)\n", + " - Client email domain: \(clientDomain)", "\n\n------------------------------------\n") } func start() { self.showSelectAPIScreen() - paymentComponentsController.delegate = self - paymentComponentsController.loadPaymentProviders() -// try! apiLib.removeStoredCredentials() } func processExternalDocument(withUrl url: URL, sourceApplication: String?) { @@ -108,10 +104,46 @@ final class AppCoordinator: Coordinator { } } - func processBankUrl() { + func processBankUrl(url: URL) { rootViewController.dismiss(animated: true) - showReturnMessage() + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return } + + if let queryItems = components.queryItems { + if let paymentRequestId = queryItems.first(where: { $0.name == "paymentRequestId" })?.value { + selectAPIViewController.showActivityIndicator() + health.getPaymentRequest(by: paymentRequestId) { [weak self] result in + DispatchQueue.main.async { + self?.selectAPIViewController.hideActivityIndicator() + } + switch result { + case .success(let paymentRequest): + GiniUtilites.Log("Successfully obtained payment request", event: .success) + DispatchQueue.main.async { + self?.showReturnMessage(message: self?.messageFor(status: PaymentStatus(rawValue: paymentRequest.status)) ?? "") + } + case .failure(let error): + GiniUtilites.Log("Failed to retrieve payment request: \(error.localizedDescription)", event: .error) + } + } + } + } + } + + private enum PaymentStatus: String { + case paid + case paidAdjusted = "paid_adjusted" + } + + private func messageFor(status: PaymentStatus?) -> String { + switch status { + case .paid: + return "Payment was successful 🎉" + case .paidAdjusted: + return "Payment was successful 🎉 with adjusted amount" + default: + return "Payment was unsuccessful 😢" + } } fileprivate func showSelectAPIScreen() { @@ -125,27 +157,28 @@ final class AppCoordinator: Coordinator { let screenAPICoordinator = ScreenAPICoordinator(configuration: giniConfiguration, importedDocuments: pages?.map { $0.document }, - client: GiniHealthAPILibrary.Client(id: self.client.id, - secret: self.client.secret, - domain: self.client.domain), + client: GiniHealthAPILibrary.Client(id: clientID, + secret: clientPassword, + domain: clientDomain), documentMetadata: metadata, hardcodedInvoicesController: HardcodedInvoicesController(), - paymentComponentController: paymentComponentsController) + paymentComponentController: health.paymentComponentsController) screenAPICoordinator.delegate = self health.delegate = self screenAPICoordinator.giniHealth = health + let apiLib = health.giniApiLib screenAPICoordinator.start(healthAPI: apiLib) add(childCoordinator: screenAPICoordinator) rootViewController.present(screenAPICoordinator.rootViewController, animated: true) } - private var testDocument: GiniHealthAPILibrary.Document? - private var testDocumentExtractions: [GiniHealthAPILibrary.Extraction]? - + private var testDocument: GiniHealthSDK.Document? + private var testDocumentExtractions: [GiniHealthSDK.Extraction]? + fileprivate func showPaymentReviewWithTestDocument() { health.delegate = self health.setConfiguration(giniHealthConfiguration) @@ -159,16 +192,16 @@ final class AppCoordinator: Coordinator { self.health.documentService.extractions(for: data.document, cancellationToken: CancellationToken()) { [weak self] result in switch result { case let .success(extractionResult): - print("✅Successfully fetched extractions for id: \(document.id)") - let invoice = DocumentWithExtractions(documentID: document.id, + GiniUtilites.Log("Successfully fetched extractions for id: \(document.id)", event: .success) + let invoice = DocumentWithExtractions(documentId: document.id, extractionResult: extractionResult) self?.showInvoicesList(invoices: [invoice]) case let .failure(error): - print("❌Obtaining extractions from document with id \(document.id) failed with error: \(String(describing: error))") + GiniUtilites.Log("Obtaining extractions from document with id \(document.id) failed with error: \(String(describing: error))", event: .error) } } case .failure(let error): - print("❌ Document data fetching failed: \(String(describing: error))") + GiniUtilites.Log("Document data fetching failed: \(String(describing: error))", event: .error) self.selectAPIViewController.hideActivityIndicator() } } @@ -185,7 +218,7 @@ final class AppCoordinator: Coordinator { metadata: nil) { result in switch result { case .success(let createdDocument): - let partialDocInfo = GiniHealthAPILibrary.PartialDocumentInfo(document: createdDocument.links.document) + let partialDocInfo = GiniHealthSDK.PartialDocumentInfo(document: createdDocument.links.document) self.health.documentService.createDocument(fileName: nil, docType: nil, type: .composite(CompositeDocumentInfo(partialDocuments: [partialDocInfo])), @@ -201,26 +234,26 @@ final class AppCoordinator: Coordinator { self?.health.documentService.extractions(for: compositeDocument, cancellationToken: CancellationToken()) { [weak self] result in switch result { case let .success(extractionResult): - print("✅Successfully fetched extractions for id: \(compositeDocument.id)") - let invoice = DocumentWithExtractions(documentID: compositeDocument.id, + GiniUtilites.Log("Successfully fetched extractions for id: \(compositeDocument.id)", event: .success) + let invoice = DocumentWithExtractions(documentId: compositeDocument.id, extractionResult: extractionResult) self?.showInvoicesList(invoices: [invoice]) case let .failure(error): - print("❌Obtaining extractions from document with id \(compositeDocument.id) failed with error: \(String(describing: error))") + GiniUtilites.Log("Obtaining extractions from document with id \(compositeDocument.id) failed with error: \(String(describing: error))", event: .error) } } case .failure(let error): - print("❌ Setting document for review failed: \(String(describing: error))") + GiniUtilites.Log("Setting document for review failed: \(String(describing: error))", event: .error) self?.selectAPIViewController.hideActivityIndicator() } } case .failure(let error): - print("❌ Document creation failed: \(String(describing: error))") + GiniUtilites.Log("Document creation failed: \(String(describing: error))", event: .error) self?.selectAPIViewController.hideActivityIndicator() } } case .failure(let error): - print("❌ Document creation failed: \(String(describing: error))") + GiniUtilites.Log("Document creation failed: \(String(describing: error))", event: .error) self.selectAPIViewController.hideActivityIndicator() } } @@ -252,9 +285,9 @@ final class AppCoordinator: Coordinator { rootViewController.present(alertViewController, animated: true) } - fileprivate func showReturnMessage() { + fileprivate func showReturnMessage(message: String) { let alertViewController = UIAlertController(title: "Congratulations", - message: "Payment was successful 🎉", + message: message, preferredStyle: .alert) alertViewController.addAction(UIAlertAction(title: "OK", style: .default) { _ in @@ -275,24 +308,40 @@ final class AppCoordinator: Coordinator { DispatchQueue.main.async { self.selectAPIViewController.hideActivityIndicator() } - let configuration = GiniHealthConfiguration() - + + giniHealthConfiguration.useInvoiceWithoutDocument = false health.setConfiguration(giniHealthConfiguration) health.delegate = self let invoicesListCoordinator = InvoicesListCoordinator() - paymentComponentsController = PaymentComponentsController(giniHealth: health) - let paymentComponentConfiguration = PaymentComponentConfiguration(isPaymentComponentBranded: isBrandedPaymentComponent) - paymentComponentsController.paymentComponentConfiguration = paymentComponentConfiguration + health.paymentComponentConfiguration.isPaymentComponentBranded = isBrandedPaymentComponent DispatchQueue.main.async { invoicesListCoordinator.start(documentService: self.health.documentService, hardcodedInvoicesController: HardcodedInvoicesController(), - paymentComponentsController: self.paymentComponentsController, + health: self.health, invoices: invoices) self.add(childCoordinator: invoicesListCoordinator) self.rootViewController.present(invoicesListCoordinator.rootViewController, animated: true) } } + + fileprivate func showOrdersList(orders: [Order]? = nil) { + DispatchQueue.main.async { + self.selectAPIViewController.hideActivityIndicator() + } + + giniHealthConfiguration.useInvoiceWithoutDocument = true + health.setConfiguration(giniHealthConfiguration) + health.delegate = self + + let orderListCoordinator = OrderListCoordinator() + orderListCoordinator.start(documentService: health.documentService, + hardcodedOrdersController: HardcodedOrdersController(), + health: health, + orders: orders) + add(childCoordinator: orderListCoordinator) + rootViewController.present(orderListCoordinator.rootViewController, animated: true) + } } // MARK: SelectAPIViewControllerDelegate @@ -308,6 +357,8 @@ extension AppCoordinator: SelectAPIViewControllerDelegate { showPaymentReviewWithTestDocument() case .invoicesList: showInvoicesList() + case .ordersList: + showOrdersList() } } } @@ -332,8 +383,8 @@ extension AppCoordinator: GiniHealthDelegate { return true } - func didCreatePaymentRequest(paymentRequestID: String) { - print("✅ Created payment request with id \(paymentRequestID)") + func didCreatePaymentRequest(paymentRequestId: String) { + GiniUtilites.Log("Created payment request with id \(paymentRequestId)", event: .success) DispatchQueue.main.async { guard let invoicesListCoordinator = self.childCoordinators.first as? InvoicesListCoordinator else { return @@ -343,44 +394,13 @@ extension AppCoordinator: GiniHealthDelegate { } } -// MARK: GiniHealthTrackingDelegate - -extension AppCoordinator: GiniHealthTrackingDelegate { - func onPaymentReviewScreenEvent(event: TrackingEvent) { - switch event.type { - case .onToTheBankButtonClicked: - print("📝 To the banking app button was tapped,\(String(describing: event.info))") - case .onCloseButtonClicked: - print("📝 Close screen was triggered") - case .onCloseKeyboardButtonClicked: - print("📝 Close keyboard was triggered") - } - } -} - -// MARK: PaymentComponentControllerDelegate - -extension AppCoordinator: PaymentComponentsControllerProtocol { - func isLoadingStateChanged(isLoading: Bool) { - if isLoading { - selectAPIViewController.showActivityIndicator() - } else { - selectAPIViewController.hideActivityIndicator() - } - } - - func didFetchedPaymentProviders() { - // - } -} - //MARK: - DebugMenuPresenterDelegate extension AppCoordinator: DebugMenuPresenter { func presentDebugMenu() { - let debugMenuViewController = DebugMenuViewController(giniHealth: health, - giniHealthConfiguration: giniHealthConfiguration, - isBrandedPaymentComponent: isBrandedPaymentComponent) + let debugMenuViewController = DebugMenuViewController(showReviewScreen: giniHealthConfiguration.showPaymentReviewScreen, + useBottomPaymentComponent: giniHealthConfiguration.useBottomPaymentComponentView, + paymentComponentConfiguration: health.paymentComponentConfiguration) debugMenuViewController.delegate = self rootViewController.present(debugMenuViewController, animated: true) } @@ -388,7 +408,19 @@ extension AppCoordinator: DebugMenuPresenter { //MARK: - DebugMenuDelegate extension AppCoordinator: DebugMenuDelegate { - func didChangeBrandedSwitchValue(isOn: Bool) { - isBrandedPaymentComponent = isOn + func didChangeSwitchValue(type: SwitchType, isOn: Bool) { + switch type { + case .showReviewScreen: + giniHealthConfiguration.showPaymentReviewScreen = isOn + case .showBrandedView: + health.paymentComponentConfiguration.isPaymentComponentBranded = isOn + case .useBottomPaymentComponent: + giniHealthConfiguration.useBottomPaymentComponentView = isOn + } + } + + func didPickNewLocalization(localization: GiniLocalization) { + giniHealthConfiguration.customLocalization = localization + health.setConfiguration(giniHealthConfiguration) } } diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/AppDelegate.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/AppDelegate.swift index db8c4a0f8..90290bc91 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/AppDelegate.swift +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/AppDelegate.swift @@ -29,7 +29,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { if (url.host == "payment-requester") { - coordinator.processBankUrl() + coordinator.processBankUrl(url: url) } else { coordinator.processExternalDocument(withUrl: url, sourceApplication: options[.sourceApplication] as? String) } diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/Credentials.plist b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/Credentials.plist deleted file mode 100644 index 13b6e1877..000000000 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/Credentials.plist +++ /dev/null @@ -1,12 +0,0 @@ - - - - - client_domain - *** - client_password - *** - client_id - *** - - diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/CredentialsManager.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/CredentialsManager.swift index 8ce5a393a..b47d94f78 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/CredentialsManager.swift +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/CredentialsManager.swift @@ -6,30 +6,7 @@ // import Foundation -import GiniHealthAPILibrary -public final class CredentialsManager { - public class func fetchClientFromBundle() -> Client { - let clientID = "client_id" - let clientPassword = "client_password" - let clientEmailDomain = "client_domain" - let credentialsPlistPath = Bundle.main.path(forResource: "Credentials", ofType: "plist") - - if let path = credentialsPlistPath, - let keys = NSDictionary(contentsOfFile: path), - let client_id = keys[clientID] as? String, - let client_password = keys[clientPassword] as? String, - let client_email_domain = keys[clientEmailDomain] as? String, - !client_id.isEmpty, !client_password.isEmpty, !client_email_domain.isEmpty { - - return Client(id: client_id, - secret: client_password, - domain: client_email_domain) - } - - print("⚠️ No credentials were fetched from the Credentials.plist file") - return Client(id: "", - secret: "", - domain: "") - } -} +let clientID = "client_id" +let clientPassword = "client_password" +let clientDomain = "client_domain" diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/DebugMenuViewController.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/DebugMenuViewController.swift index 1b1b2e3d3..7ebe031f0 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/DebugMenuViewController.swift +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/DebugMenuViewController.swift @@ -6,28 +6,35 @@ import UIKit import GiniHealthSDK +import GiniInternalPaymentSDK +import GiniUtilites + +enum SwitchType { + case showReviewScreen + case showBrandedView + case useBottomPaymentComponent +} protocol DebugMenuDelegate: AnyObject { - func didChangeBrandedSwitchValue(isOn: Bool) + func didChangeSwitchValue(type: SwitchType, isOn: Bool) + func didPickNewLocalization(localization: GiniLocalization) } class DebugMenuViewController: UIViewController { - private let giniHealth: GiniHealth - private let giniHealthConfiguration: GiniHealthConfiguration private let spacing = 20.0 private let rowHeight = 50.0 - + private lazy var titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false - label.text = "Debug Menu" + label.text = "Gini Health" label.textAlignment = .center label.font = .preferredFont(forTextStyle: .largeTitle) return label }() private lazy var localizationTitleLabel: UILabel = rowTitle("Localization") - + private lazy var localizationPicker: UIPickerView = { let picker = UIPickerView() picker.delegate = self @@ -35,64 +42,65 @@ class DebugMenuViewController: UIViewController { picker.translatesAutoresizingMaskIntoConstraints = false return picker }() - + private lazy var localizationRow: UIStackView = stackView(axis: .horizontal, subviews: [localizationTitleLabel, localizationPicker]) - private lazy var brandedOptionLabel: UILabel = rowTitle("Show Branded View") + private lazy var reviewScreenOptionLabel: UILabel = rowTitle("Show Review Screen") + private var reviewScreenSwitch: UISwitch! + private lazy var reviewScreenRow: UIStackView = stackView(axis: .horizontal, subviews: [reviewScreenOptionLabel, reviewScreenSwitch]) - private lazy var brandedSwitch: UISwitch = { - let mySwitch = UISwitch() - mySwitch.translatesAutoresizingMaskIntoConstraints = false - mySwitch.addTarget(self, action: #selector(switchValueChanged(_:)), for: .valueChanged) - mySwitch.isOn = isBrandedPaymentComponent - return mySwitch - }() + private lazy var brandedOptionLabel: UILabel = rowTitle("Show Branded View") + private var brandedSwitch: UISwitch! + private lazy var brandedEditableRow: UIStackView = stackView(axis: .horizontal, subviews: [brandedOptionLabel, brandedSwitch]) - private lazy var brandedRow: UIStackView = stackView(axis: .horizontal, subviews: [brandedOptionLabel, brandedSwitch]) + private lazy var bottomPaymentComponentOptionLabel: UILabel = rowTitle("Use bottom payment component") + private var bottomPaymentComponentSwitch: UISwitch! + private lazy var bottomPaymentComponentEditableRow: UIStackView = stackView(axis: .horizontal, subviews: [bottomPaymentComponentOptionLabel, bottomPaymentComponentSwitch]) - private var isBrandedPaymentComponent: Bool weak var delegate: DebugMenuDelegate? - init(giniHealth: GiniHealth, giniHealthConfiguration: GiniHealthConfiguration, isBrandedPaymentComponent: Bool) { - self.giniHealth = giniHealth - self.giniHealthConfiguration = giniHealthConfiguration - self.isBrandedPaymentComponent = isBrandedPaymentComponent + init(showReviewScreen: Bool, useBottomPaymentComponent: Bool, paymentComponentConfiguration: PaymentComponentConfiguration) { super.init(nibName: nil, bundle: nil) + self.reviewScreenSwitch = self.switchView(isOn: showReviewScreen) + self.brandedSwitch = self.switchView(isOn: paymentComponentConfiguration.isPaymentComponentBranded) + self.bottomPaymentComponentSwitch = self.switchView(isOn: useBottomPaymentComponent) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func viewDidLoad() { super.viewDidLoad() - + setupUI() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - - if let localiztion = giniHealthConfiguration.customLocalization, let index = GiniLocalization.allCases.firstIndex(of: localiztion) { + + if let localization = GiniHealthConfiguration.shared.customLocalization, let index = GiniLocalization.allCases.firstIndex(of: localization) { localizationPicker.selectRow(index, inComponent: 0, animated: true) } } - + private func setupUI() { view.backgroundColor = UIColor(named: "background") let spacer = UIView() - let mainStackView = stackView(axis: .vertical, subviews: [titleLabel, localizationRow, brandedRow, spacer]) + let mainStackView = stackView(axis: .vertical, subviews: [titleLabel, localizationRow, reviewScreenRow, brandedEditableRow, bottomPaymentComponentEditableRow, spacer]) view.addSubview(mainStackView) - + NSLayoutConstraint.activate([ mainStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: spacing), mainStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -spacing), mainStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: spacing), mainStackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -spacing), - + localizationRow.heightAnchor.constraint(equalToConstant: rowHeight), - brandedRow.heightAnchor.constraint(equalToConstant: rowHeight) + reviewScreenRow.heightAnchor.constraint(equalToConstant: rowHeight), + brandedEditableRow.heightAnchor.constraint(equalToConstant: rowHeight), + bottomPaymentComponentEditableRow.heightAnchor.constraint(equalToConstant: rowHeight) ]) } } @@ -104,7 +112,7 @@ private extension DebugMenuViewController { label.text = title return label } - + func stackView(axis: NSLayoutConstraint.Axis, subviews: [UIView]) -> UIStackView { let stackView = UIStackView(arrangedSubviews: subviews) stackView.translatesAutoresizingMaskIntoConstraints = false @@ -112,30 +120,46 @@ private extension DebugMenuViewController { stackView.spacing = spacing return stackView } + + func switchView(isOn: Bool) -> UISwitch { + let mySwitch = UISwitch() + mySwitch.translatesAutoresizingMaskIntoConstraints = false + mySwitch.addTarget(self, action: #selector(switchValueChanged(_:)), for: .valueChanged) + mySwitch.isOn = isOn + return mySwitch + } } extension DebugMenuViewController: UIPickerViewDelegate, UIPickerViewDataSource { func numberOfComponents(in pickerView: UIPickerView) -> Int { return 1 } - + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { return GiniLocalization.allCases.count } - + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { return GiniLocalization.allCases[row].rawValue } - + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { - giniHealthConfiguration.customLocalization = GiniLocalization.allCases[row] - giniHealth.setConfiguration(giniHealthConfiguration) + delegate?.didPickNewLocalization(localization: GiniLocalization.allCases[row]) } } -// MARK: Branded Switch functions +// MARK: Switch functions private extension DebugMenuViewController { @objc private func switchValueChanged(_ sender: UISwitch) { - delegate?.didChangeBrandedSwitchValue(isOn: sender.isOn) + switch sender { + case reviewScreenSwitch: + delegate?.didChangeSwitchValue(type: .showReviewScreen, isOn: sender.isOn) + case brandedSwitch: + delegate?.didChangeSwitchValue(type: .showBrandedView, isOn: sender.isOn) + case bottomPaymentComponentSwitch: + delegate?.didChangeSwitchValue(type: .useBottomPaymentComponent, isOn: sender.isOn) + default: + break + } } } diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/HealthNetworkingService.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/HealthNetworkingService.swift index a74c02483..1b54798a0 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/HealthNetworkingService.swift +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/HealthNetworkingService.swift @@ -21,7 +21,8 @@ class HealthNetworkingService: GiniCaptureNetworkService { id: doc.id, name: doc.name, links: links, - sourceClassification: sourceClassification, + pageCount: doc.pageCount, + sourceClassification: sourceClassification, expirationDate: nil) } diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/HardcodedInvoices/HardcodedInvoices.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/HardcodedInvoices/HardcodedInvoices.swift index e9f6fa17b..374aea64f 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/HardcodedInvoices/HardcodedInvoices.swift +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/HardcodedInvoices/HardcodedInvoices.swift @@ -6,14 +6,15 @@ import Foundation -import GiniHealthAPILibrary +import GiniHealthSDK +import GiniUtilites protocol HardcodedInvoicesControllerProtocol: AnyObject { func obtainInvoicePhotosHardcoded(completion: @escaping (([Data]) -> Void)) func storeInvoicesWithExtractions(invoices: [DocumentWithExtractions]) func getInvoicesWithExtractions() -> [DocumentWithExtractions] func appendInvoiceWithExtractions(invoice: DocumentWithExtractions) - func updateDocumentExtractions(documentID: String, extractions: ExtractionResult) + func updateDocumentExtractions(documentId: String, extractions: ExtractionResult) } final class HardcodedInvoicesController: HardcodedInvoicesControllerProtocol { @@ -68,9 +69,9 @@ final class HardcodedInvoicesController: HardcodedInvoicesControllerProtocol { storeInvoicesWithExtractions(invoices: storedInvoices) } - func updateDocumentExtractions(documentID: String, extractions: ExtractionResult) { + func updateDocumentExtractions(documentId: String, extractions: ExtractionResult) { var invoices = getInvoicesWithExtractions() - if let index = invoices.firstIndex(where: { $0.documentID == documentID }) { + if let index = invoices.firstIndex(where: { $0.documentId == documentId }) { invoices[index].recipient = extractions.payment?.first?.first(where: {$0.name == "payment_recipient"})?.value invoices[index].amountToPay = extractions.payment?.first?.first(where: {$0.name == "amount_to_pay"})?.value } diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoiceTableViewCell.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoiceTableViewCell.swift index aa55e9d2d..dab339568 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoiceTableViewCell.swift +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoiceTableViewCell.swift @@ -19,10 +19,8 @@ final class InvoiceTableViewCell: UITableViewCell { recipientLabel.isHidden = cellViewModel?.isRecipientLabelHidden ?? false dueDateLabel.isHidden = cellViewModel?.isDueDataLabelHidden ?? false - - if cellViewModel?.shouldShowPaymentComponent ?? false, let paymentComponentView = cellViewModel?.paymentComponentView { - mainStackView.addArrangedSubview(paymentComponentView) - } + + addTrustMarkersView() } } @@ -30,7 +28,26 @@ final class InvoiceTableViewCell: UITableViewCell { @IBOutlet private weak var recipientLabel: UILabel! @IBOutlet private weak var dueDateLabel: UILabel! @IBOutlet private weak var amountLabel: UILabel! - + @IBOutlet private weak var ctaButton: UIButton! { + didSet { + ctaButton.roundCorners(corners: .allCorners, radius: Constants.ctaButtonCornerRadius) + } + } + @IBAction func ctaAction(_ sender: Any) { + action?() + } + + let rightStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.spacing = Constants.rightStackViewSpacing + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.isUserInteractionEnabled = false + return stackView + }() + + var action: (() -> Void)? + override func awakeFromNib() { super.awakeFromNib() selectionStyle = .none @@ -38,8 +55,99 @@ final class InvoiceTableViewCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() - if mainStackView.arrangedSubviews.count > 1 { - mainStackView.arrangedSubviews.last?.removeFromSuperview() + removeAllViews(from: rightStackView) + } + + private func addTrustMarkersView() { + if let bankLogosToShow = cellViewModel?.bankLogosToShow { + for bankLogo in bankLogosToShow { + let logoImageView = createLogoImageView(data: bankLogo) + rightStackView.addArrangedSubview(logoImageView) + } + } + if let additionalBankNumberToShow = cellViewModel?.additionalBankNumberToShow { + let badgeView = createBadgeView(withNumber: additionalBankNumberToShow) + rightStackView.addArrangedSubview(badgeView) + } + + if cellViewModel?.bankLogosToShow?.count ?? 0 > 0 { + contentView.addSubview(rightStackView) + NSLayoutConstraint.activate([ + rightStackView.centerYAnchor.constraint(equalTo: ctaButton.centerYAnchor), + rightStackView.trailingAnchor.constraint(equalTo: ctaButton.trailingAnchor, constant: Constants.rightStackViewTrailingConstant), + rightStackView.heightAnchor.constraint(equalToConstant: Constants.rightStackViewHeight) + ]) + + ctaButton.titleEdgeInsets = Constants.ctaButtonTitleEdgeInsets } } + + private func createLogoImageView(data: Data) -> UIImageView { + let image = UIImage(data: data) + let imageView = UIImageView(image: image) + imageView.contentMode = .scaleAspectFill + imageView.layer.cornerRadius = Constants.logoImageViewCornerRadius + imageView.clipsToBounds = true + imageView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + imageView.widthAnchor.constraint(equalToConstant: Constants.logoImageViewSize), + imageView.heightAnchor.constraint(equalToConstant: Constants.logoImageViewSize) + ]) + + return imageView + } + + private func createBadgeView(withNumber number: Int) -> UIView { + let badgeView = UIView() + badgeView.backgroundColor = Constants.badgeViewBackgroundColor + badgeView.layer.cornerRadius = Constants.badgeViewCornerRadius + badgeView.translatesAutoresizingMaskIntoConstraints = false + + let badgeLabel = UILabel() + badgeLabel.text = "+\(number)" + badgeLabel.textColor = Constants.badgeLabelTextColor + badgeLabel.font = Constants.badgeLabelFont + badgeLabel.translatesAutoresizingMaskIntoConstraints = false + + badgeView.addSubview(badgeLabel) + + NSLayoutConstraint.activate([ + badgeLabel.centerXAnchor.constraint(equalTo: badgeView.centerXAnchor), + badgeLabel.centerYAnchor.constraint(equalTo: badgeView.centerYAnchor) + ]) + + NSLayoutConstraint.activate([ + badgeView.widthAnchor.constraint(equalToConstant: Constants.badgeViewSize), + badgeView.heightAnchor.constraint(equalToConstant: Constants.badgeViewSize) + ]) + + return badgeView + } + + func removeAllViews(from stackView: UIStackView) { + for view in stackView.arrangedSubviews { + stackView.removeArrangedSubview(view) // Remove from layout management + view.removeFromSuperview() // Remove from view hierarchy + } + } +} + +// MARK: - Constants + +private extension InvoiceTableViewCell { + enum Constants { + static let ctaButtonCornerRadius: CGFloat = 10 + static let rightStackViewSpacing: CGFloat = 3 + static let rightStackViewTrailingConstant: CGFloat = -60 + static let rightStackViewHeight: CGFloat = 20 + static let ctaButtonTitleEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 50) + static let logoImageViewCornerRadius: CGFloat = 4 + static let logoImageViewSize: CGFloat = 20 + static let badgeViewBackgroundColor = UIColor.lightGray + static let badgeViewCornerRadius: CGFloat = 10 + static let badgeLabelTextColor = UIColor.black + static let badgeLabelFont = UIFont.systemFont(ofSize: 10) + static let badgeViewSize: CGFloat = 20 + } } diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoiceTableViewCell.xib b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoiceTableViewCell.xib index 83e30b2db..a645570fc 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoiceTableViewCell.xib +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoiceTableViewCell.xib @@ -1,9 +1,9 @@ - + - + @@ -12,21 +12,21 @@ - - + + - + - + - + - + - - + - + + + + + + + + + + + + + @@ -76,17 +102,21 @@ + - + + + + diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoiceTableViewCellModel.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoiceTableViewCellModel.swift index 6705c43ff..90063dd88 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoiceTableViewCellModel.swift +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoiceTableViewCellModel.swift @@ -6,18 +6,18 @@ import Foundation -import GiniHealthAPILibrary import GiniHealthSDK +import GiniUtilites import UIKit final class InvoiceTableViewCellModel { private var invoice: DocumentWithExtractions - private var paymentComponentsController: PaymentComponentsController + private var health: GiniHealth init(invoice: DocumentWithExtractions, - paymentComponentsController: PaymentComponentsController) { + health: GiniHealth) { self.invoice = invoice - self.paymentComponentsController = paymentComponentsController + self.health = health } var recipientNameText: String { @@ -32,7 +32,14 @@ final class InvoiceTableViewCellModel { } var dueDateText: String { - [invoice.paymentDueDate ?? "", invoice.doctorName ?? ""].joined(separator: ", ") + var textToReturn: [String] = [] + if let paymentDueDate = invoice.paymentDueDate { + textToReturn.append(paymentDueDate) + } + if let doctorName = invoice.doctorName { + textToReturn.append(doctorName) + } + return textToReturn.joined(separator: ", ") } var isDueDataLabelHidden: Bool { @@ -46,8 +53,12 @@ final class InvoiceTableViewCellModel { var shouldShowPaymentComponent: Bool { invoice.isPayable ?? false } - - var paymentComponentView: UIView { - return paymentComponentsController.paymentView(documentId: invoice.documentID) + + var bankLogosToShow: [Data]? { + health.fetchBankLogos().logos + } + + var additionalBankNumberToShow: Int? { + health.fetchBankLogos().additionalBankCount } } diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoicesListCoordinator.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoicesListCoordinator.swift index 1832747b6..a5f93a529 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoicesListCoordinator.swift +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoicesListCoordinator.swift @@ -6,7 +6,6 @@ import UIKit -import GiniHealthAPILibrary import GiniHealthSDK final class InvoicesListCoordinator: NSObject, Coordinator { @@ -21,15 +20,20 @@ final class InvoicesListCoordinator: NSObject, Coordinator { func start(documentService: DefaultDocumentService, hardcodedInvoicesController: HardcodedInvoicesControllerProtocol, - paymentComponentsController: PaymentComponentsController, + health: GiniHealth, invoices: [DocumentWithExtractions]? = nil) { self.invoicesListViewController = InvoicesListViewController() invoicesListViewController.viewModel = InvoicesListViewModel(coordinator: self, invoices: invoices, documentService: documentService, hardcodedInvoicesController: hardcodedInvoicesController, - paymentComponentsController: paymentComponentsController) + health: health) invoicesListNavigationController = RootNavigationController(rootViewController: invoicesListViewController) + let appearance = UINavigationBarAppearance() + appearance.backgroundColor = UIColor(named: "background") + invoicesListNavigationController.navigationBar.standardAppearance = appearance + invoicesListNavigationController.navigationBar.scrollEdgeAppearance = appearance + invoicesListNavigationController.navigationBar.tintColor = .label invoicesListNavigationController.modalPresentationStyle = .fullScreen invoicesListNavigationController.interactivePopGestureRecognizer?.delegate = nil } diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoicesListViewController.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoicesListViewController.swift index 2669bd269..3d65b5e37 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoicesListViewController.swift +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoicesListViewController.swift @@ -52,11 +52,6 @@ final class InvoicesListViewController: UIViewController { var viewModel: InvoicesListViewModel! // MARK: - Functions - override func viewDidLoad() { - super.viewDidLoad() - viewModel.viewDidLoad() - } - override func loadView() { super.loadView() title = viewModel.titleText @@ -116,11 +111,18 @@ extension InvoicesListViewController: UITableViewDelegate, UITableViewDataSource return UITableViewCell() } let invoiceTableViewCellModel = viewModel.invoices.map { InvoiceTableViewCellModel(invoice: $0, - paymentComponentsController: viewModel.paymentComponentsController) }[indexPath.row] + health: viewModel.health) }[indexPath.row] cell.cellViewModel = invoiceTableViewCellModel + cell.action = { [weak self] in + self?.tapOnAction(documentID: self?.viewModel.invoices[indexPath.row].documentId ?? "") + } return cell } - + + private func tapOnAction(documentID: String) { + viewModel.didTapOnOpenFlow(documentId: documentID) + } + func numberOfSections(in tableView: UITableView) -> Int { if viewModel.invoices.isEmpty { let label = UILabel() @@ -137,7 +139,7 @@ extension InvoicesListViewController: UITableViewDelegate, UITableViewDataSource } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let documentID = viewModel.invoices[indexPath.row].documentID + let documentID = viewModel.invoices[indexPath.row].documentId viewModel.checkForErrors(documentID: documentID) } } @@ -157,7 +159,17 @@ extension InvoicesListViewController: InvoicesListViewControllerProtocol { } func showErrorAlertView(error: String) { - let alertController = UIAlertController(title: viewModel.errorTitleText, + if presentedViewController != nil { + self.presentedViewController?.dismiss(animated: true, completion: { + self.presentAlertViewController(error: error) + }) + } else { + presentAlertViewController(error: error) + } + } + + private func presentAlertViewController(error: String) { + let alertController = UIAlertController(title: viewModel.errorTitleText, message: error, preferredStyle: .alert) alertController.addAction(UIAlertAction(title: "Ok", style: .default)) diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoicesListViewModel.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoicesListViewModel.swift index 1db3ea6d1..e870ef618 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoicesListViewModel.swift +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/InvoicesList/InvoicesListViewModel.swift @@ -6,45 +6,50 @@ import UIKit -import GiniHealthAPILibrary import GiniCaptureSDK import GiniHealthSDK +import GiniUtilites struct DocumentWithExtractions: Codable { - var documentID: String + var documentId: String var amountToPay: String? var paymentDueDate: String? var recipient: String? var isPayable: Bool? var hasMultipleDocuments: Bool? var doctorName: String? + var iban: String? + var purpose: String? - init(documentID: String, extractionResult: GiniHealthAPILibrary.ExtractionResult) { - self.documentID = documentID + init(documentId: String, extractionResult: GiniHealthSDK.ExtractionResult) { + self.documentId = documentId self.amountToPay = extractionResult.payment?.first?.first(where: { $0.name == ExtractionType.amountToPay.rawValue })?.value self.paymentDueDate = extractionResult.extractions.first(where: { $0.name == ExtractionType.paymentDueDate.rawValue })?.value self.recipient = extractionResult.payment?.first?.first(where: { $0.name == ExtractionType.paymentRecipient.rawValue })?.value self.isPayable = extractionResult.extractions.first(where: { $0.name == ExtractionType.paymentState.rawValue })?.value == PaymentState.payable.rawValue self.hasMultipleDocuments = extractionResult.extractions.first(where: { $0.name == ExtractionType.containsMultipleDocs.rawValue })?.value == true.description self.doctorName = extractionResult.extractions.first(where: { $0.name == ExtractionType.doctorName.rawValue })?.value + self.iban = extractionResult.payment?.first?.first(where: { $0.name == ExtractionType.iban.rawValue })?.value + self.purpose = extractionResult.payment?.first?.first(where: { $0.name == ExtractionType.paymentPurpose.rawValue })?.value } } final class InvoicesListViewModel { private let coordinator: InvoicesListCoordinator - private var documentService: GiniHealthAPILibrary.DefaultDocumentService + private var documentService: GiniHealthSDK.DefaultDocumentService private let hardcodedInvoicesController: HardcodedInvoicesControllerProtocol - var paymentComponentsController: PaymentComponentsController + var health: GiniHealth + private let giniHealthConfiguration = GiniHealthConfiguration.shared var invoices: [DocumentWithExtractions] - let noInvoicesText = NSLocalizedString("giniHealthSDKExample.invoicesList.missingInvoices.text", comment: "") - let titleText = NSLocalizedString("giniHealthSDKExample.invoicesList.title", comment: "") - let uploadInvoicesText = NSLocalizedString("giniHealthSDKExample.uploadInvoices.button.title", comment: "") - let cancelText = NSLocalizedString("giniHealthSDKExample.cancel.button.title", comment: "") - let errorTitleText = NSLocalizedString("giniHealthSDKExample.invoicesList.error", comment: "") + let noInvoicesText = NSLocalizedString("gini.health.example.invoicesList.missingInvoices.text", comment: "") + let titleText = NSLocalizedString("gini.health.example.invoicesList.title", comment: "") + let uploadInvoicesText = NSLocalizedString("gini.health.example.uploadInvoices.button.title", comment: "") + let cancelText = NSLocalizedString("gini.health.example.cancel.button.title", comment: "") + let errorTitleText = NSLocalizedString("gini.health.example.invoicesList.error", comment: "") let backgroundColor: UIColor = GiniColor(light: .white, dark: .black).uiColor() @@ -56,42 +61,36 @@ final class InvoicesListViewModel { let dispatchGroup = DispatchGroup() var shouldRefetchExtractions = false - var documentIDToRefetch: String? + var documentIdToRefetch: String? init(coordinator: InvoicesListCoordinator, invoices: [DocumentWithExtractions]? = nil, - documentService: GiniHealthAPILibrary.DefaultDocumentService, + documentService: GiniHealthSDK.DefaultDocumentService, hardcodedInvoicesController: HardcodedInvoicesControllerProtocol, - paymentComponentsController: PaymentComponentsController) { + health: GiniHealth) { self.coordinator = coordinator self.hardcodedInvoicesController = hardcodedInvoicesController self.invoices = invoices ?? hardcodedInvoicesController.getInvoicesWithExtractions() self.documentService = documentService - self.paymentComponentsController = paymentComponentsController - self.paymentComponentsController.delegate = self - self.paymentComponentsController.viewDelegate = self - self.paymentComponentsController.bottomViewDelegate = self - } - - func viewDidLoad() { - paymentComponentsController.loadPaymentProviders() + self.health = health + self.health.paymentDelegate = self } func refetchExtractions() { guard shouldRefetchExtractions else { return } - guard let documentIDToRefetch else { return } + guard let documentIdToRefetch else { return } DispatchQueue.main.async { self.coordinator.invoicesListViewController?.showActivityIndicator() } - self.documentService.fetchDocument(with: documentIDToRefetch) { [weak self] result in + self.documentService.fetchDocument(with: documentIdToRefetch) { [weak self] result in switch result { case .success(let document): self?.documentService.extractions(for: document, cancellationToken: CancellationToken()) { resultExtractions in switch resultExtractions { case .success(let extractions): self?.shouldRefetchExtractions = false - self?.documentIDToRefetch = nil - self?.hardcodedInvoicesController.updateDocumentExtractions(documentID: document.id, extractions: extractions) + self?.documentIdToRefetch = nil + self?.hardcodedInvoicesController.updateDocumentExtractions(documentId: document.id, extractions: extractions) self?.invoices = self?.hardcodedInvoicesController.getInvoicesWithExtractions() ?? [] DispatchQueue.main.async { self?.coordinator.invoicesListViewController?.hideActivityIndicator() @@ -124,6 +123,7 @@ final class InvoicesListViewModel { if !errors.isEmpty { let uniqueErrorMessages = Array(Set(errors)) DispatchQueue.main.async { + self.coordinator.invoicesListViewController.hideActivityIndicator() self.coordinator.invoicesListViewController.showErrorAlertView(error: uniqueErrorMessages.joined(separator: ", ")) } errors = [] @@ -152,22 +152,22 @@ final class InvoicesListViewModel { metadata: nil) { [weak self] result in switch result { case .success(let createdDocument): - Log("Successfully created document with id: \(createdDocument.id)", event: .success) + GiniUtilites.Log("Successfully created document with id: \(createdDocument.id)", event: .success) self?.documentService.extractions(for: createdDocument, cancellationToken: CancellationToken()) { [weak self] result in switch result { case let .success(extractionResult): - Log("Successfully fetched extractions for id: \(createdDocument.id)", event: .success) - self?.invoices.append(DocumentWithExtractions(documentID: createdDocument.id, + GiniUtilites.Log("Successfully fetched extractions for id: \(createdDocument.id)", event: .success) + self?.invoices.append(DocumentWithExtractions(documentId: createdDocument.id, extractionResult: extractionResult)) case let .failure(error): - Log("Obtaining extractions from document with id \(createdDocument.id) failed with error: \(String(describing: error))", event: .error) + GiniUtilites.Log("Obtaining extractions from document with id \(createdDocument.id) failed with error: \(String(describing: error))", event: .error) self?.errors.append(error.message) } self?.dispatchGroup.leave() } case .failure(let error): - Log("Document creation failed: \(String(describing: error))", event: .error) + GiniUtilites.Log("Document creation failed: \(String(describing: error))", event: .error) self?.errors.append(error.message) self?.dispatchGroup.leave() } @@ -182,9 +182,9 @@ final class InvoicesListViewModel { } private func checkDocumentIsPayable(documentID: String) { - if let document = invoices.first(where: { $0.documentID == documentID }) { + if let document = invoices.first(where: { $0.documentId == documentID }) { if !(document.isPayable ?? false) { - errors.append("\(NSLocalizedStringPreferredFormat("giniHealthSDKExample.error.invoice.not.payable", comment: ""))") + errors.append("\(NSLocalizedStringPreferredFormat("gini.health.example.error.invoice.not.payable", comment: ""))") showErrorsIfAny() } } @@ -192,9 +192,9 @@ final class InvoicesListViewModel { @discardableResult private func checkDocumentForMultipleInvoices(documentID: String) -> Bool { - if let document = invoices.first(where: { $0.documentID == documentID }) { + if let document = invoices.first(where: { $0.documentId == documentID }) { if document.hasMultipleDocuments ?? false { - errors.append("\(NSLocalizedStringPreferredFormat("giniHealthSDKExample.error.contains.multiple.invoices", comment: ""))") + errors.append("\(NSLocalizedStringPreferredFormat("gini.health.example.error.contains.multiple.invoices", comment: ""))") showErrorsIfAny() return true } @@ -203,45 +203,24 @@ final class InvoicesListViewModel { } } -extension InvoicesListViewModel: PaymentComponentViewProtocol { - - func didTapOnMoreInformation(documentId: String?) { - Log("Tapped on More Information", event: .success) +extension InvoicesListViewModel { + func didTapOnOpenFlow(documentId: String?) { + documentIdToRefetch = documentId guard !checkDocumentForMultipleInvoices(documentID: documentId ?? "") else { return } - let paymentInfoViewController = paymentComponentsController.paymentInfoViewController() - if let presentedViewController = self.coordinator.invoicesListViewController.presentedViewController { - presentedViewController.dismiss(animated: true) { - self.coordinator.invoicesListViewController.navigationController?.pushViewController(paymentInfoViewController, animated: true) - } - } else { - self.coordinator.invoicesListViewController.navigationController?.pushViewController(paymentInfoViewController, animated: true) - } - } - - func didTapOnBankPicker(documentId: String?) { - guard let documentId else { return } - guard !checkDocumentForMultipleInvoices(documentID: documentId) else { return } - Log("Tapped on Bank Picker on :\(documentId)", event: .success) - let bankSelectionBottomSheet = paymentComponentsController.bankSelectionBottomSheet() - bankSelectionBottomSheet.modalPresentationStyle = .overFullScreen - self.coordinator.invoicesListViewController.present(bankSelectionBottomSheet, animated: false) + health.startPaymentFlow(documentId: documentId, paymentInfo: obtainPaymentInfo(for: documentId), navigationController: self.coordinator.invoicesListNavigationController, trackingDelegate: self) } - func didTapOnPayInvoice(documentId: String?) { - guard let documentId else { return } - guard !checkDocumentForMultipleInvoices(documentID: documentId) else { return } - Log("Tapped on Pay Invoice on :\(documentId)", event: .success) - documentIDToRefetch = documentId - paymentComponentsController.loadPaymentReviewScreenFor(documentID: documentId, trackingDelegate: self) { [weak self] viewController, error in - if let error { - self?.errors.append(error.localizedDescription) - self?.showErrorsIfAny() - } else if let viewController { - viewController.modalTransitionStyle = .coverVertical - viewController.modalPresentationStyle = .overCurrentContext - self?.coordinator.invoicesListViewController.present(viewController, animated: true) - } + private func obtainPaymentInfo(for documentId: String?) -> PaymentInfo? { + guard let index = invoices.firstIndex(where: { $0.documentId == documentId }) else { + return nil } + return PaymentInfo(recipient: invoices[index].recipient ?? "", + iban: invoices[index].iban ?? "", + bic: "", + amount: invoices[index].amountToPay ?? "", + purpose: invoices[index].purpose ?? "", + paymentUniversalLink: health.paymentComponentsController.selectedPaymentProvider?.universalLinkIOS ?? "", + paymentProviderId: health.paymentComponentsController.selectedPaymentProvider?.id ?? "") } } @@ -263,32 +242,14 @@ extension InvoicesListViewModel: PaymentComponentsControllerProtocol { } } -extension InvoicesListViewModel: PaymentProvidersBottomViewProtocol { - func didSelectPaymentProvider(paymentProvider: PaymentProvider) { - DispatchQueue.main.async { - self.coordinator.invoicesListViewController.presentedViewController?.dismiss(animated: true) - self.coordinator.invoicesListViewController.reloadTableView() - } - } - - func didTapOnClose() { - DispatchQueue.main.async { - self.coordinator.invoicesListViewController.presentedViewController?.dismiss(animated: true) - } - } -} - extension InvoicesListViewModel: GiniHealthTrackingDelegate { func onPaymentReviewScreenEvent(event: TrackingEvent) { switch event.type { case .onToTheBankButtonClicked: self.shouldRefetchExtractions = true - Log("To the banking app button was tapped,\(String(describing: event.info))", event: .success) - case .onCloseButtonClicked: - refetchExtractions() - Log("Close screen was triggered", event: .success) + GiniUtilites.Log("To the banking app button was tapped,\(String(describing: event.info))", event: .success) case .onCloseKeyboardButtonClicked: - Log("Close keyboard was triggered", event: .success) + GiniUtilites.Log("Close keyboard was triggered", event: .success) } } } diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/HardcodedOrders.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/HardcodedOrders.swift new file mode 100644 index 000000000..22e16c1d0 --- /dev/null +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/HardcodedOrders.swift @@ -0,0 +1,45 @@ +// +// HardcodedOrdersControllerProtocol.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + + +import Foundation +import GiniUtilites + +protocol HardcodedOrdersControllerProtocol { + var orders: [Order] { get } +} + +class Order: Codable { + var amountToPay = "" + var recipient = "" + var iban = "" + var purpose = "" + + var price: Price { + Price(extractionString: amountToPay) ?? + Price(value: .zero, currencyCode: "€") + } + + convenience init(amountToPay: String, recipient: String, iban: String, purpose: String) { + self.init() + self.amountToPay = amountToPay + self.recipient = recipient + self.iban = iban + self.purpose = purpose + } +} + +final class HardcodedOrdersController: HardcodedOrdersControllerProtocol { + + var orders: [Order] { + [Order(amountToPay: "709.97:€", recipient: "OTTO GMBH & CO KG", iban: "DE75201207003100124444", purpose: "RF7411164022"), + Order(amountToPay: "54.97:€", recipient: "Tchibo GmbH", iban: "DE14200800000816170700", purpose: "10020302020"), + Order(amountToPay: "126.62:€", recipient: "Zalando SE", iban: "DE86210700200123010101", purpose: "938929192"), + Order(amountToPay: "114.88:€", recipient: "bonprix Handelsgesellschaft mbH", iban: "DE68201207003100755555", purpose: "020329984871123"), + Order(amountToPay: "80.13:€", recipient: "Klarna", iban: "DE13760700120500154000", purpose: "00425818528311423079")] + } +} diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/OrderCellViewModel.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/OrderCellViewModel.swift new file mode 100644 index 000000000..24ec8c50a --- /dev/null +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/OrderCellViewModel.swift @@ -0,0 +1,35 @@ +// +// OrderCellViewModel.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + + +import Foundation +import GiniUtilites +import UIKit + +final class OrderCellViewModel { + private var order: Order + + init(_ order: Order) { + self.order = order + } + + var recipientNameText: String { + order.recipient + } + + var amountToPayText: String { + Price(extractionString: order.amountToPay)?.string ?? "" + } + + var ibanText: String { + order.iban + } + + var isRecipientLabelHidden: Bool { + recipientNameText.isEmpty + } +} diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/OrderDetailView.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/OrderDetailView.swift new file mode 100644 index 000000000..24144f9f8 --- /dev/null +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/OrderDetailView.swift @@ -0,0 +1,138 @@ +// +// OrderDetailView.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + + +import UIKit +import GiniUtilites + +class OrderDetailView: UIStackView { + + public static var textFields = [String: UITextField]() + private static var amountTextField = UITextField() + public var order: Order? + + convenience init(_ items: [(String, String)]) { + Self.textFields.removeAll() + self.init(arrangedSubviews: items.map { Self.view(for: $0) }) + + translatesAutoresizingMaskIntoConstraints = false + axis = .vertical + distribution = .fill + alignment = .fill + spacing = Constants.verticalSpacing + + Self.amountTextField.delegate = self + } + + private class func view(for text: (String, String)) -> UIView { + + let horizontalStackView = UIStackView() + horizontalStackView.translatesAutoresizingMaskIntoConstraints = false + horizontalStackView.axis = .horizontal + horizontalStackView.distribution = .fill + horizontalStackView.spacing = 0 + + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = text.0 + label.numberOfLines = 0 + label.textAlignment = .left + label.widthAnchor.constraint(equalToConstant: Constants.labelWidth).isActive = true + horizontalStackView.addArrangedSubview(label) + + let textField = UITextField() + textFields[text.0] = textField + + if text.0 == NSLocalizedString(Fields.amountToPay.rawValue, comment: "") { + textField.keyboardType = .decimalPad + amountTextField = textField + } + + textField.translatesAutoresizingMaskIntoConstraints = false + textField.text = text.1 + textField.clearButtonMode = .whileEditing + textField.font = .systemFont(ofSize: UIFont.labelFontSize) + horizontalStackView.addArrangedSubview(textField) + + let containerView = UIView() + containerView.translatesAutoresizingMaskIntoConstraints = false + containerView.backgroundColor = .systemBackground + containerView.addSubview(horizontalStackView) + + let bottomLine = UIView() + bottomLine.translatesAutoresizingMaskIntoConstraints = false + bottomLine.backgroundColor = .separator + containerView.addSubview(bottomLine) + + NSLayoutConstraint.activate([ + horizontalStackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: Constants.paddingLeadingTrailing), + horizontalStackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -Constants.paddingLeadingTrailing), + horizontalStackView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: Constants.paddingTopBottom), + horizontalStackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -Constants.paddingTopBottom), + + bottomLine.heightAnchor.constraint(equalToConstant: Constants.separatorHeight), + bottomLine.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: Constants.paddingLeadingTrailing), + bottomLine.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + bottomLine.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: Constants.separatorHeight) + ]) + return containerView + } +} + +// MARK: - UITextFieldDelegate + +extension OrderDetailView: UITextFieldDelegate { + /** + Dissmiss the keyboard when return key pressed + */ + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + return true + } + + /** + Updates amoutToPay, formated string with a currency and removes "0.00" value + */ + func updateAmoutToPayWithCurrencyFormat() { + let textField = Self.amountTextField + if textField.hasText, let text = textField.text { + if let priceValue = text.decimal(), + var price = order?.price { + price.value = priceValue + + order?.amountToPay = price.extractionString + textField.text = priceValue > 0 ? price.string : "" + } + } + } + func textFieldDidBeginEditing(_ textField: UITextField) { + // remove currency symbol and whitespaces for edit mode + let amountToPayText = order?.price.stringWithoutSymbol ?? "" + Self.amountTextField.text = amountToPayText + } + + func textFieldDidEndEditing(_ textField: UITextField) { + // add currency format when edit is finished + updateAmoutToPayWithCurrencyFormat() + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + return true + } + + func textFieldDidChangeSelection(_ textField: UITextField) {} +} + +extension OrderDetailView { + enum Constants { + static let labelWidth = 92.0 + static let verticalSpacing = 1.0 + static let paddingLeadingTrailing = 16.0 + static let paddingTopBottom = 16.0 + static let separatorHeight = 0.5 + } +} diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/OrderDetailViewController.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/OrderDetailViewController.swift new file mode 100644 index 000000000..493e5a3ad --- /dev/null +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/OrderDetailViewController.swift @@ -0,0 +1,209 @@ +// +// OrderDetailViewController.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + + +import UIKit +import GiniInternalPaymentSDK +import GiniUtilites +import GiniHealthSDK + +enum Fields: String, CaseIterable { + case recipient = "gini.health.example.order.detail.recipient" + case iban = "gini.health.example.order.detail.iban" + case amountToPay = "gini.health.example.order.detail.amount" + case purpose = "gini.health.example.order.detail.purpose" +} + +final class OrderDetailViewController: UIViewController { + + private var order: Order + + private let health: GiniHealth + private let giniHealthConfiguration = GiniHealthConfiguration.shared + + private var errors: [String] = [] + private let errorTitleText = NSLocalizedString("gini.health.example.invoicesList.error", comment: "") + + private var rowItems: [(String, String)] { + [(Fields.recipient.rawValue, order.recipient), + (Fields.iban.rawValue, order.iban), + (Fields.amountToPay.rawValue, order.amountToPay), + (Fields.purpose.rawValue, order.purpose)].map { + (NSLocalizedString($0, comment: ""), $1) + } + } + + private var detaiViewConstraints: [NSLayoutConstraint] { [ + detailView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: Constants.paddingTopBottom), + detailView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Constants.paddingLeadingTrailing), + detailView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Constants.paddingLeadingTrailing)] + } + + init(_ order: Order, health: GiniHealth) { + self.order = order + self.health = health + super.init(nibName: nil, bundle: nil) + + detailView.order = order + + view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapOnView))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private lazy var detailView: OrderDetailView = { + OrderDetailView(rowItems) + }() + + private lazy var payButton: UIButton = { + let button = UIButton(type: .system) + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle(NSLocalizedString("gini.health.example.order.detail.pay", comment: ""), for: .normal) + button.setTitleColor(.white, for: .normal) + button.backgroundColor = .systemBlue + button.layer.cornerRadius = Constants.payButtonCornerRadius + button.addTarget(self, action: #selector(payButtonTapped), for: .touchUpInside) + button.titleLabel?.font = .boldSystemFont(ofSize: UIFont.labelFontSize) + + return button + }() + + override func viewDidLoad() { + super.viewDidLoad() + + title = NSLocalizedString("gini.health.example.order.navigation.order.details", comment: "") + + view.backgroundColor = .secondarySystemBackground + view.addSubview(detailView) + view.addSubview(payButton) + + setupConstraints() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if #available(iOS 14.0, *) { + navigationController?.navigationBar.topItem?.backButtonDisplayMode = .minimal + } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + saveTextFieldData() + } + + public func setAmount(_ amount: String) { + order.amountToPay = amount + + detailView.removeFromSuperview() + detailView = OrderDetailView(rowItems) + detailView.order = order + view.addSubview(detailView) + + NSLayoutConstraint.activate(detaiViewConstraints) + } + + private func setupConstraints() { + NSLayoutConstraint.activate([ + payButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -Constants.paddingTopBottom), + payButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + payButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Constants.paddingLeadingTrailing), + payButton.heightAnchor.constraint(equalToConstant: Constants.payButtonHeight) + ] + detaiViewConstraints) + } + + @objc private func payButtonTapped() { + GiniUtilites.Log("Tapped on Pay", event: .success) + view.endEditing(true) + + let paymentInfo = obtainPaymentInfo() + if paymentInfo.isComplete && order.price.value != .zero { + guard let navigationController else { return } + health.startPaymentFlow(documentId: nil, paymentInfo: obtainPaymentInfo(), navigationController: navigationController, trackingDelegate: self) + } else { + showErrorAlertView(error: NSLocalizedString("gini.health.example.order.detail.alert.field.error", comment: "")) + } + } + + @objc private func didTapOnView() { + view.endEditing(true) + } + + private func saveTextFieldData() { + let textFields = OrderDetailView.textFields + order.iban = textFields[NSLocalizedString(Fields.iban.rawValue, comment: "")]?.text ?? "" + order.recipient = textFields[NSLocalizedString(Fields.recipient.rawValue, comment: "")]?.text ?? "" + order.purpose = textFields[NSLocalizedString(Fields.purpose.rawValue, comment: "")]?.text ?? "" + + var text = textFields[NSLocalizedString(Fields.amountToPay.rawValue, comment: "")]?.text ?? "" + text = text.replacingOccurrences(of: ",", with: ".") + if let decimalAmount = Decimal(string: text) { + var price = Price(extractionString: order.amountToPay) ?? Price(value: decimalAmount, currencyCode: "€") + price.value = decimalAmount + + order.amountToPay = price.extractionString + } else { + order.amountToPay = Price(value: .zero, currencyCode: "€").extractionString + } + } + + private func obtainPaymentInfo() -> GiniHealthSDK.PaymentInfo { + saveTextFieldData() + + return PaymentInfo(recipient: order.recipient, + iban: order.iban, + bic: "", + amount: order.amountToPay, + purpose: order.purpose, + paymentUniversalLink: health.paymentComponentsController.selectedPaymentProvider?.universalLinkIOS ?? "", + paymentProviderId: health.paymentComponentsController.selectedPaymentProvider?.id ?? "") + } + + private func showErrorsIfAny() { + if !errors.isEmpty { + let uniqueErrorMessages = Array(Set(errors)) + DispatchQueue.main.async { + self.showErrorAlertView(error: uniqueErrorMessages.joined(separator: ", ")) + } + errors = [] + } + } + + private func showErrorAlertView(error: String) { + let alertController = UIAlertController(title: errorTitleText, + message: error, + preferredStyle: .alert) + alertController.addAction( + UIAlertAction(title: NSLocalizedString("gini.health.example.order.detail.alert.ok", comment: ""), + style: .default) + ) + self.present(alertController, animated: true) + } +} + +extension OrderDetailViewController: GiniHealthTrackingDelegate { + func onPaymentReviewScreenEvent(event: TrackingEvent) { + switch event.type { + case .onToTheBankButtonClicked: + GiniUtilites.Log("To the banking app button was tapped,\(String(describing: event.info))", event: .success) + case .onCloseKeyboardButtonClicked: + GiniUtilites.Log("Close keyboard was triggered", event: .success) + } + } +} + +extension OrderDetailViewController { + enum Constants { + static let paddingTopBottom = 8.0 + static let paddingLeadingTrailing = 16.0 + static let payButtonHeight = 50.0 + static let payButtonCornerRadius = 14.0 + } +} diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/OrderListCoordinator.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/OrderListCoordinator.swift new file mode 100644 index 000000000..2e4fe4567 --- /dev/null +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/OrderListCoordinator.swift @@ -0,0 +1,44 @@ +// +// OrderListCoordinator.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + + +import UIKit +import GiniHealthSDK + +final class OrderListCoordinator: NSObject, Coordinator { + + var childCoordinators: [Coordinator] = [] + var rootViewController: UIViewController { + orderListNavigationController + } + + var orderListNavigationController: UINavigationController! + var orderListViewController: OrderListViewController! + + func start(documentService: GiniHealthSDK.DefaultDocumentService, + hardcodedOrdersController: HardcodedOrdersControllerProtocol, + health: GiniHealth, + orders: [Order]? = nil) { + self.orderListViewController = OrderListViewController() + orderListViewController.viewModel = OrderListViewModel(coordinator: self, + orders: orders, + documentService: documentService, + hardcodedOrdersController: hardcodedOrdersController, + health: health) + orderListNavigationController = RootNavigationController(rootViewController: orderListViewController) + orderListNavigationController.modalPresentationStyle = .fullScreen + orderListNavigationController.interactivePopGestureRecognizer?.delegate = nil + + orderListNavigationController.navigationBar.backgroundColor = .white + orderListNavigationController.navigationBar.isTranslucent = false + + let appearance = UINavigationBarAppearance() + orderListNavigationController.navigationBar.standardAppearance = appearance + orderListNavigationController.navigationBar.scrollEdgeAppearance = appearance + orderListNavigationController.navigationBar.tintColor = .label + } +} diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/OrderListViewController.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/OrderListViewController.swift new file mode 100644 index 000000000..8cfe0b300 --- /dev/null +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/OrderListViewController.swift @@ -0,0 +1,161 @@ +// +// OrderListViewController.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + + +import UIKit + +protocol OrderListViewControllerProtocol: AnyObject { + func showActivityIndicator() + func hideActivityIndicator() + func reloadTableView() + func showErrorAlertView(error: String) +} + +final class OrderListViewController: UIViewController { + + private lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .plain) + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.dataSource = self + tableView.delegate = self + tableView.register(OrderTableViewCell.self, forCellReuseIdentifier: OrderTableViewCell.identifier) + tableView.separatorInset = .zero + tableView.rowHeight = Constants.rowHeight + tableView.tableFooterView = UIView() + return tableView + }() + + private lazy var activityIndicator: UIActivityIndicatorView = { + let activityIndicator = UIActivityIndicatorView() + activityIndicator.style = .large + activityIndicator.center = view.center + activityIndicator.hidesWhenStopped = true + return activityIndicator + }() + + var viewModel: OrderListViewModel! + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemBackground + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + tableView.reloadData() + } + + override func loadView() { + super.loadView() + title = viewModel.titleText + setupTableView() + setupNavigationBar() + } + + private func setupTableView() { + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0 + } + view.addSubview(tableView) + + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: Constants.padding), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Constants.padding), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Constants.padding), + tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -Constants.padding) + ]) + } + + private func setupNavigationBar() { + navigationItem.rightBarButtonItem = UIBarButtonItem( + title: viewModel.customOrderText, + style: .plain, + target: self, + action: #selector(customOrderButtonTapped) + ) + + navigationItem.leftBarButtonItem = UIBarButtonItem( + title: viewModel.cancelText, + style: .plain, + target: self, + action: #selector(dismissViewControllerTapped) + ) + } + + @objc func customOrderButtonTapped() { + let newOrder = Order(amountToPay: "", recipient: "", iban: "", purpose: "") + viewModel.orders.append(newOrder) + + let orderViewController = OrderDetailViewController(newOrder, health: viewModel.health) + self.navigationController?.pushViewController(orderViewController, animated: true) + } + + @objc func dismissViewControllerTapped() { + self.dismiss(animated: true) + } +} + +extension OrderListViewController: UITableViewDelegate, UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return viewModel.orders.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: OrderTableViewCell.identifier, for: indexPath) as? OrderTableViewCell else { + return UITableViewCell() + } + cell.viewModel = viewModel.orders.map { OrderCellViewModel($0) }[indexPath.row] + return cell + } + + func numberOfSections(in tableView: UITableView) -> Int { + tableView.backgroundView = nil + tableView.separatorStyle = .singleLine + return 1 + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let order = viewModel.orders[indexPath.row] + + // Instantiate InvoiceViewController with the Order instance + let orderViewController = OrderDetailViewController(order, health: viewModel.health) + + // Present InvoiceViewController + self.navigationController?.pushViewController(orderViewController, animated: true) + } +} + +extension OrderListViewController: OrderListViewControllerProtocol { + func showActivityIndicator() { + self.activityIndicator.startAnimating() + self.view.addSubview(self.activityIndicator) + } + + func hideActivityIndicator() { + self.activityIndicator.stopAnimating() + } + + func reloadTableView() { + self.tableView.reloadData() + } + + func showErrorAlertView(error: String) { + let alertController = UIAlertController(title: viewModel.errorTitleText, + message: error, + preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: "Ok", style: .default)) + self.present(alertController, animated: true) + } +} + +extension OrderListViewController { + private enum Constants { + static let padding: CGFloat = 0 + static let rowHeight: CGFloat = 80 + } +} diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/OrderListViewModel.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/OrderListViewModel.swift new file mode 100644 index 000000000..c3d42fa74 --- /dev/null +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/OrderListViewModel.swift @@ -0,0 +1,42 @@ +// +// OrderListViewModel.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + + +import UIKit +import GiniCaptureSDK +import GiniHealthSDK + +final class OrderListViewModel { + + private let coordinator: OrderListCoordinator + private var documentService: GiniHealthSDK.DefaultDocumentService + private let hardcodedOrdersController: HardcodedOrdersControllerProtocol + + let noInvoicesText = NSLocalizedString("gini.health.example.invoicesList.missingInvoices.text", comment: "") + let titleText = NSLocalizedString("gini.health.example.invoicesList.title", comment: "") + let customOrderText = NSLocalizedString("gini.health.example.custom.order.button.title", comment: "") + let cancelText = NSLocalizedString("gini.health.example.cancel.button.title", comment: "") + let errorTitleText = NSLocalizedString("gini.health.example.invoicesList.error", comment: "") + + private var errors: [String] = [] + + var health: GiniHealth + var orders: [Order] + + init(coordinator: OrderListCoordinator, + orders: [Order]? = nil, + documentService: GiniHealthSDK.DefaultDocumentService, + hardcodedOrdersController: HardcodedOrdersControllerProtocol, + health: GiniHealth) { + self.coordinator = coordinator + self.hardcodedOrdersController = hardcodedOrdersController + self.orders = orders ?? hardcodedOrdersController.orders + self.documentService = documentService + self.health = health + } + +} diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/OrderTableViewCell.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/OrderTableViewCell.swift new file mode 100644 index 000000000..68528b74d --- /dev/null +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/OrdersList/OrderTableViewCell.swift @@ -0,0 +1,90 @@ +// +// OrderTableViewCell.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + + +import UIKit + +final class OrderTableViewCell: UITableViewCell { + + static let identifier = String(describing: OrderTableViewCell.self) + + private let recipientLabel = UILabel() + private let ibanLabel = UILabel() + private let amountLabel = UILabel() + + private lazy var horizontalStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [verticalStackView, amountLabel]) + stackView.axis = .horizontal + stackView.distribution = .equalSpacing + stackView.alignment = .fill + stackView.spacing = Constants.horizontalSpacing + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + private lazy var verticalStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [recipientLabel, ibanLabel]) + stackView.axis = .vertical + stackView.distribution = .fillEqually + stackView.alignment = .leading + stackView.spacing = Constants.verticalSpacing + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + var viewModel: OrderCellViewModel? { + didSet { + guard let viewModel = viewModel else { return } + + recipientLabel.text = viewModel.recipientNameText + recipientLabel.font = UIFont.boldSystemFont(ofSize: UIFont.labelFontSize) + recipientLabel.isHidden = viewModel.isRecipientLabelHidden + + ibanLabel.text = viewModel.ibanText + + amountLabel.text = viewModel.amountToPayText + amountLabel.textColor = UIColor(named: "amountLabelTextColor") + } + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + setupSubViews() + } + + private func setupSubViews() { + selectionStyle = .none + + recipientLabel.translatesAutoresizingMaskIntoConstraints = false + ibanLabel.translatesAutoresizingMaskIntoConstraints = false + amountLabel.translatesAutoresizingMaskIntoConstraints = false + + amountLabel.textAlignment = .right + + contentView.addSubview(horizontalStackView) + + NSLayoutConstraint.activate([ + horizontalStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: Constants.padding), + horizontalStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.padding), + horizontalStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constants.padding), + horizontalStackView.bottomAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.bottomAnchor, constant: -Constants.padding) + ]) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension OrderTableViewCell { + enum Constants { + static let padding = 16.0 + static let horizontalSpacing = 10.0 + static let verticalSpacing = 0.0 + } +} diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/ScreenAPICoordinator.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/ScreenAPICoordinator.swift index cfa9508e1..7723a3184 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/ScreenAPICoordinator.swift +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/ScreenAPICoordinator.swift @@ -9,6 +9,8 @@ import GiniBankAPILibrary import GiniCaptureSDK import GiniHealthSDK import GiniHealthAPILibrary +import GiniInternalPaymentSDK +import GiniUtilites import UIKit protocol ScreenAPICoordinatorDelegate: AnyObject { @@ -93,9 +95,9 @@ final class ScreenAPICoordinator: NSObject, Coordinator, GiniHealthTrackingDeleg healthSdk.documentService.extractions(for: data.document, cancellationToken: CancellationToken()) { [weak self] result in switch result { case let .success(extractionResult): - print("✅Successfully fetched extractions for id: \(docId)") + GiniUtilites.Log("✅Successfully fetched extractions for id: \(docId)", event: .success) // Store invoice/document into Invoices list - let invoice = DocumentWithExtractions(documentID: docId, + let invoice = DocumentWithExtractions(documentId: docId, extractionResult: extractionResult) self?.hardcodedInvoicesController.appendInvoiceWithExtractions(invoice: invoice) DispatchQueue.main.async { @@ -104,11 +106,11 @@ final class ScreenAPICoordinator: NSObject, Coordinator, GiniHealthTrackingDeleg }) } case let .failure(error): - print("❌Obtaining extractions from document with id \(docId) failed with error: \(String(describing: error))") + GiniUtilites.Log("❌Obtaining extractions from document with id \(docId) failed with error: \(String(describing: error))", event: .error) } } case .failure(let error): - print("❌ Document data fetching failed: \(String(describing: error))") + GiniUtilites.Log("❌ Document data fetching failed: \(String(describing: error))", event: .error) } } } diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/SelectAPIViewController.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/SelectAPIViewController.swift index b6d068681..0c526167f 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/SelectAPIViewController.swift +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/SelectAPIViewController.swift @@ -26,6 +26,7 @@ enum GiniCaptureAPIType { case component case paymentReview case invoicesList + case ordersList } /** @@ -39,7 +40,8 @@ final class SelectAPIViewController: UIViewController { @IBOutlet private weak var startWithTestDocumentButton: UIButton! @IBOutlet private weak var startWithGiniCaptureButton: UIButton! @IBOutlet private weak var invoicesListButton: UIButton! - + @IBOutlet private weak var ordersListButton: UIButton! + weak var delegate: SelectAPIViewControllerDelegate? weak var debugMenuPresenter: DebugMenuPresenter? @@ -74,6 +76,10 @@ final class SelectAPIViewController: UIViewController { delegate?.selectAPI(viewController: self, didSelectApi: .invoicesList) } + @IBAction func launchOrdersList(_ sender: Any) { + delegate?.selectAPI(viewController: self, didSelectApi: .ordersList) + } + @objc private func showDebugMenu() { debugMenuPresenter?.presentDebugMenu() } diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/SelectAPIViewController.xib b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/SelectAPIViewController.xib index 73fa326c3..c9f13f179 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/SelectAPIViewController.xib +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/SelectAPIViewController.xib @@ -1,9 +1,9 @@ - + - + @@ -14,6 +14,7 @@ + @@ -24,28 +25,8 @@ - @@ -79,46 +60,91 @@ - - + + + + + + + + + @@ -126,20 +152,17 @@ - + + - - + - - - @@ -161,7 +184,7 @@ - + diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/de.lproj/Localizable.strings b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/de.lproj/Localizable.strings index 31b13d0f1..818ec9492 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/de.lproj/Localizable.strings +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/de.lproj/Localizable.strings @@ -6,10 +6,20 @@ */ -"giniHealthSDKExample.invoicesList.title" = "Rechnungsliste"; -"giniHealthSDKExample.uploadInvoices.button.title" = "↑ Rechnungen"; -"giniHealthSDKExample.invoicesList.missingInvoices.text" = "Keine Rechnungen"; -"giniHealthSDKExample.invoicesList.error" = "Fehler"; -"giniHealthSDKExample.cancel.button.title" = "Abbrechen"; -"giniHealthSDKExample.error.contains.multiple.invoices" = "Dokument enthält mehrere Rechnungen"; -"giniHealthSDKExample.error.invoice.not.payable" = "Rechnung ist nicht zahlbar"; +"gini.health.example.invoicesList.title" = "Rechnungsliste"; +"gini.health.example.uploadInvoices.button.title" = "↑ Rechnungen"; +"gini.health.example.invoicesList.missingInvoices.text" = "Keine Rechnungen"; +"gini.health.example.invoicesList.error" = "Fehler"; +"gini.health.example.cancel.button.title" = "Abbrechen"; +"gini.health.example.error.contains.multiple.invoices" = "Dokument enthält mehrere Rechnungen"; +"gini.health.example.error.invoice.not.payable" = "Rechnung ist nicht zahlbar"; +"gini.health.paymentcomponent.payment.info.gini.privacypolicy.link" = "https://www.google.com/"; // This should be overwrited by client with their privacy policy link +"gini.health.example.order.detail.alert.field.error" = "Die Zahlungsdetails sind unvollständig"; +"gini.health.example.order.detail.alert.ok" = "Ok"; +"gini.health.example.order.detail.amount" = "Betrag"; +"gini.health.example.order.detail.iban" = "IBAN"; +"gini.health.example.order.detail.pay" = "Bezahlen"; +"gini.health.example.order.detail.purpose" = "Verwendungszweck"; +"gini.health.example.order.detail.recipient" = "Empfänger"; +"gini.health.example.order.navigation.order.details" = "Bestelldetails"; +"gini.health.example.custom.order.button.title" = "Kunden Bestellung"; diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/en.lproj/Localizable.strings b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/en.lproj/Localizable.strings index 26fe35ad3..a324238d6 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/en.lproj/Localizable.strings +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExample/en.lproj/Localizable.strings @@ -6,11 +6,20 @@ */ -"giniHealthSDKExample.invoicesList.title" = "Invoices List"; -"giniHealthSDKExample.uploadInvoices.button.title" = "↑ Invoices"; -"giniHealthSDKExample.invoicesList.missingInvoices.text" = "No invoices"; -"giniHealthSDKExample.invoicesList.error" = "Error"; -"giniHealthSDKExample.cancel.button.title" = "Cancel"; -"ginihealth.paymentcomponent.paymentinfo.gini.privacypolicy.link" = "https://www.google.com/"; // This should be overwrited by client with their privacy policy link -"giniHealthSDKExample.error.contains.multiple.invoices" = "Document contains multiple invoices"; -"giniHealthSDKExample.error.invoice.not.payable" = "Invoice is not payable"; +"gini.health.example.invoicesList.title" = "Invoices List"; +"gini.health.example.uploadInvoices.button.title" = "↑ Invoices"; +"gini.health.example.invoicesList.missingInvoices.text" = "No invoices"; +"gini.health.example.invoicesList.error" = "Error"; +"gini.health.example.cancel.button.title" = "Cancel"; +"gini.health.paymentcomponent.payment.info.gini.privacypolicy.link" = "https://www.google.com/"; // This should be overwrited by client with their privacy policy link +"gini.health.example.error.contains.multiple.invoices" = "Document contains multiple invoices"; +"gini.health.example.error.invoice.not.payable" = "Invoice is not payable"; +"gini.health.example.order.detail.alert.field.error" = "Payment details are incomplete"; +"gini.health.example.order.detail.alert.ok" = "Ok"; +"gini.health.example.order.detail.amount" = "Amount"; +"gini.health.example.order.detail.iban" = "IBAN"; +"gini.health.example.order.detail.pay" = "Pay"; +"gini.health.example.order.detail.purpose" = "Purpose"; +"gini.health.example.order.detail.recipient" = "Recipient"; +"gini.health.example.order.navigation.order.details" = "Order Details"; +"gini.health.example.custom.order.button.title" = "Custom Order"; diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/FileLoader.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/FileLoader.swift index 566e33975..8ec61fedc 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/FileLoader.swift +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/FileLoader.swift @@ -6,11 +6,12 @@ import Foundation +import GiniUtilites struct FileLoader { static func loadFile(withName mockFileName: String, ofType fileType: String) -> Data? { guard let filePath = Bundle.main.path(forResource: mockFileName, ofType: fileType) else { - print("File not found.") + GiniUtilites.Log("File not found.", event: .warning) return nil } @@ -19,7 +20,7 @@ struct FileLoader { let data = try Data(contentsOf: fileURL) return data } catch { - print("Error loading file:", error) + GiniUtilites.Log("Error loading file: \(error)", event: .error) return nil } } diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/GiniHealthSDKExampleTests.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/GiniHealthSDKExampleTests.swift index b2d9a1b2f..2721fb1f7 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/GiniHealthSDKExampleTests.swift +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/GiniHealthSDKExampleTests.swift @@ -6,7 +6,7 @@ // import XCTest -@testable import GiniHealthSDKExample +import GiniHealthSDKExample class GiniHealthSDKExampleTests: XCTestCase { diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/GiniHealthSDKExampleTests.xctestplan b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/GiniHealthSDKExampleTests.xctestplan new file mode 100644 index 000000000..d23becf13 --- /dev/null +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/GiniHealthSDKExampleTests.xctestplan @@ -0,0 +1,37 @@ +{ + "configurations" : [ + { + "id" : "E330AA46-C5A5-4423-9B5E-EC21CB065FCC", + "name" : "Test Scheme Action", + "options" : { + "targetForVariableExpansion" : { + "containerPath" : "container:GiniHealthSDKExample.xcodeproj", + "identifier" : "F4EC38BE273C21E4007045DC", + "name" : "GiniHealthSDKExample" + } + } + } + ], + "defaultOptions" : { + "environmentVariableEntries" : [ + { + "key" : "CLIENT_ID", + "value" : "$(CLIENT_ID)" + }, + { + "key" : "CLIENT_SECRET", + "value" : "$(CLIENT_SECRET)" + } + ] + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:GiniHealthSDKExample.xcodeproj", + "identifier" : "F4EC38D3273C21E6007045DC", + "name" : "GiniHealthSDKExampleTests" + } + } + ], + "version" : 1 +} diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExampleTests/GiniHealthSDKPinningExampleIntegrationTests.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/GiniHealthSDKPinningExampleIntegrationTests.swift similarity index 62% rename from HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExampleTests/GiniHealthSDKPinningExampleIntegrationTests.swift rename to HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/GiniHealthSDKPinningExampleIntegrationTests.swift index fd4ede164..53a04283b 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExampleTests/GiniHealthSDKPinningExampleIntegrationTests.swift +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/GiniHealthSDKPinningExampleIntegrationTests.swift @@ -1,15 +1,13 @@ // -// GiniHealthSDKPinningExampleTests.swift -// GiniHealthSDKPinningExampleTests +// GiniHealthSDKPinningExampleIntegrationTests.swift +// GiniHealthSDKExampleTests // // Copyright © 2024 Gini GmbH. All rights reserved. // import XCTest -@testable import GiniHealthSDK +import GiniHealthSDK @testable import GiniHealthAPILibrary -@testable import GiniHealthAPILibraryPinning -@testable import GiniHealthSDKPinning class GiniHealthSDKPinningExampleIntegrationTests: XCTestCase { @@ -36,13 +34,14 @@ class GiniHealthSDKPinningExampleIntegrationTests: XCTestCase { var sdk: GiniHealth! override func setUp() { + let domain = "health-sdk-pinning-example" let client = Client(id: clientId, secret: clientSecret, - domain: "health-sdk-pinning-example") + domain: domain) giniHealthAPILib = GiniHealthAPI .Builder(client: client, pinningConfig: yourPublicPinningConfig) .build() - sdk = GiniHealth.init(with: giniHealthAPILib) + sdk = GiniHealth.init(id: clientId, secret: clientSecret, domain: domain) paymentService = sdk.paymentService } @@ -51,17 +50,17 @@ class GiniHealthSDKPinningExampleIntegrationTests: XCTestCase { XCTAssertEqual(paymentService.apiDomain.domainString, "health-api.gini.net") } - func testCreatePaymentRequest(){ - let expect = expectation(description: "it creates a payment request") - - paymentService.createPaymentRequest(sourceDocumentLocation: "", paymentProvider: "dbe3a2ca-c9df-11eb-a1d8-a7efff6e88b7", recipient: "Dr. med. Hackler", iban: "DE02300209000106531065", bic: "CMCIDEDDXXX", amount: "335.50:EUR", purpose: "ReNr AZ356789Z") { result in - switch result { - case .success: - expect.fulfill() - case let .failure(error): - XCTFail(String(describing: error)) - } - } - wait(for: [expect], timeout: 10) - } +// func testCreatePaymentRequest(){ +// let expect = expectation(description: "it creates a payment request") +// +// paymentService.createPaymentRequest(sourceDocumentLocation: "", paymentProvider: "dbe3a2ca-c9df-11eb-a1d8-a7efff6e88b7", recipient: "Dr. med. Hackler", iban: "DE02300209000106531065", bic: "CMCIDEDDXXX", amount: "335.50:EUR", purpose: "ReNr AZ356789Z") { result in +// switch result { +// case .success: +// expect.fulfill() +// case let .failure(error): +// XCTFail(String(describing: error)) +// } +// } +// wait(for: [expect], timeout: 10) +// } } diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExampleTests/GiniHealthSDKPinningExampleWrongCertificatesTests.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/GiniHealthSDKPinningExampleWrongCertificatesTests.swift similarity index 60% rename from HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExampleTests/GiniHealthSDKPinningExampleWrongCertificatesTests.swift rename to HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/GiniHealthSDKPinningExampleWrongCertificatesTests.swift index 4f4982857..7410110d0 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExampleTests/GiniHealthSDKPinningExampleWrongCertificatesTests.swift +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/GiniHealthSDKPinningExampleWrongCertificatesTests.swift @@ -1,15 +1,13 @@ // // GiniHealthSDKPinningExampleWrongCertificatesTests.swift -// GiniHealthSDKPinningExampleTests +// GiniHealthSDKExampleTests // // Copyright © 2024 Gini GmbH. All rights reserved. // import XCTest -@testable import GiniHealthSDK +import GiniHealthSDK @testable import GiniHealthAPILibrary -@testable import GiniHealthAPILibraryPinning -@testable import GiniHealthSDKPinning class GiniHealthSDKPinningExampleWrongCertificatesTests: XCTestCase { @@ -35,13 +33,14 @@ class GiniHealthSDKPinningExampleWrongCertificatesTests: XCTestCase { var sdk: GiniHealth! override func setUp() { + let domain = "health-sdk-pinning-example" let client = Client(id: clientId, secret: clientSecret, - domain: "health-sdk-pinning-example") + domain: domain) giniHealthAPILib = GiniHealthAPI .Builder(client: client, pinningConfig: yourPublicPinningConfig) .build() - sdk = GiniHealth.init(with: giniHealthAPILib) + sdk = GiniHealth.init(id: clientId, secret: clientSecret, domain: domain) paymentService = sdk.paymentService } @@ -50,18 +49,18 @@ class GiniHealthSDKPinningExampleWrongCertificatesTests: XCTestCase { XCTAssertEqual(paymentService.apiDomain.domainString, "health-api.gini.net") } - func testCreatePaymentRequest(){ - let expect = expectation(description: "it creates a payment request") - - paymentService.createPaymentRequest(sourceDocumentLocation: "", paymentProvider: "dbe3a2ca-c9df-11eb-a1d8-a7efff6e88b7", recipient: "Dr. med. Hackler", iban: "DE02300209000106531065", bic: "CMCIDEDDXXX", amount: "335.50:EUR", purpose: "ReNr AZ356789Z") { result in - switch result { - case .success: - XCTFail("creating a payment request should have failed due to wrong pinning certificates") - case let .failure(error): - XCTAssertEqual(error, GiniError.noResponse) - expect.fulfill() - } - } - wait(for: [expect], timeout: 10) - } +// func testCreatePaymentRequest(){ +// let expect = expectation(description: "it creates a payment request") +// +// paymentService.createPaymentRequest(sourceDocumentLocation: "", paymentProvider: "dbe3a2ca-c9df-11eb-a1d8-a7efff6e88b7", recipient: "Dr. med. Hackler", iban: "DE02300209000106531065", bic: "CMCIDEDDXXX", amount: "335.50:EUR", purpose: "ReNr AZ356789Z") { result in +// switch result { +// case .success: +// XCTFail("creating a payment request should have failed due to wrong pinning certificates") +// case let .failure(error): +// XCTAssertEqual(error, GiniError.noResponse) +// expect.fulfill() +// } +// } +// wait(for: [expect], timeout: 10) +// } } diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/GiniSetupHelper.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/GiniSetupHelper.swift index 48d9fafef..cdeca683f 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/GiniSetupHelper.swift +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/GiniSetupHelper.swift @@ -14,7 +14,10 @@ final class GiniSetupHelper { var giniHealthAPIDocumentService: GiniHealthAPILibrary.DefaultDocumentService! func setup() { - let client: GiniHealthAPILibrary.Client = CredentialsManager.fetchClientFromBundle() + let clientId = ProcessInfo.processInfo.environment["CLIENT_ID"]! + let clientSecret = ProcessInfo.processInfo.environment["CLIENT_SECRET"]! + + let client: GiniHealthAPILibrary.Client = Client(id: clientId, secret: clientSecret, domain: "gini.net") giniHealthAPILib = GiniHealthAPI .Builder(client: client) .build() diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/UploadDocumentsTests.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/UploadDocumentsTests.swift index 2e054c352..7c6b1dc6b 100644 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/UploadDocumentsTests.swift +++ b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKExampleTests/UploadDocumentsTests.swift @@ -17,15 +17,15 @@ class UploadDocumentsTests: XCTestCase { giniHelper.setup() } - func testUploadLargeImageToGiniHealthAPI() { - let expect = expectation(description: "Upload of image above 10MB to HealthAPILibrary with a local compression before") - - guard let imageData12MB = FileLoader.loadFile(withName: "invoice-12MB", ofType: "png") else { return } - - self.uploadDocumentAndGetExtractionFromGiniHealthAPILibrary(data: imageData12MB, expect: expect) - - wait(for: [expect], timeout: 60) - } +// func testUploadLargeImageToGiniHealthAPI() { +// let expect = expectation(description: "Upload of image above 10MB to HealthAPILibrary with a local compression before") +// +// guard let imageData12MB = FileLoader.loadFile(withName: "invoice-12MB", ofType: "png") else { return } +// +// self.uploadDocumentAndGetExtractionFromGiniHealthAPILibrary(data: imageData12MB, expect: expect) +// +// wait(for: [expect], timeout: 60) +// } func testFailUploadLargePDFToGiniHealthAPI() { let expect = expectation(description: "Upload of pdf above 10MB to HealthAPILibrary should fail. Local compression won't be done for this kind of file.") diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/AppDelegate.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/AppDelegate.swift deleted file mode 100644 index 944e7054a..000000000 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/AppDelegate.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// AppDelegate.swift -// HealthSDKPinningExample -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -import UIKit - -@main -class AppDelegate: UIResponder, UIApplicationDelegate { - - - @available(iOS 13.0, *) - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - return true - } - - // MARK: UISceneSession Lifecycle - @available(iOS 13.0, *) - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - @available(iOS 13.0, *) - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - } - - -} - diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/Assets.xcassets/AccentColor.colorset/Contents.json b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb8789700..000000000 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 9221b9bb1..000000000 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "76x76" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" - }, - { - "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/Assets.xcassets/Contents.json b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/Base.lproj/LaunchScreen.storyboard b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 865e9329f..000000000 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/Base.lproj/Main.storyboard b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/Base.lproj/Main.storyboard deleted file mode 100644 index 25a763858..000000000 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/Base.lproj/Main.storyboard +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/Info.plist b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/Info.plist deleted file mode 100644 index dd3c9afda..000000000 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/Info.plist +++ /dev/null @@ -1,25 +0,0 @@ - - - - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main - - - - - - diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/SceneDelegate.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/SceneDelegate.swift deleted file mode 100644 index 19520cb5b..000000000 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/SceneDelegate.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// SceneDelegate.swift -// HealthSDKPinningExample -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -import UIKit - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - - var window: UIWindow? - - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } - } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - } - - -} - diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/ViewController.swift b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/ViewController.swift deleted file mode 100644 index c1cdf3cf6..000000000 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/ViewController.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// ViewController.swift -// HealthSDKPinningExample -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -import UIKit -import GiniHealthSDK -import GiniHealthAPILibrary -import GiniHealthAPILibraryPinning - -class ViewController: UIViewController { - override func viewDidLoad() { - super.viewDidLoad() - self.initializeSDK() - } - - func initializeSDK() { -// let yourPublicPinningConfig = [ -// kTSKPinnedDomains: [ -// "pay-api.gini.net": [ -// kTSKPublicKeyHashes: [ -// // old *.gini.net public key -// "cNzbGowA+LNeQ681yMm8ulHxXiGojHE8qAjI+M7bIxU=", -// // new *.gini.net public key, active from around June 2020 -// "zEVdOCzXU8euGVuMJYPr3DUU/d1CaKevtr0dW0XzZNo=" -// ]], -// "user.gini.net": [ -// kTSKPublicKeyHashes: [ -// // old *.gini.net public key -// "cNzbGowA+LNeQ681yMm8ulHxXiGojHE8qAjI+M7bIxU=", -// // new *.gini.net public key, active from around June 2020 -// "zEVdOCzXU8euGVuMJYPr3DUU/d1CaKevtr0dW0XzZNo=" -// ]], -// ]] as [String: Any] -// let giniApiLib = GiniHealthAPI -// .Builder(client: Client(id: "your-id", -// secret: "your-secret", -// domain: "your-domain"), -// api: .default, -// pinningConfig: yourPublicPinningConfig) -// .build() -// let sdk = GiniHealth(with: giniApiLib) -// let documentService: DefaultDocumentService = sdk.documentService - } -} - diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/de.lproj/Localizable.strings b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/de.lproj/Localizable.strings deleted file mode 100644 index e156caab4..000000000 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/de.lproj/Localizable.strings +++ /dev/null @@ -1,7 +0,0 @@ -/* - Localizable.strings - HealthSDKPinningExample - - // Copyright © 2024 Gini GmbH. All rights reserved. - -*/ diff --git a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/en.lproj/Localizable.strings b/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/en.lproj/Localizable.strings deleted file mode 100644 index 221fad5d9..000000000 --- a/HealthSDK/GiniHealthSDKExample/GiniHealthSDKPinningExample/en.lproj/Localizable.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* - Localizable.strings - HealthSDKPinningExample - - // Copyright © 2024 Gini GmbH. All rights reserved. - -*/ -"ginihealth.reviewscreen.iban.placeholder" = "IBANXXXX"; diff --git a/HealthSDK/GiniHealthSDKPinning/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/HealthSDK/GiniHealthSDKPinning/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a62..000000000 --- a/HealthSDK/GiniHealthSDKPinning/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/HealthSDK/GiniHealthSDKPinning/.swiftpm/xcode/package.xcworkspace/xcuserdata/home.xcuserdatad/UserInterfaceState.xcuserstate b/HealthSDK/GiniHealthSDKPinning/.swiftpm/xcode/package.xcworkspace/xcuserdata/home.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index 54c7236a5..000000000 Binary files a/HealthSDK/GiniHealthSDKPinning/.swiftpm/xcode/package.xcworkspace/xcuserdata/home.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ diff --git a/HealthSDK/GiniHealthSDKPinning/.swiftpm/xcode/xcshareddata/xcschemes/GiniHealthSDKPinningTests.xcscheme b/HealthSDK/GiniHealthSDKPinning/.swiftpm/xcode/xcshareddata/xcschemes/GiniHealthSDKPinningTests.xcscheme deleted file mode 100644 index 235587113..000000000 --- a/HealthSDK/GiniHealthSDKPinning/.swiftpm/xcode/xcshareddata/xcschemes/GiniHealthSDKPinningTests.xcscheme +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HealthSDK/GiniHealthSDKPinning/.swiftpm/xcode/xcuserdata/home.xcuserdatad/xcschemes/xcschememanagement.plist b/HealthSDK/GiniHealthSDKPinning/.swiftpm/xcode/xcuserdata/home.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index 718766ddd..000000000 --- a/HealthSDK/GiniHealthSDKPinning/.swiftpm/xcode/xcuserdata/home.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - SchemeUserState - - GiniHealthSDKPinning.xcscheme_^#shared#^_ - - orderHint - 2 - - GiniHealthSDKPinningTests.xcscheme_^#shared#^_ - - orderHint - 9 - - - SuppressBuildableAutocreation - - GiniHealthSDKPinning - - primary - - - GiniHealthSDKPinningTests - - primary - - - - - diff --git a/HealthSDK/GiniHealthSDKPinning/GiniHealth_Logo.png b/HealthSDK/GiniHealthSDKPinning/GiniHealth_Logo.png deleted file mode 100644 index e62a1d977..000000000 Binary files a/HealthSDK/GiniHealthSDKPinning/GiniHealth_Logo.png and /dev/null differ diff --git a/HealthSDK/GiniHealthSDKPinning/LICENSE b/HealthSDK/GiniHealthSDKPinning/LICENSE deleted file mode 100644 index d1d61e32b..000000000 --- a/HealthSDK/GiniHealthSDKPinning/LICENSE +++ /dev/null @@ -1,12 +0,0 @@ -Copyright (c) 2021, Gini GmbH -All rights reserved. - -The Gini Health SDK Pinning is licensed through Gini GmbH ("Gini") and may not be -used, altered or copied in any way without explicit permission by Gini. The -terms of usage are defined in a separate usage agreement between Gini and the -licensee, where the licensee can gain access to a non-exclusive, -non-transferable usage right which is restricted for the time of a contractual -relationship between Gini and the licensee. - -For license related inquiries contact Gini via the email address -technical-support@gini.net. \ No newline at end of file diff --git a/HealthSDK/GiniHealthSDKPinning/Package-release.swift b/HealthSDK/GiniHealthSDKPinning/Package-release.swift deleted file mode 100644 index 643074346..000000000 --- a/HealthSDK/GiniHealthSDKPinning/Package-release.swift +++ /dev/null @@ -1,31 +0,0 @@ -// swift-tools-version:5.5 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let package = Package( - name: "GiniHealthSDKPinning", - platforms: [.iOS(.v12)], - products: [ - // Products define the executables and libraries a package produces, and make them visible to other packages. - .library( - name: "GiniHealthSDKPinning", - targets: ["GiniHealthSDKPinning"]), - ], - dependencies: [ - // Dependencies declare other packages that this package depends on. - // .package(url: /* package url */, from: "1.0.0"), - .package(name: "GiniHealthAPILibraryPinning", url: "https://github.com/gini/health-api-library-pinning-ios.git", .exact("4.3.1")), - .package(name: "GiniHealthSDK", url: "https://github.com/gini/health-sdk-ios.git", .exact("4.3.0")), - ], - targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. - .target( - name: "GiniHealthSDKPinning", - dependencies: ["GiniHealthAPILibraryPinning", "GiniHealthSDK"]), - .testTarget( - name: "GiniHealthSDKPinningTests", - dependencies: ["GiniHealthSDKPinning"]), - ] -) diff --git a/HealthSDK/GiniHealthSDKPinning/Package.swift b/HealthSDK/GiniHealthSDKPinning/Package.swift deleted file mode 100644 index 5d9e4b9d2..000000000 --- a/HealthSDK/GiniHealthSDKPinning/Package.swift +++ /dev/null @@ -1,31 +0,0 @@ -// swift-tools-version:5.5 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let package = Package( - name: "GiniHealthSDKPinning", - platforms: [.iOS(.v12)], - products: [ - // Products define the executables and libraries a package produces, and make them visible to other packages. - .library( - name: "GiniHealthSDKPinning", - targets: ["GiniHealthSDKPinning"]), - ], - dependencies: [ - // Dependencies declare other packages that this package depends on. - // .package(url: /* package url */, from: "1.0.0"), - .package(name: "GiniHealthAPILibraryPinning", path: "../../HealthAPILibrary/GiniHealthAPILibraryPinning"), - .package(name: "GiniHealthSDK", path: "../GiniHealthSDK"), - ], - targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. - .target( - name: "GiniHealthSDKPinning", - dependencies: ["GiniHealthAPILibraryPinning", "GiniHealthSDK"]), - .testTarget( - name: "GiniHealthSDKPinningTests", - dependencies: ["GiniHealthSDKPinning"]), - ] -) diff --git a/HealthSDK/GiniHealthSDKPinning/README.md b/HealthSDK/GiniHealthSDKPinning/README.md deleted file mode 100644 index 10215650f..000000000 --- a/HealthSDK/GiniHealthSDKPinning/README.md +++ /dev/null @@ -1,45 +0,0 @@ -

- -

- -# Gini Health SDK Pinning for iOS - -[![Platform](https://img.shields.io/badge/platform-iOS-lightgrey.svg)]() -[![Devices](https://img.shields.io/badge/devices-iPhone%20%7C%20iPad-blue.svg)]() -[![Swift version](https://img.shields.io/badge/swift-5.0-orange.svg)]() -[![Swift package manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange?style=flat-square)]() - - -The Gini Health SDK Pinning provides components for uploading, reviewing and analyzing photos of invoices and remittance slips and supports certificate pinning. - -By integrating this SDK into your application you can allow your users to easily upload a picture of a document, review it and get analysis results from the Gini backend, create a payment and send it to the prefferable payment provider. - -## Documentation - -Further documentation with installation, integration or customization guides can be found in our [`website`](https://developer.gini.net/gini-mobile-ios/GiniHealthSDK/index.html). - -## Example apps - -We are providing example app for Swift. This app demonstrates how to integrate the Gini Health SDK with the [Gini Capture SDK](https://gini.atlassian.net/wiki/spaces/ICSV/overview). -Please, find more details [`here`](https://github.com/gini/gini-mobile-ios/tree/main/HealthSDK/GiniHealthSDK#example-apps) - -## Requirements - -- iOS 12+ -- Xcode 12+ - -**Note:** -In order to have better analysis results it is highly recommended to enable only devices with 8MP camera and flash. These devices would be: - -* iPhones with iOS 12 or higher. -* iPad Pro devices (iPad Air 2 and iPad Mini 4 have 8MP camera but no flash). - -## Author - -Gini GmbH, hello@gini.net - -## License - -The Gini Health SDK Pinning for iOS is licensed under a Private License. See [the license](http://developer.gini.net/gini-mobile-ios/GiniHealthSDK/license.html) for more info. - -**Important:** Always make sure to ship all license notices and permissions with your application. diff --git a/HealthSDK/GiniHealthSDKPinning/Sources/GiniHealthSDKPinning/GiniHealthSDKPinningVersion.swift b/HealthSDK/GiniHealthSDKPinning/Sources/GiniHealthSDKPinning/GiniHealthSDKPinningVersion.swift deleted file mode 100644 index 96f3ea891..000000000 --- a/HealthSDK/GiniHealthSDKPinning/Sources/GiniHealthSDKPinning/GiniHealthSDKPinningVersion.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// GiniHealthSDKPinningVersion.swift -// -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -public let GiniHealthSDKPinningVersion = "4.3.0" diff --git a/HealthSDK/GiniHealthSDKPinning/Tests/GiniHealthSDKPinningTests/HealthSDKPinningTests.swift b/HealthSDK/GiniHealthSDKPinning/Tests/GiniHealthSDKPinningTests/HealthSDKPinningTests.swift deleted file mode 100644 index 6c5012f9b..000000000 --- a/HealthSDK/GiniHealthSDKPinning/Tests/GiniHealthSDKPinningTests/HealthSDKPinningTests.swift +++ /dev/null @@ -1,7 +0,0 @@ -import XCTest -@testable import GiniHealthSDKPinning - -final class GiniHealthSDKPinningTests: XCTestCase { - func testExample() throws { - } -} diff --git a/MerchantSDK/GiniMerchantSDK/Documentation/source/Event tracking guide.md b/MerchantSDK/GiniMerchantSDK/Documentation/source/Event tracking guide.md index cab8a529c..803a2ee70 100644 --- a/MerchantSDK/GiniMerchantSDK/Documentation/source/Event tracking guide.md +++ b/MerchantSDK/GiniMerchantSDK/Documentation/source/Event tracking guide.md @@ -24,7 +24,7 @@ merchantSDK.delegate = self // where self conforms to the GiniMerchantDelegate p Implement the `GiniMerchantTrackingDelegate` protocol and supply the delegate when initializing `PaymentReviewViewController`. For example: ```swift -let viewController = paymentComponentsController.loadPaymentReviewScreenFor(documentID: documentId, paymentInfo: paymentInfo, trackingDelegate: self) +let viewController = paymentComponentsController.loadPaymentReviewScreenFor(documentId: documentId, paymentInfo: paymentInfo, trackingDelegate: self) ``` ## Events diff --git a/MerchantSDK/GiniMerchantSDK/Documentation/source/Integration.md b/MerchantSDK/GiniMerchantSDK/Documentation/source/Integration.md index 808abb539..1a3eea733 100644 --- a/MerchantSDK/GiniMerchantSDK/Documentation/source/Integration.md +++ b/MerchantSDK/GiniMerchantSDK/Documentation/source/Integration.md @@ -20,7 +20,7 @@ private lazy var merchant = GiniMerchant(id: clientID, secret: clientPassword, d ## Certificate pinning (optional) -If you want to use _Certificate pinning_, provide metadata for the upload process, you can pass both your public key pinning configuration for more information) +If you want to use _Certificate pinning_, provide metadata for the upload process, you can pass your public key pinning configuration as follows ```swift private lazy var mechant = GiniMerchant(id: clientID, secret: clientPassword, domain: clientDomain, pinningConfig: ["PinnedDomains" : ["PublicKeyHashes"]]) ``` diff --git a/MerchantSDK/GiniMerchantSDK/Package-release.swift b/MerchantSDK/GiniMerchantSDK/Package-release.swift index 986c32c7f..6edc5563f 100644 --- a/MerchantSDK/GiniMerchantSDK/Package-release.swift +++ b/MerchantSDK/GiniMerchantSDK/Package-release.swift @@ -17,6 +17,7 @@ let package = Package( // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), .package(name: "GiniHealthAPILibrary", url: "https://github.com/gini/health-api-library-ios.git", .exact("4.3.0")), + .package(name: "GiniInternalPaymentSDK", url: "https://github.com/gini/internal-payment-sdk-ios", .exact("1.0.0")), .package(name: "GiniUtilites", url: "https://github.com/gini/utilites-ios.git", .exact("1.0.0")), ], targets: [ @@ -25,7 +26,7 @@ let package = Package( .target( name: "GiniMerchantSDK", - dependencies: ["GiniHealthAPILibrary", "GiniUtilites"]), + dependencies: ["GiniHealthAPILibrary", "GiniInternalPaymentSDK", "GiniUtilites"]), .testTarget( name: "GiniMerchantSDKTests", dependencies: ["GiniMerchantSDK"]), diff --git a/MerchantSDK/GiniMerchantSDK/Package.swift b/MerchantSDK/GiniMerchantSDK/Package.swift index 630f5375a..127808af2 100644 --- a/MerchantSDK/GiniMerchantSDK/Package.swift +++ b/MerchantSDK/GiniMerchantSDK/Package.swift @@ -17,7 +17,8 @@ let package = Package( // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), .package(name: "GiniHealthAPILibrary", path: "../../HealthAPILibrary/GiniHealthAPILibrary"), - .package(name: "GiniUtilites", path: "../../GiniComponents/GiniUtilites"), + .package(name: "GiniInternalPaymentSDK", path: "../../GiniComponents/GiniInternalPaymentSDK"), + .package(name: "GiniUtilites", path: "../../GiniComponents/GiniUtilites") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. @@ -25,7 +26,7 @@ let package = Package( .target( name: "GiniMerchantSDK", - dependencies: ["GiniHealthAPILibrary", "GiniUtilites"]), + dependencies: ["GiniHealthAPILibrary", "GiniInternalPaymentSDK", "GiniUtilites"]), .testTarget( name: "GiniMerchantSDKTests", dependencies: ["GiniMerchantSDK"], diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/Adapter/Mapping.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/Adapter/Mapping.swift index ccf93e5ef..12c6a2bdf 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/Adapter/Mapping.swift +++ b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/Adapter/Mapping.swift @@ -7,6 +7,7 @@ import Foundation import GiniHealthAPILibrary +import GiniInternalPaymentSDK //MARK: - Mapping Extraction extension Extraction { @@ -74,6 +75,23 @@ extension PaymentProvider { gpcSupportedPlatforms: gpcSupportedPlatforms, openWithSupportedPlatforms: openWithPlatforms) } + + func toHealthPaymentProvider() -> GiniHealthAPILibrary.PaymentProvider { + let gpcSupportedPlatforms = self.gpcSupportedPlatforms.compactMap { GiniHealthAPILibrary.PlatformSupported(rawValue: $0.rawValue) } + let openWithPlatforms = openWithSupportedPlatforms.compactMap { GiniHealthAPILibrary.PlatformSupported(rawValue: $0.rawValue) } + + return GiniHealthAPILibrary.PaymentProvider(id: id, + name: name, + appSchemeIOS: appSchemeIOS, + minAppVersion: minAppVersion?.healthMinAppVersions, + colors: colors.toHealthProviderColors(), + iconData: iconData, + appStoreUrlIOS: appStoreUrlIOS, + universalLinkIOS: universalLinkIOS, + index: index, + gpcSupportedPlatforms: gpcSupportedPlatforms, + openWithSupportedPlatforms:openWithPlatforms) + } } extension ProviderColors { @@ -81,6 +99,11 @@ extension ProviderColors { self.init(background: healthProviderColors.background, text: healthProviderColors.text) } + + func toHealthProviderColors() -> GiniHealthAPILibrary.ProviderColors { + return GiniHealthAPILibrary.ProviderColors(background: background, + text: text) + } } extension MinAppVersions { @@ -112,6 +135,7 @@ extension Document { id: id, name: name, links: GiniHealthAPILibrary.Document.Links(giniAPIDocumentURL: links.extractions), + pageCount: pageCount, sourceClassification: GiniHealthAPILibrary.Document.SourceClassification(rawValue: sourceClassification.rawValue) ?? .scanned, expirationDate: expirationDate) } @@ -206,3 +230,17 @@ extension LogLevel { } } } + +//MARK: - PaymentProvider + +extension PaymentInfo { + init(paymentConponentsInfo: GiniInternalPaymentSDK.PaymentInfo) { + self.init(recipient: paymentConponentsInfo.recipient, + iban: paymentConponentsInfo.iban, + bic: paymentConponentsInfo.bic, + amount: paymentConponentsInfo.amount, + purpose: paymentConponentsInfo.purpose, + paymentUniversalLink: paymentConponentsInfo.paymentUniversalLink, + paymentProviderId: paymentConponentsInfo.paymentProviderId) + } +} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/BottomSheetController.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/BottomSheetController.swift deleted file mode 100644 index 69497ba8c..000000000 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/BottomSheetController.swift +++ /dev/null @@ -1,220 +0,0 @@ -// -// BottomSheetController.swift -// GiniMerchantSDK -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit -import GiniUtilites - -public class BottomSheetController: UIViewController { - // MARK: - UI - /// Main bottom sheet container view - private lazy var mainContainerView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = backgroundColor - view.roundCorners(corners: [.topLeft, .topRight], radius: Constants.cornerRadiusView) - view.layer.cornerRadius = Constants.cornerRadiusView - view.clipsToBounds = true - return view - }() - - /// View to hold dynamic content - private let contentView = EmptyView() - - /// Top bar view that draggable to dismiss - private let topBarView = EmptyView() - - /// Top view bar - private lazy var barLineView: UIView = { - let view = UIView() - view.backgroundColor = rectangleColor - view.layer.cornerRadius = Constants.cornerRadiusTopRectangle - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - /// Dimmed background view - private lazy var dimmedView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = dimmingBackgroundColor - view.alpha = 0 - return view - }() - - let backgroundColor: UIColor = GiniColor.standard7.uiColor() - let rectangleColor: UIColor = GiniColor.standard5.uiColor() - let dimmingBackgroundColor: UIColor = GiniColor(lightModeColor: UIColor.black, - darkModeColor: UIColor.white).uiColor().withAlphaComponent(0.4) - var minHeight: CGFloat = 0 - - // MARK: - View Setup - public override func viewDidLoad() { - super.viewDidLoad() - setupViews() - setupGestures() - } - - public override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - animatePresent() - } - - private func setupViews() { - view.backgroundColor = .clear - view.addSubview(dimmedView) - NSLayoutConstraint.activate([ - // Set dimmedView edges to superview - dimmedView.topAnchor.constraint(equalTo: view.topAnchor), - dimmedView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - dimmedView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - dimmedView.trailingAnchor.constraint(equalTo: view.trailingAnchor) - ]) - - // Container View - view.addSubview(mainContainerView) - NSLayoutConstraint.activate([ - mainContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - mainContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - mainContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor) - ]) - if minHeight > 0 { - mainContainerView.topAnchor.constraint(lessThanOrEqualTo: view.topAnchor, constant: obtainTopAnchorMinHeightConstraint()).isActive = true - } else { - mainContainerView.topAnchor.constraint(greaterThanOrEqualTo: view.topAnchor, constant: Constants.minTopSpacing).isActive = true - } - - // Top draggable bar view - mainContainerView.addSubview(topBarView) - NSLayoutConstraint.activate([ - topBarView.topAnchor.constraint(equalTo: mainContainerView.topAnchor), - topBarView.leadingAnchor.constraint(equalTo: mainContainerView.leadingAnchor), - topBarView.trailingAnchor.constraint(equalTo: mainContainerView.trailingAnchor), - topBarView.heightAnchor.constraint(equalToConstant: Constants.heightTopBarView) - ]) - topBarView.addSubview(barLineView) - NSLayoutConstraint.activate([ - barLineView.centerXAnchor.constraint(equalTo: topBarView.centerXAnchor), - barLineView.topAnchor.constraint(equalTo: topBarView.topAnchor, constant: Constants.topAnchorTopRectangle), - barLineView.widthAnchor.constraint(equalToConstant: Constants.widthTopRectangle), - barLineView.heightAnchor.constraint(equalToConstant: Constants.heightTopRectangle) - ]) - - // Content View - mainContainerView.addSubview(contentView) - contentView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - contentView.leadingAnchor.constraint(equalTo: mainContainerView.leadingAnchor), - contentView.trailingAnchor.constraint(equalTo: mainContainerView.trailingAnchor), - contentView.topAnchor.constraint(equalTo: topBarView.bottomAnchor), - contentView.bottomAnchor.constraint(equalTo: mainContainerView.bottomAnchor, constant: -Constants.bottomPaddingConstraint) - ]) - } - - private func obtainTopAnchorMinHeightConstraint() -> CGFloat { - let window = UIApplication.shared.windows.filter {$0.isKeyWindow}.first - let extraBottomSafeAreaConstant = window?.safeAreaInsets.bottom == 0 ? Constants.safeAreaBottomPadding : 0 // fix for small devices - let topAnchorWithMinHeightConstant = view.frame.height - minHeight + extraBottomSafeAreaConstant - return topAnchorWithMinHeightConstant - } - - private func setupGestures() { - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapDimmedView)) - dimmedView.addGestureRecognizer(tapGesture) - - let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) - panGesture.delaysTouchesBegan = false - panGesture.delaysTouchesEnded = false - topBarView.addGestureRecognizer(panGesture) - } - - @objc private func handleTapDimmedView() { - dismissBottomSheet() - } - - @objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) { - let translation = gesture.translation(in: view) - // get drag direction - let isDraggingDown = translation.y > 0 - guard isDraggingDown else { return } - let pannedHeight = translation.y - let currentY = self.view.frame.height - self.mainContainerView.frame.height - // handle gesture state - switch gesture.state { - case .changed: - // This state will occur when user is dragging - self.mainContainerView.frame.origin.y = currentY + pannedHeight - case .ended: - // When user stop dragging - // if fulfil the condition dismiss it, else move to original position - if pannedHeight >= Constants.minDismissiblePanHeight { - dismissBottomSheet() - } else { - self.mainContainerView.frame.origin.y = currentY - } - default: - break - } - } - - private func animatePresent() { - dimmedView.alpha = 0 - // add more animation duration for smoothness - UIView.animate(withDuration: 0.2) { [weak self] in - self?.dimmedView.alpha = Constants.maxDimmedAlpha - } - } - - func dismissBottomSheet() { - UIView.animate(withDuration: 0.2, animations: { [weak self] in - guard let self = self else { return } - self.dimmedView.alpha = Constants.maxDimmedAlpha - self.mainContainerView.frame.origin.y = self.view.frame.height - }, completion: { [weak self] _ in - self?.dismiss(animated: false) - }) - } - - // sub-view controller will call this function to set content - func setContent(content: UIView) { - contentView.addSubview(content) - NSLayoutConstraint.activate([ - content.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - content.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - content.topAnchor.constraint(equalTo: contentView.topAnchor), - content.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) - ]) - view.layoutIfNeeded() - } -} - -extension BottomSheetController { - enum Constants { - /// Maximum alpha for dimmed view - static let maxDimmedAlpha: CGFloat = 0.8 - /// Minimum drag vertically that enable bottom sheet to dismiss - static let minDismissiblePanHeight: CGFloat = 20 - /// Minimum spacing between the top edge and bottom sheet - static var minTopSpacing: CGFloat = 80 - /// Minimum bottom sheet height - static let heightTopBarView = 32.0 - static let cornerRadiusTopRectangle = 2.0 - static let cornerRadiusView = 12.0 - static let topAnchorTopRectangle = 16.0 - static let widthTopRectangle = 48.0 - static let heightTopRectangle = 4.0 - static let bottomPaddingConstraint = 34.0 - static let safeAreaBottomPadding = 32.0 - } -} - -extension UIViewController { - func presentBottomSheet(viewController: BottomSheetController) { - viewController.modalPresentationStyle = .overFullScreen - present(viewController, animated: false, completion: nil) - } -} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/GiniMerchant+PaymentComponentsConfigurationProvider.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/GiniMerchant+PaymentComponentsConfigurationProvider.swift new file mode 100644 index 000000000..4feb44401 --- /dev/null +++ b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/GiniMerchant+PaymentComponentsConfigurationProvider.swift @@ -0,0 +1,174 @@ +// +// GiniMerchant+PaymentComponentsConfigurationProvider.swift +// GiniMerchantSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import UIKit +import GiniUtilites +import GiniInternalPaymentSDK + +extension GiniMerchant: PaymentComponentsConfigurationProvider { + public var defaultStyleInputFieldConfiguration: TextFieldConfiguration { + GiniMerchantConfiguration.shared.defaultStyleInputFieldConfiguration + } + + public var errorStyleInputFieldConfiguration: TextFieldConfiguration { + GiniMerchantConfiguration.shared.errorStyleInputFieldConfiguration + } + + public var selectionStyleInputFieldConfiguration: TextFieldConfiguration { + GiniMerchantConfiguration.shared.selectionStyleInputFieldConfiguration + } + + public var showPaymentReviewCloseButton: Bool { + false + } + + public var paymentComponentButtonsHeight: CGFloat { + GiniMerchantConfiguration.shared.paymentComponentButtonsHeight + } + + public var paymentReviewContainerConfiguration: PaymentReviewContainerConfiguration { + PaymentReviewContainerConfiguration( + errorLabelTextColor: GiniColor.feedback1.uiColor(), + errorLabelFont: GiniMerchantConfiguration.shared.font(for: .captions2), + lockIcon: GiniMerchantImage.lock.preferredUIImage(), + lockedFields: true, + showBanksPicker: false, + chevronDownIcon: nil, + chevronDownIconColor: nil + ) + } + + public var installAppConfiguration: InstallAppConfiguration { + InstallAppConfiguration( + titleAccentColor: GiniColor.standard2.uiColor(), + titleFont: GiniMerchantConfiguration.shared.font(for: .subtitle1), + moreInformationFont: GiniMerchantConfiguration.shared.font(for: .captions1), + moreInformationTextColor: GiniColor.standard3.uiColor(), + moreInformationAccentColor: GiniColor.standard3.uiColor(), + moreInformationIcon: GiniMerchantImage.info.preferredUIImage(), + appStoreIcon: GiniMerchantImage.appStore.preferredUIImage(), + bankIconBorderColor: GiniColor.standard5.uiColor() + ) + } + + public var bottomSheetConfiguration: BottomSheetConfiguration { + BottomSheetConfiguration( + backgroundColor: GiniColor.standard7.uiColor(), + rectangleColor: GiniColor.standard5.uiColor(), + dimmingBackgroundColor: GiniColor(lightModeColor: UIColor.black, darkModeColor: UIColor.white).uiColor().withAlphaComponent(0.4) + ) + } + + public var shareInvoiceConfiguration: ShareInvoiceConfiguration { + ShareInvoiceConfiguration( + titleFont: GiniMerchantConfiguration.shared.font(for: .subtitle1), + titleAccentColor: GiniColor.standard2.uiColor(), + descriptionFont: GiniMerchantConfiguration.shared.font(for: .captions1), + descriptionTextColor: GiniColor.standard3.uiColor(), + descriptionAccentColor: GiniColor.standard3.uiColor(), + paymentInfoBorderColor: GiniColor.standard5.uiColor(), + titlePaymentInfoTextColor: GiniColor.standard4.uiColor(), + subtitlePaymentInfoTextColor: GiniColor.standard1.uiColor(), + titlepaymentInfoFont: GiniMerchantConfiguration.shared.font(for: .captions2), + subtitlePaymentInfoFont: GiniMerchantConfiguration.shared.font(for: .body2) + ) + } + + public var paymentInfoConfiguration: PaymentInfoConfiguration { + PaymentInfoConfiguration( + giniFont: GiniMerchantConfiguration.shared.font(for: .button), + answersFont: GiniMerchantConfiguration.shared.font(for: .body2), + answerCellTextColor: GiniColor.standard1.uiColor(), + answerCellLinkColor: GiniColor.accent1.uiColor(), + questionsTitleFont: GiniMerchantConfiguration.shared.font(for: .subtitle1), + questionsTitleColor: GiniColor.standard1.uiColor(), + questionHeaderFont: GiniMerchantConfiguration.shared.font(for: .body1), + questionHeaderTitleColor: GiniColor.standard1.uiColor(), + questionHeaderMinusIcon: GiniMerchantImage.minus.preferredUIImage(), + questionHeaderPlusIcon: GiniMerchantImage.plus.preferredUIImage(), + bankCellBorderColor: GiniColor.standard5.uiColor(), + payBillsTitleFont: GiniMerchantConfiguration.shared.font(for: .subtitle1), + payBillsTitleColor: GiniColor.standard1.uiColor(), + payBillsDescriptionFont: GiniMerchantConfiguration.shared.font(for: .body2), + linksFont: GiniMerchantConfiguration.shared.font(for: .linkBold), + linksColor: GiniColor.accent1.uiColor(), + separatorColor: GiniColor.standard5.uiColor(), + backgroundColor: GiniColor.standard7.uiColor() + ) + } + + public var bankSelectionConfiguration: BankSelectionConfiguration { + BankSelectionConfiguration( + descriptionAccentColor: GiniColor.standard3.uiColor(), + descriptionFont: GiniMerchantConfiguration.shared.font(for: .captions1), + selectBankAccentColor: GiniColor.standard2.uiColor(), + selectBankFont: GiniMerchantConfiguration.shared.font(for: .subtitle1), + closeTitleIcon: GiniMerchantImage.close.preferredUIImage(), + closeIconAccentColor: GiniColor.standard2.uiColor(), + bankCellBackgroundColor: GiniColor.standard7.uiColor(), + bankCellIconBorderColor: GiniColor.standard5.uiColor(), + bankCellNameFont: GiniMerchantConfiguration.shared.font(for: .body1), + bankCellNameAccentColor: GiniColor.standard1.uiColor(), + bankCellSelectedBorderColor: GiniColor.accent1.uiColor(), + bankCellNotSelectedBorderColor: GiniColor.standard5.uiColor(), + bankCellSelectionIndicatorImage: GiniMerchantImage.selectionIndicator.preferredUIImage() + ) + } + + public var paymentComponentsConfiguration: PaymentComponentsConfiguration { + PaymentComponentsConfiguration( + selectYourBankLabelFont: GiniMerchantConfiguration.shared.font(for: .subtitle2), + selectYourBankAccentColor: GiniColor.standard1.uiColor(), + chevronDownIcon: GiniMerchantImage.chevronDown.preferredUIImage(), + chevronDownIconColor: GiniColor(lightModeColorName: .light7, darkModeColorName: .light1).uiColor(), + notInstalledBankTextColor: GiniColor.standard4.uiColor() + ) + } + + public var paymentReviewConfiguration: PaymentReviewConfiguration { + PaymentReviewConfiguration( + loadingIndicatorStyle: UIActivityIndicatorView.Style.large, + loadingIndicatorColor: GiniMerchantColorPalette.accent1.preferredColor(), + infoBarLabelTextColor: GiniMerchantColorPalette.dark7.preferredColor(), + infoBarBackgroundColor: GiniMerchantColorPalette.success1.preferredColor(), + mainViewBackgroundColor: GiniColor.standard7.uiColor(), + infoContainerViewBackgroundColor: GiniColor.standard7.uiColor(), + paymentReviewClose: GiniMerchantImage.paymentReviewClose.preferredUIImage(), + backgroundColor: GiniColor(lightModeColorName: .light7, darkModeColorName: .light7).uiColor(), + infoBarLabelFont: GiniMerchantConfiguration.shared.font(for: .captions1), + statusBarStyle: .default, + pageIndicatorTintColor: GiniColor.standard4.uiColor(), + currentPageIndicatorTintColor: GiniColor(lightModeColorName: .dark2, darkModeColorName: .light5).uiColor(), + isInfoBarHidden: true + ) + } + + public var poweredByGiniConfiguration: PoweredByGiniConfiguration { + PoweredByGiniConfiguration( + poweredByGiniLabelFont: GiniMerchantConfiguration.shared.font(for: .captions2), + poweredByGiniLabelAccentColor: GiniColor.standard4.uiColor(), + giniIcon: GiniMerchantImage.logo.preferredUIImage() + ) + } + + public var moreInformationConfiguration: MoreInformationConfiguration { + MoreInformationConfiguration( + moreInformationAccentColor: GiniColor.standard2.uiColor(), + moreInformationTextColor: GiniColor.standard4.uiColor(), + moreInformationLinkFont: GiniMerchantConfiguration.shared.font(for: .captions2), + moreInformationIcon: GiniMerchantImage.info.preferredUIImage() + ) + } + + public var primaryButtonConfiguration: GiniInternalPaymentSDK.ButtonConfiguration { + GiniMerchantConfiguration.shared.primaryButtonConfiguration + } + + public var secondaryButtonConfiguration: GiniInternalPaymentSDK.ButtonConfiguration { + GiniMerchantConfiguration.shared.secondaryButtonConfiguration + } +} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/GiniMerchant+PaymentComponentsStringsProvider.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/GiniMerchant+PaymentComponentsStringsProvider.swift new file mode 100644 index 000000000..0dba6c476 --- /dev/null +++ b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/GiniMerchant+PaymentComponentsStringsProvider.swift @@ -0,0 +1,163 @@ +// +// GiniMerchant+PaymentComponentsStringsProvider.swift +// GiniMerchantSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +import GiniInternalPaymentSDK + +extension GiniMerchant: PaymentComponentsStringsProvider { + public var paymentReviewContainerStrings: PaymentReviewContainerStrings { + PaymentReviewContainerStrings( + emptyCheckErrorMessage: NSLocalizedStringPreferredFormat("gini.merchant.errors.failed.default.textfield.validation.check", + comment: "the field failed non empty check"), + ibanCheckErrorMessage: NSLocalizedStringPreferredFormat("gini.merchant.errors.failed.iban.validation.check", + comment: "iban failed validation check"), + recipientFieldPlaceholder: NSLocalizedStringPreferredFormat("gini.merchant.reviewscreen.recipient.placeholder", + comment: "placeholder text for recipient input field"), + ibanFieldPlaceholder: NSLocalizedStringPreferredFormat("gini.merchant.reviewscreen.iban.placeholder", + comment: "placeholder text for iban input field"), + amountFieldPlaceholder: NSLocalizedStringPreferredFormat("gini.merchant.reviewscreen.amount.placeholder", + comment: "placeholder text for amount input field"), + usageFieldPlaceholder: NSLocalizedStringPreferredFormat("gini.merchant.reviewscreen.usage.placeholder", + comment: "placeholder text for usage input field"), + recipientErrorMessage: NSLocalizedStringPreferredFormat("gini.merchant.errors.failed.recipient.non.empty.check", + comment: "recipient failed non empty check"), + ibanErrorMessage: NSLocalizedStringPreferredFormat("gini.merchant.errors.failed.iban.non.empty.check", + comment: "iban failed non empty check"), + amountErrorMessage: NSLocalizedStringPreferredFormat("gini.merchant.errors.failed.amount.non.empty.check", + comment: "amount failed non empty check"), + purposeErrorMessage: NSLocalizedStringPreferredFormat("gini.merchant.errors.failed.purpose.non.empty.check", + comment: "purpose failed non empty check"), + payInvoiceLabelText: NSLocalizedStringPreferredFormat("gini.merchant.reviewscreen.banking.app.button.label", + comment: "Title label used for the pay invoice button") + ) + } + + public var paymentComponentsStrings: PaymentComponentsStrings { + PaymentComponentsStrings( + selectYourBankLabelText: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.select.your.bank.label", + comment: "Text for the select your bank label that's above the payment provider picker"), + placeholderBankNameText: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.select.bank.label", + comment: "Placeholder text used when there isn't a payment provider app installed"), + ctaLabelText: GiniMerchantConfiguration.shared.showPaymentReviewScreen ? + NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.continue.to.overview.label", + comment: "Title label used for the pay invoice button") : + NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.to.banking.app.label", + comment: "Title label used for the pay invoice button") + ) + } + + public var installAppStrings: InstallAppStrings { + InstallAppStrings( + titlePattern: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.install.app.bottom.sheet.title", + comment: "Install App Bottom sheet title"), + moreInformationTipPattern: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.install.app.bottom.sheet.tip.description", + comment: "Text for tip information label"), + moreInformationNotePattern: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.install.app.bottom.sheet.notes.description", + comment: "Text for notes information label"), + continueLabelText: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.install.app.bottom.sheet.continue.button.text", + comment: "Title label used for the Continue button") + ) + } + + public var shareInvoiceStrings: ShareInvoiceStrings { + ShareInvoiceStrings( + continueLabelText: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.share.invoice.bottom.sheet.continue.button.text", + comment: "Title label used for the Continue button"), + titleTextPattern: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.share.invoice.bottom.sheet.title", + comment: "Share Invoice Bottom sheet title"), + descriptionTextPattern: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.share.invoice.bottom.sheet.description", + comment: "Text description for share bottom sheet"), + recipientLabelText: NSLocalizedStringPreferredFormat("gini.merchant.reviewscreen.recipient.placeholder", + comment: "placeholder text for recipient input field"), + amountLabelText: NSLocalizedStringPreferredFormat("gini.merchant.reviewscreen.amount.placeholder", + comment: "placeholder text for amount input field"), + ibanLabelText: NSLocalizedStringPreferredFormat("gini.merchant.reviewscreen.iban.placeholder", + comment: "placeholder text for iban input field"), + purposeLabelText: NSLocalizedStringPreferredFormat("gini.merchant.reviewscreen.usage.placeholder", + comment: "placeholder text for usage input field") + ) + } + + public var paymentInfoStrings: PaymentInfoStrings { + PaymentInfoStrings( + giniWebsiteText: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.pay.bills.description.clickable.text", + comment: "Word range that's clickable in pay bills description"), + giniURLText: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.gini.link", + comment: "Gini website link url"), + questionsTitleText: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.questions.title.label", + comment: "Payment Info questions title label text"), + answerPrivacyPolicyText: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.questions.answer.clickable.text", + comment: "Payment info answers clickable privacy policy"), + privacyPolicyURLText: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.gini.privacypolicy.link", + comment: "Gini privacy policy link url"), + titleText: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.title.label", + comment: "Payment Info title label text"), + payBillsTitleText: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.pay.bills.title.label", + comment: "Payment Info pay bills title label text"), + payBillsDescriptionText: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.pay.bills.description.label", + comment: "Payment Info pay bills description text"), + answers: [NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.questions.answer.1", + comment: "Answers description"), + NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.questions.answer.2", + comment: "Answers description"), + NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.questions.answer.3", + comment: "Answers description"), + NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.questions.answer.4", + comment: "Answers description"), + NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.questions.answer.5", + comment: "Answers description"), + NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.questions.answer.6", + comment: "Answers description")], + questions: [NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.questions.question.1", + comment: "Questions titles"), + NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.questions.question.2", + comment: "Questions titles"), + NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.questions.question.3", + comment: "Questions titles"), + NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.questions.question.4", + comment: "Questions titles"), + NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.questions.question.5", + comment: "Questions titles"), + NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.questions.question.6", + comment: "Questions titles")] + ) + } + + public var banksBottomStrings: BanksBottomStrings { + BanksBottomStrings( + selectBankTitleText: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.select.bank.label", + comment: "Select bank text from the top label on payment providers bottom sheet"), + descriptionText: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.providers.list.description", + comment: "Top description text on payment providers bottom sheet") + ) + } + + public var paymentReviewStrings: PaymentReviewStrings { + PaymentReviewStrings( + alertOkButtonTitle: NSLocalizedStringPreferredFormat("gini.merchant.alert.ok.title", + comment: "ok title for action"), + infoBarMessage: NSLocalizedStringPreferredFormat("gini.merchant.reviewscreen.infobar.message", + comment: "info bar message"), + defaultErrorMessage: NSLocalizedStringPreferredFormat("gini.merchant.errors.default", + comment: "default error message"), + createPaymentErrorMessage: NSLocalizedStringPreferredFormat("gini.merchant.errors.failed.payment.request.creation", + comment: "error for creating payment request") + ) + } + + public var poweredByGiniStrings: PoweredByGiniStrings { + PoweredByGiniStrings( + poweredByGiniText: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.powered.by.gini.label", comment: "") + ) + } + + public var moreInformationStrings: MoreInformationStrings { + MoreInformationStrings( + moreInformationActionablePartText: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.more.information.underlined.part", + comment: "Text for more information actionable part from the label") + ) + } +} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/GiniMerchant.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/GiniMerchant.swift index 31541d325..d0cb238ec 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/GiniMerchant.swift +++ b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/GiniMerchant.swift @@ -9,6 +9,7 @@ import UIKit import GiniHealthAPILibrary import GiniUtilites +import GiniInternalPaymentSDK /** Delegate to inform about the current status of the Gini Merchant SDK. @@ -71,6 +72,10 @@ public struct DataForReview { private var bankProviders: [PaymentProvider] = [] + public var paymentComponentConfiguration: GiniInternalPaymentSDK.PaymentComponentConfiguration = PaymentComponentConfiguration(isPaymentComponentBranded: true, + showPaymentComponentInOneRow: false, + hideInfoForReturningUser: false) + /** Initializes a new instance of GiniMerchant. @@ -113,8 +118,8 @@ public struct DataForReview { logLevel: LogLevel = .none) { let client = Client(id: id, secret: secret, domain: domain, apiVersion: apiVersion) self.giniApiLib = GiniHealthAPI.Builder(client: client, - logLevel: logLevel.toHealthLogLevel(), - sessionDelegate: GiniSessionDelegate(pinningConfig: pinningConfig)).build() + pinningConfig: pinningConfig, + logLevel: logLevel.toHealthLogLevel()).build() self.documentService = DefaultDocumentService(docService: giniApiLib.documentService()) self.paymentService = giniApiLib.paymentService(apiDomain: APIDomain.merchant, apiVersion: apiVersion) } @@ -150,7 +155,6 @@ public struct DataForReview { completion(.failure(.noInstalledApps)) } case let .failure(error): - completion(.failure(GiniMerchantError.apiError(error))) } } @@ -313,9 +317,9 @@ public struct DataForReview { paymentService.createPaymentRequest(sourceDocumentLocation: "", paymentProvider: paymentInfo.paymentProviderId, recipient: paymentInfo.recipient, iban: paymentInfo.iban, bic: "", amount: paymentInfo.amount, purpose: paymentInfo.purpose) { result in DispatchQueue.main.async { switch result { - case let .success(requestID): - completion(.success(requestID)) - self.delegate?.didCreatePaymentRequest(paymentRequestID: requestID) + case let .success(requestId): + completion(.success(requestId)) + self.delegate?.didCreatePaymentRequest(paymentRequestID: requestId) case let .failure(error): completion(.failure(GiniError.decorator(error))) } @@ -328,7 +332,7 @@ public struct DataForReview { openUrl called on main thread. - Parameters: - - requestID: Id of the created payment request. + - requestId: Id of the created payment request. - universalLink: Universal link for the selected payment provider */ public func openPaymentProviderApp(requestID: String, universalLink: String, urlOpener: URLOpener = URLOpener(UIApplication.shared), completion: GiniOpenLinkCompletionBlock? = nil) { diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/Extensions/GiniMerchantColors.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/GiniMerchantColors.swift similarity index 100% rename from MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/Extensions/GiniMerchantColors.swift rename to MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/GiniMerchantColors.swift diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/GiniMerchantConfiguration.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/GiniMerchantConfiguration.swift index 357ab533b..d5634ac39 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/GiniMerchantConfiguration.swift +++ b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/GiniMerchantConfiguration.swift @@ -7,6 +7,7 @@ import UIKit import GiniUtilites +import GiniInternalPaymentSDK /** The `GiniMerchantConfiguration` class allows customizations to the look of the Gini Merchant SDK. @@ -64,6 +65,7 @@ public final class GiniMerchantConfiguration: NSObject { public lazy var primaryButtonConfiguration = ButtonConfiguration(backgroundColor: GiniMerchantColorPalette.accent1.preferredColor().withAlphaComponent(0.4), borderColor: .clear, titleColor: .white, + titleFont: font(for: .button), shadowColor: .clear, cornerRadius: 12, borderWidth: 0, @@ -75,12 +77,13 @@ public final class GiniMerchantConfiguration: NSObject { public lazy var secondaryButtonConfiguration = ButtonConfiguration(backgroundColor: GiniColor.standard6.uiColor(), borderColor: GiniColor.standard5.uiColor(), titleColor: GiniColor.standard1.uiColor(), + titleFont: font(for: .input), shadowColor: .clear, cornerRadius: 12, borderWidth: 1, shadowRadius: 0, withBlurEffect: true) - + // MARK: - Shared properties /** @@ -88,7 +91,8 @@ public final class GiniMerchantConfiguration: NSObject { */ public lazy var defaultStyleInputFieldConfiguration = TextFieldConfiguration(backgroundColor: GiniColor.standard6.uiColor(), borderColor: GiniColor.standard5.uiColor(), - textColor: GiniColor.standard1.uiColor(), + textColor: GiniColor.standard1.uiColor(), + textFont: font(for: .captions2), cornerRadius: 12.0, borderWidth: 1.0, placeholderForegroundColor: GiniColor.standard4.uiColor()) @@ -96,21 +100,23 @@ public final class GiniMerchantConfiguration: NSObject { A error style configuration that defines the appearance of the text field, including its background color, border color, text color, corner radius, border width and the placeholder foreground color. It is used for input text fields on Payment Review Screen. */ public lazy var errorStyleInputFieldConfiguration = TextFieldConfiguration(backgroundColor: GiniColor.standard6.uiColor(), - borderColor: GiniColor(lightModeColorName: .feedback1, darkModeColorName: .feedback1).uiColor(), - textColor: GiniColor.standard1.uiColor(), - cornerRadius: 12.0, - borderWidth: 1.0, - placeholderForegroundColor: GiniColor.standard4.uiColor()) + borderColor: GiniColor(lightModeColorName: .feedback1, darkModeColorName: .feedback1).uiColor(), + textColor: GiniColor.standard1.uiColor(), + textFont: font(for: .captions2), + cornerRadius: 12.0, + borderWidth: 1.0, + placeholderForegroundColor: GiniColor.standard4.uiColor()) /** A selection style configuration that defines the appearance of the text field, including its background color, border color, text color, corner radius, border width and the placeholder foreground color. It is used for input text fields on Payment Review Screen. */ public lazy var selectionStyleInputFieldConfiguration = TextFieldConfiguration(backgroundColor: GiniColor.standard6.uiColor(), borderColor: GiniColor.accent1.uiColor(), - textColor: GiniColor.standard1.uiColor(), - cornerRadius: 12.0, - borderWidth: 1.0, - placeholderForegroundColor: GiniColor.standard4.uiColor()) - + textColor: GiniColor.standard1.uiColor(), + textFont: font(for: .captions2), + cornerRadius: 12.0, + borderWidth: 1.0, + placeholderForegroundColor: GiniColor.standard4.uiColor()) + // MARK: - Update to custom font /** Allows setting a custom font for specific text styles. The change will affect all screens where a specific text style was used. @@ -127,7 +133,7 @@ public final class GiniMerchantConfiguration: NSObject { } // We will switch this option internally to stil handle documents with extractions on GiniHealthSDK and still handle invoices without document on GiniMerchantSDK - var useInvoiceWithoutDocument: Bool = false + var useInvoiceWithoutDocument: Bool = true } extension GiniMerchantConfiguration { diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/BankSelectionTableViewCellModel.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/BankSelectionTableViewCellModel.swift deleted file mode 100644 index c7bf928ca..000000000 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/BankSelectionTableViewCellModel.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// BankSelectionTableViewCellModel.swift -// GiniMerchantSDK -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit -import GiniUtilites -import GiniHealthAPILibrary - -final class BankSelectionTableViewCellModel { - - private var isSelected: Bool = false - - var shouldShowSelectionIcon: Bool { - isSelected - } - - let backgroundColor: UIColor = GiniColor.standard7.uiColor() - - private var bankImageIconData: Data? - var bankImageIcon: UIImage { - if let bankImageIconData { - return UIImage(data: bankImageIconData) ?? UIImage() - } - return UIImage() - } - var bankIconBorderColor = GiniColor.standard5.uiColor() - - var bankName: String - var bankNameLabelFont: UIFont - let bankNameLabelAccentColor: UIColor = GiniColor.standard1.uiColor() - - let selectedBankBorderColor: UIColor = GiniColor.accent1.uiColor() - let notSelectedBankBorderColor: UIColor = GiniColor.standard5.uiColor() - - let selectionIndicatorImage: UIImage = GiniMerchantImage.selectionIndicator.preferredUIImage() - - init(paymentProvider: PaymentProviderAdditionalInfo) { - self.isSelected = paymentProvider.isSelected - self.bankImageIconData = paymentProvider.paymentProvider.iconData - self.bankName = paymentProvider.paymentProvider.name - self.bankNameLabelFont = GiniMerchantConfiguration.shared.font(for: .body1) - } -} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/BanksBottomViewModel.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/BanksBottomViewModel.swift deleted file mode 100644 index 90535b66a..000000000 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/BanksBottomViewModel.swift +++ /dev/null @@ -1,114 +0,0 @@ -// -// BanksBottomViewModel.swift -// GiniMerchantSDK -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit -import GiniUtilites - -public protocol PaymentProvidersBottomViewProtocol: AnyObject { - func didSelectPaymentProvider(paymentProvider: PaymentProvider) - func didTapOnClose() - func didTapOnMoreInformation() - func didTapOnContinueOnShareBottomSheet() - func didTapForwardOnInstallBottomSheet() - func didTapOnPayButton() -} - -struct PaymentProviderAdditionalInfo { - var isSelected: Bool - var isInstalled: Bool - let paymentProvider: PaymentProvider -} - -final class BanksBottomViewModel { - - weak var viewDelegate: PaymentProvidersBottomViewProtocol? - - var paymentProviders: [PaymentProviderAdditionalInfo] = [] - private var selectedPaymentProvider: PaymentProvider? - - let maximumViewHeight: CGFloat = UIScreen.main.bounds.height - Constants.topPaddingView - let rowHeight: CGFloat = Constants.cellSizeHeight - var bottomViewHeight: CGFloat = 0 - var heightTableView: CGFloat = 0 - - let backgroundColor: UIColor = GiniColor.standard7.uiColor() - let rectangleColor: UIColor = GiniColor.standard5.uiColor() - let dimmingBackgroundColor: UIColor = GiniColor(lightModeColor: UIColor.black, - darkModeColor: UIColor.white).uiColor().withAlphaComponent(0.4) - - let selectBankTitleText: String = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.select.bank.label", - comment: "Select bank text from the top label on payment providers bottom sheet") - let selectBankLabelAccentColor: UIColor = GiniColor(lightModeColorName: .dark1, darkModeColorName: .light2).uiColor() - var selectBankLabelFont: UIFont - - let closeTitleIcon: UIImage = GiniMerchantImage.close.preferredUIImage() - let closeIconAccentColor: UIColor = GiniColor.standard2.uiColor() - - let descriptionText: String = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.providers.list.description", - comment: "Top description text on payment providers bottom sheet") - let descriptionLabelAccentColor: UIColor = GiniColor.standard3.uiColor() - var descriptionLabelFont: UIFont - - private var urlOpener: URLOpener - - init(paymentProviders: PaymentProviders, selectedPaymentProvider: PaymentProvider?, urlOpener: URLOpener = URLOpener(UIApplication.shared)) { - self.selectedPaymentProvider = selectedPaymentProvider - self.urlOpener = urlOpener - - self.selectBankLabelFont = GiniMerchantConfiguration.shared.font(for: .subtitle2) - self.descriptionLabelFont = GiniMerchantConfiguration.shared.font(for: .captions1) - - self.paymentProviders = paymentProviders - .map({ PaymentProviderAdditionalInfo(isSelected: $0.id == selectedPaymentProvider?.id, - isInstalled: isPaymentProviderInstalled(paymentProvider: $0), - paymentProvider: $0)}) - .filter { $0.paymentProvider.gpcSupportedPlatforms.contains(.ios) || $0.paymentProvider.openWithSupportedPlatforms.contains(.ios) } - .sorted(by: { ($0.paymentProvider.index ?? 0 < $1.paymentProvider.index ?? 0) }) - .sorted(by: { ($0.isInstalled && !$1.isInstalled) }) - self.calculateHeights() - } - - private func calculateHeights() { - let totalTableViewHeight = CGFloat(paymentProviders.count) * Constants.cellSizeHeight - let totalBottomViewHeight = Constants.blankBottomViewHeight + totalTableViewHeight - if totalBottomViewHeight > maximumViewHeight { - self.heightTableView = maximumViewHeight - Constants.blankBottomViewHeight - self.bottomViewHeight = maximumViewHeight - } else { - self.heightTableView = totalTableViewHeight - self.bottomViewHeight = totalTableViewHeight + Constants.blankBottomViewHeight - } - } - - func paymentProvidersViewModel(paymentProvider: PaymentProviderAdditionalInfo) -> BankSelectionTableViewCellModel { - BankSelectionTableViewCellModel(paymentProvider: paymentProvider) - } - - func didTapOnClose() { - viewDelegate?.didTapOnClose() - } - - func didTapOnMoreInformation() { - viewDelegate?.didTapOnMoreInformation() - } - - private func isPaymentProviderInstalled(paymentProvider: PaymentProvider) -> Bool { - if let urlAppScheme = URL(string: paymentProvider.appSchemeIOS) { - return urlOpener.canOpenLink(url: urlAppScheme) - } - return false - } -} - -extension BanksBottomViewModel { - enum Constants { - static let blankBottomViewHeight: CGFloat = 200.0 - static let cellSizeHeight: CGFloat = 64.0 - static let topPaddingView: CGFloat = 100.0 - } -} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/InstallAppBottomViewModel.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/InstallAppBottomViewModel.swift deleted file mode 100644 index 7f1396c47..000000000 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/InstallAppBottomViewModel.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// InstallAppBottomViewModel.swift -// GiniMerchantSDK -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit -import GiniUtilites -import GiniHealthAPILibrary - -protocol InstallAppBottomViewProtocol: AnyObject { - func didTapOnContinue() -} - -final class InstallAppBottomViewModel { - - let giniMerchantConfiguration = GiniMerchantConfiguration.shared - - var selectedPaymentProvider: PaymentProvider? - // Payment provider colors - var paymentProviderColors: ProviderColors? - - weak var viewDelegate: InstallAppBottomViewProtocol? - - let backgroundColor: UIColor = GiniColor.standard7.uiColor() - let rectangleColor: UIColor = GiniColor.standard5.uiColor() - let dimmingBackgroundColor: UIColor = GiniColor(lightModeColor: UIColor.black, - darkModeColor: UIColor.white).uiColor().withAlphaComponent(0.4) - - var titleText: String = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.install.app.bottom.sheet.title", - comment: "Install App Bottom sheet title") - let titleLabelAccentColor: UIColor = GiniColor.standard2.uiColor() - var titleLabelFont: UIFont - - private var bankImageIconData: Data? - var bankImageIcon: UIImage { - if let bankImageIconData { - return UIImage(data: bankImageIconData) ?? UIImage() - } - return UIImage() - } - var bankIconBorderColor = GiniColor.standard5.uiColor() - - // More information part - let moreInformationLabelTextColor: UIColor = GiniColor.standard3.uiColor() - let moreInformationAccentColor: UIColor = GiniColor.standard3.uiColor() - var moreInformationLabelText: String { - isBankInstalled ? - NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.install.app.bottom.sheet.tip.description", - comment: "Text for tip information label").replacingOccurrences(of: bankToReplaceString, - with: selectedPaymentProvider?.name ?? "") : - NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.install.app.bottom.sheet.notes.description", - comment: "Text for notes information label").replacingOccurrences(of: bankToReplaceString, - with: selectedPaymentProvider?.name ?? "") - } - - - var moreInformationLabelFont: UIFont - let moreInformationIcon: UIImage = GiniMerchantImage.info.preferredUIImage() - - // Pay invoice label - let continueLabelText: String = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.install.app.bottom.sheet.continue.button.text", - comment: "Title label used for the Continue button") - - var appStoreIcon: UIImage = GiniMerchantImage.appStore.preferredUIImage() - let bankToReplaceString = "[BANK]" - - var isBankInstalled: Bool { - selectedPaymentProvider?.appSchemeIOS.canOpenURLString() == true - } - - init(selectedPaymentProvider: PaymentProvider?) { - self.selectedPaymentProvider = selectedPaymentProvider - self.bankImageIconData = selectedPaymentProvider?.iconData - self.paymentProviderColors = selectedPaymentProvider?.colors - - titleText = titleText.replacingOccurrences(of: bankToReplaceString, with: selectedPaymentProvider?.name ?? "") - - self.titleLabelFont = giniMerchantConfiguration.font(for: .subtitle1) - self.moreInformationLabelFont = giniMerchantConfiguration.font(for: .captions1) - } - - func didTapOnContinue() { - viewDelegate?.didTapOnContinue() - } -} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/MoreInformationViewModel.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/MoreInformationViewModel.swift deleted file mode 100644 index b3b5c3230..000000000 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/MoreInformationViewModel.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// MoreInformationViewModel.swift -// GiniMerchantSDK -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit -import GiniUtilites - -protocol MoreInformationViewProtocol: AnyObject { - func didTapOnMoreInformation() -} - -final class MoreInformationViewModel { - - weak var delegate: MoreInformationViewProtocol? - // More information part - let moreInformationAccentColor: UIColor = GiniColor.standard2.uiColor() - let moreInformationLabelTextColor: UIColor = GiniColor.standard4.uiColor() - let moreInformationActionablePartText = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.more.information.underlined.part", - comment: "Text for more information actionable part from the label") - var moreInformationLabelLinkFont: UIFont - let moreInformationIcon: UIImage = GiniMerchantImage.info.preferredUIImage() - - init() { - moreInformationLabelLinkFont = GiniMerchantConfiguration.shared.font(for: .captions2) - } - - func tapOnMoreInformation() { - delegate?.didTapOnMoreInformation() - } -} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/OnboardingShareInvoiceScreenCount.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/OnboardingShareInvoiceScreenCount.swift deleted file mode 100644 index 4df435bde..000000000 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/OnboardingShareInvoiceScreenCount.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// OnboardingShareInvoiceScreenCount.swift -// GiniMerchantSDK -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import Foundation - -struct OnboardingShareInvoiceScreenCount: Codable { - var providerCounts: [String: Int] // Dictionary to store count for each provider -} - -extension OnboardingShareInvoiceScreenCount { - // UserDefaults key for storing onboarding presentation counts - private static let onboardingShareScreenCountKey = "OnboardingShareInvoiceScreenCount" - - // Load onboarding presentation counts from UserDefaults - static func load() -> OnboardingShareInvoiceScreenCount { - if let data = UserDefaults.standard.data(forKey: onboardingShareScreenCountKey), - let counts = try? JSONDecoder().decode(OnboardingShareInvoiceScreenCount.self, from: data) { - return counts - } - return OnboardingShareInvoiceScreenCount(providerCounts: [:]) - } - - // Save onboarding presentation counts to UserDefaults - func save() { - if let data = try? JSONEncoder().encode(self) { - UserDefaults.standard.set(data, forKey: OnboardingShareInvoiceScreenCount.onboardingShareScreenCountKey) - } - } - - // Get presentation count for a specific provider - func presentationCount(forProvider providerID: String?) -> Int { - guard let providerID else { return 0 } - return providerCounts[providerID] ?? 0 - } - - // Increment presentation count for a specific provider - mutating func incrementPresentationCount(forProvider providerID: String?) { - guard let providerID else { return } - if let count = providerCounts[providerID] { - providerCounts[providerID] = count + 1 - } else { - providerCounts[providerID] = 1 - } - save() // Save updated counts to UserDefaults - } -} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentComponentViewModel.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentComponentViewModel.swift deleted file mode 100644 index fd27fe30c..000000000 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentComponentViewModel.swift +++ /dev/null @@ -1,182 +0,0 @@ -// -// PaymentComponentViewModel.swift -// GiniMerchantSDK -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit -import GiniUtilites -import GiniHealthAPILibrary - -/** - Delegate to inform about the actions happened of the custom payment component view. - You may find out when the user tapped on more information area, on the payment provider picker or on the pay invoice button - - */ -public protocol PaymentComponentViewProtocol: AnyObject { - /** - Called when the user tapped on the more information actionable label or the information icon - - - parameter documentId: Id of document - */ - func didTapOnMoreInformation(documentId: String?) - - /** - Called when the user tapped on payment provider picker to change the selected payment provider or install it - - - parameter documentId: Id of document - */ - func didTapOnBankPicker(documentId: String?) - - /** - Called when the user tapped on the pay the invoice button to pay the invoice/document - - parameter documentId: Id of document - */ - func didTapOnPayInvoice(documentId: String?) -} - -/** - Helping extension for using the PaymentComponentViewProtocol methods without the document ID. This should be kept by the document view model and passed hierarchically from there. - - */ -extension PaymentComponentViewProtocol { - public func didTapOnMoreInformation() { - didTapOnMoreInformation(documentId: nil) - } - public func didTapOnBankPicker() { - didTapOnBankPicker(documentId: nil) - } - public func didTapOnPayInvoice() { - didTapOnPayInvoice(documentId: nil) - } -} - -final class PaymentComponentViewModel { - let giniMerchantConfiguration: GiniMerchantConfiguration - - let backgroundColor: UIColor = UIColor.from(giniColor: GiniColor(lightModeColor: .clear, - darkModeColor: .clear)) - - // More information part - let moreInformationAccentColor: UIColor = GiniColor.standard2.uiColor() - let moreInformationLabelTextColor: UIColor = GiniColor.standard4.uiColor() - let moreInformationLabelText = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.more.information.label", - comment: "Text for more information label") - let moreInformationActionablePartText = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.more.information.underlined.part", - comment: "Text for more information actionable part from the label") - var moreInformationLabelFont: UIFont - var moreInformationLabelLinkFont: UIFont - - // Select bank label - let selectYourBankLabelText = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.select.your.bank.label", - comment: "Text for the select your bank label that's above the payment provider picker") - let selectYourBankLabelFont: UIFont - let selectYourBankAccentColor: UIColor = GiniColor.standard1.uiColor() - - // Bank image icon - private var bankImageIconData: Data? - var bankImageIcon: UIImage? { - guard let bankImageIconData else { return nil } - return UIImage(data: bankImageIconData) - } - - // Primary button - let notInstalledBankTextColor: UIColor = GiniColor.standard4.uiColor() - private let placeholderBankNameText: String = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.select.bank.label", - comment: "Placeholder text used when there isn't a payment provider app installed") - var selectBankButtonText: String { - showPaymentComponentInOneRow ? placeholderBankNameText : bankName ?? placeholderBankNameText - } - - let chevronDownIcon: UIImage = GiniMerchantImage.chevronDown.preferredUIImage() - let chevronDownIconColor: UIColor = GiniColor(lightModeColorName: .light7, darkModeColorName: .light1).uiColor() - - // Payment provider colors - var paymentProviderColors: ProviderColors? - - // CTA button - private let goToBankingAppLabelText: String = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.to.banking.app.label", - comment: "Title label used for the cta button when review screen is not present") - private let continueToOverviewLabelText: String = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.continue.to.overview.label", - comment: "Title label used for the cta button when review screen is present") - - var ctaButtonText: String { - isReviewScreenOn ? continueToOverviewLabelText : goToBankingAppLabelText - } - - private var paymentProviderScheme: String? - - weak var delegate: PaymentComponentViewProtocol? - - var documentId: String? - - var minimumButtonsHeight: CGFloat - - var hasBankSelected: Bool - - private var bankName: String? - private var isReviewScreenOn: Bool - - private var paymentComponentConfiguration: PaymentComponentConfiguration? - var shouldShowBrandedView: Bool { - paymentComponentConfiguration?.isPaymentComponentBranded ?? true - } - var showPaymentComponentInOneRow: Bool { - paymentComponentConfiguration?.showPaymentComponentInOneRow ?? false - } - var hideInfoForReturningUser: Bool { - paymentComponentConfiguration?.hideInfoForReturningUser ?? false - } - - init(paymentProvider: PaymentProvider?, - giniMerchantConfiguration: GiniMerchantConfiguration, - paymentComponentConfiguration: PaymentComponentConfiguration? = nil) { - self.giniMerchantConfiguration = giniMerchantConfiguration - - self.moreInformationLabelFont = giniMerchantConfiguration.font(for: .captions1) - self.moreInformationLabelLinkFont = giniMerchantConfiguration.font(for: .linkBold) - self.selectYourBankLabelFont = giniMerchantConfiguration.font(for: .subtitle2) - - self.hasBankSelected = paymentProvider != nil - self.bankImageIconData = paymentProvider?.iconData - self.paymentProviderColors = paymentProvider?.colors - self.paymentProviderScheme = paymentProvider?.appSchemeIOS - self.bankName = paymentProvider?.name - self.isReviewScreenOn = giniMerchantConfiguration.showPaymentReviewScreen - - self.minimumButtonsHeight = giniMerchantConfiguration.paymentComponentButtonsHeight - - self.paymentComponentConfiguration = paymentComponentConfiguration - } - - func tapOnMoreInformation() { - delegate?.didTapOnMoreInformation(documentId: documentId) - } - - func tapOnBankPicker() { - delegate?.didTapOnBankPicker(documentId: documentId) - } - - func tapOnPayInvoiceView() { - savePaymentComponentViewUsageStatus() - delegate?.didTapOnPayInvoice(documentId: documentId) - } - - // Function to check if Payment was used at least once - func isPaymentComponentUsed() -> Bool { - return UserDefaults.standard.bool(forKey: Constants.paymentComponentViewUsedKey) - } - - // Function to save the boolean value indicating whether Payment was used - private func savePaymentComponentViewUsageStatus() { - UserDefaults.standard.set(true, forKey: Constants.paymentComponentViewUsedKey) - } -} - -extension PaymentComponentViewModel { - private enum Constants { - static let paymentComponentViewUsedKey = "kPaymentComponentViewUsed" - } -} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentComponentsController.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentComponentsController.swift deleted file mode 100644 index 6925be285..000000000 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentComponentsController.swift +++ /dev/null @@ -1,489 +0,0 @@ -// -// PaymentComponentController.swift -// GiniMerchantSDK -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit -import GiniHealthAPILibrary -import GiniUtilites -/** - Protocol used to provide updates on the current status of the Payment Components Controller. - Uses a callback mechanism to handle payment provider requests. - */ -public protocol PaymentComponentsControllerProtocol: AnyObject { - func isLoadingStateChanged(isLoading: Bool) // Because we can't use Combine - func didFetchedPaymentProviders() -} - -protocol PaymentComponentsProtocol { - var isLoading: Bool { get set } - var selectedPaymentProvider: PaymentProvider? { get set } - func loadPaymentProviders() - func checkIfDocumentIsPayable(docId: String, completion: @escaping (Result) -> Void) - func paymentView(documentId: String?) -> UIView - func bankSelectionBottomSheet() -> UIViewController - func loadPaymentReviewScreenFor(documentID: String?, paymentInfo: PaymentInfo?, trackingDelegate: GiniMerchantTrackingDelegate?, completion: @escaping (UIViewController?, GiniMerchantError?) -> Void) - func paymentInfoViewController() -> UIViewController - func paymentViewBottomSheet(documentID: String?) -> UIViewController -} - -private enum PaymentComponentScreenType { - case paymentComponent - case bankPicker -} - -/** - The `PaymentComponentsController` class allows control over the payment components. - */ -public final class PaymentComponentsController: PaymentComponentsProtocol { - /// handling the Payment Component Controller delegate - public weak var delegate: PaymentComponentsControllerProtocol? - /// handling the Payment Component view delegate - public weak var viewDelegate: PaymentComponentViewProtocol? - /// handling the Payment Bottom view delegate - public weak var bottomViewDelegate: PaymentProvidersBottomViewProtocol? - - private var giniMerchant: GiniMerchant - private let giniMerchantConfiguration = GiniMerchantConfiguration.shared - private var paymentProviders: PaymentProviders = [] - - /// storing the current selected payment provider - public var selectedPaymentProvider: PaymentProvider? - - /// Payment Component View Configuration - public var paymentComponentConfiguration: PaymentComponentConfiguration? - - /// Previous presented view - private var previousPresentedView: PaymentComponentScreenType? - - /// reponsible for storing the loading state of the controller and passing it to the delegate listeners - var isLoading: Bool = false { - didSet { - delegate?.isLoadingStateChanged(isLoading: isLoading) - } - } - - var paymentComponentView: PaymentComponentView! - - /** - Initializer of the Payment Component Controller class. - - - Parameters: - - giniMerchant: An instance of GiniMerchant initialized with GiniHealthAPI. - - Returns: - - instance of the payment component controller class - */ - public init(giniMerchant: GiniMerchant) { - self.giniMerchant = giniMerchant - giniMerchantConfiguration.useInvoiceWithoutDocument = true - setupObservers() - } - - deinit { - NotificationCenter.default.removeObserver(self) - } - - /** - Retrieves the default installed payment provider, if available. - - Returns: a Payment Provider object. - */ - private func defaultInstalledPaymentProvider() -> PaymentProvider? { - savedPaymentProvider() - } - - /** - Loads the payment providers list and stores them. - - note: Also triggers a function that checks if the payment providers are installed. - */ - public func loadPaymentProviders() { - self.isLoading = true - self.giniMerchant.fetchBankingApps { [weak self] result in - self?.isLoading = false - switch result { - case let .success(paymentProviders): - self?.paymentProviders = paymentProviders - self?.selectedPaymentProvider = self?.defaultInstalledPaymentProvider() - self?.delegate?.didFetchedPaymentProviders() - case let .failure(error): - print("Couldn't load payment providers: \(error.localizedDescription)") - } - } - } - - private func storeDefaultPaymentProvider(paymentProvider: PaymentProvider) { - do { - let encoder = JSONEncoder() - let data = try encoder.encode(paymentProvider) - UserDefaults.standard.set(data, forKey: Constants.kDefaultPaymentProvider) - } catch { - print("Unable to encode payment provider: (\(error))") - } - } - - private func savedPaymentProvider() -> PaymentProvider? { - if let data = UserDefaults.standard.data(forKey: Constants.kDefaultPaymentProvider) { - do { - let decoder = JSONDecoder() - let paymentProvider = try decoder.decode(PaymentProvider.self, from: data) - if self.paymentProviders.contains(where: { $0.id == paymentProvider.id }) { - return paymentProvider - } - } catch { - print("Unable to decode payment provider: (\(error))") - } - } - return nil - } - - /** - Checks if the document is payable by extracting the IBAN. - - Parameters: - - docId: The ID of the uploaded document. - - completion: A closure for processing asynchronous data received from the service. It has a Result type parameter, representing either success or failure. The completion block is called on the main thread. - In the case of success, it includes a boolean value indicating whether the IBAN was extracted successfully. - In case of failure, it returns an error from the server side. - */ - public func checkIfDocumentIsPayable(docId: String, completion: @escaping (Result) -> Void) { - giniMerchant.checkIfDocumentIsPayable(docId: docId, completion: completion) - } - - /** - Provides a custom Gini view that contains more information, bank selection if available and a tappable button to pay the document/invoice - - - Parameters: - - Returns: a custom view - */ - public func paymentView(documentId: String?) -> UIView { - paymentComponentView = PaymentComponentView() - let paymentComponentViewModel = PaymentComponentViewModel(paymentProvider: selectedPaymentProvider, giniMerchantConfiguration: giniMerchantConfiguration, paymentComponentConfiguration: paymentComponentConfiguration) - paymentComponentViewModel.delegate = viewDelegate - paymentComponentViewModel.documentId = documentId - paymentComponentView.viewModel = paymentComponentViewModel - return paymentComponentView - } - - public func loadPaymentReviewScreenFor(documentID: String?, paymentInfo: PaymentInfo?, trackingDelegate: GiniMerchantTrackingDelegate?, completion: @escaping (UIViewController?, GiniMerchantError?) -> Void) { - previousPresentedView = nil - if !giniMerchantConfiguration.useInvoiceWithoutDocument { - guard let documentID else { - completion(nil, nil) - return - } - self.isLoading = true - self.giniMerchant.fetchDataForReview(documentId: documentID) { [weak self] result in - self?.isLoading = false - switch result { - case .success(let data): - guard let self else { - completion(nil, nil) - return - } - guard let selectedPaymentProvider else { - completion(nil, nil) - return - } - let vc = PaymentReviewViewController.instantiate(with: self.giniMerchant, - data: data, - paymentInfo: nil, - selectedPaymentProvider: selectedPaymentProvider, - trackingDelegate: trackingDelegate, - paymentComponentsController: self) - completion(vc, nil) - case .failure(let error): - completion(nil, error) - } - } - } else { - loadPaymentReviewScreenWithoutDocument(paymentInfo: paymentInfo, trackingDelegate: trackingDelegate, completion: completion) - } - } - - private func loadPaymentReviewScreenWithoutDocument(paymentInfo: PaymentInfo?, trackingDelegate: GiniMerchantTrackingDelegate?, completion: @escaping (UIViewController?, GiniMerchantError?) -> Void) { - previousPresentedView = nil - guard let selectedPaymentProvider else { - completion(nil, nil) - return - } - - let paymentReviewViewController = PaymentReviewViewController.instantiate(with: self.giniMerchant, - data: nil, - paymentInfo: paymentInfo, - selectedPaymentProvider: selectedPaymentProvider, - trackingDelegate: trackingDelegate, - paymentComponentsController: self) - completion(paymentReviewViewController, nil) - } - - // MARK: - Bottom Sheets - - public func paymentViewBottomSheet(documentID: String?) -> UIViewController { - previousPresentedView = .paymentComponent - let paymentComponentBottomView = PaymentComponentBottomView(paymentView: paymentView(documentId: documentID)) - return paymentComponentBottomView - } - - public func bankSelectionBottomSheet() -> UIViewController { - previousPresentedView = .bankPicker - let paymentProvidersBottomViewModel = BanksBottomViewModel(paymentProviders: paymentProviders, - selectedPaymentProvider: selectedPaymentProvider) - let paymentProvidersBottomView = BanksBottomView(viewModel: paymentProvidersBottomViewModel) - paymentProvidersBottomViewModel.viewDelegate = self - paymentProvidersBottomView.viewModel = paymentProvidersBottomViewModel - return paymentProvidersBottomView - } - - public func paymentInfoViewController() -> UIViewController { - let paymentInfoViewController = PaymentInfoViewController() - let paymentInfoViewModel = PaymentInfoViewModel(paymentProviders: paymentProviders) - paymentInfoViewController.viewModel = paymentInfoViewModel - return paymentInfoViewController - } - - public func installAppBottomSheet() -> UIViewController { - previousPresentedView = nil - let installAppBottomViewModel = InstallAppBottomViewModel(selectedPaymentProvider: selectedPaymentProvider) - installAppBottomViewModel.viewDelegate = self - let installAppBottomView = InstallAppBottomView(viewModel: installAppBottomViewModel) - return installAppBottomView - } - - public func shareInvoiceBottomSheet() -> UIViewController { - previousPresentedView = nil - let shareInvoiceBottomViewModel = ShareInvoiceBottomViewModel(selectedPaymentProvider: selectedPaymentProvider) - shareInvoiceBottomViewModel.viewDelegate = self - let shareInvoiceBottomView = ShareInvoiceBottomView(viewModel: shareInvoiceBottomViewModel) - incrementOnboardingCountFor(paymentProvider: selectedPaymentProvider) - return shareInvoiceBottomView - } - - // MARK: - Helping functions - public func canOpenPaymentProviderApp() -> Bool { - if supportsGPC() { - if selectedPaymentProvider?.appSchemeIOS.canOpenURLString() == true { - return true - } - } - return false - } - - public func supportsOpenWith() -> Bool { - if selectedPaymentProvider?.openWithSupportedPlatforms.contains(.ios) == true { - return true - } - return false - } - - public func supportsGPC() -> Bool { - if selectedPaymentProvider?.gpcSupportedPlatforms.contains(.ios) == true { - return true - } - return false - } - - public func obtainPDFURLFromPaymentRequest(paymentInfo: PaymentInfo, viewController: UIViewController) { - createPaymentRequest(paymentInfo: paymentInfo, completion: { [weak self] paymentRequestID, error in - if let paymentRequestID { - self?.loadPDFData(paymentRequestID: paymentRequestID, viewController: viewController) - } - }) - } - - public func createPaymentRequest(paymentInfo: PaymentInfo, completion: @escaping (_ paymentRequestID: String?, _ error: GiniMerchantError?) -> Void) { - giniMerchant.createPaymentRequest(paymentInfo: paymentInfo) { result in - switch result { - case let .success(requestId): - completion(requestId, nil) - case let .failure(error): - completion(nil, GiniMerchantError.apiError(error)) - } - } - } - - public func openPaymentProviderApp(requestId: String, universalLink: String) { - giniMerchant.openPaymentProviderApp(requestID: requestId, universalLink: universalLink) - } - - public func shouldShowOnboardingScreenFor() -> Bool { - let onboardingCounts = OnboardingShareInvoiceScreenCount.load() - let count = onboardingCounts.presentationCount(forProvider: selectedPaymentProvider?.name) - return count < Constants.numberOfTimesOnboardingShareScreenShouldAppear - } - - private func setupObservers() { - NotificationCenter.default.addObserver(self, selector: #selector(paymentInfoDissapeared), name: .paymentInfoDissapeared, object: nil) - } - - @objc - private func paymentInfoDissapeared() { - if previousPresentedView == .bankPicker { - didTapOnBankPicker() - } else if previousPresentedView == .paymentComponent { - didTapOnPayButton() - } - previousPresentedView = nil - } -} - -extension PaymentComponentsController: PaymentComponentViewProtocol { - public func didTapOnMoreInformation(documentId: String?) { - viewDelegate?.didTapOnMoreInformation() - } - - public func didTapOnBankPicker(documentId: String?) { - viewDelegate?.didTapOnBankPicker() - } - - public func didTapOnPayInvoice(documentId: String?) { - viewDelegate?.didTapOnPayInvoice() - } -} - -extension PaymentComponentsController: PaymentProvidersBottomViewProtocol { - public func didTapForwardOnInstallBottomSheet() { - print("Tapped Forward on Install Bottom Sheet") - } - - public func didTapOnContinueOnShareBottomSheet() { - print("Tapped Continue on Share Bottom Sheet") - } - - public func didSelectPaymentProvider(paymentProvider: PaymentProvider) { - selectedPaymentProvider = paymentProvider - storeDefaultPaymentProvider(paymentProvider: paymentProvider) - bottomViewDelegate?.didSelectPaymentProvider(paymentProvider: paymentProvider) - } - - public func didTapOnClose() { - bottomViewDelegate?.didTapOnClose() - } - - public func didTapOnMoreInformation() { - viewDelegate?.didTapOnMoreInformation() - } - - public func didTapOnPayButton() { - bottomViewDelegate?.didTapOnPayButton() - } -} - -extension PaymentComponentsController { - - private func incrementOnboardingCountFor(paymentProvider: PaymentProvider?) { - var onboardingCounts = OnboardingShareInvoiceScreenCount.load() - onboardingCounts.incrementPresentationCount(forProvider: paymentProvider?.name) - } - - private func loadPDFData(paymentRequestID: String, viewController: UIViewController) { - self.loadPDF(paymentRequestID: paymentRequestID, completion: { [weak self] pdfData in - let pdfPath = self?.writePDFDataToFile(data: pdfData, fileName: paymentRequestID) - - guard let pdfPath else { - print("Couldn't retrieve pdf URL") - return - } - - self?.sharePDF(pdfURL: pdfPath, paymentRequestID: paymentRequestID, viewController: viewController) { [weak self] (activity, _, _, _) in - guard activity != nil else { - return - } - - // Publish the payment request id only after a user has picked an activity (app) - self?.giniMerchant.delegate?.didCreatePaymentRequest(paymentRequestID: paymentRequestID) - } - }) - } - - private func writePDFDataToFile(data: Data, fileName: String) -> URL? { - do { - let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) - guard let docDirectoryPath = paths.first else { return nil} - let pdfFileName = fileName + Constants.pdfExtension - let pdfPath = docDirectoryPath.appendingPathComponent(pdfFileName) - try data.write(to: pdfPath) - return pdfPath - } catch { - print("Error while write pdf file to location: \(error.localizedDescription)") - return nil - } - } - - private func sharePDF(pdfURL: URL, paymentRequestID: String, viewController: UIViewController, - completionWithItemsHandler: @escaping UIActivityViewController.CompletionWithItemsHandler) { - // Create UIActivityViewController with the PDF file - let activityViewController = UIActivityViewController(activityItems: [pdfURL], applicationActivities: nil) - activityViewController.completionWithItemsHandler = completionWithItemsHandler - - // Exclude some activities if needed - activityViewController.excludedActivityTypes = [ - .addToReadingList, - .assignToContact, - .airDrop, - .mail, - .message, - .postToFacebook, - .postToVimeo, - .postToWeibo, - .postToFlickr, - .postToTwitter, - .postToTencentWeibo, - .copyToPasteboard, - .markupAsPDF, - .openInIBooks, - .print, - .saveToCameraRoll - ] - - // Present the UIActivityViewController - DispatchQueue.main.async { - if let popoverController = activityViewController.popoverPresentationController { - popoverController.sourceView = viewController.view - popoverController.sourceRect = CGRect(x: viewController.view.bounds.midX, y: viewController.view.bounds.midY, width: 0, height: 0) - popoverController.permittedArrowDirections = [] - } - - if (viewController.presentedViewController != nil) { - viewController.presentedViewController?.dismiss(animated: true, completion: { - viewController.present(activityViewController, animated: true, completion: nil) - }) - } else { - viewController.present(activityViewController, animated: true, completion: nil) - } - } - } - - private func loadPDF(paymentRequestID: String, completion: @escaping (Data) -> ()) { - isLoading = true - giniMerchant.paymentService.pdfWithQRCode(paymentRequestId: paymentRequestID) { [weak self] result in - self?.isLoading = false - switch result { - case .success(let data): - completion(data) - case .failure: - break - } - } - } -} - -extension PaymentComponentsController: ShareInvoiceBottomViewProtocol { - func didTapOnContinueToShareInvoice() { - bottomViewDelegate?.didTapOnContinueOnShareBottomSheet() - } -} - -extension PaymentComponentsController: InstallAppBottomViewProtocol { - func didTapOnContinue() { - bottomViewDelegate?.didTapForwardOnInstallBottomSheet() - } -} - -extension PaymentComponentsController { - private enum Constants { - static let kDefaultPaymentProvider = "defaultPaymentProvider" - static let pdfExtension = ".pdf" - static let numberOfTimesOnboardingShareScreenShouldAppear = 3 - } -} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentInfoViewModel.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentInfoViewModel.swift deleted file mode 100644 index 8a7e1072d..000000000 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PaymentInfoViewModel.swift +++ /dev/null @@ -1,142 +0,0 @@ -// -// PaymentInfoViewModel.swift -// GiniMerchantSDK -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit -import GiniUtilites -import GiniHealthAPILibrary - -struct FAQSection { - let title: String - var description: NSAttributedString - var isExtended: Bool -} - -final class PaymentInfoViewModel { - - var paymentProviders: PaymentProviders - - let backgroundColor: UIColor = GiniColor.standard7.uiColor() - - let titleText: String = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.title.label", - comment: "Payment Info title label text") - - let payBillsTitleText: String = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.pay.bills.title.label", - comment: "Payment Info pay bills title label text") - let payBillsTitleFont: UIFont - let payBillsTitleTextColor: UIColor = GiniColor.standard1.uiColor() - - private let payBillsDescriptionText: String = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.pay.bills.description.label", - comment: "Payment Info pay bills description text") - var payBillsDescriptionAttributedText: NSMutableAttributedString = NSMutableAttributedString() - var payBillsDescriptionLinkAttributes: [NSAttributedString.Key: Any] - private let payBillsDescriptionFont: UIFont - private let payBillsDescriptionTextColor: UIColor = GiniColor.standard1.uiColor() - private let giniWebsiteText = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.pay.bills.description.clickable.text", - comment: "Word range that's clickable in pay bills description") - private let giniFont: UIFont - private let giniURLText = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.gini.link", - comment: "Gini website link url") - - let questionsTitleText: String = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.paymentinfo.questions.title.label", - comment: "Payment Info questions title label text") - let questionsTitleFont: UIFont - let questionsTitleTextColor: UIColor = GiniColor.standard1.uiColor() - - private var answersFont: UIFont - private let answerPrivacyPolicyText = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.questions.answer.clickable.text", - comment: "Payment info answers clickable privacy policy") - private let privacyPolicyURLText = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.gini.privacypolicy.link", - comment: "Gini privacy policy link url") - private var linksFont: UIFont - private let linksTextColor: UIColor = GiniColor.accent1.uiColor() - - let separatorColor: UIColor = GiniColor.standard5.uiColor() - - var questions: [FAQSection] = [] - - init(paymentProviders: PaymentProviders) { - self.paymentProviders = paymentProviders - - let giniConfiguration = GiniMerchantConfiguration.shared - - payBillsTitleFont = giniConfiguration.font(for: .subtitle1) - payBillsDescriptionFont = giniConfiguration.font(for: .body2) - questionsTitleFont = giniConfiguration.font(for: .subtitle1) - giniFont = giniConfiguration.font(for: .button) - answersFont = giniConfiguration.font(for: .body2) - linksFont = giniConfiguration.font(for: .linkBold) - - payBillsDescriptionLinkAttributes = [.foregroundColor: linksTextColor] - - configurePayBillsGiniLink() - setupQuestions() - } - - private func setupQuestions() { - for index in 1 ... Constants.numberOfQuestions { - let answerAttributedString = answerWithAttributes(answer: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.questions.answer.\(index)", - comment: "Answers description")) - let questionSection = FAQSection(title: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.payment.info.questions.question.\(index)", - comment: "Questions titles"), - description: textWithLinks(linkFont: linksFont, - attributedString: answerAttributedString), - isExtended: false) - questions.append(questionSection) - } - } - - private func configurePayBillsGiniLink() { - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineHeightMultiple = Constants.payBillsDescriptionLineHeight - paragraphStyle.paragraphSpacing = Constants.payBillsParagraphSpacing - payBillsDescriptionAttributedText = NSMutableAttributedString(string: payBillsDescriptionText, - attributes: [.paragraphStyle: paragraphStyle, - .font: payBillsDescriptionFont, - .foregroundColor: payBillsTitleTextColor]) - payBillsDescriptionAttributedText = textWithLinks(linkFont: giniFont, - attributedString: payBillsDescriptionAttributedText) - } - - private func answerWithAttributes(answer: String) -> NSMutableAttributedString { - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineHeightMultiple = Constants.answersLineHeight - paragraphStyle.paragraphSpacing = Constants.answersParagraphSpacing - let answerAttributedText = NSMutableAttributedString(string: answer, - attributes: [.font: answersFont, .paragraphStyle: paragraphStyle]) - return answerAttributedText - } - - private func textWithLinks(linkFont: UIFont, attributedString: NSMutableAttributedString) -> NSMutableAttributedString { - let attributedString = attributedString - let giniRange = (attributedString.string as NSString).range(of: giniWebsiteText) - attributedString.addLinkToRange(link: giniURLText, - range: giniRange, - linkFont: linkFont, - textToRemove: Constants.linkTextToRemove) - let privacyPolicyRange = (attributedString.string as NSString).range(of: answerPrivacyPolicyText) - attributedString.addLinkToRange(link: privacyPolicyURLText, - range: privacyPolicyRange, - linkFont: linkFont, - textToRemove: Constants.linkTextToRemove) - return attributedString - } -} - -extension PaymentInfoViewModel { - private enum Constants { - static let numberOfQuestions = 6 - - static let payBillsDescriptionLineHeight = 1.32 - static let payBillsParagraphSpacing = 10.0 - - static let answersLineHeight = 1.32 - static let answersParagraphSpacing = 10.0 - - static let linkTextToRemove = "[LINK]" - } -} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PoweredByGiniViewModel.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PoweredByGiniViewModel.swift deleted file mode 100644 index 5674f4939..000000000 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/PoweredByGiniViewModel.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// PoweredByGiniViewModel.swift -// GiniMerchantSDK -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit -import GiniUtilites - -final class PoweredByGiniViewModel { - - // powered by Gini view - let poweredByGiniLabelText: String = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.powered.by.gini.label", comment: "") - let poweredByGiniLabelFont: UIFont - let poweredByGiniLabelAccentColor: UIColor = GiniColor.standard4.uiColor() - let giniIcon: UIImage = GiniMerchantImage.logo.preferredUIImage() - - init() { - self.poweredByGiniLabelFont = GiniMerchantConfiguration.shared.font(for: .captions2) - } -} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/ShareInvoiceBottomView.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/ShareInvoiceBottomView.swift deleted file mode 100644 index 0107f6383..000000000 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/ShareInvoiceBottomView.swift +++ /dev/null @@ -1,309 +0,0 @@ -// -// ShareInvoiceBottomView.swift -// GiniMerchantSDK -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit -import GiniUtilites - -class ShareInvoiceBottomView: BottomSheetViewController { - - var viewModel: ShareInvoiceBottomViewModel - - private let contentStackView = EmptyStackView(orientation: .vertical) - - private let titleView = EmptyView() - - private lazy var titleLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.text = viewModel.titleText - label.textColor = viewModel.titleLabelAccentColor - label.font = viewModel.titleLabelFont - label.numberOfLines = 0 - label.lineBreakMode = .byTruncatingTail - return label - }() - - private let descriptionView = EmptyView() - - private lazy var descriptionLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.text = viewModel.descriptionLabelText - label.textColor = viewModel.descriptionAccentColor - label.font = viewModel.descriptionLabelFont - label.numberOfLines = 0 - label.lineBreakMode = .byTruncatingTail - return label - }() - - private lazy var appsView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = viewModel.appsBackgroundColor - return view - }() - - private lazy var appsStackView: UIStackView = { - let stackView = EmptyStackView(orientation: .horizontal) - stackView.distribution = .fillEqually - stackView.spacing = Constants.appsViewSpacing - return stackView - }() - - private lazy var bankIconImageView: UIImageView = { - let imageView = UIImageView(image: viewModel.bankImageIcon) - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.frame = CGRect(x: 0, y: 0, width: Constants.bankIconSize, height: Constants.bankIconSize) - imageView.roundCorners(corners: .allCorners, radius: Constants.bankIconCornerRadius) - imageView.layer.borderWidth = Constants.bankIconBorderWidth - imageView.layer.borderColor = viewModel.bankIconBorderColor.cgColor - return imageView - }() - - private let tipView = EmptyView() - - private lazy var tipStackView: UIStackView = { - let stackView = EmptyStackView(orientation: .horizontal) - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.spacing = Constants.viewPaddingConstraint - stackView.distribution = .fillProportionally - return stackView - }() - - private lazy var tipLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = viewModel.tipAccentColor - label.font = viewModel.tipLabelFont - label.numberOfLines = 0 - label.text = viewModel.tipLabelText - - let tipActionableAttributtedString = NSMutableAttributedString(string: viewModel.tipLabelText) - let tipPartString = (viewModel.tipLabelText as NSString).range(of: viewModel.tipActionablePartText) - tipActionableAttributtedString.addAttribute(.foregroundColor, - value: viewModel.tipAccentColor, - range: tipPartString) - tipActionableAttributtedString.addAttribute(NSAttributedString.Key.underlineStyle, - value: NSUnderlineStyle.single.rawValue, - range: tipPartString) - tipActionableAttributtedString.addAttribute(NSAttributedString.Key.font, - value: viewModel.tipLabelLinkFont, - range: tipPartString) - let tapOnMoreInformation = UITapGestureRecognizer(target: self, - action: #selector(tapOnLabelAction(gesture:))) - label.isUserInteractionEnabled = true - label.addGestureRecognizer(tapOnMoreInformation) - label.attributedText = tipActionableAttributtedString - return label - }() - - private lazy var tipButton: UIButton = { - let button = UIButton(type: .system) - button.translatesAutoresizingMaskIntoConstraints = false - button.setImage(viewModel.tipIcon, for: .normal) - button.tintColor = viewModel.tipAccentColor - button.isUserInteractionEnabled = false - button.imageView?.contentMode = .scaleAspectFit - return button - }() - - private let continueView = EmptyView() - - private lazy var continueButton: PaymentPrimaryButton = { - let button = PaymentPrimaryButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.configure(with: viewModel.giniMerchantConfiguration.primaryButtonConfiguration) - button.customConfigure(paymentProviderColors: viewModel.paymentProviderColors, - text: viewModel.continueLabelText) - return button - }() - - private let bottomView = EmptyView() - - private let bottomStackView = EmptyStackView(orientation: .horizontal) - - private lazy var poweredByGiniView: PoweredByGiniView = { - let view = PoweredByGiniView() - view.viewModel = PoweredByGiniViewModel() - return view - }() - - override func viewDidLoad() { - super.viewDidLoad() - setupView() - } - - init(viewModel: ShareInvoiceBottomViewModel) { - self.viewModel = viewModel - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupView() { - setupViewHierarchy() - setupLayout() - setButtonsState() - } - - private func setupViewHierarchy() { - titleView.addSubview(titleLabel) - contentStackView.addArrangedSubview(titleView) - descriptionView.addSubview(descriptionLabel) - contentStackView.addArrangedSubview(descriptionView) - generateAppViews().forEach { appView in - appsStackView.addArrangedSubview(appView) - } - appsView.addSubview(appsStackView) - contentStackView.addArrangedSubview(appsView) - tipStackView.addArrangedSubview(tipButton) - tipStackView.addArrangedSubview(tipLabel) - tipView.addSubview(tipStackView) - contentStackView.addArrangedSubview(tipView) - continueView.addSubview(continueButton) - contentStackView.addArrangedSubview(continueView) - bottomStackView.addArrangedSubview(UIView()) - bottomStackView.addArrangedSubview(poweredByGiniView) - bottomView.addSubview(bottomStackView) - contentStackView.addArrangedSubview(bottomView) - self.setContent(content: contentStackView) - } - - private func setupLayout() { - setupTitleViewConstraints() - setupDescriptionViewConstraints() - setupAppsView() - setupTipViewConstraints() - setupContinueButtonConstraints() - setupPoweredByGiniConstraints() - } - - private func setButtonsState() { - continueButton.didTapButton = { [weak self] in - self?.tapOnContinueButton() - } - } - - private func setupTitleViewConstraints() { - NSLayoutConstraint.activate([ - titleLabel.leadingAnchor.constraint(equalTo: titleView.leadingAnchor, constant: Constants.viewPaddingConstraint), - titleLabel.trailingAnchor.constraint(equalTo: titleView.trailingAnchor, constant: -Constants.viewPaddingConstraint), - titleLabel.topAnchor.constraint(equalTo: titleView.topAnchor, constant: Constants.topBottomPaddingConstraint), - titleLabel.bottomAnchor.constraint(equalTo: titleView.bottomAnchor, constant: -Constants.topBottomPaddingConstraint) - ]) - } - - private func setupDescriptionViewConstraints() { - NSLayoutConstraint.activate([ - descriptionLabel.leadingAnchor.constraint(equalTo: descriptionView.leadingAnchor, constant: Constants.viewPaddingConstraint), - descriptionLabel.trailingAnchor.constraint(equalTo: descriptionView.trailingAnchor, constant: -Constants.viewPaddingConstraint), - descriptionLabel.topAnchor.constraint(equalTo: descriptionView.topAnchor, constant: Constants.topBottomPaddingConstraint), - descriptionLabel.bottomAnchor.constraint(equalTo: descriptionView.bottomAnchor, constant: -Constants.bottomDescriptionConstraint) - ]) - } - - private func setupAppsView() { - NSLayoutConstraint.activate([ - appsView.heightAnchor.constraint(equalToConstant: Constants.appsViewHeight), - appsStackView.leadingAnchor.constraint(equalTo: appsView.leadingAnchor), - appsStackView.topAnchor.constraint(equalTo: appsView.topAnchor, constant: Constants.topAnchorAppsViewConstraint), - appsStackView.bottomAnchor.constraint(equalTo: appsView.bottomAnchor, constant: -Constants.viewPaddingConstraint), - appsStackView.trailingAnchor.constraint(equalTo: appsView.trailingAnchor, constant: Constants.trailingAppsViewConstraint) - ]) - } - - private func setupTipViewConstraints() { - NSLayoutConstraint.activate([ - tipStackView.leadingAnchor.constraint(equalTo: tipView.leadingAnchor, constant: Constants.viewPaddingConstraint), - tipStackView.trailingAnchor.constraint(equalTo: tipView.trailingAnchor, constant: -Constants.viewPaddingConstraint), - tipStackView.topAnchor.constraint(equalTo: tipView.topAnchor, constant: Constants.topAnchorTipViewConstraint), - tipStackView.bottomAnchor.constraint(equalTo: tipView.bottomAnchor, constant: -Constants.topBottomPaddingConstraint), - tipButton.widthAnchor.constraint(equalToConstant: Constants.tipIconSize) - ]) - } - - private func setupContinueButtonConstraints() { - NSLayoutConstraint.activate([ - continueButton.leadingAnchor.constraint(equalTo: continueView.leadingAnchor, constant: Constants.viewPaddingConstraint), - continueButton.trailingAnchor.constraint(equalTo: continueView.trailingAnchor, constant: -Constants.viewPaddingConstraint), - continueButton.heightAnchor.constraint(equalToConstant: Constants.continueButtonViewHeight), - continueButton.topAnchor.constraint(equalTo: continueView.topAnchor, constant: Constants.topBottomPaddingConstraint), - continueButton.bottomAnchor.constraint(equalTo: continueView.bottomAnchor) - ]) - } - - private func setupPoweredByGiniConstraints() { - NSLayoutConstraint.activate([ - bottomStackView.leadingAnchor.constraint(equalTo: bottomView.leadingAnchor, constant: Constants.viewPaddingConstraint), - bottomStackView.trailingAnchor.constraint(equalTo: bottomView.trailingAnchor, constant: -Constants.viewPaddingConstraint), - bottomStackView.topAnchor.constraint(equalTo: bottomView.topAnchor, constant: Constants.topAnchorPoweredByGiniConstraint), - bottomStackView.bottomAnchor.constraint(equalTo: bottomView.bottomAnchor), - bottomStackView.heightAnchor.constraint(equalToConstant: Constants.bottomViewHeight) - ]) - } - - @objc - private func tapOnContinueButton() { - viewModel.didTapOnContinue() - } - - @objc - private func tapOnAppStoreButton() { - openPaymentProvidersAppStoreLink(urlString: viewModel.selectedPaymentProvider?.appStoreUrlIOS) - } - - @objc - private func tapOnLabelAction(gesture: UITapGestureRecognizer) { - if gesture.didTapAttributedTextInLabel(label: tipLabel, - targetText: viewModel.tipActionablePartText) { - tapOnAppStoreButton() - } - } - - private func openPaymentProvidersAppStoreLink(urlString: String?) { - guard let urlString = urlString else { - print("AppStore link unavailable for this payment provider") - return - } - if let url = URL(string: urlString), UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url) - } - } - - private func generateAppViews() -> [ShareInvoiceSingleAppView] { - var viewsToReturn: [ShareInvoiceSingleAppView] = [] - viewModel.appsMocked.forEach { singleApp in - let view = ShareInvoiceSingleAppView() - view.configure(image: singleApp.image, title: singleApp.title, isMoreButton: singleApp.isMoreButton) - viewsToReturn.append(view) - } - return viewsToReturn - } -} - -extension ShareInvoiceBottomView { - enum Constants { - static let viewPaddingConstraint = 16.0 - static let topBottomPaddingConstraint = 10.0 - static let bottomDescriptionConstraint = 20.0 - static let bankIconSize = 36 - static let bankIconCornerRadius = 6.0 - static let bankIconBorderWidth = 1.0 - static let continueButtonViewHeight = 56.0 - static let appsViewSpacing: CGFloat = -20 - static let appsViewHeight: CGFloat = 112.0 - static let topAnchorAppsViewConstraint = 20.0 - static let trailingAppsViewConstraint = 40.0 - static let topAnchorTipViewConstraint = 5.0 - static let topAnchorPoweredByGiniConstraint = 5.0 - static let tipIconSize = 24.0 - static let bottomViewHeight = 44.0 - } -} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/ShareInvoiceBottomViewModel.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/ShareInvoiceBottomViewModel.swift deleted file mode 100644 index 5b566aa1e..000000000 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponent/ShareInvoiceBottomViewModel.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// ShareInvoiceBottomViewModel.swift -// GiniMerchantSDK -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import UIKit -import GiniUtilites -import GiniHealthAPILibrary - -protocol ShareInvoiceBottomViewProtocol: AnyObject { - func didTapOnContinueToShareInvoice() -} - -struct SingleApp { - var title: String - var image: UIImage? - var isMoreButton: Bool -} - -final class ShareInvoiceBottomViewModel { - - var giniMerchantConfiguration = GiniMerchantConfiguration.shared - - var selectedPaymentProvider: PaymentProvider? - // Payment provider colors - var paymentProviderColors: ProviderColors? - - weak var viewDelegate: ShareInvoiceBottomViewProtocol? - - let backgroundColor: UIColor = GiniColor.standard7.uiColor() - let rectangleColor: UIColor = GiniColor.standard5.uiColor() - let dimmingBackgroundColor: UIColor = GiniColor(lightModeColor: UIColor.black, - darkModeColor: UIColor.white).uiColor().withAlphaComponent(0.4) - let appRectangleBackgroundColor: UIColor = GiniColor.standard6.uiColor() - - // Title label - var titleText: String = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.share.invoice.bottom.sheet.title", - comment: "Share Invoice Bottom sheet title") - let titleLabelAccentColor: UIColor = GiniColor.standard2.uiColor() - var titleLabelFont: UIFont - - private var bankImageIconData: Data? - var bankImageIcon: UIImage { - if let bankImageIconData { - return UIImage(data: bankImageIconData) ?? UIImage() - } - return UIImage() - } - var bankIconBorderColor = GiniColor.standard5.uiColor() - - // Description label - let descriptionLabelTextColor: UIColor = GiniColor.standard3.uiColor() - let descriptionAccentColor: UIColor = GiniColor.standard3.uiColor() - var descriptionLabelText: String = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.share.invoice.bottom.sheet.description", - comment: "Text description for share bottom sheet") - var descriptionLabelFont: UIFont - - // Apps View - let appsBackgroundColor: UIColor = GiniColor.standard6.uiColor() - let moreIcon: UIImage = GiniMerchantImage.more.preferredUIImage() - - // Tip label - let tipAccentColor: UIColor = GiniColor.standard2.uiColor() - let tipLabelTextColor: UIColor = GiniColor.standard4.uiColor() - var tipLabelText = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.share.invoice.bottom.sheet.tip.description", - comment: "Text for tip label") - let tipActionablePartText = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.share.invoice.bottom.sheet.tip.underlined.part", - comment: "Text for tip actionable part from the label") - var tipLabelFont: UIFont - var tipLabelLinkFont: UIFont - let tipIcon: UIImage = GiniMerchantImage.info.preferredUIImage() - - // Continue label - let continueLabelText: String = NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.share.invoice.bottom.sheet.continue.button.text", - comment: "Title label used for the Continue button") - - let bankToReplaceString = "[BANK]" - - var appsMocked: [SingleApp] = [] - - init(selectedPaymentProvider: PaymentProvider?) { - self.selectedPaymentProvider = selectedPaymentProvider - self.bankImageIconData = selectedPaymentProvider?.iconData - self.paymentProviderColors = selectedPaymentProvider?.colors - - titleText = titleText.replacingOccurrences(of: bankToReplaceString, with: selectedPaymentProvider?.name ?? "") - descriptionLabelText = descriptionLabelText.replacingOccurrences(of: bankToReplaceString, with: selectedPaymentProvider?.name ?? "") - tipLabelText = tipLabelText.replacingOccurrences(of: bankToReplaceString, with: selectedPaymentProvider?.name ?? "") - self.titleLabelFont = giniMerchantConfiguration.font(for: .subtitle1) - self.descriptionLabelFont = giniMerchantConfiguration.font(for: .captions1) - self.tipLabelFont = giniMerchantConfiguration.font(for: .captions1) - self.tipLabelLinkFont = giniMerchantConfiguration.font(for: .linkBold) - - self.generateAppMockedElements() - } - - private func generateAppMockedElements() { - for _ in 0..<2 { - self.appsMocked.append(SingleApp(title: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.share.invoice.bottom.sheet.app", comment: ""), isMoreButton: false)) - } - self.appsMocked.append(SingleApp(title: selectedPaymentProvider?.name ?? "", image: bankImageIcon, isMoreButton: false)) - self.appsMocked.append(SingleApp(title: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.share.invoice.bottom.sheet.app", comment: ""), isMoreButton: false)) - self.appsMocked.append(SingleApp(title: NSLocalizedStringPreferredFormat("gini.merchant.paymentcomponent.share.invoice.bottom.sheet.more", comment: ""), image: moreIcon, isMoreButton: true)) - - } - - func didTapOnContinue() { - viewDelegate?.didTapOnContinueToShareInvoice() - } -} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponentsController.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponentsController.swift new file mode 100644 index 000000000..347a0f23a --- /dev/null +++ b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentComponentsController.swift @@ -0,0 +1,813 @@ +// +// PaymentComponentController.swift +// GiniMerchantSDK +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + + +import UIKit +import GiniInternalPaymentSDK +import GiniHealthAPILibrary +import GiniUtilites + +/** + Protocol used to provide updates on the current status of the Payment Components Controller. + Uses a callback mechanism to handle payment provider requests. + */ +public protocol PaymentComponentsControllerProtocol: AnyObject { + func isLoadingStateChanged(isLoading: Bool) // Because we can't use Combine + func didFetchedPaymentProviders() +} + +/// A protocol for handling user actions in the Payment Providers bottom views. +public protocol PaymentProvidersBottomViewProtocol: AnyObject { + func didSelectPaymentProvider(paymentProvider: PaymentProvider) + func didTapOnClose() + func didTapOnMoreInformation() + func didTapOnContinueOnShareBottomSheet() + func didTapForwardOnInstallBottomSheet() + func didTapOnPayButton() +} + +/// A protocol that provides configuration settings for various payment components. +public protocol PaymentComponentsConfigurationProvider { + var paymentReviewContainerConfiguration: PaymentReviewContainerConfiguration { get } + var installAppConfiguration: InstallAppConfiguration { get } + var bottomSheetConfiguration: BottomSheetConfiguration { get } + var shareInvoiceConfiguration: ShareInvoiceConfiguration { get } + var paymentInfoConfiguration: PaymentInfoConfiguration { get } + var bankSelectionConfiguration: BankSelectionConfiguration { get } + var paymentComponentsConfiguration: PaymentComponentsConfiguration { get } + var paymentReviewConfiguration: PaymentReviewConfiguration { get } + var poweredByGiniConfiguration: PoweredByGiniConfiguration { get } + var moreInformationConfiguration: MoreInformationConfiguration { get } + var paymentComponentConfiguration: PaymentComponentConfiguration { get set } + + var primaryButtonConfiguration: ButtonConfiguration { get } + var secondaryButtonConfiguration: ButtonConfiguration { get } + var defaultStyleInputFieldConfiguration: TextFieldConfiguration { get } + var errorStyleInputFieldConfiguration: TextFieldConfiguration { get } + var selectionStyleInputFieldConfiguration: TextFieldConfiguration { get } + + var showPaymentReviewCloseButton: Bool { get } + var paymentComponentButtonsHeight: CGFloat { get } +} + +/// A protocol that provides localized string resources for various payment components. +public protocol PaymentComponentsStringsProvider { + var paymentReviewContainerStrings: PaymentReviewContainerStrings { get } + var paymentComponentsStrings: PaymentComponentsStrings { get } + var installAppStrings: InstallAppStrings { get } + var shareInvoiceStrings: ShareInvoiceStrings { get } + var paymentInfoStrings: PaymentInfoStrings { get } + var banksBottomStrings: BanksBottomStrings { get } + var paymentReviewStrings: PaymentReviewStrings { get } + var poweredByGiniStrings: PoweredByGiniStrings { get } + var moreInformationStrings: MoreInformationStrings { get } +} + +protocol PaymentComponentsProtocol { + var isLoading: Bool { get set } + var selectedPaymentProvider: PaymentProvider? { get set } + func loadPaymentProviders() + func checkIfDocumentIsPayable(docId: String, completion: @escaping (Result) -> Void) + func paymentView(documentId: String?) -> UIView + func bankSelectionBottomSheet() -> UIViewController + func loadPaymentReviewScreenFor(documentID: String?, paymentInfo: PaymentInfo?, trackingDelegate: GiniMerchantTrackingDelegate?, completion: @escaping (UIViewController?, GiniMerchantError?) -> Void) + func paymentInfoViewController() -> UIViewController + func paymentViewBottomSheet(documentID: String?) -> UIViewController +} +/** + The `PaymentComponentsController` class allows control over the payment components. + */ +public final class PaymentComponentsController: PaymentComponentsProtocol, BottomSheetsProviderProtocol { + /// handling the Payment Component Controller delegate + public weak var delegate: PaymentComponentsControllerProtocol? + /// handling the Payment Component view delegate + public weak var viewDelegate: PaymentComponentViewProtocol? + /// handling the Payment Bottom view delegate + public weak var bottomViewDelegate: PaymentProvidersBottomViewProtocol? + + private let giniSDK: GiniMerchant + private var trackingDelegate: GiniMerchantTrackingDelegate? + + private var paymentProviders: GiniHealthAPILibrary.PaymentProviders = [] + + private let configurationProvider: PaymentComponentsConfigurationProvider + private let stringsProvider: PaymentComponentsStringsProvider + + /// storing the current selected payment provider + public var selectedPaymentProvider: PaymentProvider? + private var healthSelectedPaymentProvider: GiniHealthAPILibrary.PaymentProvider? { + selectedPaymentProvider?.toHealthPaymentProvider() + } + + /// reponsible for storing the loading state of the controller and passing it to the delegate listeners + var isLoading: Bool = false { + didSet { + delegate?.isLoadingStateChanged(isLoading: isLoading) + } + } + + var paymentComponentView: PaymentComponentView! + + /// Previous presented view + private var previousPresentedView: PaymentComponentScreenType? + + /** + Initializer of the Payment Component Controller class. + + - Parameters: + - giniMerchant: An instance of GiniMerchant initialized with GiniHealthAPI. + - Returns: + - instance of the payment component controller class + */ + public init(giniMerchant: GiniMerchant & PaymentComponentsConfigurationProvider & PaymentComponentsStringsProvider) { + self.giniSDK = giniMerchant + self.configurationProvider = giniMerchant + self.stringsProvider = giniMerchant + setupObservers() + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + /** + Retrieves the default installed payment provider, if available. + - Returns: a Payment Provider object. + */ + private func defaultInstalledPaymentProvider() -> PaymentProvider? { + savedPaymentProvider() + } + + /** + Loads the payment providers list and stores them. + - note: Also triggers a function that checks if the payment providers are installed. + */ + public func loadPaymentProviders() { + self.isLoading = true + self.giniSDK.fetchBankingApps { [weak self] result in + self?.isLoading = false + switch result { + case let .success(paymentProviders): + self?.paymentProviders = paymentProviders.map{ $0.toHealthPaymentProvider() } + self?.selectedPaymentProvider = self?.defaultInstalledPaymentProvider() + self?.delegate?.didFetchedPaymentProviders() + case let .failure(error): + print("Couldn't load payment providers: \(error.localizedDescription)") + } + } + } + + private func storeDefaultPaymentProvider(paymentProvider: PaymentProvider) { + do { + let encoder = JSONEncoder() + let data = try encoder.encode(paymentProvider) + UserDefaults.standard.set(data, forKey: Constants.kDefaultPaymentProvider) + } catch { + print("Unable to encode payment provider: (\(error))") + } + } + + private func savedPaymentProvider() -> PaymentProvider? { + if let data = UserDefaults.standard.data(forKey: Constants.kDefaultPaymentProvider) { + do { + let decoder = JSONDecoder() + let paymentProvider = try decoder.decode(PaymentProvider.self, from: data) + if self.paymentProviders.contains(where: { $0.id == paymentProvider.id }) { + return paymentProvider + } + } catch { + print("Unable to decode payment provider: (\(error))") + } + } + return nil + } + + /** + Checks if the document is payable by extracting the IBAN. + - Parameters: + - docId: The ID of the uploaded document. + - completion: A closure for processing asynchronous data received from the service. It has a Result type parameter, representing either success or failure. The completion block is called on the main thread. + In the case of success, it includes a boolean value indicating whether the IBAN was extracted successfully. + In case of failure, it returns an error from the server side. + */ + public func checkIfDocumentIsPayable(docId: String, completion: @escaping (Result) -> Void) { + giniSDK.checkIfDocumentIsPayable(docId: docId, completion: completion) + } + + /** + Provides a custom Gini view that contains more information, bank selection if available and a tappable button to pay the document/invoice + + - Parameters: + - Returns: a custom view + */ + public func paymentView(documentId: String?) -> UIView { + let paymentComponentViewModel = PaymentComponentViewModel( + paymentProvider: healthSelectedPaymentProvider, + primaryButtonConfiguration: configurationProvider.primaryButtonConfiguration, + secondaryButtonConfiguration: configurationProvider.secondaryButtonConfiguration, + configuration: configurationProvider.paymentComponentsConfiguration, + strings: stringsProvider.paymentComponentsStrings, + poweredByGiniConfiguration: configurationProvider.poweredByGiniConfiguration, + poweredByGiniStrings: stringsProvider.poweredByGiniStrings, + moreInformationConfiguration: configurationProvider.moreInformationConfiguration, + moreInformationStrings: stringsProvider.moreInformationStrings, + minimumButtonsHeight: configurationProvider.paymentComponentButtonsHeight, + paymentComponentConfiguration: configurationProvider.paymentComponentConfiguration + ) + paymentComponentViewModel.delegate = self + paymentComponentViewModel.documentId = documentId + return PaymentComponentView(viewModel: paymentComponentViewModel) + } + + /** + Loads the payment review screen for the specified document or for the provided payment information + + This method fetches data for review based on the provided document ID. If the configuration + allows for invoice handling without a document, it directly loads the payment review screen + using the provided payment information provided. + + - Parameters: + - documentId: An optional identifier for the document being reviewed. + - paymentInfo: An optional `PaymentInfo` object containing details about the payment. + - trackingDelegate: An optional delegate for tracking events related to Gini Health. + - completion: A closure that is called with the resulting `UIViewController` and an optional + `GiniHealthError` once the loading process is complete. + */ + public func loadPaymentReviewScreenFor(documentID: String?, paymentInfo: PaymentInfo?, trackingDelegate: GiniMerchantTrackingDelegate?, completion: @escaping (UIViewController?, GiniMerchantError?) -> Void) { + previousPresentedView = nil + if !GiniMerchantConfiguration.shared.useInvoiceWithoutDocument { + guard let documentID else { + completion(nil, nil) + return + } + self.isLoading = true + self.giniSDK.fetchDataForReview(documentId: documentID) { [weak self] result in + self?.isLoading = false + switch result { + case .success(let data): + guard let self else { + completion(nil, nil) + return + } + guard let healthSelectedPaymentProvider else { + completion(nil, nil) + return + } + let viewModel = PaymentReviewModel(delegate: self, + bottomSheetsProvider: self, + document: data.document.toHealthDocument(), + extractions: data.extractions.map { $0.toHealthExtraction() }, + paymentInfo: nil, + selectedPaymentProvider: healthSelectedPaymentProvider, + configuration: configurationProvider.paymentReviewConfiguration, + strings: stringsProvider.paymentReviewStrings, + containerConfiguration: configurationProvider.paymentReviewContainerConfiguration, + containerStrings: stringsProvider.paymentReviewContainerStrings, + defaultStyleInputFieldConfiguration: configurationProvider.defaultStyleInputFieldConfiguration, + errorStyleInputFieldConfiguration: configurationProvider.errorStyleInputFieldConfiguration, + selectionStyleInputFieldConfiguration: configurationProvider.selectionStyleInputFieldConfiguration, + primaryButtonConfiguration: configurationProvider.primaryButtonConfiguration, + secondaryButtonConfiguration: configurationProvider.secondaryButtonConfiguration, + poweredByGiniConfiguration: configurationProvider.poweredByGiniConfiguration, + poweredByGiniStrings: stringsProvider.poweredByGiniStrings, + bottomSheetConfiguration: configurationProvider.bottomSheetConfiguration, + showPaymentReviewCloseButton: configurationProvider.showPaymentReviewCloseButton) + + let vc = PaymentReviewViewController.instantiate(viewModel: viewModel, + selectedPaymentProvider: healthSelectedPaymentProvider) + completion(vc, nil) + case .failure(let error): + completion(nil, error) + } + } + } else { + loadPaymentReviewScreenWithoutDocument(paymentInfo: paymentInfo, trackingDelegate: trackingDelegate, completion: completion) + } + } + + private func loadPaymentReviewScreenWithoutDocument(paymentInfo: PaymentInfo?, trackingDelegate: GiniMerchantTrackingDelegate?, completion: @escaping (UIViewController?, GiniMerchantError?) -> Void) { + previousPresentedView = nil + guard let healthSelectedPaymentProvider else { + completion(nil, nil) + return + } + + let viewModel = PaymentReviewModel(delegate: self, + bottomSheetsProvider: self, + document: nil, + extractions: nil, + paymentInfo: paymentInfo, + selectedPaymentProvider: healthSelectedPaymentProvider, + configuration: configurationProvider.paymentReviewConfiguration, + strings: stringsProvider.paymentReviewStrings, + containerConfiguration: configurationProvider.paymentReviewContainerConfiguration, + containerStrings: stringsProvider.paymentReviewContainerStrings, + defaultStyleInputFieldConfiguration: configurationProvider.defaultStyleInputFieldConfiguration, + errorStyleInputFieldConfiguration: configurationProvider.errorStyleInputFieldConfiguration, + selectionStyleInputFieldConfiguration: configurationProvider.selectionStyleInputFieldConfiguration, + primaryButtonConfiguration: configurationProvider.primaryButtonConfiguration, + secondaryButtonConfiguration: configurationProvider.secondaryButtonConfiguration, + poweredByGiniConfiguration: configurationProvider.poweredByGiniConfiguration, + poweredByGiniStrings: stringsProvider.poweredByGiniStrings, + bottomSheetConfiguration: configurationProvider.bottomSheetConfiguration, + showPaymentReviewCloseButton: configurationProvider.showPaymentReviewCloseButton) + + let vc = PaymentReviewViewController.instantiate(viewModel: viewModel, + selectedPaymentProvider: healthSelectedPaymentProvider) + completion(vc, nil) + } + + // MARK: - Bottom Sheets + + /** + Provides a custom Gini for view the payment view that is going to be presented as a bottom sheet. + + - Parameter documentId: An optional identifier for the document associated id with the payment. + - Returns: A configured `UIViewController` for displaying the payment bottom view. + */ + public func paymentViewBottomSheet(documentID: String?) -> UIViewController { + previousPresentedView = .paymentComponent + let paymentComponentBottomView = PaymentComponentBottomView(paymentView: paymentView(documentId: documentID), bottomSheetConfiguration: configurationProvider.bottomSheetConfiguration) + return paymentComponentBottomView + } + + /** + Provides a custom Gini view for the bank selection bottom sheet. + + - Parameter documentId: An optional identifier for the document associated id with the bank selection. + - Returns: A configured `UIViewController` for displaying the bank selection options. + */ + public func bankSelectionBottomSheet() -> UIViewController { + previousPresentedView = .bankPicker + let paymentProvidersBottomViewModel = BanksBottomViewModel(paymentProviders: paymentProviders, + selectedPaymentProvider: healthSelectedPaymentProvider, + configuration: configurationProvider.bankSelectionConfiguration, + strings: stringsProvider.banksBottomStrings, + poweredByGiniConfiguration: configurationProvider.poweredByGiniConfiguration, + poweredByGiniStrings: stringsProvider.poweredByGiniStrings, + moreInformationConfiguration: configurationProvider.moreInformationConfiguration, + moreInformationStrings: stringsProvider.moreInformationStrings) + paymentProvidersBottomViewModel.viewDelegate = self + return BanksBottomView(viewModel: paymentProvidersBottomViewModel, bottomSheetConfiguration: configurationProvider.bottomSheetConfiguration) + } + + /** + Provides a custom Gini view for displaying payment more information view. + + This method initializes a `PaymentInfoViewModel` with the necessary configurations and + localized strings, then returns a `PaymentInfoViewController` with the view model. + + - Returns: A configured `UIViewController` for displaying payment information. + */ + public func paymentInfoViewController() -> UIViewController { + let paymentInfoViewModel = PaymentInfoViewModel(paymentProviders: paymentProviders, + configuration: configurationProvider.paymentInfoConfiguration, + strings: stringsProvider.paymentInfoStrings, + poweredByGiniConfiguration: configurationProvider.poweredByGiniConfiguration, + poweredByGiniStrings: stringsProvider.poweredByGiniStrings) + return PaymentInfoViewController(viewModel: paymentInfoViewModel) + } + + /** + Provides a custom Gini view for installing the app if not present. + + This method initializes an `InstallAppBottomViewModel` with the necessary configurations and + localized strings, and returns an `InstallAppBottomView` configured with the view model. + + - Returns: A configured `BottomSheetViewController` for the app installation process. + */ + public func installAppBottomSheet() -> BottomSheetViewController { + previousPresentedView = nil + let installAppBottomViewModel = InstallAppBottomViewModel(selectedPaymentProvider: healthSelectedPaymentProvider, + installAppConfiguration: configurationProvider.installAppConfiguration, + strings: stringsProvider.installAppStrings, + primaryButtonConfiguration: configurationProvider.primaryButtonConfiguration, + poweredByGiniConfiguration: configurationProvider.poweredByGiniConfiguration, + poweredByGiniStrings: stringsProvider.poweredByGiniStrings) + installAppBottomViewModel.viewDelegate = self + let installAppBottomView = InstallAppBottomView(viewModel: installAppBottomViewModel, bottomSheetConfiguration: configurationProvider.bottomSheetConfiguration) + return installAppBottomView + } + + /** + Provides a custom Gini view for onboarding the user about the sharing invoices flow. + + This method initializes a `ShareInvoiceBottomViewModel` with the necessary configurations and + localized strings, and returns a `ShareInvoiceBottomView` configured with the view model. + It also increments the onboarding count for the selected payment provider. + + - Parameter documentId: An optional identifier for the document associated id with the invoice. + - Returns: A configured `BottomSheetViewController` for sharing invoices. + */ + public func shareInvoiceBottomSheet(qrCodeData: Data) -> BottomSheetViewController { + previousPresentedView = nil + let shareInvoiceBottomViewModel = ShareInvoiceBottomViewModel(selectedPaymentProvider: healthSelectedPaymentProvider, + configuration: configurationProvider.shareInvoiceConfiguration, + strings: stringsProvider.shareInvoiceStrings, + primaryButtonConfiguration: configurationProvider.primaryButtonConfiguration, + poweredByGiniConfiguration: configurationProvider.poweredByGiniConfiguration, + poweredByGiniStrings: stringsProvider.poweredByGiniStrings, + qrCodeData: qrCodeData, + paymentInfo: nil) + shareInvoiceBottomViewModel.viewDelegate = self + let shareInvoiceBottomView = ShareInvoiceBottomView(viewModel: shareInvoiceBottomViewModel, bottomSheetConfiguration: configurationProvider.bottomSheetConfiguration) + return shareInvoiceBottomView + } + + /** + Provides a custom Gini view for the bank selection bottom sheet. + + - Parameter documentId: An optional identifier for the document associated id with the bank selection. + - Returns: A configured `UIViewController` for displaying the bank selection options. + */ + public func bankSelectionBottomSheet(documentId: String?) -> UIViewController { + previousPresentedView = .bankPicker + let paymentProvidersBottomViewModel = BanksBottomViewModel(paymentProviders: paymentProviders, + selectedPaymentProvider: healthSelectedPaymentProvider, + configuration: configurationProvider.bankSelectionConfiguration, + strings: stringsProvider.banksBottomStrings, + poweredByGiniConfiguration: configurationProvider.poweredByGiniConfiguration, + poweredByGiniStrings: stringsProvider.poweredByGiniStrings, + moreInformationConfiguration: configurationProvider.moreInformationConfiguration, + moreInformationStrings: stringsProvider.moreInformationStrings) + paymentProvidersBottomViewModel.viewDelegate = self + paymentProvidersBottomViewModel.documentId = documentId + return BanksBottomView(viewModel: paymentProvidersBottomViewModel, bottomSheetConfiguration: configurationProvider.bottomSheetConfiguration) + } + + // MARK: - Helping functions + + /// Checks if the payment provider app can be opened based on the selected payment provider and GPC(Gini Pay Connect) support. + public func canOpenPaymentProviderApp() -> Bool { + if supportsGPC() { + if healthSelectedPaymentProvider?.appSchemeIOS.canOpenURLString() == true { + return true + } + } + return false + } + + /// Checks if the selected payment provider supports the "Open With" feature on iOS. + public func supportsOpenWith() -> Bool { + healthSelectedPaymentProvider?.openWithSupportedPlatforms.contains(.ios) == true + } + + /// Checks if the selected payment provider supports GPC(Gini Pay Connect) on iOS. + public func supportsGPC() -> Bool { + healthSelectedPaymentProvider?.openWithSupportedPlatforms.contains(.ios) == true + } + + /** + Creates a payment request and obtains the PDF URL using the provided payment information. + + - Parameter paymentInfo: The payment information for the request. + - Parameter viewController: The view controller used to present any necessary UI related to the request. + */ + public func obtainPDFURLFromPaymentRequest(paymentInfo: PaymentInfo, viewController: UIViewController) { + createPaymentRequest(paymentInfo: paymentInfo, completion: { [weak self] paymentRequestID, error in + if let paymentRequestID { + self?.loadPDFData(paymentRequestID: paymentRequestID, viewController: viewController) + } + }) + } + + /** + Creates a payment request with the provided payment information. + + - Parameters: + - paymentInfo: The information needed to create the payment request. + - completion: A closure that is called with the payment request ID or an error. + */ + public func createPaymentRequest(paymentInfo: PaymentInfo, completion: @escaping (_ paymentRequestID: String?, _ error: GiniMerchantError?) -> Void) { + giniSDK.createPaymentRequest(paymentInfo: paymentInfo) { result in + switch result { + case let .success(requestId): + completion(requestId, nil) + case let .failure(error): + completion(nil, GiniMerchantError.apiError(error)) + } + } + } + + private func setupObservers() { + NotificationCenter.default.addObserver(self, selector: #selector(paymentInfoDissapeared), name: .paymentInfoDissapeared, object: nil) + } + + @objc + private func paymentInfoDissapeared() { + if previousPresentedView == .bankPicker { + didTapOnBankPicker() + } else if previousPresentedView == .paymentComponent { + didTapOnPayButton() + } + previousPresentedView = nil + } +} + +extension PaymentComponentsController: PaymentComponentViewProtocol { + /// Handles the action when the more information button is tapped on the payment component view, using the provided document ID. + public func didTapOnMoreInformation(documentId: String?) { + viewDelegate?.didTapOnMoreInformation(documentId: documentId) + } + + /// Handles the action when the bank picker button is tapped on the payment component view, using the provided document ID. + public func didTapOnBankPicker(documentId: String?) { + viewDelegate?.didTapOnBankPicker(documentId: documentId) + } + + /// Handles the action when the pay invoice button is tapped on the payment component view, using the provided document ID. + public func didTapOnPayInvoice(documentId: String?) { + viewDelegate?.didTapOnPayInvoice(documentId: documentId) + } +} + +extension PaymentComponentsController { + private func loadPDFData(paymentRequestID: String, viewController: UIViewController) { + self.loadPDF(paymentRequestID: paymentRequestID, completion: { [weak self] pdfData in + let pdfPath = self?.writePDFDataToFile(data: pdfData, fileName: paymentRequestID) + + guard let pdfPath else { + print("Couldn't retrieve pdf URL") + return + } + + self?.sharePDF(pdfURL: pdfPath, paymentRequestID: paymentRequestID, viewController: viewController) { [weak self] (activity, _, _, _) in + guard activity != nil else { + return + } + + // Publish the payment request id only after a user has picked an activity (app) + self?.giniSDK.delegate?.didCreatePaymentRequest(paymentRequestID: paymentRequestID) + } + }) + } + + private func writePDFDataToFile(data: Data, fileName: String) -> URL? { + do { + let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + guard let docDirectoryPath = paths.first else { return nil} + let pdfFileName = fileName + Constants.pdfExtension + let pdfPath = docDirectoryPath.appendingPathComponent(pdfFileName) + try data.write(to: pdfPath) + return pdfPath + } catch { + print("Error while write pdf file to location: \(error.localizedDescription)") + return nil + } + } + + private func sharePDF(pdfURL: URL, paymentRequestID: String, viewController: UIViewController, + completionWithItemsHandler: @escaping UIActivityViewController.CompletionWithItemsHandler) { + // Create UIActivityViewController with the PDF file + let activityViewController = UIActivityViewController(activityItems: [pdfURL], applicationActivities: nil) + activityViewController.completionWithItemsHandler = completionWithItemsHandler + + // Exclude some activities if needed + activityViewController.excludedActivityTypes = [ + .addToReadingList, + .assignToContact, + .airDrop, + .mail, + .message, + .postToFacebook, + .postToVimeo, + .postToWeibo, + .postToFlickr, + .postToTwitter, + .postToTencentWeibo, + .copyToPasteboard, + .markupAsPDF, + .openInIBooks, + .print, + .saveToCameraRoll + ] + + // Present the UIActivityViewController + DispatchQueue.main.async { + if let popoverController = activityViewController.popoverPresentationController { + popoverController.sourceView = viewController.view + popoverController.sourceRect = CGRect(x: viewController.view.bounds.midX, y: viewController.view.bounds.midY, width: 0, height: 0) + popoverController.permittedArrowDirections = [] + } + + if (viewController.presentedViewController != nil) { + viewController.presentedViewController?.dismiss(animated: true, completion: { + viewController.present(activityViewController, animated: true, completion: nil) + }) + } else { + viewController.present(activityViewController, animated: true, completion: nil) + } + } + } + + private func loadPDF(paymentRequestID: String, completion: @escaping (Data) -> ()) { + isLoading = true + giniSDK.paymentService.pdfWithQRCode(paymentRequestId: paymentRequestID) { [weak self] result in + self?.isLoading = false + switch result { + case .success(let data): + completion(data) + case .failure: + break + } + } + } +} + +extension PaymentComponentsController: BanksSelectionProtocol { + /// Handles the action when the forward button is tapped on the install bottom sheet. + public func didTapForwardOnInstallBottomSheet() { + print("Tapped Forward on Install Bottom Sheet") + } + + /// Handles the action when the continue button is tapped on the share bottom sheet. + public func didTapOnContinueOnShareBottomSheet() { + print("Tapped Continue on Share Bottom Sheet") + } + + /// Handles the action when the pay button is tapped on install bottom sheet. + public func didTapOnPayButton() { + bottomViewDelegate?.didTapOnPayButton() + } + + /// Notifies the delegate when the close button is tapped on bank selection bottom view + public func didTapOnClose() { + bottomViewDelegate?.didTapOnClose() + } + + /// Notifies the delegate when the more information button is tapped on the bank selection bottom view + public func didTapOnMoreInformation() { + viewDelegate?.didTapOnMoreInformation(documentId: nil) + } + + /// Updates the selected payment provider from the bank selection bottom view and notifies the delegate with the selected provider and document ID. + public func didSelectPaymentProvider(paymentProvider: GiniHealthAPILibrary.PaymentProvider) { + selectedPaymentProvider = PaymentProvider(healthPaymentProvider: paymentProvider) + if let provider = selectedPaymentProvider { + storeDefaultPaymentProvider(paymentProvider: provider) + bottomViewDelegate?.didSelectPaymentProvider(paymentProvider: provider) + } + } +} + +extension PaymentComponentsController: ShareInvoiceBottomViewProtocol { + /// Notifies the delegate to continue sharing the invoice with the provided document ID. + public func didTapOnContinueToShareInvoice() { + bottomViewDelegate?.didTapOnContinueOnShareBottomSheet() + } +} + +extension PaymentComponentsController: InstallAppBottomViewProtocol { + // Notifies the delegate to proceed when the continue button is tapped in the install app bottom view. This happens after the user installed the app from AppStore + public func didTapOnContinue() { + bottomViewDelegate?.didTapForwardOnInstallBottomSheet() + } +} + +extension PaymentComponentsController: PaymentReviewProtocol { + /** + Submits feedback for the specified document and its updated extractions. Method used to update the information extracted from a document. + + - Parameters: + - document: The document for which feedback is being submitted. + - updatedExtractions: The updated extractions related to the document. + - completion: An optional closure to be executed upon completion, containing the result of the submission. + */ + public func submitFeedback(for document: GiniHealthAPILibrary.Document, updatedExtractions: [GiniHealthAPILibrary.Extraction], completion: ((Result) -> Void)?) { + let newDocument = Document(healthDocument: document) + let extractions = updatedExtractions.map { Extraction(healthExtraction: $0) } + giniSDK.documentService.submitFeedback(for: newDocument, with: [], and: ["payment": [extractions]]) { result in + switch result { + case .success(let result): + completion?(.success(result)) + case .failure(let error): + let healthError = GiniHealthAPILibrary.GiniError.unknown(response: error.response, data: error.data) + completion?(.failure(healthError)) + } + } + } + + /** + Creates a payment request using the provided payment information. + + - Parameter paymentInfo: The payment information to be used for the request. + - Parameter completion: A closure to be executed once the request is completed, containing the result of the operation. + */ + public func createPaymentRequest(paymentInfo: GiniInternalPaymentSDK.PaymentInfo, completion: @escaping (Result) -> Void) { + let info = PaymentInfo(paymentConponentsInfo: paymentInfo) + giniSDK.createPaymentRequest(paymentInfo: info, completion: { result in + switch result { + case .success(let paymentRequestID): + completion(.success(paymentRequestID)) + case .failure(let error): + let healthError = GiniHealthAPILibrary.GiniError.unknown(response: error.response, data: error.data) + completion(.failure(healthError)) + } + }) + } + + /** + Determines if the specified error should be handled internally by the SDK. + + - Parameter error: The Gini error to evaluate. + - Returns: A Boolean value indicating whether the error should be handled internally. + */ + public func shouldHandleErrorInternally(error: GiniHealthAPILibrary.GiniError) -> Bool { + let merchantError = GiniMerchantError.apiError(GiniError.decorator(error)) + return giniSDK.delegate?.shouldHandleErrorInternally(error: merchantError) == true + } + + /** + Retrieves a preview for the specified document and page number. + + - Parameters: + - documentId: The ID of the document to preview. + - pageNumber: The page number of the document to retrieve. + - completion: A closure that gets called with the result containing either the preview data or an error. + */ + public func preview(for documentId: String, pageNumber: Int, completion: @escaping (Result) -> Void) { + giniSDK.documentService.preview(for: documentId, pageNumber: pageNumber) { result in + switch result { + case .success(let data): + completion(.success(data)) + case .failure(let error): + let healthError = GiniHealthAPILibrary.GiniError.unknown(response: error.response, data: error.data) + completion(.failure(healthError)) + } + } + } + + /** + Opens the payment provider app using the specified request ID and universal link. + + - Parameters: + - requestId: The ID of the payment request. + - universalLink: The universal link to open the payment provider app. + */ + public func openPaymentProviderApp(requestId: String, universalLink: String) { + giniSDK.openPaymentProviderApp(requestID: requestId, universalLink: universalLink) + } + + /** + Tracks the event when the keyboard is closed on the payment review screen. + + This method informs the tracking delegate about the keyboard close event. + */ + public func trackOnPaymentReviewCloseKeyboardClicked() { + trackingDelegate?.onPaymentReviewScreenEvent(event: TrackingEvent.init(type: .onCloseKeyboardButtonClicked)) + } + + /** + Tracks the event when the close button is clicked on the payment review screen. + + This method notifies the tracking delegate about the close button click event. + */ + public func trackOnPaymentReviewCloseButtonClicked() { + trackingDelegate?.onPaymentReviewScreenEvent(event: TrackingEvent.init(type: .onCloseButtonClicked)) + } + + /** + Tracks the event when the bank button is clicked on the payment review screen. + + - Parameters: + - providerName: The name of the payment provider associated with the button click. + */ + public func trackOnPaymentReviewBankButtonClicked(providerName: String) { + var event = TrackingEvent.init(type: PaymentReviewScreenEventType.onToTheBankButtonClicked) + event.info = ["paymentProvider": providerName] + trackingDelegate?.onPaymentReviewScreenEvent(event: event) + } + + /** + Updates the selected payment provider with the given payment provider. This method is used when updating the payment provider from Payment Review Screen + + - Parameters: + - paymentProvider: The new payment provider to be set. + */ + public func updatedPaymentProvider(_ paymentProvider: GiniHealthAPILibrary.PaymentProvider) { + self.selectedPaymentProvider = PaymentProvider(healthPaymentProvider: paymentProvider) + } + + /** + Opens the more information view controller by notifying the view delegate. This method is used when opening the More Information screen inside the bank selection bottom sheet that's presented in the Payment Review Screen. + + This method triggers the delegate's action for displaying more information. + */ + public func openMoreInformationViewController() { + viewDelegate?.didTapOnMoreInformation() + } + + public func presentShareInvoiceBottomSheet(paymentRequestId: String, paymentInfo: GiniInternalPaymentSDK.PaymentInfo) {} +} + +extension PaymentComponentsController { + private enum Constants { + static let kDefaultPaymentProvider = "defaultPaymentProvider" + static let pdfExtension = ".pdf" + static let numberOfTimesOnboardingShareScreenShouldAppear = 3 + } +} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentReviewContainerViewModel.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentReviewContainerViewModel.swift deleted file mode 100644 index 9ae5748ca..000000000 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentReviewContainerViewModel.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// PaymentReviewContainerViewModel.swift -// GiniMerchantSDK -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - - -import Foundation - -class PaymentReviewContainerViewModel { - var onExtractionFetched: (() -> Void)? - var selectedPaymentProvider: PaymentProvider - - // Pay invoice label - let payInvoiceLabelText: String = NSLocalizedStringPreferredFormat("gini.merchant.reviewscreen.banking.app.button.label", - comment: "Title label used for the pay invoice button") - - public var extractions: [Extraction]? { - didSet { - self.onExtractionFetched?() - } - } - - public var paymentInfo: PaymentInfo? - - init(extractions: [Extraction]?, paymentInfo: PaymentInfo?, selectedPaymentProvider: PaymentProvider) { - self.extractions = extractions - self.selectedPaymentProvider = selectedPaymentProvider - self.paymentInfo = paymentInfo - } -} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentReviewModel.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentReviewModel.swift deleted file mode 100644 index 35d73fadf..000000000 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/PaymentReviewModel.swift +++ /dev/null @@ -1,186 +0,0 @@ -// -// PaymentReviewModer.swift -// GiniMerchantSDK -// -// Copyright © 2024 Gini GmbH. All rights reserved. -// - -import GiniHealthAPILibrary -import UIKit - -protocol PaymentReviewViewModelDelegate: AnyObject { - func presentInstallAppBottomSheet(bottomSheet: BottomSheetViewController) - func presentShareInvoiceBottomSheet(bottomSheet: BottomSheetViewController) - func createPaymentRequestAndOpenBankApp() - func obtainPDFFromPaymentRequest() -} - -/** - View model class for review screen - */ -public class PaymentReviewModel: NSObject { - - var onPreviewImagesFetched: (() -> Void)? - var reloadCollectionViewClosure: (() -> Void)? - var updateLoadingStatus: (() -> Void)? - var updateImagesLoadingStatus: (() -> Void)? - - var onErrorHandling: ((_ error: GiniMerchantError) -> Void)? - - var onCreatePaymentRequestErrorHandling: (() -> Void)? - - weak var viewModelDelegate: PaymentReviewViewModelDelegate? - - public var document: Document? - - public var extractions: [Extraction]? - public var paymentInfo: PaymentInfo? - - public var documentId: String? - private var merchantSDK: GiniMerchant - private var selectedPaymentProvider: PaymentProvider? - - private var cellViewModels: [PageCollectionCellViewModel] = [PageCollectionCellViewModel]() { - didSet { - self.reloadCollectionViewClosure?() - } - } - - var numberOfCells: Int { - return cellViewModels.count - } - - var isLoading: Bool = false { - didSet { - self.updateLoadingStatus?() - } - } - - var isImagesLoading: Bool = false { - didSet { - self.updateImagesLoadingStatus?() - } - } - - var paymentComponentsController: PaymentComponentsController - - public init(with giniMerchant: GiniMerchant, document: Document?, extractions: [Extraction]?, paymentInfo: PaymentInfo?, selectedPaymentProvider: PaymentProvider?, paymentComponentsController: PaymentComponentsController) { - self.merchantSDK = giniMerchant - self.documentId = document?.id - self.document = document - self.extractions = extractions - self.paymentInfo = paymentInfo - self.selectedPaymentProvider = selectedPaymentProvider - self.paymentComponentsController = paymentComponentsController - } - - func getCellViewModel(at indexPath: IndexPath) -> PageCollectionCellViewModel { - return cellViewModels[indexPath.section] - } - - private func createCellViewModel(previewImage: UIImage) -> PageCollectionCellViewModel { - return PageCollectionCellViewModel(preview: previewImage) - } - - func sendFeedback(updatedExtractions: [Extraction]) { - guard let document else { return } - merchantSDK.documentService.submitFeedback(for: document, with: [], and: ["payment": [updatedExtractions]], completion: { _ in }) - } - - func createPaymentRequest(paymentInfo: PaymentInfo, completion: ((_ paymentRequestID: String) -> ())? = nil) { - isLoading = true - self.paymentInfo = paymentInfo - merchantSDK.createPaymentRequest(paymentInfo: paymentInfo) {[weak self] result in - self?.isLoading = false - switch result { - case let .success(requestId): - completion?(requestId) - case let .failure(error): - if let delegate = self?.merchantSDK.delegate, delegate.shouldHandleErrorInternally(error: GiniMerchantError.apiError(error)) { - self?.onCreatePaymentRequestErrorHandling?() - } - } - } - } - - func openInstallAppBottomSheet() { - guard let installAppBottomSheet = paymentComponentsController.installAppBottomSheet() as? BottomSheetViewController else { return } - installAppBottomSheet.modalPresentationStyle = .overFullScreen - viewModelDelegate?.presentInstallAppBottomSheet(bottomSheet: installAppBottomSheet) - } - - func openOnboardingShareInvoiceBottomSheet() { - guard let shareInvoiceBottomSheet = paymentComponentsController.shareInvoiceBottomSheet() as? BottomSheetViewController else { return } - shareInvoiceBottomSheet.modalPresentationStyle = .overFullScreen - viewModelDelegate?.presentShareInvoiceBottomSheet(bottomSheet: shareInvoiceBottomSheet) - } - - func openPaymentProviderApp(requestId: String, universalLink: String) { - merchantSDK.openPaymentProviderApp(requestID: requestId, universalLink: universalLink) - } - - func fetchImages() { - guard let document else { return } - self.isImagesLoading = true - let dispatchGroup = DispatchGroup() - let dispatchQueue = DispatchQueue(label: "imagesQueue") - let dispatchSemaphore = DispatchSemaphore(value: 0) - var vms = [PageCollectionCellViewModel]() - dispatchQueue.async { - for page in 1 ... document.pageCount { - dispatchGroup.enter() - - self.merchantSDK.documentService.preview(for: document.id, pageNumber: page) { [weak self] result in - if let cellModel = self?.proccessPreview(result) { - vms.append(cellModel) - } - dispatchSemaphore.signal() - dispatchGroup.leave() - } - dispatchSemaphore.wait() - } - - dispatchGroup.notify(queue: dispatchQueue) { - DispatchQueue.main.async { - self.isImagesLoading = false - self.cellViewModels.append(contentsOf: vms) - self.onPreviewImagesFetched?() - } - } - } - } - - private func proccessPreview(_ result: Result) -> PageCollectionCellViewModel? { - switch result { - case let .success(dataImage): - if let image = UIImage(data: dataImage) { - return createCellViewModel(previewImage: image) - } - case let .failure(error): - if let delegate = merchantSDK.delegate, delegate.shouldHandleErrorInternally(error: GiniMerchantError.apiError(error)) { - onErrorHandling?(.apiError(error)) - } - } - return nil - } -} - -extension PaymentReviewModel: InstallAppBottomViewProtocol { - func didTapOnContinue() { - viewModelDelegate?.createPaymentRequestAndOpenBankApp() - } -} - -extension PaymentReviewModel: ShareInvoiceBottomViewProtocol { - func didTapOnContinueToShareInvoice() { - viewModelDelegate?.obtainPDFFromPaymentRequest() - } -} - -/** - View model class for collection view cell - - */ -public struct PageCollectionCellViewModel { - let preview: UIImage -} diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Resources/de.lproj/Localizable.strings b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Resources/de.lproj/Localizable.strings index 9dca26d87..610260140 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Resources/de.lproj/Localizable.strings +++ b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Resources/de.lproj/Localizable.strings @@ -1,5 +1,5 @@ /* - File.strings + Localizable.strings Pods // Copyright © 2024 Gini GmbH. All rights reserved. diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Resources/en.lproj/Localizable.strings b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Resources/en.lproj/Localizable.strings index 4e2c0f4dd..97c58500e 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Resources/en.lproj/Localizable.strings +++ b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Resources/en.lproj/Localizable.strings @@ -1,5 +1,5 @@ /* - File.strings + Localizable.strings Pods // Copyright © 2024 Gini GmbH. All rights reserved. diff --git a/MerchantSDK/GiniMerchantSDK/Tests/GiniMerchantSDKTests/GiniMerchantSDKTests.swift b/MerchantSDK/GiniMerchantSDK/Tests/GiniMerchantSDKTests/GiniMerchantSDKTests.swift index 342bc1c29..37e618ea9 100644 --- a/MerchantSDK/GiniMerchantSDK/Tests/GiniMerchantSDKTests/GiniMerchantSDKTests.swift +++ b/MerchantSDK/GiniMerchantSDK/Tests/GiniMerchantSDKTests/GiniMerchantSDKTests.swift @@ -9,6 +9,7 @@ import XCTest @testable import GiniUtilites @testable import GiniMerchantSDK @testable import GiniHealthAPILibrary +@testable import GiniInternalPaymentSDK final class GiniMerchantTests: XCTestCase { diff --git a/MerchantSDK/GiniMerchantSDK/Tests/GiniMerchantSDKTests/MockHelpers/MockPaymentComponents.swift b/MerchantSDK/GiniMerchantSDK/Tests/GiniMerchantSDKTests/MockHelpers/MockPaymentComponents.swift index 06c124410..64775cb43 100644 --- a/MerchantSDK/GiniMerchantSDK/Tests/GiniMerchantSDKTests/MockHelpers/MockPaymentComponents.swift +++ b/MerchantSDK/GiniMerchantSDK/Tests/GiniMerchantSDKTests/MockHelpers/MockPaymentComponents.swift @@ -7,6 +7,7 @@ import UIKit @testable import GiniMerchantSDK +@testable import GiniInternalPaymentSDK @testable import GiniHealthAPILibrary class MockPaymentComponents: PaymentComponentsProtocol { @@ -63,25 +64,41 @@ class MockPaymentComponents: PaymentComponentsProtocol { } } + func paymentView(documentId: String?) -> UIView { - let viewModel = PaymentComponentViewModel(paymentProvider: selectedPaymentProvider, giniMerchantConfiguration: giniMerchantConfiguration) + let viewModel = PaymentComponentViewModel(paymentProvider: selectedPaymentProvider?.toHealthPaymentProvider(), + primaryButtonConfiguration: giniMerchant.primaryButtonConfiguration, + secondaryButtonConfiguration: giniMerchant.secondaryButtonConfiguration, + configuration: giniMerchant.paymentComponentsConfiguration, + strings: giniMerchant.paymentComponentsStrings, + poweredByGiniConfiguration: giniMerchant.poweredByGiniConfiguration, + poweredByGiniStrings: giniMerchant.poweredByGiniStrings, + moreInformationConfiguration: giniMerchant.moreInformationConfiguration, + moreInformationStrings: giniMerchant.moreInformationStrings, + minimumButtonsHeight: giniMerchant.paymentComponentButtonsHeight, + paymentComponentConfiguration: giniMerchant.paymentComponentConfiguration) viewModel.documentId = documentId - let view = PaymentComponentView() - view.viewModel = viewModel + let view = PaymentComponentView(viewModel: viewModel) return view } func bankSelectionBottomSheet() -> UIViewController { - let paymentProvidersBottomViewModel = BanksBottomViewModel(paymentProviders: paymentProviders, - selectedPaymentProvider: selectedPaymentProvider) - let paymentProvidersBottomView = BanksBottomView(viewModel: paymentProvidersBottomViewModel) + let paymentProvidersBottomViewModel = BanksBottomViewModel(paymentProviders: paymentProviders.map { $0.toHealthPaymentProvider() }, + selectedPaymentProvider: selectedPaymentProvider?.toHealthPaymentProvider(), + configuration: giniMerchant.bankSelectionConfiguration, + strings: giniMerchant.banksBottomStrings, + poweredByGiniConfiguration: giniMerchant.poweredByGiniConfiguration, + poweredByGiniStrings: giniMerchant.poweredByGiniStrings, + moreInformationConfiguration: giniMerchant.moreInformationConfiguration, + moreInformationStrings: giniMerchant.moreInformationStrings) + let paymentProvidersBottomView = BanksBottomView(viewModel: paymentProvidersBottomViewModel, bottomSheetConfiguration: giniMerchant.bottomSheetConfiguration) return paymentProvidersBottomView } - func loadPaymentReviewScreenFor(documentID: String?, paymentInfo: GiniMerchantSDK.PaymentInfo?, trackingDelegate: (any GiniMerchantSDK.GiniMerchantTrackingDelegate)?, completion: @escaping (UIViewController?, GiniMerchantSDK.GiniMerchantError?) -> Void) { + func loadPaymentReviewScreenFor(documentID: String?, paymentInfo: PaymentInfo?, trackingDelegate: (any GiniMerchantSDK.GiniMerchantTrackingDelegate)?, completion: @escaping (UIViewController?, GiniMerchantSDK.GiniMerchantError?) -> Void) { switch documentID { case MockSessionManager.payableDocumentID: - completion(PaymentReviewViewController(), nil) + completion(UIViewController(), nil) case MockSessionManager.missingDocumentID: completion(nil, .apiError(GiniError.decorator(.noResponse))) default: @@ -90,14 +107,18 @@ class MockPaymentComponents: PaymentComponentsProtocol { } func paymentInfoViewController() -> UIViewController { - let paymentInfoViewController = PaymentInfoViewController() - let paymentInfoViewModel = PaymentInfoViewModel(paymentProviders: paymentProviders) - paymentInfoViewController.viewModel = paymentInfoViewModel + let paymentInfoViewModel = PaymentInfoViewModel(paymentProviders: paymentProviders.map { $0.toHealthPaymentProvider() }, + configuration: giniMerchant.paymentInfoConfiguration, + strings: giniMerchant.paymentInfoStrings, + poweredByGiniConfiguration: giniMerchant.poweredByGiniConfiguration, + poweredByGiniStrings: giniMerchant.poweredByGiniStrings) + let paymentInfoViewController = PaymentInfoViewController(viewModel: paymentInfoViewModel) return paymentInfoViewController } - + func paymentViewBottomSheet(documentID: String?) -> UIViewController { - let paymentComponentBottomView = PaymentComponentBottomView(paymentView: paymentView(documentId: documentID)) + let paymentComponentBottomView = PaymentComponentBottomView(paymentView: paymentView(documentId: documentID), + bottomSheetConfiguration: giniMerchant.bottomSheetConfiguration) return paymentComponentBottomView } } diff --git a/MerchantSDK/GiniMerchantSDK/Tests/GiniMerchantSDKTests/MockHelpers/MockUIApplication.swift b/MerchantSDK/GiniMerchantSDK/Tests/GiniMerchantSDKTests/MockHelpers/MockUIApplication.swift index 4e5ce8df1..7a105c740 100644 --- a/MerchantSDK/GiniMerchantSDK/Tests/GiniMerchantSDKTests/MockHelpers/MockUIApplication.swift +++ b/MerchantSDK/GiniMerchantSDK/Tests/GiniMerchantSDKTests/MockHelpers/MockUIApplication.swift @@ -23,8 +23,14 @@ struct MockUIApplication: URLOpenerProtocol { func open(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey : Any], completionHandler completion: GiniOpenLinkCompletionBlock?) { if canOpen { - Task { @MainActor in - completion?(true) + if #available(iOS 13, *) { + Task { @MainActor in + completion?(true) + } + } else { + DispatchQueue.main.async { + completion?(true) + } } } } diff --git a/MerchantSDK/GiniMerchantSDK/Tests/GiniMerchantSDKTests/PaymentComponentsControllerTests.swift b/MerchantSDK/GiniMerchantSDK/Tests/GiniMerchantSDKTests/PaymentComponentsControllerTests.swift index 65da76c3d..02635e2d7 100644 --- a/MerchantSDK/GiniMerchantSDK/Tests/GiniMerchantSDKTests/PaymentComponentsControllerTests.swift +++ b/MerchantSDK/GiniMerchantSDK/Tests/GiniMerchantSDKTests/PaymentComponentsControllerTests.swift @@ -9,9 +9,11 @@ import XCTest @testable import GiniUtilites @testable import GiniMerchantSDK @testable import GiniHealthAPILibrary +@testable import GiniInternalPaymentSDK final class PaymentComponentsControllerTests: XCTestCase { private var giniHealthAPI: GiniHealthAPI! + private var giniMerchant: GiniMerchant! private var mockPaymentComponentsController: PaymentComponentsProtocol! private let giniMerchantConfiguration = GiniMerchantConfiguration.shared private let versionAPI = 1 @@ -22,12 +24,13 @@ final class PaymentComponentsControllerTests: XCTestCase { let documentService = DefaultDocumentService(sessionManager: sessionManagerMock, apiDomain: .merchant, apiVersion: versionAPI) let paymentService = PaymentService(sessionManager: sessionManagerMock, apiDomain: .merchant, apiVersion: versionAPI) giniHealthAPI = GiniHealthAPI(documentService: documentService, paymentService: paymentService) - let giniMerchant = GiniMerchant(giniApiLib: giniHealthAPI) + giniMerchant = GiniMerchant(giniApiLib: giniHealthAPI) mockPaymentComponentsController = MockPaymentComponents(giniMerchant: giniMerchant) } override func tearDown() { giniHealthAPI = nil + giniMerchant = nil mockPaymentComponentsController = nil super.tearDown() } @@ -80,9 +83,19 @@ final class PaymentComponentsControllerTests: XCTestCase { func testPaymentView_ReturnsView() { // Given let documentId = "123456" - let expectedViewModel = PaymentComponentViewModel(paymentProvider: nil, giniMerchantConfiguration: giniMerchantConfiguration) - let expectedView = PaymentComponentView() - expectedView.viewModel = expectedViewModel + let expectedViewModel = PaymentComponentViewModel(paymentProvider: nil, + primaryButtonConfiguration: giniMerchant.primaryButtonConfiguration, + secondaryButtonConfiguration: giniMerchant.secondaryButtonConfiguration, + configuration: giniMerchant.paymentComponentsConfiguration, + strings: giniMerchant.paymentComponentsStrings, + poweredByGiniConfiguration: giniMerchant.poweredByGiniConfiguration, + poweredByGiniStrings: giniMerchant.poweredByGiniStrings, + moreInformationConfiguration: giniMerchant.moreInformationConfiguration, + moreInformationStrings: giniMerchant.moreInformationStrings, + minimumButtonsHeight: giniMerchant.paymentComponentButtonsHeight, + paymentComponentConfiguration: giniMerchant.paymentComponentConfiguration) + expectedViewModel.documentId = documentId + let expectedView = PaymentComponentView(viewModel: expectedViewModel) // When let view = mockPaymentComponentsController.paymentView(documentId: documentId) @@ -93,7 +106,7 @@ final class PaymentComponentsControllerTests: XCTestCase { XCTFail("Error finding correct view.") return } - XCTAssertEqual(view.viewModel?.documentId, documentId) + XCTAssertEqual(view.viewModel.documentId, expectedView.viewModel.documentId) } func testBankSelectionBottomSheet_ReturnsViewController() { @@ -111,12 +124,12 @@ final class PaymentComponentsControllerTests: XCTestCase { func testLoadPaymentReviewScreenFor_Success() { // Given - let documentID = MockSessionManager.payableDocumentID + let documentId = MockSessionManager.payableDocumentID // When var receivedViewController: UIViewController? var receivedError: GiniMerchantError? - mockPaymentComponentsController.loadPaymentReviewScreenFor(documentID: documentID, paymentInfo: nil, trackingDelegate: nil) { viewController, error in + mockPaymentComponentsController.loadPaymentReviewScreenFor(documentID: documentId, paymentInfo: nil, trackingDelegate: nil) { viewController, error in receivedViewController = viewController receivedError = error } @@ -124,17 +137,16 @@ final class PaymentComponentsControllerTests: XCTestCase { // Then XCTAssertNil(receivedError) XCTAssertNotNil(receivedViewController) - XCTAssertTrue(receivedViewController is PaymentReviewViewController) } func testLoadPaymentReviewScreenFor_Failure() { // Given - let documentID = MockSessionManager.missingDocumentID + let documentId = MockSessionManager.missingDocumentID // When var receivedViewController: UIViewController? var receivedError: GiniMerchantError? - mockPaymentComponentsController.loadPaymentReviewScreenFor(documentID: documentID, paymentInfo: nil, trackingDelegate: nil) { viewController, error in + mockPaymentComponentsController.loadPaymentReviewScreenFor(documentID: documentId, paymentInfo: nil, trackingDelegate: nil) { viewController, error in receivedViewController = viewController receivedError = error } @@ -156,10 +168,7 @@ final class PaymentComponentsControllerTests: XCTestCase { return } XCTAssertNotNil(paymentInfoVC.viewModel) - guard let paymentInfoViewModel = paymentInfoVC.viewModel else { - XCTFail("Error finding payment info viewModel.") - return - } + let paymentInfoViewModel = paymentInfoVC.viewModel XCTAssertEqual(paymentInfoViewModel.paymentProviders, []) } @@ -171,10 +180,19 @@ final class PaymentComponentsControllerTests: XCTestCase { } let expectedPaymentProviders = loadProviders(fileName: "sortedBanks") - - let bottomViewModel = BanksBottomViewModel(paymentProviders: givenPaymentProviders, selectedPaymentProvider: nil, urlOpener: URLOpener(MockUIApplication(canOpen: false))) - + + let bottomViewModel = BanksBottomViewModel(paymentProviders: givenPaymentProviders.map { $0.toHealthPaymentProvider() }, + selectedPaymentProvider: nil, + configuration: giniMerchant.bankSelectionConfiguration, + strings: giniMerchant.banksBottomStrings, + poweredByGiniConfiguration: giniMerchant.poweredByGiniConfiguration, + poweredByGiniStrings: giniMerchant.poweredByGiniStrings, + moreInformationConfiguration: giniMerchant.moreInformationConfiguration, + moreInformationStrings: giniMerchant.moreInformationStrings, + urlOpener: URLOpener(MockUIApplication(canOpen: false))) + + XCTAssertEqual(bottomViewModel.paymentProviders.count, 11) - XCTAssertEqual(bottomViewModel.paymentProviders.map { $0.paymentProvider }, expectedPaymentProviders) + XCTAssertEqual(bottomViewModel.paymentProviders.map { PaymentProvider(healthPaymentProvider: $0.paymentProvider) }, expectedPaymentProviders) } } diff --git a/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample.xcodeproj/xcshareddata/xcschemes/GiniMerchantSDKExampleTests.xcscheme b/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample.xcodeproj/xcshareddata/xcschemes/GiniMerchantSDKExampleTests.xcscheme index 97df19d85..e17385691 100644 --- a/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample.xcodeproj/xcshareddata/xcschemes/GiniMerchantSDKExampleTests.xcscheme +++ b/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample.xcodeproj/xcshareddata/xcschemes/GiniMerchantSDKExampleTests.xcscheme @@ -13,8 +13,9 @@ buildForProfiling = "NO" buildForArchiving = "NO" buildForAnalyzing = "NO"> - - + + - + - + diff --git a/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample/Sources/AppCoordinator.swift b/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample/Sources/AppCoordinator.swift index cba82b13d..efc3cb7eb 100644 --- a/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample/Sources/AppCoordinator.swift +++ b/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample/Sources/AppCoordinator.swift @@ -9,6 +9,7 @@ import UIKit import GiniCaptureSDK import GiniMerchantSDK +import GiniInternalPaymentSDK final class AppCoordinator: Coordinator { @@ -49,8 +50,6 @@ final class AppCoordinator: Coordinator { private lazy var merchant = GiniMerchant(id: clientID, secret: clientPassword, domain: clientDomain) private lazy var paymentComponentsController = PaymentComponentsController(giniMerchant: merchant) - private lazy var paymentComponentConfiguration = PaymentComponentConfiguration() - init(window: UIWindow) { self.window = window print("------------------------------------\n\n", @@ -233,7 +232,7 @@ extension AppCoordinator: GiniMerchantDelegate { } reviewController.dismiss(animated: false) - orderController.setAmount(reviewController.model?.paymentInfo?.amount ?? "") + orderController.setAmount(reviewController.model.paymentInfo?.amount ?? "") } } } @@ -258,7 +257,7 @@ extension AppCoordinator: PaymentComponentsControllerProtocol { extension AppCoordinator: DebugMenuPresenter { func presentDebugMenu() { - let debugMenuViewController = DebugMenuViewController(showReviewScreen: configuration.showPaymentReviewScreen, paymentComponentConfiguration: paymentComponentConfiguration) + let debugMenuViewController = DebugMenuViewController(showReviewScreen: configuration.showPaymentReviewScreen, paymentComponentConfiguration: merchant.paymentComponentConfiguration) debugMenuViewController.delegate = self rootViewController.present(debugMenuViewController, animated: true) } @@ -270,7 +269,7 @@ extension AppCoordinator: DebugMenuDelegate { case .showReviewScreen: configuration.showPaymentReviewScreen = isOn case .showBrandedView: - paymentComponentConfiguration.isPaymentComponentBranded = isOn + merchant.paymentComponentConfiguration.isPaymentComponentBranded = isOn } } } diff --git a/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample/Sources/DebugMenuViewController.swift b/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample/Sources/DebugMenuViewController.swift index 9885af5d9..dbfe86fd1 100644 --- a/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample/Sources/DebugMenuViewController.swift +++ b/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample/Sources/DebugMenuViewController.swift @@ -6,6 +6,7 @@ import UIKit import GiniMerchantSDK +import GiniInternalPaymentSDK enum SwitchType { case showReviewScreen diff --git a/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample/Sources/InvoicesList/OrderDetailView.swift b/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample/Sources/InvoicesList/OrderDetailView.swift index 36c7e2fa2..437b77975 100644 --- a/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample/Sources/InvoicesList/OrderDetailView.swift +++ b/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample/Sources/InvoicesList/OrderDetailView.swift @@ -99,7 +99,7 @@ extension OrderDetailView: UITextFieldDelegate { func updateAmoutToPayWithCurrencyFormat() { let textField = Self.amountTextField if textField.hasText, let text = textField.text { - if let priceValue = text.toDecimal(), + if let priceValue = text.decimal(), var price = order?.price { price.value = priceValue diff --git a/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample/Sources/InvoicesList/OrderDetailViewController.swift b/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample/Sources/InvoicesList/OrderDetailViewController.swift index 3f9ca028c..eb0d99b29 100644 --- a/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample/Sources/InvoicesList/OrderDetailViewController.swift +++ b/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample/Sources/InvoicesList/OrderDetailViewController.swift @@ -6,8 +6,9 @@ import UIKit -import GiniMerchantSDK +import GiniInternalPaymentSDK import GiniUtilites +import GiniMerchantSDK enum Fields: String, CaseIterable { case recipient = "example.order.detail.Recipient" @@ -141,7 +142,7 @@ final class OrderDetailViewController: UIViewController { } } -extension OrderDetailViewController: PaymentComponentViewProtocol { +extension OrderDetailViewController: GiniInternalPaymentSDK.PaymentComponentViewProtocol { func didTapOnMoreInformation(documentId: String?) { print("✅ Tapped on More Information") let paymentInfoViewController = paymentComponentsController.paymentInfoViewController() @@ -176,13 +177,10 @@ extension OrderDetailViewController: PaymentComponentViewProtocol { } } else { if paymentComponentsController.supportsOpenWith() { - if paymentComponentsController.shouldShowOnboardingScreenFor() { - let shareInvoiceBottomSheet = paymentComponentsController.shareInvoiceBottomSheet() - shareInvoiceBottomSheet.modalPresentationStyle = .overFullScreen - self.dismissAndPresent(viewController: shareInvoiceBottomSheet, animated: false) - } else { - paymentComponentsController.obtainPDFURLFromPaymentRequest(paymentInfo: obtainPaymentInfo(), viewController: self) - } + // TODO: Fix share onboarding flow in MerchantSDK + let shareInvoiceBottomSheet = paymentComponentsController.shareInvoiceBottomSheet(qrCodeData: Data()) + shareInvoiceBottomSheet.modalPresentationStyle = .overFullScreen + self.dismissAndPresent(viewController: shareInvoiceBottomSheet, animated: false) } else if paymentComponentsController.supportsGPC() { if paymentComponentsController.canOpenPaymentProviderApp() { paymentComponentsController.createPaymentRequest(paymentInfo: obtainPaymentInfo()) { [weak self] paymentRequestID, error in @@ -220,7 +218,7 @@ extension OrderDetailViewController: PaymentComponentViewProtocol { } } - private func obtainPaymentInfo() -> PaymentInfo { + private func obtainPaymentInfo() -> GiniInternalPaymentSDK.PaymentInfo { saveTextFieldData() return PaymentInfo(recipient: order.recipient, @@ -278,6 +276,10 @@ extension OrderDetailViewController: GiniMerchantTrackingDelegate { } extension OrderDetailViewController: PaymentProvidersBottomViewProtocol { + func didTapOnMoreInformation() { + didTapOnMoreInformation(documentId: nil) + } + func didSelectPaymentProvider(paymentProvider: PaymentProvider) { DispatchQueue.main.async { self.presentedViewController?.dismiss(animated: true, completion: { diff --git a/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample/Sources/ScreenAPICoordinator.swift b/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample/Sources/ScreenAPICoordinator.swift index f19a27b20..b4a1d2b3e 100644 --- a/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample/Sources/ScreenAPICoordinator.swift +++ b/MerchantSDK/GiniMerchantSDKExample/GiniMerchantSDKExample/Sources/ScreenAPICoordinator.swift @@ -8,6 +8,7 @@ import GiniBankAPILibrary import GiniCaptureSDK import GiniMerchantSDK +import GiniInternalPaymentSDK import UIKit protocol ScreenAPICoordinatorDelegate: AnyObject {