Implemented SwiftBankIconFinder
This commit is contained in:
parent
e8a27b1a83
commit
b898f9d17e
|
@ -6,16 +6,15 @@ import net.dankito.banking.persistence.IBankingPersistence
|
||||||
import net.dankito.banking.search.IRemitteeSearcher
|
import net.dankito.banking.search.IRemitteeSearcher
|
||||||
import net.dankito.banking.ui.IRouter
|
import net.dankito.banking.ui.IRouter
|
||||||
import net.dankito.banking.ui.presenter.BankingPresenter
|
import net.dankito.banking.ui.presenter.BankingPresenter
|
||||||
import net.dankito.banking.util.IAsyncRunner
|
import net.dankito.banking.util.*
|
||||||
import net.dankito.banking.util.NoOpBankIconFinder
|
|
||||||
import net.dankito.banking.util.NoOpSerializer
|
|
||||||
import net.dankito.banking.util.extraction.NoOpInvoiceDataExtractor
|
import net.dankito.banking.util.extraction.NoOpInvoiceDataExtractor
|
||||||
import net.dankito.banking.util.extraction.NoOpTextExtractorRegistry
|
import net.dankito.banking.util.extraction.NoOpTextExtractorRegistry
|
||||||
import net.dankito.utils.multiplatform.File
|
import net.dankito.utils.multiplatform.File
|
||||||
|
|
||||||
|
|
||||||
class BankingPresenterSwift(dataFolder: File, router: IRouter, webClient: IWebClient, persistence: IBankingPersistence, remitteeSearcher: IRemitteeSearcher, asyncRunner: IAsyncRunner)
|
class BankingPresenterSwift(dataFolder: File, router: IRouter, webClient: IWebClient, persistence: IBankingPersistence,
|
||||||
: BankingPresenter(fints4kBankingClientCreator(NoOpSerializer(), webClient), InMemoryBankFinder(), dataFolder, persistence, router,
|
remitteeSearcher: IRemitteeSearcher, bankIconFinder: IBankIconFinder, serializer: ISerializer, asyncRunner: IAsyncRunner)
|
||||||
remitteeSearcher, NoOpBankIconFinder(), NoOpTextExtractorRegistry(), NoOpInvoiceDataExtractor(), NoOpSerializer(), asyncRunner) {
|
: BankingPresenter(fints4kBankingClientCreator(serializer, webClient), InMemoryBankFinder(), dataFolder, persistence, router,
|
||||||
|
remitteeSearcher, bankIconFinder, NoOpTextExtractorRegistry(), NoOpInvoiceDataExtractor(), serializer, asyncRunner) {
|
||||||
|
|
||||||
}
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 50;
|
objectVersion = 52;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
@ -51,6 +51,14 @@
|
||||||
36BE069124CEF52800CBBB68 /* UpdateButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE069024CEF52800CBBB68 /* UpdateButton.swift */; };
|
36BE069124CEF52800CBBB68 /* UpdateButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE069024CEF52800CBBB68 /* UpdateButton.swift */; };
|
||||||
36BE06B324CF133400CBBB68 /* JsonEncoderSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE06B224CF133400CBBB68 /* JsonEncoderSerializer.swift */; };
|
36BE06B324CF133400CBBB68 /* JsonEncoderSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE06B224CF133400CBBB68 /* JsonEncoderSerializer.swift */; };
|
||||||
36BE06B524CF85A300CBBB68 /* AmountLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE06B424CF85A300CBBB68 /* AmountLabel.swift */; };
|
36BE06B524CF85A300CBBB68 /* AmountLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE06B424CF85A300CBBB68 /* AmountLabel.swift */; };
|
||||||
|
36BE06B824D077EC00CBBB68 /* SwiftBankIconFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE06B724D077EC00CBBB68 /* SwiftBankIconFinder.swift */; };
|
||||||
|
36BE06BA24D0783900CBBB68 /* FaviconFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE06B924D0783800CBBB68 /* FaviconFinder.swift */; };
|
||||||
|
36BE06C024D07CCD00CBBB68 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 36BE06BF24D07CCD00CBBB68 /* SwiftSoup */; };
|
||||||
|
36BE06C224D07FB100CBBB68 /* Favicon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE06C124D07FB100CBBB68 /* Favicon.swift */; };
|
||||||
|
36BE06C424D0801A00CBBB68 /* Size.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE06C324D0801A00CBBB68 /* Size.swift */; };
|
||||||
|
36BE06C624D080C900CBBB68 /* FaviconType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE06C524D080C900CBBB68 /* FaviconType.swift */; };
|
||||||
|
36BE06C824D0DE7400CBBB68 /* UIKitTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36BE06C724D0DE7400CBBB68 /* UIKitTextField.swift */; };
|
||||||
|
36C4009824D23580005227AD /* SwiftBankIconFinderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36C4009724D23580005227AD /* SwiftBankIconFinderTest.swift */; };
|
||||||
36E7BA1424B3D05C00757859 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36E7BA1324B3D05C00757859 /* ViewExtensions.swift */; };
|
36E7BA1424B3D05C00757859 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36E7BA1324B3D05C00757859 /* ViewExtensions.swift */; };
|
||||||
36FC929C24B39A05002B12E9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36FC929B24B39A05002B12E9 /* AppDelegate.swift */; };
|
36FC929C24B39A05002B12E9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36FC929B24B39A05002B12E9 /* AppDelegate.swift */; };
|
||||||
36FC929E24B39A05002B12E9 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36FC929D24B39A05002B12E9 /* SceneDelegate.swift */; };
|
36FC929E24B39A05002B12E9 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36FC929D24B39A05002B12E9 /* SceneDelegate.swift */; };
|
||||||
|
@ -145,6 +153,13 @@
|
||||||
36BE069024CEF52800CBBB68 /* UpdateButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateButton.swift; sourceTree = "<group>"; };
|
36BE069024CEF52800CBBB68 /* UpdateButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateButton.swift; sourceTree = "<group>"; };
|
||||||
36BE06B224CF133400CBBB68 /* JsonEncoderSerializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonEncoderSerializer.swift; sourceTree = "<group>"; };
|
36BE06B224CF133400CBBB68 /* JsonEncoderSerializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonEncoderSerializer.swift; sourceTree = "<group>"; };
|
||||||
36BE06B424CF85A300CBBB68 /* AmountLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountLabel.swift; sourceTree = "<group>"; };
|
36BE06B424CF85A300CBBB68 /* AmountLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountLabel.swift; sourceTree = "<group>"; };
|
||||||
|
36BE06B724D077EC00CBBB68 /* SwiftBankIconFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftBankIconFinder.swift; sourceTree = "<group>"; };
|
||||||
|
36BE06B924D0783800CBBB68 /* FaviconFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconFinder.swift; sourceTree = "<group>"; };
|
||||||
|
36BE06C124D07FB100CBBB68 /* Favicon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Favicon.swift; sourceTree = "<group>"; };
|
||||||
|
36BE06C324D0801A00CBBB68 /* Size.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Size.swift; sourceTree = "<group>"; };
|
||||||
|
36BE06C524D080C900CBBB68 /* FaviconType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconType.swift; sourceTree = "<group>"; };
|
||||||
|
36BE06C724D0DE7400CBBB68 /* UIKitTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitTextField.swift; sourceTree = "<group>"; };
|
||||||
|
36C4009724D23580005227AD /* SwiftBankIconFinderTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SwiftBankIconFinderTest.swift; path = BankingiOSAppTests/BankIconFinder/SwiftBankIconFinderTest.swift; sourceTree = SOURCE_ROOT; };
|
||||||
36E7BA1324B3D05C00757859 /* ViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewExtensions.swift; sourceTree = "<group>"; };
|
36E7BA1324B3D05C00757859 /* ViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewExtensions.swift; sourceTree = "<group>"; };
|
||||||
36E7BA1824B9E70C00757859 /* xcode-frameworks */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "xcode-frameworks"; path = "../../tools/BankFinder/build/xcode-frameworks"; sourceTree = "<group>"; };
|
36E7BA1824B9E70C00757859 /* xcode-frameworks */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "xcode-frameworks"; path = "../../tools/BankFinder/build/xcode-frameworks"; sourceTree = "<group>"; };
|
||||||
36FC929824B39A05002B12E9 /* BankingiOSApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BankingiOSApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
36FC929824B39A05002B12E9 /* BankingiOSApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BankingiOSApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -181,6 +196,7 @@
|
||||||
36FC92D024B39C47002B12E9 /* fints4k.framework in Frameworks */,
|
36FC92D024B39C47002B12E9 /* fints4k.framework in Frameworks */,
|
||||||
36BCF87024BB0F8A005BEC29 /* fints4kBankingClient.framework in Frameworks */,
|
36BCF87024BB0F8A005BEC29 /* fints4kBankingClient.framework in Frameworks */,
|
||||||
36BCF87324BB2706005BEC29 /* BankingUiSwift.framework in Frameworks */,
|
36BCF87324BB2706005BEC29 /* BankingUiSwift.framework in Frameworks */,
|
||||||
|
36BE06C024D07CCD00CBBB68 /* SwiftSoup in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -213,6 +229,18 @@
|
||||||
path = persistence;
|
path = persistence;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
36BE06B624D077B400CBBB68 /* BankIconFinder */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
36BE06C124D07FB100CBBB68 /* Favicon.swift */,
|
||||||
|
36BE06C524D080C900CBBB68 /* FaviconType.swift */,
|
||||||
|
36BE06C324D0801A00CBBB68 /* Size.swift */,
|
||||||
|
36BE06B924D0783800CBBB68 /* FaviconFinder.swift */,
|
||||||
|
36BE06B724D077EC00CBBB68 /* SwiftBankIconFinder.swift */,
|
||||||
|
);
|
||||||
|
path = BankIconFinder;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
36FC928F24B39A05002B12E9 = {
|
36FC928F24B39A05002B12E9 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -237,8 +265,9 @@
|
||||||
36FC929A24B39A05002B12E9 /* BankingiOSApp */ = {
|
36FC929A24B39A05002B12E9 /* BankingiOSApp */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
36BCF87924BFA679005BEC29 /* persistence */,
|
|
||||||
36FC92D424B3A389002B12E9 /* fints4k */,
|
36FC92D424B3A389002B12E9 /* fints4k */,
|
||||||
|
36BCF87924BFA679005BEC29 /* persistence */,
|
||||||
|
36BE06B624D077B400CBBB68 /* BankIconFinder */,
|
||||||
36FC92D924B3A479002B12E9 /* ui */,
|
36FC92D924B3A479002B12E9 /* ui */,
|
||||||
36FC929B24B39A05002B12E9 /* AppDelegate.swift */,
|
36FC929B24B39A05002B12E9 /* AppDelegate.swift */,
|
||||||
36FC929D24B39A05002B12E9 /* SceneDelegate.swift */,
|
36FC929D24B39A05002B12E9 /* SceneDelegate.swift */,
|
||||||
|
@ -321,6 +350,7 @@
|
||||||
36BE068C24CE41E700CBBB68 /* Styles.swift */,
|
36BE068C24CE41E700CBBB68 /* Styles.swift */,
|
||||||
36BE069024CEF52800CBBB68 /* UpdateButton.swift */,
|
36BE069024CEF52800CBBB68 /* UpdateButton.swift */,
|
||||||
36BE06B424CF85A300CBBB68 /* AmountLabel.swift */,
|
36BE06B424CF85A300CBBB68 /* AmountLabel.swift */,
|
||||||
|
36BE06C724D0DE7400CBBB68 /* UIKitTextField.swift */,
|
||||||
);
|
);
|
||||||
path = ui;
|
path = ui;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -334,6 +364,7 @@
|
||||||
36BCF88224C098BB005BEC29 /* BankListItem.swift */,
|
36BCF88224C098BB005BEC29 /* BankListItem.swift */,
|
||||||
36BCF88424C098C8005BEC29 /* BankAccountListItem.swift */,
|
36BCF88424C098C8005BEC29 /* BankAccountListItem.swift */,
|
||||||
36BCF88A24C0BD2D005BEC29 /* AccountTransactionsDialog.swift */,
|
36BCF88A24C0BD2D005BEC29 /* AccountTransactionsDialog.swift */,
|
||||||
|
36C4009724D23580005227AD /* SwiftBankIconFinderTest.swift */,
|
||||||
36BE066424CDE62800CBBB68 /* AccountTransactionListItem.swift */,
|
36BE066424CDE62800CBBB68 /* AccountTransactionListItem.swift */,
|
||||||
36BCF88C24C1C1EA005BEC29 /* TransferMoneyDialog.swift */,
|
36BCF88C24C1C1EA005BEC29 /* TransferMoneyDialog.swift */,
|
||||||
366FA4DB24C479120094F009 /* BankInfoListItem.swift */,
|
366FA4DB24C479120094F009 /* BankInfoListItem.swift */,
|
||||||
|
@ -366,6 +397,9 @@
|
||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
name = BankingiOSApp;
|
name = BankingiOSApp;
|
||||||
|
packageProductDependencies = (
|
||||||
|
36BE06BF24D07CCD00CBBB68 /* SwiftSoup */,
|
||||||
|
);
|
||||||
productName = BankingiOSApp;
|
productName = BankingiOSApp;
|
||||||
productReference = 36FC929824B39A05002B12E9 /* BankingiOSApp.app */;
|
productReference = 36FC929824B39A05002B12E9 /* BankingiOSApp.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
|
@ -439,6 +473,9 @@
|
||||||
de,
|
de,
|
||||||
);
|
);
|
||||||
mainGroup = 36FC928F24B39A05002B12E9;
|
mainGroup = 36FC928F24B39A05002B12E9;
|
||||||
|
packageReferences = (
|
||||||
|
36BE06BE24D07CCC00CBBB68 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
|
||||||
|
);
|
||||||
productRefGroup = 36FC929924B39A05002B12E9 /* Products */;
|
productRefGroup = 36FC929924B39A05002B12E9 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
|
@ -505,16 +542,21 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
36BE065924CA3CAB00CBBB68 /* UIKitSearchBar.swift in Sources */,
|
36BE065924CA3CAB00CBBB68 /* UIKitSearchBar.swift in Sources */,
|
||||||
|
36BE06C824D0DE7400CBBB68 /* UIKitTextField.swift in Sources */,
|
||||||
36BE064F24C9A17F00CBBB68 /* ImageTanView.swift in Sources */,
|
36BE064F24C9A17F00CBBB68 /* ImageTanView.swift in Sources */,
|
||||||
36BE068D24CE41E700CBBB68 /* Styles.swift in Sources */,
|
36BE068D24CE41E700CBBB68 /* Styles.swift in Sources */,
|
||||||
366FA4E224C4ED6C0094F009 /* EnterTanDialog.swift in Sources */,
|
366FA4E224C4ED6C0094F009 /* EnterTanDialog.swift in Sources */,
|
||||||
36FC92DC24B3A4A0002B12E9 /* AccountsTab.swift in Sources */,
|
36FC92DC24B3A4A0002B12E9 /* AccountsTab.swift in Sources */,
|
||||||
36BCF86E24BA691B005BEC29 /* DependencyInjector.swift in Sources */,
|
36BCF86E24BA691B005BEC29 /* DependencyInjector.swift in Sources */,
|
||||||
|
36BE06C224D07FB100CBBB68 /* Favicon.swift in Sources */,
|
||||||
|
36BE06B824D077EC00CBBB68 /* SwiftBankIconFinder.swift in Sources */,
|
||||||
36BCF89124C25971005BEC29 /* CoreDataBankingPersistence.swift in Sources */,
|
36BCF89124C25971005BEC29 /* CoreDataBankingPersistence.swift in Sources */,
|
||||||
36FC92A124B39A05002B12E9 /* BankingiOSApp.xcdatamodeld in Sources */,
|
36FC92A124B39A05002B12E9 /* BankingiOSApp.xcdatamodeld in Sources */,
|
||||||
|
36BE06C624D080C900CBBB68 /* FaviconType.swift in Sources */,
|
||||||
36BCF89324C25BC3005BEC29 /* Mapper.swift in Sources */,
|
36BCF89324C25BC3005BEC29 /* Mapper.swift in Sources */,
|
||||||
36FC92D724B3A3BA002B12E9 /* NSUrlWebClient.swift in Sources */,
|
36FC92D724B3A3BA002B12E9 /* NSUrlWebClient.swift in Sources */,
|
||||||
36BE06B324CF133400CBBB68 /* JsonEncoderSerializer.swift in Sources */,
|
36BE06B324CF133400CBBB68 /* JsonEncoderSerializer.swift in Sources */,
|
||||||
|
36BE06BA24D0783900CBBB68 /* FaviconFinder.swift in Sources */,
|
||||||
36BCF89524C31F02005BEC29 /* AppData.swift in Sources */,
|
36BCF89524C31F02005BEC29 /* AppData.swift in Sources */,
|
||||||
36BE065B24CA4B3500CBBB68 /* SelectBankDialog.swift in Sources */,
|
36BE065B24CA4B3500CBBB68 /* SelectBankDialog.swift in Sources */,
|
||||||
36BE068924CE288800CBBB68 /* CollapsibleText.swift in Sources */,
|
36BE068924CE288800CBBB68 /* CollapsibleText.swift in Sources */,
|
||||||
|
@ -532,6 +574,7 @@
|
||||||
36FC929C24B39A05002B12E9 /* AppDelegate.swift in Sources */,
|
36FC929C24B39A05002B12E9 /* AppDelegate.swift in Sources */,
|
||||||
36BCF88B24C0BD2D005BEC29 /* AccountTransactionsDialog.swift in Sources */,
|
36BCF88B24C0BD2D005BEC29 /* AccountTransactionsDialog.swift in Sources */,
|
||||||
36BCF87624BF114F005BEC29 /* UrlSessionWebClient.swift in Sources */,
|
36BCF87624BF114F005BEC29 /* UrlSessionWebClient.swift in Sources */,
|
||||||
|
36BE06C424D0801A00CBBB68 /* Size.swift in Sources */,
|
||||||
36FC92A324B39A05002B12E9 /* ContentView.swift in Sources */,
|
36FC92A324B39A05002B12E9 /* ContentView.swift in Sources */,
|
||||||
366FA4E624C6EBF40094F009 /* EnterTanState.swift in Sources */,
|
366FA4E624C6EBF40094F009 /* EnterTanState.swift in Sources */,
|
||||||
36BE065724C9E04800CBBB68 /* UIKitImageView.swift in Sources */,
|
36BE065724C9E04800CBBB68 /* UIKitImageView.swift in Sources */,
|
||||||
|
@ -552,6 +595,7 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
36C4009824D23580005227AD /* SwiftBankIconFinderTest.swift in Sources */,
|
||||||
36FC92B624B39A08002B12E9 /* BankingiOSAppTests.swift in Sources */,
|
36FC92B624B39A08002B12E9 /* BankingiOSAppTests.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -895,6 +939,25 @@
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
36BE06BE24D07CCC00CBBB68 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/scinfu/SwiftSoup.git";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 2.3.2;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
36BE06BF24D07CCD00CBBB68 /* SwiftSoup */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 36BE06BE24D07CCC00CBBB68 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
|
||||||
|
productName = SwiftSoup;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
|
|
||||||
/* Begin XCVersionGroup section */
|
/* Begin XCVersionGroup section */
|
||||||
36FC929F24B39A05002B12E9 /* BankingiOSApp.xcdatamodeld */ = {
|
36FC929F24B39A05002B12E9 /* BankingiOSApp.xcdatamodeld */ = {
|
||||||
isa = XCVersionGroup;
|
isa = XCVersionGroup;
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
|
class Favicon {
|
||||||
|
|
||||||
|
let url: String
|
||||||
|
|
||||||
|
let iconType: FaviconType
|
||||||
|
|
||||||
|
let size: Size?
|
||||||
|
|
||||||
|
let type: String?
|
||||||
|
|
||||||
|
|
||||||
|
init(url: String, iconType: FaviconType, size: Size? = nil, type: String? = nil) {
|
||||||
|
self.url = url
|
||||||
|
self.iconType = iconType
|
||||||
|
|
||||||
|
self.size = size
|
||||||
|
self.type = type
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,186 @@
|
||||||
|
import Foundation
|
||||||
|
import SwiftSoup
|
||||||
|
import fints4k // TODO: get rid of this
|
||||||
|
|
||||||
|
|
||||||
|
class FaviconFinder {
|
||||||
|
|
||||||
|
private let webClient = UrlSessionWebClient() // TODO: create interface and pass from SwiftBankIconFinder
|
||||||
|
|
||||||
|
|
||||||
|
func extractFavicons(url: String, callback: @escaping ([Favicon]) -> Void) {
|
||||||
|
webClient.getAsync(url) { response in
|
||||||
|
if response.successful, let html = response.body {
|
||||||
|
callback(self.extractFavicons(url: url, html: html))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
callback([])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback([])
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractFavicons(url: String, html: String) -> [Favicon] {
|
||||||
|
if let document = try? SwiftSoup.parse(html) {
|
||||||
|
return extractFavicons(document: document, url: url)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractFavicons(document: Document, url: String) -> [Favicon] {
|
||||||
|
var extractedFavicons = (try? document.head()?.select("link, meta").map { mapElementToFavicon($0, url) } as? [Favicon]) ?? []
|
||||||
|
|
||||||
|
tryToFindDefaultFavicon(url, &extractedFavicons)
|
||||||
|
|
||||||
|
return extractedFavicons
|
||||||
|
}
|
||||||
|
|
||||||
|
private func tryToFindDefaultFavicon(_ url: String, _ extractedFavicons: inout [Favicon]) {
|
||||||
|
let urlInstance = URL(string: url)
|
||||||
|
let defaultFaviconUrl = "\(urlInstance?.scheme ?? "https")://\(urlInstance?.host ?? "")/favicon.ico"
|
||||||
|
|
||||||
|
if (containsIconWithUrl(extractedFavicons, defaultFaviconUrl) == false) {
|
||||||
|
let response = webClient.head(defaultFaviconUrl)
|
||||||
|
if (response.successful) {
|
||||||
|
extractedFavicons.append(Favicon(url: defaultFaviconUrl, iconType: FaviconType.ShortcutIcon))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func containsIconWithUrl(_ favicons: [Favicon], _ faviconUrl: String) -> Bool {
|
||||||
|
return favicons.first(where: { favicon in favicon.url == faviconUrl } ) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Possible formats are documented here https://stackoverflow.com/questions/21991044/how-to-get-high-resolution-website-logo-favicon-for-a-given-url#answer-22007642
|
||||||
|
* and here https://en.wikipedia.org/wiki/Favicon
|
||||||
|
*/
|
||||||
|
private func mapElementToFavicon(_ linkOrMetaElement: Element, _ siteUrl: String) -> Favicon? {
|
||||||
|
if (linkOrMetaElement.nodeName() == "link") {
|
||||||
|
return mapLinkElementToFavicon(linkOrMetaElement, siteUrl)
|
||||||
|
}
|
||||||
|
else if (linkOrMetaElement.nodeName() == "meta") {
|
||||||
|
return mapMetaElementToFavicon(linkOrMetaElement, siteUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func mapLinkElementToFavicon(_ linkElement: Element, _ siteUrl: String) -> Favicon? {
|
||||||
|
if linkElement.hasAttr("rel") {
|
||||||
|
if let faviconType = getFaviconTypeForLinkElements(linkElement) {
|
||||||
|
let href = try? linkElement.attr("href")
|
||||||
|
let sizes = try? linkElement.attr("sizes")
|
||||||
|
let type = try? linkElement.attr("type")
|
||||||
|
|
||||||
|
if href?.starts(with: "data:;base64") == false {
|
||||||
|
return createFavicon(url: href, siteUrl: siteUrl, iconType: faviconType, sizesString: sizes, type: type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getFaviconTypeForLinkElements(_ linkElement: Element) -> FaviconType? {
|
||||||
|
if let relValue = try? linkElement.attr("rel") {
|
||||||
|
switch relValue {
|
||||||
|
case "icon":
|
||||||
|
return FaviconType.Icon
|
||||||
|
case "apple-touch-icon-precomposed":
|
||||||
|
return FaviconType.AppleTouchPrecomposed
|
||||||
|
case "apple-touch-icon":
|
||||||
|
return FaviconType.AppleTouch
|
||||||
|
case "shortcut icon":
|
||||||
|
return FaviconType.ShortcutIcon
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func mapMetaElementToFavicon(_ metaElement: Element, _ siteUrl: String) -> Favicon? {
|
||||||
|
if let content = try? metaElement.attr("content") {
|
||||||
|
if isOpenGraphImageDeclaration(metaElement) {
|
||||||
|
return Favicon(url: makeLinkAbsolute(url: content, siteUrl: siteUrl), iconType: FaviconType.OpenGraphImage)
|
||||||
|
}
|
||||||
|
else if isMsTileMetaElement(metaElement) {
|
||||||
|
return Favicon(url: makeLinkAbsolute(url: content, siteUrl: siteUrl), iconType: FaviconType.MsTileImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isOpenGraphImageDeclaration(_ metaElement: Element) -> Bool {
|
||||||
|
return metaElement.hasAttr("property")
|
||||||
|
&& "og:image" == (try? metaElement.attr("property"))
|
||||||
|
&& metaElement.hasAttr("content")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isMsTileMetaElement(_ metaElement: Element) -> Bool {
|
||||||
|
return metaElement.hasAttr("name")
|
||||||
|
&& "msapplication-TileImage" == (try? metaElement.attr("name"))
|
||||||
|
&& metaElement.hasAttr("content")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func createFavicon(url: String?, siteUrl: String, iconType: FaviconType, sizesString: String?, type: String?) -> Favicon? {
|
||||||
|
if let url = url {
|
||||||
|
let absoluteUrl = makeLinkAbsolute(url: url, siteUrl: siteUrl)
|
||||||
|
let size = extractSizesFromString(sizesString)
|
||||||
|
|
||||||
|
return Favicon(url: absoluteUrl, iconType: iconType, size: size, type: type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeLinkAbsolute(url: String, siteUrl: String) -> String {
|
||||||
|
return url // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
private func extractSizesFromString(_ sizesString: String?) -> Size? {
|
||||||
|
if let sizesString = sizesString {
|
||||||
|
let sizes = extractSizesFromString(sizesString)
|
||||||
|
|
||||||
|
if sizes.isEmpty == false {
|
||||||
|
return sizes.max(by: { lhs, rhs in lhs >= rhs } )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func extractSizesFromString(_ sizesString: String) -> [Size] {
|
||||||
|
let sizes = sizesString.split(separator: " ").map { mapSizeString(String($0)) } as? [Size] ?? []
|
||||||
|
|
||||||
|
return sizes
|
||||||
|
}
|
||||||
|
|
||||||
|
private func mapSizeString(_ sizeString: String) -> Size? {
|
||||||
|
var parts = sizeString.split(separator: "x")
|
||||||
|
if parts.count != 2 {
|
||||||
|
parts = sizeString.split(separator: "×") // actually doesn't meet specification, see https://www.w3schools.com/tags/att_link_sizes.asp, but New York Times uses it
|
||||||
|
}
|
||||||
|
if parts.count != 2 {
|
||||||
|
parts = sizeString.split(separator: "X")
|
||||||
|
}
|
||||||
|
|
||||||
|
if parts.count == 2 {
|
||||||
|
let width = Int(parts[0])
|
||||||
|
let height = Int(parts[1])
|
||||||
|
|
||||||
|
if let width = width, let height = height {
|
||||||
|
return Size(width: width, height: height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
|
enum FaviconType {
|
||||||
|
|
||||||
|
case ShortcutIcon
|
||||||
|
|
||||||
|
case Icon
|
||||||
|
|
||||||
|
case OpenGraphImage
|
||||||
|
|
||||||
|
case AppleTouch
|
||||||
|
|
||||||
|
case AppleTouchPrecomposed
|
||||||
|
|
||||||
|
case MsTileImage
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
|
class Size : Comparable, CustomStringConvertible {
|
||||||
|
|
||||||
|
|
||||||
|
let width: Int
|
||||||
|
|
||||||
|
let height: Int
|
||||||
|
|
||||||
|
|
||||||
|
init(width: Int, height: Int) {
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var isSquare: Bool {
|
||||||
|
return width == height
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayText: String {
|
||||||
|
return "\(width) x \(height)"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static func < (lhs: Size, rhs: Size) -> Bool {
|
||||||
|
return lhs.width < rhs.width
|
||||||
|
&& lhs.height < rhs.height
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: Size, rhs: Size) -> Bool {
|
||||||
|
return lhs.width == rhs.width
|
||||||
|
&& lhs.height == rhs.height
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
return displayText
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,279 @@
|
||||||
|
import Foundation
|
||||||
|
import SwiftSoup
|
||||||
|
import BankingUiSwift
|
||||||
|
import MultiplatformUtils
|
||||||
|
|
||||||
|
|
||||||
|
class SwiftBankIconFinder : IBankIconFinder {
|
||||||
|
|
||||||
|
static let SearchBankWebsiteBaseUrlQwant = "https://lite.qwant.com/?l=de&t=mobile&q="
|
||||||
|
|
||||||
|
static let SearchBankWebsiteBaseUrlEcosia = "https://www.ecosia.org/search?q="
|
||||||
|
|
||||||
|
static let SearchBankWebsiteBaseUrlDuckDuckGo = "https://duckduckgo.com/html/?q="
|
||||||
|
|
||||||
|
|
||||||
|
//static let ReplaceGfRegex = Pattern.compile(" \\(Gf [\\w]+\\)").toRegex()
|
||||||
|
|
||||||
|
|
||||||
|
private let log = LoggerFactory.Companion().getLogger(name: "BankIconFinder")
|
||||||
|
|
||||||
|
|
||||||
|
private let webClient = UrlSessionWebClient()
|
||||||
|
|
||||||
|
private let faviconFinder = FaviconFinder()
|
||||||
|
|
||||||
|
// private let faviconComparator = FaviconComparator(webClient)
|
||||||
|
|
||||||
|
|
||||||
|
func findIconForBankAsync(bankName: String, prefSize: Int32, result: @escaping (String?) -> Void) {
|
||||||
|
DispatchQueue.global(qos: .background).async {
|
||||||
|
let bestUrl = self.findIconForBank(bankName: bankName, prefSize: prefSize)
|
||||||
|
|
||||||
|
DispatchQueue.main.async { // dispatch reuslt back to main thread as mutable BankingPresenter / result callback can only be used on the thread they have been created on
|
||||||
|
result(bestUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func findIconForBank(bankName: String, prefSize: Int32) -> String? {
|
||||||
|
if let bankUrl = findBankWebsite(bankName: bankName) {
|
||||||
|
if let bankHomepageResponse = webClient.get(bankUrl).body {
|
||||||
|
if let document = try? SwiftSoup.parse(bankHomepageResponse) {
|
||||||
|
let favicons = faviconFinder.extractFavicons(document: document, url: bankUrl)
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// faviconComparator.getBestIcon(favicons, prefSize, prefSize + 32, true)?.let { prefFavicon ->
|
||||||
|
// return prefFavicon.url
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return faviconComparator.getBestIcon(favicons, 16)?.url
|
||||||
|
|
||||||
|
return favicons.first?.url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findBankWebsite(bankName: String) -> String? {
|
||||||
|
let adjustedBankName = bankName.replacingOccurrences(of: "-alt-", with: "")/*.replace(ReplaceGfRegex, "") */ // TODO
|
||||||
|
|
||||||
|
if let url = findBankWebsiteWithQwant(adjustedBankName) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Could not find bank website with Qwant for '\(bankName)'")
|
||||||
|
|
||||||
|
if let url = findBankWebsiteWithEcosia(adjustedBankName) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Could not find bank website with Ecosia for '\(bankName)'")
|
||||||
|
|
||||||
|
if let url = findBankWebsiteWithDuckDuckGo(adjustedBankName) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Could not find bank website with DuckDuckGo for '\(bankName)'")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findBankWebsiteWithQwant(_ bankName: String) -> String? {
|
||||||
|
return findBankWebsite(bankName, Self.SearchBankWebsiteBaseUrlQwant) { searchResponseDoc in
|
||||||
|
findLinksInQwantWebpage(searchResponseDoc)
|
||||||
|
?? []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findLinksInQwantWebpage(_ searchResponseDoc: Document) -> [String]? {
|
||||||
|
return (try? searchResponseDoc.select(".url"))?
|
||||||
|
.filter { (try? $0.select("span").first()) == nil }
|
||||||
|
.map { (try? $0.text()) ?? "" }
|
||||||
|
.filter { $0.isNotBlank }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findBankWebsiteWithEcosia(_ bankName: String) -> String? {
|
||||||
|
return findBankWebsite(bankName, Self.SearchBankWebsiteBaseUrlEcosia) { searchResponseDoc in
|
||||||
|
(try? searchResponseDoc.select(".js-result-url"))?
|
||||||
|
.map { (try? $0.attr("href")) ?? "" }
|
||||||
|
.filter { $0.isNotBlank }
|
||||||
|
?? []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findBankWebsiteWithDuckDuckGo(_ bankName: String) -> String? {
|
||||||
|
return findBankWebsite(bankName, Self.SearchBankWebsiteBaseUrlDuckDuckGo) { searchResponseDoc in
|
||||||
|
(try? searchResponseDoc.select(".result__url"))?
|
||||||
|
.map { (try? $0.attr("href")) ?? "" }
|
||||||
|
.filter { $0.isNotBlank }
|
||||||
|
?? []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findBankWebsite(_ bankName: String, _ searchBaseUrl: String, extractUrls: (Document) -> [String]) -> String? {
|
||||||
|
let encodedBankName = bankName.replacingOccurrences(of: " ", with: "+")
|
||||||
|
|
||||||
|
let exactSearchUrl = searchBaseUrl + "\"" + encodedBankName + "\""
|
||||||
|
if let searchResponseDocument = getSearchResultForBank(exactSearchUrl) {
|
||||||
|
if let bestUrl = findBestUrlForBank(bankName: bankName, unmappedUrls: extractUrls(searchResponseDocument)) {
|
||||||
|
return bestUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let searchUrl = searchBaseUrl + encodedBankName
|
||||||
|
if let searchResponseDocument = getSearchResultForBank(searchUrl) {
|
||||||
|
return findBestUrlForBank(bankName: bankName, unmappedUrls: extractUrls(searchResponseDocument))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getSearchResultForBank(_ searchUrl: String) -> Document? {
|
||||||
|
let response = webClient.get(searchUrl)
|
||||||
|
|
||||||
|
if let responseBody = response.body {
|
||||||
|
return try? SwiftSoup.parse(responseBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func findBestUrlForBank(bankName: String, unmappedUrls: [String]) -> String? {
|
||||||
|
let urlCandidates = getUrlCandidates(unmappedUrls)
|
||||||
|
let urlCandidatesWithoutUnlikely = urlCandidates.filter { isUnlikelyBankUrl(bankName: bankName, urlCandidate: $0) == false }
|
||||||
|
|
||||||
|
let urlForBank = findUrlThatContainsBankName(bankName: bankName, urlCandidates: urlCandidatesWithoutUnlikely)
|
||||||
|
|
||||||
|
// cut off stuff like 'filalsuche' etc., they most like don't contain as many favicons as main page
|
||||||
|
return getMainPageForBankUrl(urlForBank: urlForBank, urlCandidates: urlCandidatesWithoutUnlikely) ?? urlForBank
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getUrlCandidates(_ urls: [String?]) -> [String] {
|
||||||
|
return urls.map { fixUrl($0) }.filter { $0 != nil} as? [String] ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fixUrl(_ url: String?) -> String? {
|
||||||
|
if let url = url, url.isNotBlank {
|
||||||
|
let urlEncoded = url.replacingOccurrences(of: " ", with: "%20F")
|
||||||
|
|
||||||
|
if urlEncoded.starts(with: "http") {
|
||||||
|
return urlEncoded
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "https://" + urlEncoded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findUrlThatContainsBankName(bankName: String, urlCandidates: [String]) -> String? {
|
||||||
|
let bankNameParts = bankName.replacingOccurrences(of: ",", with: "")
|
||||||
|
.replacingOccurrences(of: "-", with: " ") // to find 'Sparda-Bank' in 'sparda.de'
|
||||||
|
.replacingOccurrences(of: "ä", with: "ae").replacingOccurrences(of: "Ä", with: "ae")
|
||||||
|
.replacingOccurrences(of: "ö", with: "oe").replacingOccurrences(of: "Ö", with: "oe")
|
||||||
|
.replacingOccurrences(of: "ü", with: "ue").replacingOccurrences(of: "Ü", with: "ue")
|
||||||
|
.split(separator: " ").map { String($0) }
|
||||||
|
.filter { $0.isBlank == false }
|
||||||
|
|
||||||
|
var urlsContainsPartsOfBankName = [Int : [String]]()
|
||||||
|
|
||||||
|
urlCandidates.forEach { urlCandidate in
|
||||||
|
if let containingCountParts = findBankNameInUrlHost(urlCandidate: urlCandidate, bankNameParts: bankNameParts) {
|
||||||
|
if (urlsContainsPartsOfBankName.keys.contains(containingCountParts) == false) {
|
||||||
|
urlsContainsPartsOfBankName[containingCountParts] = [urlCandidate]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
urlsContainsPartsOfBankName[containingCountParts]?.append(urlCandidate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let countMostMatches = urlsContainsPartsOfBankName.keys.max() {
|
||||||
|
return urlsContainsPartsOfBankName[countMostMatches]?.first
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findBankNameInUrlHost(urlCandidate: String, bankNameParts: [String]) -> Int? {
|
||||||
|
if let candidateHost = URL(string: urlCandidate.replacingOccurrences(of: "onlinebanking-", with: ""))?.host {
|
||||||
|
return bankNameParts.filter { part in candidateHost.localizedCaseInsensitiveContains(part) }.count
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getMainPageForBankUrl(urlForBank: String?, urlCandidates: [String]) -> String? {
|
||||||
|
if let urlForBank = urlForBank {
|
||||||
|
if isHomePage(urlForBank) {
|
||||||
|
return urlForBank
|
||||||
|
}
|
||||||
|
|
||||||
|
if let bankUri = URL(string: urlForBank) {
|
||||||
|
if let bankUriHost = bankUri.host {
|
||||||
|
let candidateUrl = urlCandidates.first { candidateUrl in
|
||||||
|
return URL(string: candidateUrl)?.host == bankUriHost && isHomePage(candidateUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let candidateUrl = candidateUrl {
|
||||||
|
return candidateUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let urlForBank = urlForBank {
|
||||||
|
if let bankUri = URL(string: urlForBank) {
|
||||||
|
return "\(bankUri.scheme ?? "")://\(bankUri.host ?? "")"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isHomePage(_ url: String) -> Bool {
|
||||||
|
let uri = URL(string: url)
|
||||||
|
|
||||||
|
if let uri = uri, uri.path.isBlank && uri.host?.starts(with: "www.") == true {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isUnlikelyBankUrl(bankName: String, urlCandidate: String) -> Bool {
|
||||||
|
return urlCandidate.contains("meinprospekt.de/")
|
||||||
|
|| urlCandidate.contains("onlinestreet.de/")
|
||||||
|
|| urlCandidate.contains("iban-blz.de/")
|
||||||
|
|| urlCandidate.contains("bankleitzahlen.ws/")
|
||||||
|
|| urlCandidate.contains("bankleitzahl-finden.de/")
|
||||||
|
|| urlCandidate.contains("bankleitzahl-bic.de/")
|
||||||
|
|| urlCandidate.contains("bankleitzahlensuche.org/")
|
||||||
|
|| urlCandidate.contains("bankleitzahlensuche.com/")
|
||||||
|
|| urlCandidate.contains("bankverzeichnis.com")
|
||||||
|
|| urlCandidate.contains("banksuche.com/")
|
||||||
|
|| urlCandidate.contains("bank-code.net/")
|
||||||
|
|| urlCandidate.contains("thebankcodes.com/")
|
||||||
|
|| urlCandidate.contains("zinsen-berechnen.de/")
|
||||||
|
|| urlCandidate.contains("kredit-anzeiger.com/")
|
||||||
|
|| urlCandidate.contains("kreditbanken.de/")
|
||||||
|
|| urlCandidate.contains("nifox.de/")
|
||||||
|
|| urlCandidate.contains("wikipedia.org/")
|
||||||
|
|| urlCandidate.contains("transferwise.com/")
|
||||||
|
|| urlCandidate.contains("wogibtes.info/")
|
||||||
|
|| urlCandidate.contains("11880.com/")
|
||||||
|
|| urlCandidate.contains("kaufda.de/")
|
||||||
|
|| urlCandidate.contains("boomle.com/")
|
||||||
|
|| urlCandidate.contains("berlin.de/")
|
||||||
|
|| urlCandidate.contains("berliner-zeitung.de")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -42,7 +42,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
|
|
||||||
let dataFolder = URL(fileURLWithPath: "data", isDirectory: true, relativeTo: URL(fileURLWithPath: appDataFolder))
|
let dataFolder = URL(fileURLWithPath: "data", isDirectory: true, relativeTo: URL(fileURLWithPath: appDataFolder))
|
||||||
|
|
||||||
let presenter = BankingPresenterSwift(dataFolder: dataFolder, router: SwiftUiRouter(), webClient: UrlSessionWebClient(), persistence: persistence, remitteeSearcher: persistence, serializer: JsonEncoderSerializer(), asyncRunner: DispatchQueueAsyncRunner())
|
let presenter = BankingPresenterSwift(dataFolder: dataFolder, router: SwiftUiRouter(), webClient: UrlSessionWebClient(), persistence: persistence, remitteeSearcher: persistence, bankIconFinder: SwiftBankIconFinder(), serializer: NoOpSerializer(), asyncRunner: DispatchQueueAsyncRunner())
|
||||||
|
|
||||||
DependencyInjector.register(dependency: persistence)
|
DependencyInjector.register(dependency: persistence)
|
||||||
DependencyInjector.register(dependency: presenter)
|
DependencyInjector.register(dependency: presenter)
|
||||||
|
|
|
@ -6,12 +6,51 @@ import MultiplatformUtils
|
||||||
class UrlSessionWebClient : Fints4kIWebClient {
|
class UrlSessionWebClient : Fints4kIWebClient {
|
||||||
|
|
||||||
func post(url: String, body: String, contentType: String, userAgent: String, callback: @escaping (Fints4kWebClientResponse) -> Void) {
|
func post(url: String, body: String, contentType: String, userAgent: String, callback: @escaping (Fints4kWebClientResponse) -> Void) {
|
||||||
guard let requestUrl = URL(string: url) else { fatalError() }
|
let request = requestFor(url, "POST", body)
|
||||||
|
|
||||||
|
executeRequestAsync(request, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAsync(_ url: String, callback: @escaping (Fints4kWebClientResponse) -> Void) {
|
||||||
|
let request = requestFor(url, "GET")
|
||||||
|
|
||||||
|
executeRequestAsync(request, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func get(_ url: String) -> Fints4kWebClientResponse {
|
||||||
|
let request = requestFor(url, "GET")
|
||||||
|
|
||||||
|
return executeRequestSynchronous(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getData(_ url: String) -> Data? {
|
||||||
|
let request = requestFor(url, "GET")
|
||||||
|
|
||||||
|
return executeDataRequestSynchronous(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func head(_ url: String) -> Fints4kWebClientResponse {
|
||||||
|
let request = requestFor(url, "HEAD")
|
||||||
|
|
||||||
|
return executeRequestSynchronous(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func requestFor(_ url: String, _ method: String, _ body: String? = nil) -> URLRequest {
|
||||||
|
guard let requestUrl = URL.encoded(url) else { fatalError() }
|
||||||
|
|
||||||
var request = URLRequest(url: requestUrl)
|
var request = URLRequest(url: requestUrl)
|
||||||
request.httpMethod = "POST"
|
request.httpMethod = method
|
||||||
request.httpBody = body.data(using: String.Encoding.utf8)
|
|
||||||
|
|
||||||
|
if let body = body {
|
||||||
|
request.httpBody = body.data(using: String.Encoding.utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeRequestAsync(_ request: URLRequest, _ callback: @escaping (Fints4kWebClientResponse) -> Void) {
|
||||||
let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
|
let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
|
||||||
// we have to dispatch response back to main thread as in Kotlin/Native objects can only be changed on one thread -> do all logic on main thread only network access in backbackground thread
|
// we have to dispatch response back to main thread as in Kotlin/Native objects can only be changed on one thread -> do all logic on main thread only network access in backbackground thread
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
@ -28,4 +67,57 @@ class UrlSessionWebClient : Fints4kIWebClient {
|
||||||
dataTask.resume()
|
dataTask.resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
func executeRequestSynchronous(_ request: URLRequest) -> Fints4kWebClientResponse {
|
||||||
|
var data: Data?, urlResponse: URLResponse?, error: Error?
|
||||||
|
|
||||||
|
let semaphore = DispatchSemaphore(value: 0)
|
||||||
|
|
||||||
|
let dataTask = URLSession.shared.dataTask(with: request) {
|
||||||
|
data = $0
|
||||||
|
urlResponse = $1
|
||||||
|
error = $2
|
||||||
|
|
||||||
|
semaphore.signal()
|
||||||
|
}
|
||||||
|
|
||||||
|
dataTask.resume()
|
||||||
|
|
||||||
|
_ = semaphore.wait(timeout: .distantFuture)
|
||||||
|
|
||||||
|
return mapResponse(data, urlResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func mapResponse(_ data: Data?, _ response: URLResponse?, _ error: Error?) -> Fints4kWebClientResponse {
|
||||||
|
if let httpResponse = response as? HTTPURLResponse {
|
||||||
|
if let data = data {
|
||||||
|
return Fints4kWebClientResponse(successful: true, responseCode: Int32(httpResponse.statusCode), error: nil, body: String(data: data, encoding: .ascii))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return Fints4kWebClientResponse(successful: true, responseCode: Int32(httpResponse.statusCode), error: nil, body: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return Fints4kWebClientResponse(successful: false, responseCode: -1, error: KotlinException(message: error?.localizedDescription), body: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func executeDataRequestSynchronous(_ request: URLRequest) -> Data? {
|
||||||
|
var data: Data?
|
||||||
|
|
||||||
|
let semaphore = DispatchSemaphore(value: 0)
|
||||||
|
|
||||||
|
let dataTask = URLSession.shared.dataTask(with: request) { receivedData, _, _ in
|
||||||
|
data = receivedData
|
||||||
|
|
||||||
|
semaphore.signal()
|
||||||
|
}
|
||||||
|
|
||||||
|
dataTask.resume()
|
||||||
|
|
||||||
|
_ = semaphore.wait(timeout: .distantFuture)
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import CoreData
|
import CoreData
|
||||||
|
import UIKit
|
||||||
import BankingUiSwift
|
import BankingUiSwift
|
||||||
|
|
||||||
|
|
||||||
|
@ -70,7 +71,15 @@ class CoreDataBankingPersistence: IBankingPersistence, IRemitteeSearcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveUrlToFile(url: String, file: URL) {
|
func saveUrlToFile(url: String, file: URL) {
|
||||||
// TODO
|
let response = UrlSessionWebClient().getData(url)
|
||||||
|
|
||||||
|
if let response = response {
|
||||||
|
do {
|
||||||
|
try UIImage(data: response)?.pngData()?.write(to: file)
|
||||||
|
} catch {
|
||||||
|
NSLog("Could not save url '\(url)' to file '\(file): \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -88,3 +88,21 @@ extension Array where Element == NSDecimalNumber {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension NSObject {
|
||||||
|
|
||||||
|
var className: String {
|
||||||
|
return String(describing: type(of: self))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension URL {
|
||||||
|
|
||||||
|
static func encoded(_ url: String) -> URL? {
|
||||||
|
return URL(string: url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? url)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import XCTest
|
||||||
|
@testable import BankingiOSApp
|
||||||
|
|
||||||
|
|
||||||
|
class SwiftBankIconFinderTest: XCTestCase {
|
||||||
|
|
||||||
|
private let underTest = SwiftBankIconFinder()
|
||||||
|
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDownWithError() throws {
|
||||||
|
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func testBerlinerSparkasse() throws {
|
||||||
|
|
||||||
|
// when
|
||||||
|
let result = underTest.findBankWebsite(bankName: "Sparkasse Berlin")
|
||||||
|
|
||||||
|
// then
|
||||||
|
XCTAssertNotNil(result)
|
||||||
|
XCTAssertEqual(result!, "https://www.berliner-sparkasse.de")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue