Mercurial > public > simoleon
changeset 156:84137052813d
Refactor code
line wrap: on
line diff
--- a/Simoleon.xcodeproj/project.pbxproj Wed Aug 25 11:00:21 2021 +0100 +++ b/Simoleon.xcodeproj/project.pbxproj Sat Aug 28 11:15:25 2021 +0100 @@ -8,53 +8,57 @@ /* Begin PBXBuildFile section */ 950093CA26CBC7A200FEBF67 /* SimoleonAppPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950093C926CBC7A200FEBF67 /* SimoleonAppPreview.swift */; }; - 9501E13826D3A52000D14DE5 /* CurrencyDetails.json in Resources */ = {isa = PBXBuildFile; fileRef = 9501E13726D3A52000D14DE5 /* CurrencyDetails.json */; }; + 9501E13826D3A52000D14DE5 /* Currencies.json in Resources */ = {isa = PBXBuildFile; fileRef = 9501E13726D3A52000D14DE5 /* Currencies.json */; }; 9501E13A26D3A55200D14DE5 /* CurrencyPairsSupported.json in Resources */ = {isa = PBXBuildFile; fileRef = 9501E13926D3A55200D14DE5 /* CurrencyPairsSupported.json */; }; - 950A377726A820F800CAB175 /* DefaultCurrency+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A377626A820F400CAB175 /* DefaultCurrency+CoreDataProperties.swift */; }; - 950A377826A820F800CAB175 /* DefaultCurrency+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A377526A820F400CAB175 /* DefaultCurrency+CoreDataClass.swift */; }; 9522BEA926B5A4D20076B098 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9522BEA826B5A4D20076B098 /* AppDelegate.swift */; }; 9522BEAB26B5AACB0076B098 /* ListModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9522BEAA26B5AACB0076B098 /* ListModifier.swift */; }; 9522CD9D26CED2E100DD9D03 /* ErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9522CD9C26CED2E100DD9D03 /* ErrorHandling.swift */; }; + 9531D44226D8E4CF00665D2A /* SimoleonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9531D44126D8E4CF00665D2A /* SimoleonTests.swift */; }; 953B8B1726D3A970003CF530 /* CurrencyDetailsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 953B8B1626D3A970003CF530 /* CurrencyDetailsModel.swift */; }; - 9555933A269B0AB8000FD726 /* Read.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95559339269B0AB8000FD726 /* Read.swift */; }; 95561E3F26AF25EF00CCB543 /* SubscriptionFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95561E3E26AF25EF00CCB543 /* SubscriptionFeature.swift */; }; 95562D4D26A8962A0047E778 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 95562D4C26A8962A0047E778 /* StoreKit.framework */; }; 95562D5226A8AEF60047E778 /* Purchases in Frameworks */ = {isa = PBXBuildFile; productRef = 95562D5126A8AEF60047E778 /* Purchases */; }; - 957065E226A5FE0400523E68 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 957065E126A5FE0400523E68 /* Settings.swift */; }; - 9585BB1226A6B71B00E3193E /* ReadConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9585BB1126A6B71B00E3193E /* ReadConfig.swift */; }; - 9585BB1426A6B7F400E3193E /* NetworkRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9585BB1326A6B7F400E3193E /* NetworkRequest.swift */; }; - 9585BB1A26A6E8FD00E3193E /* SimpleSuccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9585BB1926A6E8FD00E3193E /* SimpleSuccess.swift */; }; + 957065E226A5FE0400523E68 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 957065E126A5FE0400523E68 /* SettingsView.swift */; }; + 957DCF3326D7ADEA00BCAB1E /* CurrencyPairModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 957DCF3226D7ADEA00BCAB1E /* CurrencyPairModel.swift */; }; + 95851CE326D4DAAE004ADA79 /* CurrencyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95851CE226D4DAAE004ADA79 /* CurrencyButton.swift */; }; + 95851CE526D4DB4C004ADA79 /* Flag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95851CE426D4DB4C004ADA79 /* Flag.swift */; }; + 95851CE826D4E552004ADA79 /* DefaultCurrency+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95851CE626D4E552004ADA79 /* DefaultCurrency+CoreDataClass.swift */; }; + 95851CE926D4E552004ADA79 /* DefaultCurrency+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95851CE726D4E552004ADA79 /* DefaultCurrency+CoreDataProperties.swift */; }; + 95851CF026D4E89C004ADA79 /* Favorite+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95851CEE26D4E89C004ADA79 /* Favorite+CoreDataClass.swift */; }; + 95851CF126D4E89C004ADA79 /* Favorite+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95851CEF26D4E89C004ADA79 /* Favorite+CoreDataProperties.swift */; }; + 9585BB1426A6B7F400E3193E /* NetworkHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9585BB1326A6B7F400E3193E /* NetworkHelper.swift */; }; + 9585BB1A26A6E8FD00E3193E /* HapticsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9585BB1926A6E8FD00E3193E /* HapticsHelper.swift */; }; 95909CB326B07BFC00D051AB /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95909CB226B07BFC00D051AB /* SearchBar.swift */; }; 959F6DEB26BBD53500101E53 /* SimoleonScreenshots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 959F6DEA26BBD53500101E53 /* SimoleonScreenshots.swift */; }; - 959F6DF326BBD54400101E53 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956088B526B9307600A4FD6C /* SnapshotHelper.swift */; }; + 95AA42ED26D78A4A0085570D /* FileHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95AA42EC26D78A4A0085570D /* FileHelper.swift */; }; 95AEBC9526A03ECB00613729 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95AEBC9426A03ECB00613729 /* ContentView.swift */; }; 95AEBC9D26A04D4600613729 /* CurrencyRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95AEBC9C26A04D4600613729 /* CurrencyRow.swift */; }; 95AEBCA326A0900E00613729 /* CurrencyQuoteModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95AEBCA226A0900E00613729 /* CurrencyQuoteModel.swift */; }; - 95B54F4426A4842C001DC0D8 /* Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B54F4326A4842C001DC0D8 /* Conversion.swift */; }; - 95B54F4626A48852001DC0D8 /* CurrencySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B54F4526A48852001DC0D8 /* CurrencySelector.swift */; }; + 95B54F4426A4842C001DC0D8 /* ConversionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B54F4326A4842C001DC0D8 /* ConversionView.swift */; }; 95B54F4A26A4A450001DC0D8 /* ConversionBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B54F4926A4A450001DC0D8 /* ConversionBox.swift */; }; 95B54F5126A4ACAC001DC0D8 /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B54F5026A4ACAC001DC0D8 /* Sidebar.swift */; }; 95B5F53126AADE4B00BDCE89 /* en.xliff in Resources */ = {isa = PBXBuildFile; fileRef = 95B5F52826AADE4B00BDCE89 /* en.xliff */; }; 95B5F53226AADE4B00BDCE89 /* contents.json in Resources */ = {isa = PBXBuildFile; fileRef = 95B5F52A26AADE4B00BDCE89 /* contents.json */; }; 95B5F53326AADE4B00BDCE89 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 95B5F52D26AADE4B00BDCE89 /* Localizable.strings */; }; 95B5F53426AADE4B00BDCE89 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 95B5F52F26AADE4B00BDCE89 /* InfoPlist.strings */; }; + 95C1DD6A26D8DF9400315C3F /* CurrencySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C1DD6926D8DF9400315C3F /* CurrencySelector.swift */; }; 95C5179126A5DC8E00BC2B24 /* ConditionalWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C5179026A5DC8E00BC2B24 /* ConditionalWrapper.swift */; }; 95C5179926A5EC9F00BC2B24 /* FavoriteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C5179826A5EC9F00BC2B24 /* FavoriteButton.swift */; }; - 95C5179C26A5EFBE00BC2B24 /* Favorite+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C5179A26A5EFBE00BC2B24 /* Favorite+CoreDataClass.swift */; }; - 95C5179D26A5EFBE00BC2B24 /* Favorite+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C5179B26A5EFBE00BC2B24 /* Favorite+CoreDataProperties.swift */; }; - 95C5179F26A5F34200BC2B24 /* Favorites.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C5179E26A5F34200BC2B24 /* Favorites.swift */; }; + 95C5179F26A5F34200BC2B24 /* FavoritesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C5179E26A5F34200BC2B24 /* FavoritesView.swift */; }; 95C517A126A5F6C000BC2B24 /* ResignKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C517A026A5F6C000BC2B24 /* ResignKeyboard.swift */; }; 95C5B2282697752600941585 /* SimoleonApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C5B2272697752600941585 /* SimoleonApp.swift */; }; 95C5B22C2697752700941585 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 95C5B22B2697752700941585 /* Assets.xcassets */; }; 95C5B22F2697752700941585 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 95C5B22E2697752700941585 /* Preview Assets.xcassets */; }; 95C5B2312697752700941585 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C5B2302697752700941585 /* Persistence.swift */; }; 95C5B2342697752700941585 /* Simoleon.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 95C5B2322697752700941585 /* Simoleon.xcdatamodeld */; }; - 95C5B23F2697752700941585 /* SimoleonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C5B23E2697752700941585 /* SimoleonTests.swift */; }; 95C5B24A2697752700941585 /* SimoleonUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C5B2492697752700941585 /* SimoleonUITests.swift */; }; + 95CE6A3626D50B7700D9DCBD /* CurrencyList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CE6A3526D50B7700D9DCBD /* CurrencyList.swift */; }; 95D8C8C726A95D2900BCC188 /* SubscriptionPaywall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D8C8C626A95D2900BCC188 /* SubscriptionPaywall.swift */; }; 95D8C8CD26A9784500BCC188 /* SubscribeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D8C8CC26A9784500BCC188 /* SubscribeButton.swift */; }; 95D8C8CF26A98A7900BCC188 /* RestoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D8C8CE26A98A7900BCC188 /* RestoreButton.swift */; }; 95D8C8D126A9BC6200BCC188 /* LockedCurrencyPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D8C8D026A9BC6200BCC188 /* LockedCurrencyPicker.swift */; }; + 95DA4B5626D7D10100566C5E /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956088B526B9307600A4FD6C /* SnapshotHelper.swift */; }; + 95DA4B5926D7E2DE00566C5E /* ChildListResets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95DA4B5826D7E2DE00566C5E /* ChildListResets.swift */; }; 95E76436269DFC1A008E9F31 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 95E76435269DFC1A008E9F31 /* LaunchScreen.storyboard */; }; 95E7643A269E0037008E9F31 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 95E76439269E0037008E9F31 /* CloudKit.framework */; }; /* End PBXBuildFile section */ @@ -67,14 +71,14 @@ remoteGlobalIDString = 95C5B2232697752600941585; remoteInfo = Simoleon; }; - 959F6DED26BBD53500101E53 /* PBXContainerItemProxy */ = { + 9531D44426D8E4CF00665D2A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 95C5B21C2697752600941585 /* Project object */; proxyType = 1; remoteGlobalIDString = 95C5B2232697752600941585; remoteInfo = Simoleon; }; - 95C5B23B2697752700941585 /* PBXContainerItemProxy */ = { + 959F6DED26BBD53500101E53 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 95C5B21C2697752600941585 /* Project object */; proxyType = 1; @@ -107,23 +111,29 @@ 950093C726CBC7A200FEBF67 /* SimoleonAppPreview.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SimoleonAppPreview.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 950093C926CBC7A200FEBF67 /* SimoleonAppPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimoleonAppPreview.swift; sourceTree = "<group>"; }; 950093CB26CBC7A200FEBF67 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; - 9501E13726D3A52000D14DE5 /* CurrencyDetails.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = CurrencyDetails.json; sourceTree = "<group>"; }; + 9501E13726D3A52000D14DE5 /* Currencies.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Currencies.json; sourceTree = "<group>"; }; 9501E13926D3A55200D14DE5 /* CurrencyPairsSupported.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = CurrencyPairsSupported.json; sourceTree = "<group>"; }; - 950A377526A820F400CAB175 /* DefaultCurrency+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DefaultCurrency+CoreDataClass.swift"; sourceTree = "<group>"; }; - 950A377626A820F400CAB175 /* DefaultCurrency+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DefaultCurrency+CoreDataProperties.swift"; sourceTree = "<group>"; }; 9522BEA826B5A4D20076B098 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 9522BEAA26B5AACB0076B098 /* ListModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListModifier.swift; sourceTree = "<group>"; }; 9522CD9C26CED2E100DD9D03 /* ErrorHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandling.swift; sourceTree = "<group>"; }; + 9531D43F26D8E4CF00665D2A /* SimoleonTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SimoleonTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 9531D44126D8E4CF00665D2A /* SimoleonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimoleonTests.swift; sourceTree = "<group>"; }; + 9531D44326D8E4CF00665D2A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 953B8B1626D3A970003CF530 /* CurrencyDetailsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyDetailsModel.swift; sourceTree = "<group>"; }; - 95559339269B0AB8000FD726 /* Read.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Read.swift; sourceTree = "<group>"; }; 95561E3E26AF25EF00CCB543 /* SubscriptionFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionFeature.swift; sourceTree = "<group>"; }; 95562D4C26A8962A0047E778 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; - 956088B526B9307600A4FD6C /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SnapshotHelper.swift; path = fastlane/SnapshotHelper.swift; sourceTree = "<group>"; }; - 957065E126A5FE0400523E68 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; }; + 956088B526B9307600A4FD6C /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotHelper.swift; sourceTree = "<group>"; }; + 957065E126A5FE0400523E68 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; }; + 957DCF3226D7ADEA00BCAB1E /* CurrencyPairModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyPairModel.swift; sourceTree = "<group>"; }; + 95851CE226D4DAAE004ADA79 /* CurrencyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyButton.swift; sourceTree = "<group>"; }; + 95851CE426D4DB4C004ADA79 /* Flag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Flag.swift; sourceTree = "<group>"; }; + 95851CE626D4E552004ADA79 /* DefaultCurrency+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DefaultCurrency+CoreDataClass.swift"; sourceTree = "<group>"; }; + 95851CE726D4E552004ADA79 /* DefaultCurrency+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DefaultCurrency+CoreDataProperties.swift"; sourceTree = "<group>"; }; + 95851CEE26D4E89C004ADA79 /* Favorite+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Favorite+CoreDataClass.swift"; sourceTree = "<group>"; }; + 95851CEF26D4E89C004ADA79 /* Favorite+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Favorite+CoreDataProperties.swift"; sourceTree = "<group>"; }; 9585BB0F26A6B58500E3193E /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; }; - 9585BB1126A6B71B00E3193E /* ReadConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadConfig.swift; sourceTree = "<group>"; }; - 9585BB1326A6B7F400E3193E /* NetworkRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkRequest.swift; sourceTree = "<group>"; }; - 9585BB1926A6E8FD00E3193E /* SimpleSuccess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleSuccess.swift; sourceTree = "<group>"; }; + 9585BB1326A6B7F400E3193E /* NetworkHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkHelper.swift; sourceTree = "<group>"; }; + 9585BB1926A6E8FD00E3193E /* HapticsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticsHelper.swift; sourceTree = "<group>"; }; 9587597726B2A59D004086F0 /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/Localizable.strings"; sourceTree = "<group>"; }; 9587597826B2A59D004086F0 /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/InfoPlist.strings"; sourceTree = "<group>"; }; 9587597926B2A5B6004086F0 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; }; @@ -143,11 +153,11 @@ 959F6DEA26BBD53500101E53 /* SimoleonScreenshots.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimoleonScreenshots.swift; sourceTree = "<group>"; }; 959F6DEC26BBD53500101E53 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 95A70BE926B0550000CC0273 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS7.4.sdk/System/Library/Frameworks/CloudKit.framework; sourceTree = DEVELOPER_DIR; }; + 95AA42EC26D78A4A0085570D /* FileHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileHelper.swift; sourceTree = "<group>"; }; 95AEBC9426A03ECB00613729 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; 95AEBC9C26A04D4600613729 /* CurrencyRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyRow.swift; sourceTree = "<group>"; }; 95AEBCA226A0900E00613729 /* CurrencyQuoteModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyQuoteModel.swift; sourceTree = "<group>"; }; - 95B54F4326A4842C001DC0D8 /* Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conversion.swift; sourceTree = "<group>"; }; - 95B54F4526A48852001DC0D8 /* CurrencySelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencySelector.swift; sourceTree = "<group>"; }; + 95B54F4326A4842C001DC0D8 /* ConversionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversionView.swift; sourceTree = "<group>"; }; 95B54F4926A4A450001DC0D8 /* ConversionBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversionBox.swift; sourceTree = "<group>"; }; 95B54F5026A4ACAC001DC0D8 /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = "<group>"; }; 95B5F52826AADE4B00BDCE89 /* en.xliff */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = en.xliff; sourceTree = "<group>"; }; @@ -156,11 +166,10 @@ 95B5F53026AADE4B00BDCE89 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 95B5F53526AADE5200BDCE89 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; }; 95B5F53626AADE5500BDCE89 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + 95C1DD6926D8DF9400315C3F /* CurrencySelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencySelector.swift; sourceTree = "<group>"; }; 95C5179026A5DC8E00BC2B24 /* ConditionalWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalWrapper.swift; sourceTree = "<group>"; }; 95C5179826A5EC9F00BC2B24 /* FavoriteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteButton.swift; sourceTree = "<group>"; }; - 95C5179A26A5EFBE00BC2B24 /* Favorite+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Favorite+CoreDataClass.swift"; sourceTree = "<group>"; }; - 95C5179B26A5EFBE00BC2B24 /* Favorite+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Favorite+CoreDataProperties.swift"; sourceTree = "<group>"; }; - 95C5179E26A5F34200BC2B24 /* Favorites.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Favorites.swift; sourceTree = "<group>"; }; + 95C5179E26A5F34200BC2B24 /* FavoritesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesView.swift; sourceTree = "<group>"; }; 95C517A026A5F6C000BC2B24 /* ResignKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResignKeyboard.swift; sourceTree = "<group>"; }; 95C5B2242697752600941585 /* Simoleon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Simoleon.app; sourceTree = BUILT_PRODUCTS_DIR; }; 95C5B2272697752600941585 /* SimoleonApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimoleonApp.swift; sourceTree = "<group>"; }; @@ -169,16 +178,15 @@ 95C5B2302697752700941585 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; }; 95C5B2332697752700941585 /* Simoleon.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Simoleon.xcdatamodel; sourceTree = "<group>"; }; 95C5B2352697752700941585 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; - 95C5B23A2697752700941585 /* SimoleonTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SimoleonTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 95C5B23E2697752700941585 /* SimoleonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimoleonTests.swift; sourceTree = "<group>"; }; - 95C5B2402697752700941585 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 95C5B2452697752700941585 /* SimoleonUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SimoleonUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 95C5B2492697752700941585 /* SimoleonUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimoleonUITests.swift; sourceTree = "<group>"; }; 95C5B24B2697752700941585 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; + 95CE6A3526D50B7700D9DCBD /* CurrencyList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyList.swift; sourceTree = "<group>"; }; 95D8C8C626A95D2900BCC188 /* SubscriptionPaywall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPaywall.swift; sourceTree = "<group>"; }; 95D8C8CC26A9784500BCC188 /* SubscribeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribeButton.swift; sourceTree = "<group>"; }; 95D8C8CE26A98A7900BCC188 /* RestoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreButton.swift; sourceTree = "<group>"; }; 95D8C8D026A9BC6200BCC188 /* LockedCurrencyPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedCurrencyPicker.swift; sourceTree = "<group>"; }; + 95DA4B5826D7E2DE00566C5E /* ChildListResets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChildListResets.swift; sourceTree = "<group>"; }; 95E76435269DFC1A008E9F31 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; }; 95E76437269E0033008E9F31 /* Simoleon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Simoleon.entitlements; sourceTree = "<group>"; }; 95E76439269E0037008E9F31 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; @@ -192,6 +200,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 9531D43C26D8E4CF00665D2A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 959F6DE526BBD53500101E53 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -209,13 +224,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 95C5B2372697752700941585 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 95C5B2422697752700941585 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -235,34 +243,33 @@ path = SimoleonAppPreview; sourceTree = "<group>"; }; + 9531D44026D8E4CF00665D2A /* SimoleonTests */ = { + isa = PBXGroup; + children = ( + 9531D44126D8E4CF00665D2A /* SimoleonTests.swift */, + 9531D44326D8E4CF00665D2A /* Info.plist */, + ); + path = SimoleonTests; + sourceTree = "<group>"; + }; 95559331269B094A000FD726 /* Models */ = { isa = PBXGroup; children = ( - 950A377526A820F400CAB175 /* DefaultCurrency+CoreDataClass.swift */, - 950A377626A820F400CAB175 /* DefaultCurrency+CoreDataProperties.swift */, - 95C5179A26A5EFBE00BC2B24 /* Favorite+CoreDataClass.swift */, - 95C5179B26A5EFBE00BC2B24 /* Favorite+CoreDataProperties.swift */, + 95851CEE26D4E89C004ADA79 /* Favorite+CoreDataClass.swift */, + 95851CEF26D4E89C004ADA79 /* Favorite+CoreDataProperties.swift */, + 95851CE626D4E552004ADA79 /* DefaultCurrency+CoreDataClass.swift */, + 95851CE726D4E552004ADA79 /* DefaultCurrency+CoreDataProperties.swift */, 95AEBCA226A0900E00613729 /* CurrencyQuoteModel.swift */, 953B8B1626D3A970003CF530 /* CurrencyDetailsModel.swift */, + 957DCF3226D7ADEA00BCAB1E /* CurrencyPairModel.swift */, ); path = Models; sourceTree = "<group>"; }; - 95559338269B0AAA000FD726 /* Functions */ = { - isa = PBXGroup; - children = ( - 95559339269B0AB8000FD726 /* Read.swift */, - 9585BB1126A6B71B00E3193E /* ReadConfig.swift */, - 9585BB1326A6B7F400E3193E /* NetworkRequest.swift */, - 9585BB1926A6E8FD00E3193E /* SimpleSuccess.swift */, - ); - path = Functions; - sourceTree = "<group>"; - }; 9555933B269B0DF9000FD726 /* Resources */ = { isa = PBXGroup; children = ( - 9501E13726D3A52000D14DE5 /* CurrencyDetails.json */, + 9501E13726D3A52000D14DE5 /* Currencies.json */, 9501E13926D3A55200D14DE5 /* CurrencyPairsSupported.json */, ); path = Resources; @@ -323,9 +330,8 @@ 95C5B21B2697752600941585 = { isa = PBXGroup; children = ( - 956088B526B9307600A4FD6C /* SnapshotHelper.swift */, 95C5B2262697752600941585 /* Simoleon */, - 95C5B23D2697752700941585 /* SimoleonTests */, + 9531D44026D8E4CF00665D2A /* SimoleonTests */, 95C5B2482697752700941585 /* SimoleonUITests */, 959F6DE926BBD53500101E53 /* SimoleonScreenshots */, 950093C826CBC7A200FEBF67 /* SimoleonAppPreview */, @@ -338,10 +344,10 @@ isa = PBXGroup; children = ( 95C5B2242697752600941585 /* Simoleon.app */, - 95C5B23A2697752700941585 /* SimoleonTests.xctest */, 95C5B2452697752700941585 /* SimoleonUITests.xctest */, 959F6DE826BBD53500101E53 /* SimoleonScreenshots.xctest */, 950093C726CBC7A200FEBF67 /* SimoleonAppPreview.xctest */, + 9531D43F26D8E4CF00665D2A /* SimoleonTests.xctest */, ); name = Products; sourceTree = "<group>"; @@ -351,25 +357,23 @@ children = ( 95E76437269E0033008E9F31 /* Simoleon.entitlements */, 9585BB0F26A6B58500E3193E /* Config.xcconfig */, + 95C5B22B2697752700941585 /* Assets.xcassets */, + 95C5B2352697752700941585 /* Info.plist */, + 95C5B2322697752700941585 /* Simoleon.xcdatamodeld */, + 95E76435269DFC1A008E9F31 /* LaunchScreen.storyboard */, 9522BEA826B5A4D20076B098 /* AppDelegate.swift */, 95C5B2272697752600941585 /* SimoleonApp.swift */, 95AEBC9426A03ECB00613729 /* ContentView.swift */, - 95B54F4326A4842C001DC0D8 /* Conversion.swift */, - 95C5179E26A5F34200BC2B24 /* Favorites.swift */, - 957065E126A5FE0400523E68 /* Settings.swift */, - 95D8C8C626A95D2900BCC188 /* SubscriptionPaywall.swift */, - 95C5B22B2697752700941585 /* Assets.xcassets */, - 95C5B2302697752700941585 /* Persistence.swift */, - 9522CD9C26CED2E100DD9D03 /* ErrorHandling.swift */, - 95C5B2352697752700941585 /* Info.plist */, - 95C5B2322697752700941585 /* Simoleon.xcdatamodeld */, - 95E76435269DFC1A008E9F31 /* LaunchScreen.storyboard */, + 95B54F4326A4842C001DC0D8 /* ConversionView.swift */, + 95C5179E26A5F34200BC2B24 /* FavoritesView.swift */, + 957065E126A5FE0400523E68 /* SettingsView.swift */, + 95DA4B5226D7D09A00566C5E /* UI */, 95FE659A269AFB44008745DE /* Helpers */, 95559331269B094A000FD726 /* Models */, - 95559338269B0AAA000FD726 /* Functions */, 9555933B269B0DF9000FD726 /* Resources */, 95B5F52526AADE4B00BDCE89 /* Localization */, 95C5B22D2697752700941585 /* Preview Content */, + 95DA4B5726D7E2B400566C5E /* Tests */, ); path = Simoleon; sourceTree = "<group>"; @@ -382,15 +386,6 @@ path = "Preview Content"; sourceTree = "<group>"; }; - 95C5B23D2697752700941585 /* SimoleonTests */ = { - isa = PBXGroup; - children = ( - 95C5B23E2697752700941585 /* SimoleonTests.swift */, - 95C5B2402697752700941585 /* Info.plist */, - ); - path = SimoleonTests; - sourceTree = "<group>"; - }; 95C5B2482697752700941585 /* SimoleonUITests */ = { isa = PBXGroup; children = ( @@ -400,6 +395,35 @@ path = SimoleonUITests; sourceTree = "<group>"; }; + 95DA4B5226D7D09A00566C5E /* UI */ = { + isa = PBXGroup; + children = ( + 95C1DD6926D8DF9400315C3F /* CurrencySelector.swift */, + 95851CE226D4DAAE004ADA79 /* CurrencyButton.swift */, + 95CE6A3526D50B7700D9DCBD /* CurrencyList.swift */, + 95C5179826A5EC9F00BC2B24 /* FavoriteButton.swift */, + 95B54F4926A4A450001DC0D8 /* ConversionBox.swift */, + 95AEBC9C26A04D4600613729 /* CurrencyRow.swift */, + 95851CE426D4DB4C004ADA79 /* Flag.swift */, + 95B54F5026A4ACAC001DC0D8 /* Sidebar.swift */, + 95909CB226B07BFC00D051AB /* SearchBar.swift */, + 95D8C8D026A9BC6200BCC188 /* LockedCurrencyPicker.swift */, + 95D8C8C626A95D2900BCC188 /* SubscriptionPaywall.swift */, + 95D8C8CC26A9784500BCC188 /* SubscribeButton.swift */, + 95D8C8CE26A98A7900BCC188 /* RestoreButton.swift */, + 95561E3E26AF25EF00CCB543 /* SubscriptionFeature.swift */, + ); + path = UI; + sourceTree = "<group>"; + }; + 95DA4B5726D7E2B400566C5E /* Tests */ = { + isa = PBXGroup; + children = ( + 95DA4B5826D7E2DE00566C5E /* ChildListResets.swift */, + ); + path = Tests; + sourceTree = "<group>"; + }; 95E76438269E0037008E9F31 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -413,19 +437,15 @@ 95FE659A269AFB44008745DE /* Helpers */ = { isa = PBXGroup; children = ( - 95B54F4526A48852001DC0D8 /* CurrencySelector.swift */, - 95AEBC9C26A04D4600613729 /* CurrencyRow.swift */, - 95B54F4926A4A450001DC0D8 /* ConversionBox.swift */, - 95B54F5026A4ACAC001DC0D8 /* Sidebar.swift */, + 95C5B2302697752700941585 /* Persistence.swift */, + 9522CD9C26CED2E100DD9D03 /* ErrorHandling.swift */, + 9585BB1326A6B7F400E3193E /* NetworkHelper.swift */, + 9585BB1926A6E8FD00E3193E /* HapticsHelper.swift */, + 95AA42EC26D78A4A0085570D /* FileHelper.swift */, + 956088B526B9307600A4FD6C /* SnapshotHelper.swift */, 95C5179026A5DC8E00BC2B24 /* ConditionalWrapper.swift */, - 95C5179826A5EC9F00BC2B24 /* FavoriteButton.swift */, + 9522BEAA26B5AACB0076B098 /* ListModifier.swift */, 95C517A026A5F6C000BC2B24 /* ResignKeyboard.swift */, - 95D8C8CC26A9784500BCC188 /* SubscribeButton.swift */, - 95D8C8CE26A98A7900BCC188 /* RestoreButton.swift */, - 95D8C8D026A9BC6200BCC188 /* LockedCurrencyPicker.swift */, - 95561E3E26AF25EF00CCB543 /* SubscriptionFeature.swift */, - 95909CB226B07BFC00D051AB /* SearchBar.swift */, - 9522BEAA26B5AACB0076B098 /* ListModifier.swift */, ); path = Helpers; sourceTree = "<group>"; @@ -451,6 +471,24 @@ productReference = 950093C726CBC7A200FEBF67 /* SimoleonAppPreview.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; + 9531D43E26D8E4CF00665D2A /* SimoleonTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 9531D44626D8E4CF00665D2A /* Build configuration list for PBXNativeTarget "SimoleonTests" */; + buildPhases = ( + 9531D43B26D8E4CF00665D2A /* Sources */, + 9531D43C26D8E4CF00665D2A /* Frameworks */, + 9531D43D26D8E4CF00665D2A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 9531D44526D8E4CF00665D2A /* PBXTargetDependency */, + ); + name = SimoleonTests; + productName = SimoleonTests; + productReference = 9531D43F26D8E4CF00665D2A /* SimoleonTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 959F6DE726BBD53500101E53 /* SimoleonScreenshots */ = { isa = PBXNativeTarget; buildConfigurationList = 959F6DF126BBD53500101E53 /* Build configuration list for PBXNativeTarget "SimoleonScreenshots" */; @@ -490,26 +528,6 @@ productReference = 95C5B2242697752600941585 /* Simoleon.app */; productType = "com.apple.product-type.application"; }; - 95C5B2392697752700941585 /* SimoleonTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 95C5B2512697752700941585 /* Build configuration list for PBXNativeTarget "SimoleonTests" */; - buildPhases = ( - 95C5B2362697752700941585 /* Sources */, - 95C5B2372697752700941585 /* Frameworks */, - 95C5B2382697752700941585 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 95C5B23C2697752700941585 /* PBXTargetDependency */, - ); - name = SimoleonTests; - packageProductDependencies = ( - ); - productName = SimoleonTests; - productReference = 95C5B23A2697752700941585 /* SimoleonTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; 95C5B2442697752700941585 /* SimoleonUITests */ = { isa = PBXNativeTarget; buildConfigurationList = 95C5B2542697752700941585 /* Build configuration list for PBXNativeTarget "SimoleonUITests" */; @@ -541,6 +559,10 @@ CreatedOnToolsVersion = 12.5.1; TestTargetID = 95C5B2232697752600941585; }; + 9531D43E26D8E4CF00665D2A = { + CreatedOnToolsVersion = 12.5.1; + TestTargetID = 95C5B2232697752600941585; + }; 959F6DE726BBD53500101E53 = { CreatedOnToolsVersion = 12.5.1; TestTargetID = 95C5B2232697752600941585; @@ -548,10 +570,6 @@ 95C5B2232697752600941585 = { CreatedOnToolsVersion = 12.5.1; }; - 95C5B2392697752700941585 = { - CreatedOnToolsVersion = 12.5.1; - TestTargetID = 95C5B2232697752600941585; - }; 95C5B2442697752700941585 = { CreatedOnToolsVersion = 12.5.1; TestTargetID = 95C5B2232697752600941585; @@ -583,7 +601,7 @@ projectRoot = ""; targets = ( 95C5B2232697752600941585 /* Simoleon */, - 95C5B2392697752700941585 /* SimoleonTests */, + 9531D43E26D8E4CF00665D2A /* SimoleonTests */, 95C5B2442697752700941585 /* SimoleonUITests */, 959F6DE726BBD53500101E53 /* SimoleonScreenshots */, 950093C626CBC7A200FEBF67 /* SimoleonAppPreview */, @@ -599,6 +617,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 9531D43D26D8E4CF00665D2A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 959F6DE626BBD53500101E53 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -616,19 +641,12 @@ 9501E13A26D3A55200D14DE5 /* CurrencyPairsSupported.json in Resources */, 95B5F53326AADE4B00BDCE89 /* Localizable.strings in Resources */, 95B5F53226AADE4B00BDCE89 /* contents.json in Resources */, - 9501E13826D3A52000D14DE5 /* CurrencyDetails.json in Resources */, + 9501E13826D3A52000D14DE5 /* Currencies.json in Resources */, 95B5F53426AADE4B00BDCE89 /* InfoPlist.strings in Resources */, 95C5B22C2697752700941585 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 95C5B2382697752700941585 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 95C5B2432697752700941585 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -647,12 +665,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 9531D43B26D8E4CF00665D2A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9531D44226D8E4CF00665D2A /* SimoleonTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 959F6DE426BBD53500101E53 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 959F6DEB26BBD53500101E53 /* SimoleonScreenshots.swift in Sources */, - 959F6DF326BBD54400101E53 /* SnapshotHelper.swift in Sources */, + 95DA4B5626D7D10100566C5E /* SnapshotHelper.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -661,46 +687,42 @@ buildActionMask = 2147483647; files = ( 95C5179926A5EC9F00BC2B24 /* FavoriteButton.swift in Sources */, - 95C5179C26A5EFBE00BC2B24 /* Favorite+CoreDataClass.swift in Sources */, - 950A377826A820F800CAB175 /* DefaultCurrency+CoreDataClass.swift in Sources */, + 95851CF126D4E89C004ADA79 /* Favorite+CoreDataProperties.swift in Sources */, 9522CD9D26CED2E100DD9D03 /* ErrorHandling.swift in Sources */, + 95AA42ED26D78A4A0085570D /* FileHelper.swift in Sources */, + 95851CE326D4DAAE004ADA79 /* CurrencyButton.swift in Sources */, 95C5B2312697752700941585 /* Persistence.swift in Sources */, - 9585BB1226A6B71B00E3193E /* ReadConfig.swift in Sources */, + 95DA4B5926D7E2DE00566C5E /* ChildListResets.swift in Sources */, + 95851CF026D4E89C004ADA79 /* Favorite+CoreDataClass.swift in Sources */, 95AEBC9526A03ECB00613729 /* ContentView.swift in Sources */, 9522BEAB26B5AACB0076B098 /* ListModifier.swift in Sources */, 95909CB326B07BFC00D051AB /* SearchBar.swift in Sources */, + 95C1DD6A26D8DF9400315C3F /* CurrencySelector.swift in Sources */, 9522BEA926B5A4D20076B098 /* AppDelegate.swift in Sources */, + 95851CE526D4DB4C004ADA79 /* Flag.swift in Sources */, 95D8C8CD26A9784500BCC188 /* SubscribeButton.swift in Sources */, - 950A377726A820F800CAB175 /* DefaultCurrency+CoreDataProperties.swift in Sources */, - 9585BB1A26A6E8FD00E3193E /* SimpleSuccess.swift in Sources */, - 9555933A269B0AB8000FD726 /* Read.swift in Sources */, + 95851CE926D4E552004ADA79 /* DefaultCurrency+CoreDataProperties.swift in Sources */, + 9585BB1A26A6E8FD00E3193E /* HapticsHelper.swift in Sources */, 95D8C8CF26A98A7900BCC188 /* RestoreButton.swift in Sources */, - 95C5179D26A5EFBE00BC2B24 /* Favorite+CoreDataProperties.swift in Sources */, - 95C5179F26A5F34200BC2B24 /* Favorites.swift in Sources */, + 95C5179F26A5F34200BC2B24 /* FavoritesView.swift in Sources */, 95C5B2282697752600941585 /* SimoleonApp.swift in Sources */, 95B54F4A26A4A450001DC0D8 /* ConversionBox.swift in Sources */, 95D8C8C726A95D2900BCC188 /* SubscriptionPaywall.swift in Sources */, 95D8C8D126A9BC6200BCC188 /* LockedCurrencyPicker.swift in Sources */, 95C517A126A5F6C000BC2B24 /* ResignKeyboard.swift in Sources */, + 95CE6A3626D50B7700D9DCBD /* CurrencyList.swift in Sources */, + 957DCF3326D7ADEA00BCAB1E /* CurrencyPairModel.swift in Sources */, 95AEBC9D26A04D4600613729 /* CurrencyRow.swift in Sources */, 95AEBCA326A0900E00613729 /* CurrencyQuoteModel.swift in Sources */, - 9585BB1426A6B7F400E3193E /* NetworkRequest.swift in Sources */, - 957065E226A5FE0400523E68 /* Settings.swift in Sources */, - 95B54F4426A4842C001DC0D8 /* Conversion.swift in Sources */, + 9585BB1426A6B7F400E3193E /* NetworkHelper.swift in Sources */, + 957065E226A5FE0400523E68 /* SettingsView.swift in Sources */, + 95B54F4426A4842C001DC0D8 /* ConversionView.swift in Sources */, 95C5B2342697752700941585 /* Simoleon.xcdatamodeld in Sources */, 953B8B1726D3A970003CF530 /* CurrencyDetailsModel.swift in Sources */, 95C5179126A5DC8E00BC2B24 /* ConditionalWrapper.swift in Sources */, 95B54F5126A4ACAC001DC0D8 /* Sidebar.swift in Sources */, - 95B54F4626A48852001DC0D8 /* CurrencySelector.swift in Sources */, 95561E3F26AF25EF00CCB543 /* SubscriptionFeature.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 95C5B2362697752700941585 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 95C5B23F2697752700941585 /* SimoleonTests.swift in Sources */, + 95851CE826D4E552004ADA79 /* DefaultCurrency+CoreDataClass.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -720,16 +742,16 @@ target = 95C5B2232697752600941585 /* Simoleon */; targetProxy = 950093CC26CBC7A200FEBF67 /* PBXContainerItemProxy */; }; + 9531D44526D8E4CF00665D2A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 95C5B2232697752600941585 /* Simoleon */; + targetProxy = 9531D44426D8E4CF00665D2A /* PBXContainerItemProxy */; + }; 959F6DEE26BBD53500101E53 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 95C5B2232697752600941585 /* Simoleon */; targetProxy = 959F6DED26BBD53500101E53 /* PBXContainerItemProxy */; }; - 95C5B23C2697752700941585 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 95C5B2232697752600941585 /* Simoleon */; - targetProxy = 95C5B23B2697752700941585 /* PBXContainerItemProxy */; - }; 95C5B2472697752700941585 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 95C5B2232697752600941585 /* Simoleon */; @@ -833,6 +855,69 @@ }; name = Release; }; + 9531D44726D8E4CF00665D2A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = MTX83R5H8X; + INFOPLIST_FILE = SimoleonTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.dennistech.SimoleonTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Simoleon.app/Simoleon"; + }; + name = Debug; + }; + 9531D44826D8E4CF00665D2A /* Screenshots */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = MTX83R5H8X; + INFOPLIST_FILE = SimoleonTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.dennistech.SimoleonTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Simoleon.app/Simoleon"; + }; + name = Screenshots; + }; + 9531D44926D8E4CF00665D2A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = MTX83R5H8X; + INFOPLIST_FILE = SimoleonTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.dennistech.SimoleonTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Simoleon.app/Simoleon"; + }; + name = Release; + }; 959F6DEF26BBD53500101E53 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -966,28 +1051,6 @@ }; name = Screenshots; }; - 95B9EECF26CAC9EF00D94D60 /* Screenshots */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = MTX83R5H8X; - INFOPLIST_FILE = SimoleonTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.dennistech.SimoleonTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Simoleon.app/Simoleon"; - }; - name = Screenshots; - }; 95B9EED026CAC9EF00D94D60 /* Screenshots */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1200,50 +1263,6 @@ }; name = Release; }; - 95C5B2522697752700941585 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = MTX83R5H8X; - INFOPLIST_FILE = SimoleonTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.dennistech.SimoleonTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Simoleon.app/Simoleon"; - }; - name = Debug; - }; - 95C5B2532697752700941585 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = MTX83R5H8X; - INFOPLIST_FILE = SimoleonTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.dennistech.SimoleonTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Simoleon.app/Simoleon"; - }; - name = Release; - }; 95C5B2552697752700941585 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1297,6 +1316,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 9531D44626D8E4CF00665D2A /* Build configuration list for PBXNativeTarget "SimoleonTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9531D44726D8E4CF00665D2A /* Debug */, + 9531D44826D8E4CF00665D2A /* Screenshots */, + 9531D44926D8E4CF00665D2A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 959F6DF126BBD53500101E53 /* Build configuration list for PBXNativeTarget "SimoleonScreenshots" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1327,16 +1356,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 95C5B2512697752700941585 /* Build configuration list for PBXNativeTarget "SimoleonTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 95C5B2522697752700941585 /* Debug */, - 95B9EECF26CAC9EF00D94D60 /* Screenshots */, - 95C5B2532697752700941585 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 95C5B2542697752700941585 /* Build configuration list for PBXNativeTarget "SimoleonUITests" */ = { isa = XCConfigurationList; buildConfigurations = (
--- a/Simoleon.xcodeproj/xcshareddata/xcschemes/Simoleon.xcscheme Wed Aug 25 11:00:21 2021 +0100 +++ b/Simoleon.xcodeproj/xcshareddata/xcschemes/Simoleon.xcscheme Sat Aug 28 11:15:25 2021 +0100 @@ -42,7 +42,7 @@ buildForAnalyzing = "YES"> <BuildableReference BuildableIdentifier = "primary" - BlueprintIdentifier = "95C5B2392697752700941585" + BlueprintIdentifier = "9531D43E26D8E4CF00665D2A" BuildableName = "SimoleonTests.xctest" BlueprintName = "SimoleonTests" ReferencedContainer = "container:Simoleon.xcodeproj"> @@ -69,13 +69,14 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" - language = "en"> + language = "en" + codeCoverageEnabled = "YES"> <Testables> <TestableReference skipped = "NO"> <BuildableReference BuildableIdentifier = "primary" - BlueprintIdentifier = "95C5B2392697752700941585" + BlueprintIdentifier = "9531D43E26D8E4CF00665D2A" BuildableName = "SimoleonTests.xctest" BlueprintName = "SimoleonTests" ReferencedContainer = "container:Simoleon.xcodeproj">
--- a/Simoleon/ContentView.swift Wed Aug 25 11:00:21 2021 +0100 +++ b/Simoleon/ContentView.swift Sat Aug 28 11:15:25 2021 +0100 @@ -10,7 +10,6 @@ struct ContentView: View { @Environment(\.managedObjectContext) private var viewContext @FetchRequest(sortDescriptors: []) private var defaultCurrency: FetchedResults<DefaultCurrency> - @State private var tab: Tab = .convert private enum Tab { @@ -18,32 +17,34 @@ } @ViewBuilder var adjustedView: some View { + let currencyPair = CurrencyPairModel(baseSymbol: "USD", quoteSymbol: "EUR") + // MARK: - iPad if UIDevice.current.userInterfaceIdiom == .pad { NavigationView { - Sidebar() - Conversion(currencyPair: defaultCurrency.first?.pair ?? "USD/GBP") +// Sidebar() + ConversionView(currencyPair: currencyPair) } } else { // MARK: - iPhone TabView(selection: $tab) { - Conversion(currencyPair: defaultCurrency.first?.pair ?? "USD/GBP") + ConversionView(currencyPair: currencyPair) .tabItem { Label("Convert", systemImage: "arrow.counterclockwise.circle") } .tag(Tab.convert) - Favorites() - .tabItem { - Label("Favorites", systemImage: "star") - } - .tag(Tab.favorites) - - Settings() - .tabItem { - Label("Settings", systemImage: "gear") - } - .tag(Tab.settings) +// FavoritesView() +// .tabItem { +// Label("Favorites", systemImage: "star") +// } +// .tag(Tab.favorites) +// +// SettingsView() +// .tabItem { +// Label("Settings", systemImage: "gear") +// } +// .tag(Tab.settings) } } }
--- a/Simoleon/Conversion.swift Wed Aug 25 11:00:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,90 +0,0 @@ -// -// Conversion.swift -// Simoleon -// -// Created by Dennis Concepción Martín on 18/07/2021. -// - -import SwiftUI -import Purchases - -struct Conversion: View { - var showNavigationView: Bool? - - @State var currencyPair: String - @State private var amountToConvert = "" - @State private var price: Double = 1.00 - @State private var showingConversion = false - @State private var showingCurrencySelector = false - @State private var amountIsEditing = false - - var body: some View { - ScrollView(showsIndicators: false) { - VStack(alignment: .leading) { - HStack { - Button(action: { showingCurrencySelector = true }) { - RoundedRectangle(cornerRadius: 15) - .foregroundColor(Color(.secondarySystemBackground)) - .frame(height: 60) - .overlay( - CurrencyRow(currencyPairName: currencyPair) - .padding(.horizontal) - ) - } - .accessibilityIdentifier("OpenCurrencySelector") - - FavoriteButton(currencyPair: currencyPair) - } - - ConversionBox( - currencyPair: $currencyPair, - amountToConvert: $amountToConvert, - price: $price, - showingConversion: $showingConversion, - amountIsEditing: $amountIsEditing - ) - } - .padding() - .sheet(isPresented: $showingCurrencySelector, onDismiss: request) { - CurrencySelector(currencyPair: $currencyPair, showingCurrencySelector: $showingCurrencySelector) - } - } - .onAppear(perform: request) - .navigationTitle("Convert") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - if amountIsEditing { - Button(action: { - UIApplication.shared.dismissKeyboard() - amountIsEditing = false - }) { - Text("Done") - } - } - } - } - .if(UIDevice.current.userInterfaceIdiom == .phone && showNavigationView ?? true) { content in - NavigationView { content } - } - } - - private func request() { - showingConversion = false - let url = "\(readConfig("API_URL")!)quotes?pairs=\(currencyPair)&api_key=\(readConfig("API_KEY")!)" - networkRequest(url: url, model: [CurrencyQuoteModel].self) { response in - if let price = response.first?.price { - self.price = price - showingConversion = true - } else { - // Handle error - } - } - } -} - - -struct Conversion_Previews: PreviewProvider { - static var previews: some View { - Conversion(currencyPair: "USD/GBP") - } -}
--- a/Simoleon/ErrorHandling.swift Wed Aug 25 11:00:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,16 +0,0 @@ -// -// ErrorHandling.swift -// Simoleon -// -// Created by Dennis Concepción Martín on 19/8/21. -// - -import Foundation - -class ErrorHandling { - enum Json: Error { - case fileMissing - case loadFailed(cause: String) - case parseFailed(cause: String) - } -}
--- a/Simoleon/Favorites.swift Wed Aug 25 11:00:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,105 +0,0 @@ -// -// Favorites.swift -// Simoleon -// -// Created by Dennis Concepción Martín on 19/07/2021. -// - -import SwiftUI - -struct Favorites: View { - @Environment(\.managedObjectContext) private var viewContext - @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \Favorite.currencyPair, ascending: true)], - animation: .default) private var favorites: FetchedResults<Favorite> - - var body: some View { - VStack { - if favorites.isEmpty { - Group { - Image(systemName: "star") - .font(.title) - - Text("Search a currency pair and add it to favorites.") - .padding(.top, 5) - } - .multilineTextAlignment(.center) - .foregroundColor(.secondary) - .padding(.horizontal, 50) - } else { - List { - ForEach(favorites) { favorite in - NavigationLink(destination: Conversion(showNavigationView: false, currencyPair: favorite.currencyPair)) { - CurrencyRow(currencyPairName: favorite.currencyPair) - } - } - .onDelete(perform: removeFromFavorites) - } - .listStyle(PlainListStyle()) - .accessibilityIdentifier("FavoritesList") - } - } - .navigationTitle("Favorites") - .toolbar { - #if os(iOS) - EditButton() - #endif - } - .if(UIDevice.current.userInterfaceIdiom == .phone) { content in - NavigationView { content } - } - .onAppear { - #if SCREENSHOTS - generateFavoritesForScreenshots() - #endif - } - } - - private func removeFromFavorites(offsets: IndexSet) { - withAnimation { - offsets.map { favorites[$0] }.forEach(viewContext.delete) - - do { - try viewContext.save() - } catch { - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } - } - - #if SCREENSHOTS - /* - Save currencies to favourites to take screenshots for the App Store - */ - private func generateFavoritesForScreenshots() { - let favoriteCurrencies = [ - "EUR/USD", "BTC/USD", "USD/HKD", "USD/JPY", "AUD/USD", - "XAU/GBP", "DASH/ETH", "EUR/USD", "XAG/CAD" - ] - - let coreDataCurrencyPairs = favorites.map { $0.currencyPair } - - for favoriteCurrency in favoriteCurrencies { - if !coreDataCurrencyPairs.contains(favoriteCurrency) { - let favorites = Favorite(context: viewContext) - favorites.currencyPair = favoriteCurrency - - do { - try viewContext.save() - } catch { - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } - } - } - #endif -} - -struct Favorites_Previews: PreviewProvider { - static var previews: some View { - Favorites() - .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/FavoritesView.swift Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,105 @@ +//// +//// FavoritesView.swift +//// Simoleon +//// +//// Created by Dennis Concepción Martín on 19/07/2021. +//// +// +//import SwiftUI +// +//struct FavoritesView: View { +// @Environment(\.managedObjectContext) private var viewContext +// @FetchRequest( +// sortDescriptors: [NSSortDescriptor(keyPath: \Favorite.currencyPair, ascending: true)], +// animation: .default) private var favorites: FetchedResults<Favorite> +// +// var body: some View { +// VStack { +// if favorites.isEmpty { +// Group { +// Image(systemName: "star") +// .font(.title) +// +// Text("Search a currency pair and add it to favorites.") +// .padding(.top, 5) +// } +// .multilineTextAlignment(.center) +// .foregroundColor(.secondary) +// .padding(.horizontal, 50) +// } else { +// List { +// ForEach(favorites) { favorite in +// NavigationLink(destination: Conversion()) { +//// CurrencyRow(currencyPairName: favorite.currencyPair) +// } +// } +// .onDelete(perform: removeFromFavorites) +// } +// .listStyle(PlainListStyle()) +// .accessibilityIdentifier("FavoritesList") +// } +// } +// .navigationTitle("Favorites") +// .toolbar { +// #if os(iOS) +// EditButton() +// #endif +// } +// .if(UIDevice.current.userInterfaceIdiom == .phone) { content in +// NavigationView { content } +// } +// .onAppear { +// #if SCREENSHOTS +// generateFavoritesForScreenshots() +// #endif +// } +// } +// +// private func removeFromFavorites(offsets: IndexSet) { +// withAnimation { +// offsets.map { favorites[$0] }.forEach(viewContext.delete) +// +// do { +// try viewContext.save() +// } catch { +// let nsError = error as NSError +// fatalError("Unresolved error \(nsError), \(nsError.userInfo)") +// } +// } +// } +// +// #if SCREENSHOTS +// /* +// Save currencies to favourites to take screenshots for the App Store +// */ +// private func generateFavoritesForScreenshots() { +// let favoriteCurrencies = [ +// "EUR/USD", "BTC/USD", "USD/HKD", "USD/JPY", "AUD/USD", +// "XAU/GBP", "DASH/ETH", "EUR/USD", "XAG/CAD" +// ] +// +// let coreDataCurrencyPairs = favorites.map { $0.currencyPair } +// +// for favoriteCurrency in favoriteCurrencies { +// if !coreDataCurrencyPairs.contains(favoriteCurrency) { +// let favorites = Favorite(context: viewContext) +// favorites.currencyPair = favoriteCurrency +// +// do { +// try viewContext.save() +// } catch { +// let nsError = error as NSError +// fatalError("Unresolved error \(nsError), \(nsError.userInfo)") +// } +// } +// } +// } +// #endif +//} +// +//struct FavoritesView_Previews: PreviewProvider { +// static var previews: some View { +// FavoritesView() +// .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) +// } +//}
--- a/Simoleon/Helpers/ConversionBox.swift Wed Aug 25 11:00:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,90 +0,0 @@ -// -// ConversionBox.swift -// Simoleon -// -// Created by Dennis Concepción Martín on 18/07/2021. -// - -import SwiftUI - -struct ConversionBox: View { - @Binding var currencyPair: String - @Binding var amountToConvert: String - @Binding var price: Double - @Binding var showingConversion: Bool - @Binding var amountIsEditing: Bool - - var body: some View { - VStack(alignment: .leading) { - let currencyDetails: [String: CurrencyDetailsModel] = try! read(json: "CurrencyDetails.json") - let currencies = currencyPair.split(separator: "/") - Text("\(currencyDetails[String(currencies[0])]!.name) (\(String(currencies[0])))") - .font(.callout) - .fontWeight(.semibold) - .padding(.top, 40) - - ZStack(alignment: .trailing) { - TextField("Enter amount", text: $amountToConvert) { startedEditing in - if startedEditing { - withAnimation { - amountIsEditing = true - } - } - } - onCommit: { - withAnimation { - amountIsEditing = false - } - } - .keyboardType(.decimalPad) - .font(Font.title.weight(.semibold)) - .lineLimit(1) - .accessibilityIdentifier("ConversionTextField") - } - - Divider() - - Text("\(currencyDetails[String(currencies[1])]!.name) (\(String(currencies[1])))") - .font(.callout) - .fontWeight(.semibold) - .padding(.top, 10) - - if showingConversion { - Text("\(makeConversion(), specifier: "%.2f")") - .font(Font.title.weight(.semibold)) - .lineLimit(1) - .padding(.top, 5) - } else { - ProgressView() - .padding(.top, 5) - } - } - } - - /* - if the amount can be converted to Double: - * Return amount - else: - * Return zero - */ - func makeConversion() -> Double { - if let amountToConvert = Double(amountToConvert) { - return amountToConvert * price // Conversion - } else { - return 0 - } - } -} - - -struct ConversionBox_Previews: PreviewProvider { - static var previews: some View { - ConversionBox( - currencyPair: .constant("USD/GBP"), - amountToConvert: .constant("1000"), - price: .constant(1), - showingConversion: .constant(false), - amountIsEditing: .constant(false) - ) - } -}
--- a/Simoleon/Helpers/CurrencyRow.swift Wed Aug 25 11:00:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,53 +0,0 @@ -// -// CurrencyRow.swift -// Simoleon -// -// Created by Dennis Concepción Martín on 15/07/2021. -// - -import SwiftUI - -struct CurrencyRow: View { - var currencyPairName: String - var isLocked: Bool? - - var body: some View { - HStack { - let currencyDetails: [String: CurrencyDetailsModel] = try! read(json: "CurrencyDetails.json") - let currencies = currencyPairName.split(separator: "/") - Image(currencyDetails[String(currencies[0])]!.flag) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 30, height: 30) - .clipShape(Circle()) - .overlay(Circle().stroke(Color(.secondaryLabel), lineWidth: 1)) - - Image(currencyDetails[String(currencies[1])]!.flag) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 30, height: 30) - .clipShape(Circle()) - .overlay(Circle().stroke(Color(.secondaryLabel), lineWidth: 1)) - .offset(x: -20) - .padding(.trailing, -20) - - Text("From \(String(currencies[0])) to \(String(currencies[1]))") - .fontWeight(.semibold) - .foregroundColor(Color("PlainButton")) - .padding(.leading) - - Spacer() - - if isLocked ?? false { - Image(systemName: "lock") - .foregroundColor(.secondary) - } - } - } -} - -struct CurrencyRow_Previews: PreviewProvider { - static var previews: some View { - CurrencyRow(currencyPairName: "USD/GBP", isLocked: true) - } -}
--- a/Simoleon/Helpers/CurrencySelector.swift Wed Aug 25 11:00:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,131 +0,0 @@ -// -// CurrencySelector.swift -// Simoleon -// -// Created by Dennis Concepción Martín on 18/07/2021. -// - -import SwiftUI -import Purchases - -struct CurrencySelector: View { - @Binding var currencyPair: String - @Binding var showingCurrencySelector: Bool - - @State private var entitlementIsActive = false - @State private var searchCurrency = "" - @State private var showingSubscriptionPaywall = false - @State private var alertTitle = "" - @State private var alertMessage = "" - @State private var showingAlert = false - - /* - If searched currency string is empty: - * Show all currencies - else: - * Show filtered list of currencies containing searched currency string - */ - var searchResults: [String] { - let currencyPairsSupported: [String] = try! read(json: "CurrencyPairs.json") - if searchCurrency.isEmpty { - return currencyPairsSupported.sorted() - } else { - return currencyPairsSupported.filter { $0.contains(searchCurrency.uppercased()) } - } - } - - var body: some View { - NavigationView { - VStack { - SearchBar(placeholder: "Search...", text: $searchCurrency) - .padding() - .accessibilityIdentifier("CurrencySearchBar") - - List { - if entitlementIsActive { - ForEach(searchResults, id: \.self) { currencyPairsSupported in - Button(action: { - self.currencyPair = currencyPairsSupported - showingCurrencySelector = false - }) { - CurrencyRow(currencyPairName: currencyPairsSupported) - } - } - } else { - ForEach(searchResults, id: \.self) { currencyPairsSupported in - Button(action: { select(currencyPairsSupported) }) { - CurrencyRow(currencyPairName: currencyPairsSupported, isLocked: false) - } - } - } - } - .id(UUID()) - } - .navigationTitle("Currencies") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button(action: { showingCurrencySelector = false }) { - Text("Cancel") - } - } - } - } - .onAppear(perform: checkEntitlement) - .alert(isPresented: $showingAlert) { - Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("Ok"))) - } - .sheet(isPresented: $showingSubscriptionPaywall, onDismiss: checkEntitlement) { - SubscriptionPaywall(showingSubscriptionPaywall: $showingSubscriptionPaywall) - } - } - - /* - If user is subscribed: - * Select currency and dismiss currency selector - else: - * Show subscription paywall - */ - private func select(_ currencyPair: String) { -// if currencyPair.isLocked { -// showingSubscriptionPaywall = true -// } else { -// self.currencyPair = currencyPair.name -// showingCurrencySelector = false -// } - } - - // Check if user subscription is active - private func checkEntitlement() { - #if SCREENSHOTS - entitlementIsActive = true - #else - Purchases.shared.purchaserInfo { (purchaserInfo, error) in - if purchaserInfo?.entitlements["all"]?.isActive == true { - entitlementIsActive = true - } - - if let error = error as NSError? { - alertTitle = error.localizedDescription - alertMessage = error.localizedFailureReason ?? "" - showingAlert = true - } - } - #endif - } -} -extension View { - func listStyle() -> some View { - self.modifier(ListModifier()) - } -} - - -struct CurrencySelector_Previews: PreviewProvider { - static var previews: some View { - CurrencySelector( - currencyPair: .constant("USD/GBP"), - showingCurrencySelector: .constant(false) - ) - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/Helpers/ErrorHandling.swift Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,16 @@ +// +// ErrorHandling.swift +// Simoleon +// +// Created by Dennis Concepción Martín on 19/8/21. +// + +import Foundation + +class ErrorHandling { + enum Json: Error { + case fileMissing + case loadFailed(cause: String) + case parseFailed(cause: String) + } +}
--- a/Simoleon/Helpers/FavoriteButton.swift Wed Aug 25 11:00:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,104 +0,0 @@ -// -// FavoriteButton.swift -// Simoleon -// -// Created by Dennis Concepción Martín on 19/07/2021. -// - -import SwiftUI - -struct FavoriteButton: View { - var currencyPair: String - - @Environment(\.managedObjectContext) private var viewContext - @FetchRequest(sortDescriptors: []) private var favorites: FetchedResults<Favorite> - - @State private var starSymbol = "star" - - var body: some View { - let favoriteCurrencyPairs = favorites.map { $0.currencyPair } - Button(action: { favoriteAction(favoriteCurrencyPairs) }) { - RoundedRectangle(cornerRadius: 15) - .foregroundColor(Color(.secondarySystemBackground)) - .frame(width: 60, height: 60) - .overlay( - Image(systemName: generateStar(favoriteCurrencyPairs)) - .font(.system(size: 28)) - .foregroundColor(Color(.systemYellow)) - ) - } - .accessibilityIdentifier("AddToFavorites") - } - - /* - If currency pair is favorite: - * Button action is to remove from favorites - else: - * Button action is to add to favorites - */ - private func favoriteAction(_ favoriteCurrencyPairs: [String]) { - if favoriteCurrencyPairs.contains(currencyPair) { - removeFromFavorites() - } else { - addToFavorites() - } - - simpleSuccess() - } - - /* - if currency pair is favorite: - * Return "star.fill" symbol - else: - * Return "star" - */ - private func generateStar(_ favoriteCurrencyPairs: [String]) -> String { - if favoriteCurrencyPairs.contains(currencyPair) { - return "star.fill" - } else { - return "star" - } - } - - /* - * Get first favorite core data object that matches the specified currency pair - * Delete it - */ - private func removeFromFavorites() { - withAnimation { - let favoriteObject = favorites.first(where: { $0.currencyPair == currencyPair }) - viewContext.delete(favoriteObject ?? Favorite()) - - do { - try viewContext.save() - } catch { - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } - } - - /* - * Create a favorite core data object - * Save it - */ - private func addToFavorites() { - withAnimation { - let favorite = Favorite(context: viewContext) - favorite.currencyPair = currencyPair - - do { - try viewContext.save() - } catch { - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } - } -} - -struct FavoriteButton_Previews: PreviewProvider { - static var previews: some View { - FavoriteButton(currencyPair: "USD/GBP") - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/Helpers/FileHelper.swift Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,41 @@ +// +// File.swift +// Simoleon +// +// Created by Dennis Concepción Martín on 26/8/21. +// + +import Foundation + +/* + Decode and read json file + */ +func read<T: Decodable>(json filename: String) throws -> T { + let data: Data + + guard let file = Bundle.main.url(forResource: filename, withExtension: nil) + else { + throw ErrorHandling.Json.fileMissing + } + + do { + data = try Data(contentsOf: file) + } catch { + throw ErrorHandling.Json.loadFailed(cause: error.localizedDescription) + } + + do { + let decoder = JSONDecoder() + return try decoder.decode(T.self, from: data) + } catch { + throw ErrorHandling.Json.parseFailed(cause: error.localizedDescription) + } +} + +/* + Read configuration variables from Config.xconfig + */ +func readConfigVariable(withKey: String) -> String? { + return (Bundle.main.infoDictionary?[withKey] as? String)? + .replacingOccurrences(of: "\\", with: "") +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/Helpers/HapticsHelper.swift Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,17 @@ +// +// SimpleSuccess.swift +// Simoleon +// +// Created by Dennis Concepción Martín on 20/07/2021. +// + +import SwiftUI + +class Haptics { + + // MARK: - Simple success + func simpleSuccess() { + let generator = UINotificationFeedbackGenerator() + generator.notificationOccurred(.success) + } +}
--- a/Simoleon/Helpers/LockedCurrencyPicker.swift Wed Aug 25 11:00:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,31 +0,0 @@ -// -// LockedCurrencyPicker.swift -// Simoleon -// -// Created by Dennis Concepción Martín on 22/07/2021. -// - -import SwiftUI - -struct LockedCurrencyPicker: View { - @Environment(\.managedObjectContext) private var viewContext - @FetchRequest(sortDescriptors: []) private var defaultCurrency: FetchedResults<DefaultCurrency> - - var body: some View { - HStack { - Text("Default currency") - Spacer() - Text(defaultCurrency.first?.pair ?? "USD/GBP") - .foregroundColor(.secondary) - - Image(systemName: "lock") - .foregroundColor(.secondary) - } - } -} - -struct LockedCurrencyPicker_Previews: PreviewProvider { - static var previews: some View { - LockedCurrencyPicker() - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/Helpers/NetworkHelper.swift Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,38 @@ +// +// Request.swift +// Simoleon +// +// Created by Dennis Concepción Martín on 20/07/2021. +// + +import Foundation + +// MARK: - HTTP Request +func httpRequest<T: Decodable>(url: String, model: T.Type, completion: @escaping (_ result: T) -> Void) { + + // We take some model data T.Type + guard let url = URL(string: url) else { + print("Invalid URL") + return + } + + let request = URLRequest(url: url) + URLSession.shared.dataTask(with: request) { data, response, error in + if let data = data { + do { + // Decode response with the model passed + let decodedResponse = try JSONDecoder().decode(model, from: data) + DispatchQueue.main.async { + completion(decodedResponse) + } + return + } catch { + // Return error regarding the escaping code + print(error) + } + } + // Error with the request + print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")") + } + .resume() +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/Helpers/Persistence.swift Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,64 @@ +// +// Persistence.swift +// Simoleon +// +// Created by Dennis Concepción Martín on 08/07/2021. +// + +import CoreData + +struct PersistenceController { + static let shared = PersistenceController() + + static var preview: PersistenceController = { + let result = PersistenceController(inMemory: true) + let viewContext = result.container.viewContext + + for _ in 0..<10 { + let favorite = Favorite(context: viewContext) + favorite.currencyPair = "GBP/USD" + } + do { + try viewContext.save() + } catch { + /* + Replace this implementation with code to handle the error appropriately. + fatalError() causes the application to generate a crash log and terminate. + You should not use this function in a shipping application, although it may be useful during development. + */ + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + return result + }() + + let container: NSPersistentCloudKitContainer + + init(inMemory: Bool = false) { + container = NSPersistentCloudKitContainer(name: "Simoleon") + container.viewContext.automaticallyMergesChangesFromParent = true + container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy + if inMemory { + container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") + } + container.loadPersistentStores(completionHandler: { (storeDescription, error) in + if let error = error as NSError? { + /* + Replace this implementation with code to handle the error appropriately. + fatalError() causes the application to generate a crash log and terminate. + You should not use this function in a shipping application, although it may be useful during development. + */ + + /* + Typical reasons for an error here include: + * The parent directory does not exist, cannot be created, or disallows writing. + * The persistent store is not accessible, due to permissions or data protection when the device is locked. + * The device is out of space. + * The store could not be migrated to the current model version. + Check the error message to determine what the actual problem was. + */ + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + } +}
--- a/Simoleon/Helpers/RestoreButton.swift Wed Aug 25 11:00:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,58 +0,0 @@ -// -// RestoreButton.swift -// Simoleon -// -// Created by Dennis Concepción Martín on 22/07/2021. -// - -import SwiftUI -import Purchases - -struct RestoreButton: View { - @Binding var showingSubscriptionPaywall: Bool - - @State private var alertTitle: LocalizedStringKey = "" - @State private var alertMessage: LocalizedStringKey = "" - @State private var restoringPurchases = false - @State private var showingAlert = false - - var body: some View { - Button(action: restorePurchases) { - if restoringPurchases { - ProgressView() - } else { - Text("Restore purchases") - } - } - .alert(isPresented: $showingAlert) { - Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("Ok"))) - } - } - - private func restorePurchases() { - restoringPurchases = true - - Purchases.shared.restoreTransactions { purchaserInfo, error in - if purchaserInfo?.entitlements["all"]?.isActive == true { - showingSubscriptionPaywall = false - } else { - alertTitle = LocalizedStringKey("No subscriptions found") - alertMessage = LocalizedStringKey("You are not subscripted to Simoleon yet.") - restoringPurchases = false - showingAlert = true - } - - if let error = error as NSError? { - alertTitle = LocalizedStringKey(error.localizedDescription) - alertMessage = LocalizedStringKey(error.localizedFailureReason ?? "") - showingAlert = true - } - } - } -} - -struct RestoreButton_Previews: PreviewProvider { - static var previews: some View { - RestoreButton(showingSubscriptionPaywall: .constant(true)) - } -}
--- a/Simoleon/Helpers/SearchBar.swift Wed Aug 25 11:00:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,30 +0,0 @@ -// -// SearchBar.swift -// Simoleon -// -// Created by Dennis Concepción Martín on 27/07/2021. -// - -import SwiftUI - -struct SearchBar: View { - var placeholder: LocalizedStringKey - - @Binding var text: String - - var body: some View { - TextField(placeholder, text: $text) - .disableAutocorrection(true) - .padding(10) - .background( - RoundedRectangle(cornerRadius: 15) - .foregroundColor(Color(.tertiarySystemFill)) - ) - } -} - -struct SearchBar_Previews: PreviewProvider { - static var previews: some View { - SearchBar(placeholder: "Search ...", text: .constant("")) - } -}
--- a/Simoleon/Helpers/Sidebar.swift Wed Aug 25 11:00:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,43 +0,0 @@ -// -// Sidebar.swift -// Simoleon -// -// Created by Dennis Concepción Martín on 18/07/2021. -// - -import SwiftUI - -struct Sidebar: View { - @Environment(\.managedObjectContext) private var viewContext - @FetchRequest(sortDescriptors: []) private var defaultCurrency: FetchedResults<DefaultCurrency> - - var body: some View { - List { - NavigationLink(destination: Conversion(currencyPair: defaultCurrency.first?.pair ?? "USD/GBP")) { - Label("Convert", systemImage: "arrow.counterclockwise.circle") - } - .accessibilityIdentifier("NavigateToConversion") - - NavigationLink(destination: Favorites()) { - Label("Favorites", systemImage: "star") - } - .accessibilityIdentifier("NavigateToFavorites") - - NavigationLink(destination: Settings()) { - Label("Settings", systemImage: "gear") - } - .accessibilityIdentifier("NavigateToSettings") - } - .listStyle(SidebarListStyle()) - .navigationTitle("Categories") - .accessibilityIdentifier("Sidebar") - } -} - -struct Sidebar_Previews: PreviewProvider { - static var previews: some View { - NavigationView { - Sidebar() - } - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/Helpers/SnapshotHelper.swift Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,309 @@ +// +// SnapshotHelper.swift +// Example +// +// Created by Felix Krause on 10/8/15. +// + +// ----------------------------------------------------- +// IMPORTANT: When modifying this file, make sure to +// increment the version number at the very +// bottom of the file to notify users about +// the new SnapshotHelper.swift +// ----------------------------------------------------- + +import Foundation +import XCTest + +var deviceLanguage = "" +var locale = "" + +func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) { + Snapshot.setupSnapshot(app, waitForAnimations: waitForAnimations) +} + +func snapshot(_ name: String, waitForLoadingIndicator: Bool) { + if waitForLoadingIndicator { + Snapshot.snapshot(name) + } else { + Snapshot.snapshot(name, timeWaitingForIdle: 0) + } +} + +/// - Parameters: +/// - name: The name of the snapshot +/// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait. +func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { + Snapshot.snapshot(name, timeWaitingForIdle: timeout) +} + +enum SnapshotError: Error, CustomDebugStringConvertible { + case cannotFindSimulatorHomeDirectory + case cannotRunOnPhysicalDevice + + var debugDescription: String { + switch self { + case .cannotFindSimulatorHomeDirectory: + return "Couldn't find simulator home location. Please, check SIMULATOR_HOST_HOME env variable." + case .cannotRunOnPhysicalDevice: + return "Can't use Snapshot on a physical device." + } + } +} + +@objcMembers +open class Snapshot: NSObject { + static var app: XCUIApplication? + static var waitForAnimations = true + static var cacheDirectory: URL? + static var screenshotsDirectory: URL? { + return cacheDirectory?.appendingPathComponent("screenshots", isDirectory: true) + } + + open class func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) { + + Snapshot.app = app + Snapshot.waitForAnimations = waitForAnimations + + do { + let cacheDir = try getCacheDirectory() + Snapshot.cacheDirectory = cacheDir + setLanguage(app) + setLocale(app) + setLaunchArguments(app) + } catch let error { + NSLog(error.localizedDescription) + } + } + + class func setLanguage(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + NSLog("CacheDirectory is not set - probably running on a physical device?") + return + } + + let path = cacheDirectory.appendingPathComponent("language.txt") + + do { + let trimCharacterSet = CharacterSet.whitespacesAndNewlines + deviceLanguage = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) + app.launchArguments += ["-AppleLanguages", "(\(deviceLanguage))"] + } catch { + NSLog("Couldn't detect/set language...") + } + } + + class func setLocale(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + NSLog("CacheDirectory is not set - probably running on a physical device?") + return + } + + let path = cacheDirectory.appendingPathComponent("locale.txt") + + do { + let trimCharacterSet = CharacterSet.whitespacesAndNewlines + locale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) + } catch { + NSLog("Couldn't detect/set locale...") + } + + if locale.isEmpty && !deviceLanguage.isEmpty { + locale = Locale(identifier: deviceLanguage).identifier + } + + if !locale.isEmpty { + app.launchArguments += ["-AppleLocale", "\"\(locale)\""] + } + } + + class func setLaunchArguments(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + NSLog("CacheDirectory is not set - probably running on a physical device?") + return + } + + let path = cacheDirectory.appendingPathComponent("snapshot-launch_arguments.txt") + app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"] + + do { + let launchArguments = try String(contentsOf: path, encoding: String.Encoding.utf8) + let regex = try NSRegularExpression(pattern: "(\\\".+?\\\"|\\S+)", options: []) + let matches = regex.matches(in: launchArguments, options: [], range: NSRange(location: 0, length: launchArguments.count)) + let results = matches.map { result -> String in + (launchArguments as NSString).substring(with: result.range) + } + app.launchArguments += results + } catch { + NSLog("Couldn't detect/set launch_arguments...") + } + } + + open class func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { + if timeout > 0 { + waitForLoadingIndicatorToDisappear(within: timeout) + } + + NSLog("snapshot: \(name)") // more information about this, check out https://docs.fastlane.tools/actions/snapshot/#how-does-it-work + + if Snapshot.waitForAnimations { + sleep(1) // Waiting for the animation to be finished (kind of) + } + + #if os(OSX) + guard let app = self.app else { + NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + return + } + + app.typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: []) + #else + + guard self.app != nil else { + NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + return + } + + let screenshot = XCUIScreen.main.screenshot() + #if os(iOS) + let image = XCUIDevice.shared.orientation.isLandscape ? fixLandscapeOrientation(image: screenshot.image) : screenshot.image + #else + let image = screenshot.image + #endif + + guard var simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return } + + do { + // The simulator name contains "Clone X of " inside the screenshot file when running parallelized UI Tests on concurrent devices + let regex = try NSRegularExpression(pattern: "Clone [0-9]+ of ") + let range = NSRange(location: 0, length: simulator.count) + simulator = regex.stringByReplacingMatches(in: simulator, range: range, withTemplate: "") + + let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png") + #if swift(<5.0) + UIImagePNGRepresentation(image)?.write(to: path, options: .atomic) + #else + try image.pngData()?.write(to: path, options: .atomic) + #endif + } catch let error { + NSLog("Problem writing screenshot: \(name) to \(screenshotsDir)/\(simulator)-\(name).png") + NSLog(error.localizedDescription) + } + #endif + } + + class func fixLandscapeOrientation(image: UIImage) -> UIImage { + #if os(watchOS) + return image + #else + if #available(iOS 10.0, *) { + let format = UIGraphicsImageRendererFormat() + format.scale = image.scale + let renderer = UIGraphicsImageRenderer(size: image.size, format: format) + return renderer.image { context in + image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)) + } + } else { + return image + } + #endif + } + + class func waitForLoadingIndicatorToDisappear(within timeout: TimeInterval) { + #if os(tvOS) + return + #endif + + guard let app = self.app else { + NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + return + } + + let networkLoadingIndicator = app.otherElements.deviceStatusBars.networkLoadingIndicators.element + let networkLoadingIndicatorDisappeared = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == false"), object: networkLoadingIndicator) + _ = XCTWaiter.wait(for: [networkLoadingIndicatorDisappeared], timeout: timeout) + } + + class func getCacheDirectory() throws -> URL { + let cachePath = "Library/Caches/tools.fastlane" + // on OSX config is stored in /Users/<username>/Library + // and on iOS/tvOS/WatchOS it's in simulator's home dir + #if os(OSX) + let homeDir = URL(fileURLWithPath: NSHomeDirectory()) + return homeDir.appendingPathComponent(cachePath) + #elseif arch(i386) || arch(x86_64) || arch(arm64) + guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else { + throw SnapshotError.cannotFindSimulatorHomeDirectory + } + let homeDir = URL(fileURLWithPath: simulatorHostHome) + return homeDir.appendingPathComponent(cachePath) + #else + throw SnapshotError.cannotRunOnPhysicalDevice + #endif + } +} + +private extension XCUIElementAttributes { + var isNetworkLoadingIndicator: Bool { + if hasAllowListedIdentifier { return false } + + let hasOldLoadingIndicatorSize = frame.size == CGSize(width: 10, height: 20) + let hasNewLoadingIndicatorSize = frame.size.width.isBetween(46, and: 47) && frame.size.height.isBetween(2, and: 3) + + return hasOldLoadingIndicatorSize || hasNewLoadingIndicatorSize + } + + var hasAllowListedIdentifier: Bool { + let allowListedIdentifiers = ["GeofenceLocationTrackingOn", "StandardLocationTrackingOn"] + + return allowListedIdentifiers.contains(identifier) + } + + func isStatusBar(_ deviceWidth: CGFloat) -> Bool { + if elementType == .statusBar { return true } + guard frame.origin == .zero else { return false } + + let oldStatusBarSize = CGSize(width: deviceWidth, height: 20) + let newStatusBarSize = CGSize(width: deviceWidth, height: 44) + + return [oldStatusBarSize, newStatusBarSize].contains(frame.size) + } +} + +private extension XCUIElementQuery { + var networkLoadingIndicators: XCUIElementQuery { + let isNetworkLoadingIndicator = NSPredicate { (evaluatedObject, _) in + guard let element = evaluatedObject as? XCUIElementAttributes else { return false } + + return element.isNetworkLoadingIndicator + } + + return self.containing(isNetworkLoadingIndicator) + } + + var deviceStatusBars: XCUIElementQuery { + guard let app = Snapshot.app else { + fatalError("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + } + + let deviceWidth = app.windows.firstMatch.frame.width + + let isStatusBar = NSPredicate { (evaluatedObject, _) in + guard let element = evaluatedObject as? XCUIElementAttributes else { return false } + + return element.isStatusBar(deviceWidth) + } + + return self.containing(isStatusBar) + } +} + +private extension CGFloat { + func isBetween(_ numberA: CGFloat, and numberB: CGFloat) -> Bool { + return numberA...numberB ~= self + } +} + +// Please don't remove the lines below +// They are used to detect outdated configuration files +// SnapshotHelperVersion [1.27]
--- a/Simoleon/Helpers/SubscribeButton.swift Wed Aug 25 11:00:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,105 +0,0 @@ -// -// SubscribeButton.swift -// Simoleon -// -// Created by Dennis Concepción Martín on 22/07/2021. -// - -import SwiftUI -import Purchases - -struct SubscribeButton: View { - @Binding var showingSubscriptionPaywall: Bool - - @State private var price = "" - @State private var alertTitle = "" - @State private var alertMessage = "" - @State private var showingAlert = false - @State private var showingPrice = false - - var body: some View { - Button(action: purchaseMonthlySubscription) { - RoundedRectangle(cornerRadius: 15) - .frame(height: 60) - .overlay( - VStack { - if showingPrice { - Text("Subscribe for \(price) / month") - .foregroundColor(.white) - .fontWeight(.semibold) - } else { - ProgressView() - } - } - ) - } - .onAppear(perform: fetchMonthlySubscription) - .alert(isPresented: $showingAlert) { - Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("Ok"))) - } - } - - private func fetchMonthlySubscription() { - Purchases.shared.offerings { (offerings, error) in - if let product = offerings?.current?.monthly?.product { - price = formatCurrency(product.priceLocale, product.price) - showingPrice = true - } - - if let error = error as NSError? { - alertTitle = error.localizedDescription - alertMessage = error.localizedFailureReason ?? "" - price = "-" - showingPrice = true - showingAlert = true - } - } - } - - private func purchaseMonthlySubscription() { - showingPrice = false - - Purchases.shared.offerings { (offerings, error) in - if let package = offerings?.current?.monthly { - - Purchases.shared.purchasePackage(package) { (transaction, purchaserInfo, error, userCancelled) in - if purchaserInfo?.entitlements["all"]?.isActive == true { - showingPrice = true - showingSubscriptionPaywall = false - } - - if let error = error as NSError? { - alertTitle = error.localizedDescription - alertMessage = error.localizedFailureReason ?? "" - showingPrice = true - showingAlert = true - } - } - - if let error = error as NSError? { - alertTitle = error.localizedDescription - alertMessage = error.localizedFailureReason ?? "" - showingPrice = true - showingAlert = true - } - } - } - } - - private func formatCurrency(_ locale: Locale, _ amount: NSDecimalNumber) -> String { - let formatter = NumberFormatter() - formatter.locale = locale - formatter.numberStyle = .currency - - // It won't fail. Check unit test - let formattedAmount = formatter.string(from: amount as NSNumber)! - - return formattedAmount - } -} - -struct SubscribeButton_Previews: PreviewProvider { - static var previews: some View { - SubscribeButton(showingSubscriptionPaywall: .constant(true)) - } -}
--- a/Simoleon/Helpers/SubscriptionFeature.swift Wed Aug 25 11:00:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,41 +0,0 @@ -// -// SubscriptionFeature.swift -// Simoleon -// -// Created by Dennis Concepción Martín on 26/07/2021. -// - -import SwiftUI - -struct SubscriptionFeature: View { - var symbol: String - var colour: Color - var title: LocalizedStringKey - var description: LocalizedStringKey - - var body: some View { - HStack(alignment:.top) { - Image(systemName: symbol) - .foregroundColor(colour) - .font(.title) - - VStack(alignment: .leading) { - Text(title) - .font(.headline) - - Text(description) - } - } - } -} - -struct SubscriptionFeature_Previews: PreviewProvider { - static var previews: some View { - SubscriptionFeature( - symbol: "star.circle.fill", - colour: Color(.systemYellow), - title: "Favorite Currencies", - description: "Save your favorite currencies to access them quickly." - ) - } -}
--- a/Simoleon/Jobs/CurrenciesController.swift Wed Aug 25 11:00:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,54 +0,0 @@ -// -// GetCompatibleCurrencies.swift -// Simoleon -// -// Created by Dennis Concepción Martín on 24/8/21. -// - -import Foundation - -class CurrenciesController { - let fileController = FileController() - - func get(currenciesCompatibleWith currencySymbol: String?, currencies: Bool?) -> [String] { - // If currencies not false -> return all currencies - guard currencies == false else { return allCurrencies() } - - // This block won't be executed if the previous check fails - return compatibleCurrencies(with: currencySymbol!) - } - - /* - * Input all currencies supported by vendor - * Return individual currency symbols without duplicates - */ - private func allCurrencies() -> [String] { - let currencyPairsSupported: [String] = try! fileController.read(json: "CurrencyPairsSupported.json") - - var currencies = Set<String>() - for currencyPairSupported in currencyPairsSupported { - let currency = currencyPairSupported.components(separatedBy: "/")[0] - currencies.insert(currency) - } - - return Array(currencies) - } - - /* - * Given the first symbol of the currency pair - * Return all compatible symbols - */ - private func compatibleCurrencies(with currencySymbol: String) -> [String] { - let currencyPairsSupported: [String] = try! fileController.read(json: "CurrencyPairsSupported.json") - - var currencies = [String]() - for currencyPairSupported in currencyPairsSupported { - if currencyPairSupported.hasPrefix(currencySymbol) { - let compatibleCurrency = currencyPairSupported.components(separatedBy: "/")[1] - currencies.append(compatibleCurrency) - } - } - - return currencies - } -}
--- a/Simoleon/Jobs/FileController.swift Wed Aug 25 11:00:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,44 +0,0 @@ -// -// ReadConfig.swift -// Simoleon -// -// Created by Dennis Concepción Martín on 20/07/2021. -// - -import Foundation - -class FileController { - - /* - Read configuration variables from Config.xconfig - */ - func readConfigVariable(withKey: String) -> String? { - return (Bundle.main.infoDictionary?[withKey] as? String)? - .replacingOccurrences(of: "\\", with: "") - } - - /* - Decode and read json file - */ - func read<T: Decodable>(json filename: String) throws -> T { - let data: Data - - guard let file = Bundle.main.url(forResource: filename, withExtension: nil) - else { - throw ErrorHandling.Json.fileMissing - } - - do { - data = try Data(contentsOf: file) - } catch { - throw ErrorHandling.Json.loadFailed(cause: error.localizedDescription) - } - - do { - let decoder = JSONDecoder() - return try decoder.decode(T.self, from: data) - } catch { - throw ErrorHandling.Json.parseFailed(cause: error.localizedDescription) - } - } -}
--- a/Simoleon/Jobs/HapticsController.swift Wed Aug 25 11:00:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,19 +0,0 @@ -// -// SimpleSuccess.swift -// Simoleon -// -// Created by Dennis Concepción Martín on 20/07/2021. -// - -import SwiftUI - -class HapticsController { - - /* - Default haptic for success action - */ - func simpleSuccess() { - let generator = UINotificationFeedbackGenerator() - generator.notificationOccurred(.success) - } -}
--- a/Simoleon/Jobs/NetworkController.swift Wed Aug 25 11:00:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,43 +0,0 @@ -// -// Request.swift -// Simoleon -// -// Created by Dennis Concepción Martín on 20/07/2021. -// - -import Foundation - -class NetworkController { - - /* - Get http response and decode it with specified model - */ - func httpRequest<T: Decodable>(url: String, model: T.Type, completion: @escaping (_ result: T) -> Void) { - - // We take some model data T.Type - guard let url = URL(string: url) else { - print("Invalid URL") - return - } - - let request = URLRequest(url: url) - URLSession.shared.dataTask(with: request) { data, response, error in - if let data = data { - do { - // Decode response with the model passed - let decodedResponse = try JSONDecoder().decode(model, from: data) - DispatchQueue.main.async { - completion(decodedResponse) - } - return - } catch { - // Return error regarding the escaping code - print(error) - } - } - // Error with the request - print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")") - } - .resume() - } -}
--- a/Simoleon/Models/CurrencyDetailsModel.swift Wed Aug 25 11:00:21 2021 +0100 +++ b/Simoleon/Models/CurrencyDetailsModel.swift Sat Aug 28 11:15:25 2021 +0100 @@ -7,7 +7,8 @@ import Foundation -struct CurrencyDetailsModel: Codable { +struct CurrencyModel: Codable { + var symbol: String var name: String var flag: String var isCrypto: Bool
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/Models/CurrencyPairModel.swift Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,18 @@ +// +// CurrencyPairModel.swift +// Simoleon +// +// Created by Dennis Concepción Martín on 26/8/21. +// + +import Foundation + +struct CurrencyPairModel { + /* + Forex pair -> XXX/YYY + Where XXX is the base currency, and YYY the quote currency + */ + + var baseSymbol: String + var quoteSymbol: String +}
--- a/Simoleon/Models/DefaultCurrency+CoreDataClass.swift Wed Aug 25 11:00:21 2021 +0100 +++ b/Simoleon/Models/DefaultCurrency+CoreDataClass.swift Sat Aug 28 11:15:25 2021 +0100 @@ -2,7 +2,7 @@ // DefaultCurrency+CoreDataClass.swift // Simoleon // -// Created by Dennis Concepción Martín on 21/07/2021. +// Created by Dennis Concepción Martín on 24/8/21. // // @@ -11,5 +11,5 @@ @objc(DefaultCurrency) public class DefaultCurrency: NSManagedObject { - + }
--- a/Simoleon/Models/DefaultCurrency+CoreDataProperties.swift Wed Aug 25 11:00:21 2021 +0100 +++ b/Simoleon/Models/DefaultCurrency+CoreDataProperties.swift Sat Aug 28 11:15:25 2021 +0100 @@ -2,7 +2,7 @@ // DefaultCurrency+CoreDataProperties.swift // Simoleon // -// Created by Dennis Concepción Martín on 21/07/2021. +// Created by Dennis Concepción Martín on 24/8/21. // // @@ -11,11 +11,16 @@ extension DefaultCurrency { - + @nonobjc public class func fetchRequest() -> NSFetchRequest<DefaultCurrency> { return NSFetchRequest<DefaultCurrency>(entityName: "DefaultCurrency") } - - @NSManaged public var pair: String? - + + @NSManaged public var firstSymbol: String + @NSManaged public var secondSymbol: String + } + +extension DefaultCurrency : Identifiable { + +}
--- a/Simoleon/Models/Favorite+CoreDataClass.swift Wed Aug 25 11:00:21 2021 +0100 +++ b/Simoleon/Models/Favorite+CoreDataClass.swift Sat Aug 28 11:15:25 2021 +0100 @@ -2,7 +2,7 @@ // Favorite+CoreDataClass.swift // Simoleon // -// Created by Dennis Concepción Martín on 19/07/2021. +// Created by Dennis Concepción Martín on 24/8/21. // // @@ -11,5 +11,5 @@ @objc(Favorite) public class Favorite: NSManagedObject { - + }
--- a/Simoleon/Models/Favorite+CoreDataProperties.swift Wed Aug 25 11:00:21 2021 +0100 +++ b/Simoleon/Models/Favorite+CoreDataProperties.swift Sat Aug 28 11:15:25 2021 +0100 @@ -2,7 +2,7 @@ // Favorite+CoreDataProperties.swift // Simoleon // -// Created by Dennis Concepción Martín on 19/07/2021. +// Created by Dennis Concepción Martín on 24/8/21. // // @@ -11,15 +11,15 @@ extension Favorite { - + @nonobjc public class func fetchRequest() -> NSFetchRequest<Favorite> { return NSFetchRequest<Favorite>(entityName: "Favorite") } - + @NSManaged public var currencyPair: String - + } extension Favorite : Identifiable { - + }
--- a/Simoleon/Persistence.swift Wed Aug 25 11:00:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,64 +0,0 @@ -// -// Persistence.swift -// Simoleon -// -// Created by Dennis Concepción Martín on 08/07/2021. -// - -import CoreData - -struct PersistenceController { - static let shared = PersistenceController() - - static var preview: PersistenceController = { - let result = PersistenceController(inMemory: true) - let viewContext = result.container.viewContext - - for _ in 0..<10 { - let favorite = Favorite(context: viewContext) - favorite.currencyPair = "USD/GBP" - } - do { - try viewContext.save() - } catch { - /* - Replace this implementation with code to handle the error appropriately. - fatalError() causes the application to generate a crash log and terminate. - You should not use this function in a shipping application, although it may be useful during development. - */ - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - return result - }() - - let container: NSPersistentCloudKitContainer - - init(inMemory: Bool = false) { - container = NSPersistentCloudKitContainer(name: "Simoleon") - container.viewContext.automaticallyMergesChangesFromParent = true - container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy - if inMemory { - container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") - } - container.loadPersistentStores(completionHandler: { (storeDescription, error) in - if let error = error as NSError? { - /* - Replace this implementation with code to handle the error appropriately. - fatalError() causes the application to generate a crash log and terminate. - You should not use this function in a shipping application, although it may be useful during development. - */ - - /* - Typical reasons for an error here include: - * The parent directory does not exist, cannot be created, or disallows writing. - * The persistent store is not accessible, due to permissions or data protection when the device is locked. - * The device is out of space. - * The store could not be migrated to the current model version. - Check the error message to determine what the actual problem was. - */ - fatalError("Unresolved error \(error), \(error.userInfo)") - } - }) - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/Resources/Currencies.json Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,1374 @@ +{ + "AED": + { + "symbol": "AED", + "name": "United Arab Emirates Dirham", + "flag": "AE", + "isCrypto": false + }, + "AFN": + { + "symbol": "AFN", + "name": "Afghan Afghani", + "flag": "AF", + "isCrypto": false + }, + "ALL": + { + "symbol": "ALL", + "name": "Albanian Lek", + "flag": "AL", + "isCrypto": false + }, + "AMD": + { + "symbol": "AMD", + "name": "Armenian Dram", + "flag": "AM", + "isCrypto": false + }, + "ANG": + { + "symbol": "ANG", + "name": "Netherlands Antillean Guilder", + "flag": "CW", + "isCrypto": false + }, + "AOA": + { + "symbol": "AOA", + "name": "Angolan Kwanza", + "flag": "AO", + "isCrypto": false + }, + "ARE": + { + "symbol": "ARE", + "name": "Areg", + "flag": "AR", + "isCrypto": false + }, + "ARS": + { + "symbol": "ARS", + "name": "Argentine Peso", + "flag": "AR", + "isCrypto": false + }, + "AUD": + { + "symbol": "AUD", + "name": "Australian Dollar", + "flag": "AU", + "isCrypto": false + }, + "AUN": + { + "symbol": "AUN", + "name": "Australian Nugget", + "flag": "AU", + "isCrypto": false + }, + "AWG": + { + "symbol": "AWG", + "name": "Aruban Florin", + "flag": "AW", + "isCrypto": false + }, + "BAM": + { + "symbol": "BAM", + "name": "Bosnia And Herzegovina Convertible Mark", + "flag": "BA", + "isCrypto": false + }, + "BBD": + { + "symbol": "BBD", + "name": "Barbados Dollar", + "flag": "BB", + "isCrypto": false + }, + "BDT": + { + "symbol": "BDT", + "name": "Bangladeshi Taka", + "flag": "BD", + "isCrypto": false + }, + "BGN": + { + "symbol": "BGN", + "name": "Bulgarian Lev", + "flag": "BG", + "isCrypto": false + }, + "BHD": + { + "symbol": "BHD", + "name": "Bahraini Dinar", + "flag": "BH", + "isCrypto": false + }, + "BIF": + { + "symbol": "BIF", + "name": "Burundian Franc", + "flag": "BI", + "isCrypto": false + }, + "BMD": + { + "symbol": "BMD", + "name": "Bermudian Dollar", + "flag": "BM", + "isCrypto": false + }, + "BND": + { + "symbol": "BND", + "name": "Brunei Dollar", + "flag": "BN", + "isCrypto": false + }, + "BOB": + { + "symbol": "BOB", + "name": "Boliviano", + "flag": "BO", + "isCrypto": false + }, + "BRI": + { + "symbol": "BRI", + "name": "Britannia", + "flag": "BR", + "isCrypto": false + }, + "BRL": + { + "symbol": "BRL", + "name": "Brazilian Real", + "flag": "BR", + "isCrypto": false + }, + "BSD": + { + "symbol": "BSD", + "name": "Bahamian Dollar", + "flag": "BS", + "isCrypto": false + }, + "BTN": + { + "symbol": "BTN", + "name": "Bhutanese Ngultrum", + "flag": "BT", + "isCrypto": false + }, + "BWP": + { + "symbol": "BWP", + "name": "Botswana Pula", + "flag": "BW", + "isCrypto": false + }, + "BYN": + { + "symbol": "BYN", + "name": "Belarusian Ruble", + "flag": "BY", + "isCrypto": false + }, + "BZD": + { + "symbol": "BZD", + "name": "Belize Dollar", + "flag": "BZ", + "isCrypto": false + }, + "CAD": + { + "symbol": "CAD", + "name": "Canadian Dollar", + "flag": "CA", + "isCrypto": false + }, + "CDF": + { + "symbol": "CDF", + "name": "Congolese Franc", + "flag": "CD", + "isCrypto": false + }, + "CHF": + { + "symbol": "CHF", + "name": "Swiss Franc", + "flag": "CH", + "isCrypto": false + }, + "CLF": + { + "symbol": "CLF", + "name": "Chilean Unit Of Account", + "flag": "CL", + "isCrypto": false + }, + "CLP": + { + "symbol": "CLP", + "name": "Chilean Peso", + "flag": "CL", + "isCrypto": false + }, + "CNH": + { + "symbol": "CNH", + "name": "Chinese Yuan", + "flag": "CN", + "isCrypto": false + }, + "CNY": + { + "symbol": "CNY", + "name": "Chinese Yuan", + "flag": "CN", + "isCrypto": false + }, + "COP": + { + "symbol": "COP", + "name": "Colombian Peso", + "flag": "CO", + "isCrypto": false + }, + "CRC": + { + "symbol": "CRC", + "name": "Costa Rican Colon", + "flag": "CR", + "isCrypto": false + }, + "CUP": + { + "symbol": "CUP", + "name": "Cuban Peso", + "flag": "CU", + "isCrypto": false + }, + "CVE": + { + "symbol": "CVE", + "name": "Cape Verde Escudo", + "flag": "CV", + "isCrypto": false + }, + "CYP": + { + "symbol": "CYP", + "name": "Cypriot Pound", + "flag": "CY", + "isCrypto": false + }, + "CZK": + { + "symbol": "CZK", + "name": "Czech Koruna", + "flag": "CZ", + "isCrypto": false + }, + "DJF": + { + "symbol": "DJF", + "name": "Djiboutian Franc", + "flag": "DJ", + "isCrypto": false + }, + "DKK": + { + "symbol": "DKK", + "name": "Danish Krone", + "flag": "DK", + "isCrypto": false + }, + "DOE": + { + "symbol": "DOE", + "name": "Double Eagle", + "flag": "DO", + "isCrypto": false + }, + "DOP": + { + "symbol": "DOP", + "name": "Dominican Peso", + "flag": "DO", + "isCrypto": false + }, + "DZD": + { + "symbol": "DZD", + "name": "Algerian Dinar", + "flag": "DZ", + "isCrypto": false + }, + "EGP": + { + "symbol": "EGP", + "name": "Egyptian Pound", + "flag": "EG", + "isCrypto": false + }, + "ETB": + { + "symbol": "ETB", + "name": "Ethiopian Birr", + "flag": "ET", + "isCrypto": false + }, + "EUR": + { + "symbol": "EUR", + "name": "Euro", + "flag": "EU", + "isCrypto": false + }, + "FJD": + { + "symbol": "FJD", + "name": "Fiji Dollar", + "flag": "FJ", + "isCrypto": false + }, + "FRN": + { + "symbol": "FRN", + "name": "French Napoleon", + "flag": "FR", + "isCrypto": false + }, + "GBP": + { + "symbol": "GBP", + "name": "Pound Sterling", + "flag": "GB", + "isCrypto": false + }, + "GEL": + { + "symbol": "GEL", + "name": "Georgian Lari", + "flag": "GE", + "isCrypto": false + }, + "GHS": + { + "symbol": "GHS", + "name": "Ghanaian Cedi", + "flag": "GH", + "isCrypto": false + }, + "GMD": + { + "symbol": "GMD", + "name": "Gambian Dalasi", + "flag": "GM", + "isCrypto": false + }, + "GNF": + { + "symbol": "GNF", + "name": "Guinean Franc", + "flag": "GN", + "isCrypto": false + }, + "GTQ": + { + "symbol": "GTQ", + "name": "Guatemalan Quetzal", + "flag": "GT", + "isCrypto": false + }, + "GYD": + { + "symbol": "GYD", + "name": "Guyanese Dollar", + "flag": "GY", + "isCrypto": false + }, + "HKD": + { + "symbol": "HKD", + "name": "Hong Kong Dollar", + "flag": "HK", + "isCrypto": false + }, + "HNL": + { + "symbol": "HNL", + "name": "Honduran Lempira", + "flag": "HN", + "isCrypto": false + }, + "HRK": + { + "symbol": "HRK", + "name": "Croatian Kuna", + "flag": "HR", + "isCrypto": false + }, + "HTG": + { + "symbol": "HTG", + "name": "Haitian Gourde", + "flag": "HT", + "isCrypto": false + }, + "HUF": + { + "symbol": "HUF", + "name": "Hungarian Forint", + "flag": "HU", + "isCrypto": false + }, + "IDR": + { + "symbol": "IDR", + "name": "Indonesian Rupiah", + "flag": "ID", + "isCrypto": false + }, + "ILS": + { + "symbol": "ILS", + "name": "Israeli New Shekel", + "flag": "IL", + "isCrypto": false + }, + "INR": + { + "symbol": "INR", + "name": "Indian Rupee", + "flag": "IN", + "isCrypto": false + }, + "IQD": + { + "symbol": "IQD", + "name": "Iraqi Dinar", + "flag": "IQ", + "isCrypto": false + }, + "IRR": + { + "symbol": "IRR", + "name": "Iranian Rial", + "flag": "IR", + "isCrypto": false + }, + "ISK": + { + "symbol": "ISK", + "name": "Icelandic Króna", + "flag": "IS", + "isCrypto": false + }, + "JMD": + { + "symbol": "JMD", + "name": "Jamaican Dollar", + "flag": "JM", + "isCrypto": false + }, + "JOD": + { + "symbol": "JOD", + "name": "Jordanian Dinar", + "flag": "JO", + "isCrypto": false + }, + "JPY": + { + "symbol": "JPY", + "name": "Japanese Yen", + "flag": "JP", + "isCrypto": false + }, + "KES": + { + "symbol": "KES", + "name": "Kenyan Shilling", + "flag": "KE", + "isCrypto": false + }, + "KHR": + { + "symbol": "KHR", + "name": "Cambodian Riel", + "flag": "KH", + "isCrypto": false + }, + "KMF": + { + "symbol": "KMF", + "name": "Comoro Franc", + "flag": "KM", + "isCrypto": false + }, + "KRU": + { + "symbol": "KRU", + "name": "South African Krugerrand", + "flag": "KR", + "isCrypto": false + }, + "KRW": + { + "symbol": "KRW", + "name": "South Korean Won", + "flag": "KR", + "isCrypto": false + }, + "KWD": + { + "symbol": "KWD", + "name": "Kuwaiti Dinar", + "flag": "KW", + "isCrypto": false + }, + "KYD": + { + "symbol": "KYD", + "name": "Cayman Islands Dollar", + "flag": "KY", + "isCrypto": false + }, + "KZT": + { + "symbol": "KZT", + "name": "Kazakhstani Tenge", + "flag": "KZ", + "isCrypto": false + }, + "LAK": + { + "symbol": "LAK", + "name": "Lao Kip", + "flag": "LA", + "isCrypto": false + }, + "LBP": + { + "symbol": "LBP", + "name": "Lebanese Pound", + "flag": "LB", + "isCrypto": false + }, + "LFX": + { + "symbol": "LFX", + "name": "Khazanah Sukuk", + "flag": "MY", + "isCrypto": false + }, + "LKR": + { + "symbol": "LKR", + "name": "Sri Lankan Rupee", + "flag": "LK", + "isCrypto": false + }, + "LRD": + { + "symbol": "LRD", + "name": "Liberian Dollar", + "flag": "LR", + "isCrypto": false + }, + "LSL": + { + "symbol": "LSL", + "name": "Lesotho Loti", + "flag": "LS", + "isCrypto": false + }, + "LTL": + { + "symbol": "LTL", + "name": "Lithuanian Litas", + "flag": "LT", + "isCrypto": false + }, + "LYD": + { + "symbol": "LYD", + "name": "Libyan Dinar", + "flag": "LY", + "isCrypto": false + }, + "M5P": + { + "symbol": "M5P", + "name": "Mexican 50 Peso", + "flag": "MX", + "isCrypto": false + }, + "MAD": + { + "symbol": "MAD", + "name": "Moroccan Dirham", + "flag": "MA", + "isCrypto": false + }, + "MAL": + { + "symbol": "MAL", + "name": "Maple Leaf", + "flag": "MA", + "isCrypto": false + }, + "MDL": + { + "symbol": "MDL", + "name": "Moldovan Leu", + "flag": "MD", + "isCrypto": false + }, + "MGA": + { + "symbol": "MGA", + "name": "Malagasy Ariary", + "flag": "MG", + "isCrypto": false + }, + "MKD": + { + "symbol": "MKD", + "name": "Macedonian Denar", + "flag": "MK", + "isCrypto": false + }, + "MMK": + { + "symbol": "MMK", + "name": "Myanma Kyat", + "flag": "MM", + "isCrypto": false + }, + "MOP": + { + "symbol": "MOP", + "name": "Macanese Pataca", + "flag": "MO", + "isCrypto": false + }, + "MRU": + { + "symbol": "MRU", + "name": "Mauritanian Ouguiya", + "flag": "MR", + "isCrypto": false + }, + "MTL": + { + "symbol": "MTL", + "name": "Maltese Lira", + "flag": "MT", + "isCrypto": false + }, + "MUR": + { + "symbol": "MUR", + "name": "Mauritian Rupee", + "flag": "MU", + "isCrypto": false + }, + "MVR": + { + "symbol": "MVR", + "name": "Maldivian Rufiyaa", + "flag": "MV", + "isCrypto": false + }, + "MWK": + { + "symbol": "MWK", + "name": "Malawian Kwacha", + "flag": "MW", + "isCrypto": false + }, + "MXN": + { + "symbol": "MXN", + "name": "Mexican Peso", + "flag": "MX", + "isCrypto": false + }, + "MYR": + { + "symbol": "MYR", + "name": "Malaysian Ringgit", + "flag": "MY", + "isCrypto": false + }, + "MZN": + { + "symbol": "MZN", + "name": "Mozambican Metical", + "flag": "MZ", + "isCrypto": false + }, + "NAD": + { + "symbol": "NAD", + "name": "Namibian Dollar", + "flag": "NA", + "isCrypto": false + }, + "NBL": + { + "symbol": "NBL", + "name": "Isle Of Man Noble", + "flag": "IM", + "isCrypto": false + }, + "NGN": + { + "symbol": "NGN", + "name": "Nigerian Naira", + "flag": "NG", + "isCrypto": false + }, + "NIO": + { + "symbol": "NIO", + "name": "Nicaraguan Córdoba", + "flag": "NI", + "isCrypto": false + }, + "NOK": + { + "symbol": "NOK", + "name": "Norwegian Krone", + "flag": "NO", + "isCrypto": false + }, + "NPR": + { + "symbol": "NPR", + "name": "Nepalese Rupee", + "flag": "NP", + "isCrypto": false + }, + "NSO": + { + "symbol": "NSO", + "name": "New Sovereign", + "flag": "GB", + "isCrypto": false + }, + "NZD": + { + "symbol": "NZD", + "name": "New Zealand Dollar", + "flag": "NZ", + "isCrypto": false + }, + "OMR": + { + "symbol": "OMR", + "name": "Omani Rial", + "flag": "OM", + "isCrypto": false + }, + "OSO": + { + "symbol": "OSO", + "name": "Old Sovereign", + "flag": "GB", + "isCrypto": false + }, + "PAB": + { + "symbol": "PAB", + "name": "Panamanian Balboa", + "flag": "PA", + "isCrypto": false + }, + "PEN": + { + "symbol": "PEN", + "name": "Peruvian Nuevo Sol", + "flag": "PE", + "isCrypto": false + }, + "PGK": + { + "symbol": "PGK", + "name": "Papua New Guinean Kina", + "flag": "PG", + "isCrypto": false + }, + "PHP": + { + "symbol": "PHP", + "name": "Philippine Peso", + "flag": "PH", + "isCrypto": false + }, + "PKR": + { + "symbol": "PKR", + "name": "Pakistani Rupee", + "flag": "PK", + "isCrypto": false + }, + "PLN": + { + "symbol": "PLN", + "name": "Polish Zloty", + "flag": "PL", + "isCrypto": false + }, + "PYG": + { + "symbol": "PYG", + "name": "Paraguayan Guaraní", + "flag": "PY", + "isCrypto": false + }, + "QAR": + { + "symbol": "QAR", + "name": "Qatari Riyal", + "flag": "QA", + "isCrypto": false + }, + "RON": + { + "symbol": "RON", + "name": "Romanian New Leu", + "flag": "RO", + "isCrypto": false + }, + "RSD": + { + "symbol": "RSD", + "name": "Serbian Dinar", + "flag": "RS", + "isCrypto": false + }, + "RUB": + { + "symbol": "RUB", + "name": "Russian Rouble", + "flag": "RU", + "isCrypto": false + }, + "RWF": + { + "symbol": "RWF", + "name": "Rwandan Franc", + "flag": "RW", + "isCrypto": false + }, + "SAR": + { + "symbol": "SAR", + "name": "Saudi Riyal", + "flag": "SA", + "isCrypto": false + }, + "SBD": + { + "symbol": "SBD", + "name": "Solomon Islands Dollar", + "flag": "SB", + "isCrypto": false + }, + "SCR": + { + "symbol": "SCR", + "name": "Seychelles Rupee", + "flag": "SC", + "isCrypto": false + }, + "SDG": + { + "symbol": "SDG", + "name": "Sudanese Pound", + "flag": "SD", + "isCrypto": false + }, + "SEK": + { + "symbol": "SEK", + "name": "Swedish Krona", + "flag": "SE", + "isCrypto": false + }, + "SGD": + { + "symbol": "SGD", + "name": "Singapore Dollar", + "flag": "SG", + "isCrypto": false + }, + "SHP": + { + "symbol": "SHP", + "name": "Saint Helena Pound", + "flag": "SH", + "isCrypto": false + }, + "SLL": + { + "symbol": "SLL", + "name": "Sierra Leonean Leone", + "flag": "SL", + "isCrypto": false + }, + "SOS": + { + "symbol": "SOS", + "name": "Somali Shilling", + "flag": "SO", + "isCrypto": false + }, + "SRD": + { + "symbol": "SRD", + "name": "Surinamese Dollar", + "flag": "SR", + "isCrypto": false + }, + "STN": + { + "symbol": "STN", + "name": "Sao Tomean Dobra", + "flag": "ST", + "isCrypto": false + }, + "SVC": + { + "symbol": "SVC", + "name": "Salvadoran Colón", + "flag": "SV", + "isCrypto": false + }, + "SZL": + { + "symbol": "SZL", + "name": "Swazi Lilangeni", + "flag": "SZ", + "isCrypto": false + }, + "THB": + { + "symbol": "THB", + "name": "Thai Baht", + "flag": "TH", + "isCrypto": false + }, + "TJS": + { + "symbol": "TJS", + "name": "Tajikistani Somoni", + "flag": "TJ", + "isCrypto": false + }, + "TMT": + { + "symbol": "TMT", + "name": "Turkmenistani Manat", + "flag": "TM", + "isCrypto": false + }, + "TND": + { + "symbol": "TND", + "name": "Tunisian Dinar", + "flag": "TN", + "isCrypto": false + }, + "TOP": + { + "symbol": "TOP", + "name": "Tongan PaʻAnga", + "flag": "TO", + "isCrypto": false + }, + "TRY": + { + "symbol": "TRY", + "name": "Turkish Lira", + "flag": "TR", + "isCrypto": false + }, + "TTD": + { + "symbol": "TTD", + "name": "Trinidad And Tobago Dollar", + "flag": "TT", + "isCrypto": false + }, + "TWD": + { + "symbol": "TWD", + "name": "New Taiwan Dollar", + "flag": "TW", + "isCrypto": false + }, + "TZS": + { + "symbol": "TZS", + "name": "Tanzanian Shilling", + "flag": "TZ", + "isCrypto": false + }, + "UAH": + { + "symbol": "UAH", + "name": "Ukrainian Hryvnia", + "flag": "UA", + "isCrypto": false + }, + "UGX": + { + "symbol": "UGX", + "name": "Ugandan Shilling", + "flag": "UG", + "isCrypto": false + }, + "USD": + { + "symbol": "USD", + "name": "United States Dollar", + "flag": "US", + "isCrypto": false + }, + "UYU": + { + "symbol": "UYU", + "name": "Uruguayan Peso", + "flag": "UY", + "isCrypto": false + }, + "UZS": + { + "symbol": "UZS", + "name": "Uzbekistan Som", + "flag": "UZ", + "isCrypto": false + }, + "VES": + { + "symbol": "VES", + "name": "Venezuelan Bolivar Soberano", + "flag": "VE", + "isCrypto": false + }, + "VND": + { + "symbol": "VND", + "name": "Vietnamese Dong", + "flag": "VN", + "isCrypto": false + }, + "VRL": + { + "symbol": "VRL", + "name": "Vreneli 10F.", + "flag": "CH", + "isCrypto": false + }, + "VRN": + { + "symbol": "VRN", + "name": "Vreneli 20F", + "flag": "CH", + "isCrypto": false + }, + "XAG": + { + "symbol": "XAG", + "name": "Silver (One Troy Ounce)", + "flag": "XAG", + "isCrypto": false + }, + "XAGK": + { + "symbol": "XAGK", + "name": "Silver (Kg)", + "flag": "XAG", + "isCrypto": false + }, + "XAU": + { + "symbol": "XAU", + "name": "Gold (One Troy Ounce)", + "flag": "XAU", + "isCrypto": false + }, + "XAUK": + { + "symbol": "XAUK", + "name": "Gold (Kg)", + "flag": "XAU", + "isCrypto": false + }, + "XCD": + { + "symbol": "XCD", + "name": "East Caribbean Dollar", + "flag": "AI", + "isCrypto": false + }, + "XOF": + { + "symbol": "XOF", + "name": "Cfa Franc Bceao", + "flag": "SN", + "isCrypto": false + }, + "XPD": + { + "symbol": "XPD", + "name": "Palladium (One Troy Ounce)", + "flag": "XPD", + "isCrypto": false + }, + "XPDK": + { + "symbol": "XPDK", + "name": "Palladium (Kg)", + "flag": "XPD", + "isCrypto": false + }, + "XPF": + { + "symbol": "XPF", + "name": "Cfp Franc", + "flag": "PF", + "isCrypto": false + }, + "XPT": + { + "symbol": "XPT", + "name": "Platinum (One Troy Ounce)", + "flag": "XPT", + "isCrypto": false + }, + "XPTK": + { + "symbol": "XPTK", + "name": "Platinum (Kg)", + "flag": "XPT", + "isCrypto": false + }, + "YER": + { + "symbol": "YER", + "name": "Yemeni Rial", + "flag": "YE", + "isCrypto": false + }, + "ZAR": + { + "symbol": "ZAR", + "name": "South African Rand", + "flag": "ZA", + "isCrypto": false + }, + "ZMW": + { + "symbol": "ZMW", + "name": "Zambian Kwacha", + "flag": "ZM", + "isCrypto": false + }, + "ZWD": + { + "symbol": "ZWD", + "name": "Zimbabwe Dollar", + "flag": "ZW", + "isCrypto": false + }, + "DASH": + { + "symbol": "DASH", + "name": "Dash", + "flag": "DASH", + "isCrypto": true + }, + "BTC": + { + "symbol": "BTC", + "name": "Bitcoin", + "flag": "BTC", + "isCrypto": true + }, + "DSH": + { + "symbol": "DSH", + "name": "Dash Coin", + "flag": "DASH", + "isCrypto": true + }, + "LTC": + { + "symbol": "LTC", + "name": "Litecoin", + "flag": "LTC", + "isCrypto": true + }, + "ETH": + { + "symbol": "ETH", + "name": "Ethereum", + "flag": "ETH", + "isCrypto": true + }, + "BCH": + { + "symbol": "BCH", + "name": "Bitcoin Cash", + "flag": "BTC", + "isCrypto": true + }, + "XRP": + { + "symbol": "XRP", + "name": "Xrp Cryptocurrency", + "flag": "XRP", + "isCrypto": true + }, + "XLM": + { + "symbol": "XLM", + "name": "Stellar", + "flag": "XLM", + "isCrypto": true + }, + "ADA": + { + "symbol": "ADA", + "name": "Cardano", + "flag": "ADA", + "isCrypto": true + }, + "UKO": + { + "symbol": "UKO", + "name": "Brent Crude Oil", + "flag": "UKO", + "isCrypto": false + }, + "AAVE": + { + "symbol": "AAVE", + "name": "Aave Token", + "flag": "AAVE", + "isCrypto": true + }, + "UNI": + { + "symbol": "UNI", + "name": "Uniswap", + "flag": "UNI", + "isCrypto": true + }, + "LUNA": + { + "symbol": "LUNA", + "name": "Terra", + "flag": "LUNA", + "isCrypto": true + }, + "XMR": + { + "symbol": "XMR", + "name": "Monero", + "flag": "XMR", + "isCrypto": true + }, + "XDR": + { + "symbol": "XDR", + "name": "Special Drawing Rights", + "flag": "XDR", + "isCrypto": false + }, + "SOL": + { + "symbol": "SOL", + "name": "Solana", + "flag": "SOL", + "isCrypto": true + }, + "DOGE": + { + "symbol": "DOGE", + "name": "Dogecoin", + "flag": "DOGE", + "isCrypto": true + }, + "VET": + { + "symbol": "VET", + "name": "VeChain", + "flag": "VET", + "isCrypto": true + }, + "DOT": + { + "symbol": "DOT", + "name": "Polkadot", + "flag": "DOT", + "isCrypto": true + }, + "USDC": + { + "symbol": "USDC", + "name": "USD Coin", + "flag": "USDC", + "isCrypto": true + }, + "FIL": + { + "symbol": "FIL", + "name": "File Coin", + "flag": "FIL", + "isCrypto": true + }, + "LINK": + { + "symbol": "LINK", + "name": "Chainlink", + "flag": "LINK", + "isCrypto": true + }, + "MATIC": + { + "symbol": "MATIC", + "name": "Polygon", + "flag": "MATIC", + "isCrypto": true + }, + "THETA": + { + "symbol": "THETA", + "name": "Theta", + "flag": "THETA", + "isCrypto": true + }, + "BNB": + { + "symbol": "BNB", + "name": "Binance Coin", + "flag": "BNB", + "isCrypto": true + } +}
--- a/Simoleon/Resources/CurrencyDetails.json Wed Aug 25 11:00:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,982 +0,0 @@ -{ - "AED": { - "name": "United Arab Emirates Dirham", - "flag": "AE", - "isCrypto": false - }, - "AFN": { - "name": "Afghan Afghani", - "flag": "AF", - "isCrypto": false - }, - "ALL": { - "name": "Albanian Lek", - "flag": "AL", - "isCrypto": false - }, - "AMD": { - "name": "Armenian Dram", - "flag": "AM", - "isCrypto": false - }, - "ANG": { - "name": "Netherlands Antillean Guilder", - "flag": "CW", - "isCrypto": false - }, - "AOA": { - "name": "Angolan Kwanza", - "flag": "AO", - "isCrypto": false - }, - "ARE": { - "name": "Areg", - "flag": "AR", - "isCrypto": false - }, - "ARS": { - "name": "Argentine Peso", - "flag": "AR", - "isCrypto": false - }, - "AUD": { - "name": "Australian Dollar", - "flag": "AU", - "isCrypto": false - }, - "AUN": { - "name": "Australian Nugget", - "flag": "AU", - "isCrypto": false - }, - "AWG": { - "name": "Aruban Florin", - "flag": "AW", - "isCrypto": false - }, - "BAM": { - "name": "Bosnia And Herzegovina Convertible Mark", - "flag": "BA", - "isCrypto": false - }, - "BBD": { - "name": "Barbados Dollar", - "flag": "BB", - "isCrypto": false - }, - "BDT": { - "name": "Bangladeshi Taka", - "flag": "BD", - "isCrypto": false - }, - "BGN": { - "name": "Bulgarian Lev", - "flag": "BG", - "isCrypto": false - }, - "BHD": { - "name": "Bahraini Dinar", - "flag": "BH", - "isCrypto": false - }, - "BIF": { - "name": "Burundian Franc", - "flag": "BI", - "isCrypto": false - }, - "BMD": { - "name": "Bermudian Dollar", - "flag": "BM", - "isCrypto": false - }, - "BND": { - "name": "Brunei Dollar", - "flag": "BN", - "isCrypto": false - }, - "BOB": { - "name": "Boliviano", - "flag": "BO", - "isCrypto": false - }, - "BRI": { - "name": "Britannia", - "flag": "BR", - "isCrypto": false - }, - "BRL": { - "name": "Brazilian Real", - "flag": "BR", - "isCrypto": false - }, - "BSD": { - "name": "Bahamian Dollar", - "flag": "BS", - "isCrypto": false - }, - "BTN": { - "name": "Bhutanese Ngultrum", - "flag": "BT", - "isCrypto": false - }, - "BWP": { - "name": "Botswana Pula", - "flag": "BW", - "isCrypto": false - }, - "BYN": { - "name": "Belarusian Ruble", - "flag": "BY", - "isCrypto": false - }, - "BZD": { - "name": "Belize Dollar", - "flag": "BZ", - "isCrypto": false - }, - "CAD": { - "name": "Canadian Dollar", - "flag": "CA", - "isCrypto": false - }, - "CDF": { - "name": "Congolese Franc", - "flag": "CD", - "isCrypto": false - }, - "CHF": { - "name": "Swiss Franc", - "flag": "CH", - "isCrypto": false - }, - "CLF": { - "name": "Chilean Unit Of Account", - "flag": "CL", - "isCrypto": false - }, - "CLP": { - "name": "Chilean Peso", - "flag": "CL", - "isCrypto": false - }, - "CNH": { - "name": "Chinese Yuan", - "flag": "CN", - "isCrypto": false - }, - "CNY": { - "name": "Chinese Yuan", - "flag": "CN", - "isCrypto": false - }, - "COP": { - "name": "Colombian Peso", - "flag": "CO", - "isCrypto": false - }, - "CRC": { - "name": "Costa Rican Colon", - "flag": "CR", - "isCrypto": false - }, - "CUP": { - "name": "Cuban Peso", - "flag": "CU", - "isCrypto": false - }, - "CVE": { - "name": "Cape Verde Escudo", - "flag": "CV", - "isCrypto": false - }, - "CYP": { - "name": "Cypriot Pound", - "flag": "CY", - "isCrypto": false - }, - "CZK": { - "name": "Czech Koruna", - "flag": "CZ", - "isCrypto": false - }, - "DJF": { - "name": "Djiboutian Franc", - "flag": "DJ", - "isCrypto": false - }, - "DKK": { - "name": "Danish Krone", - "flag": "DK", - "isCrypto": false - }, - "DOE": { - "name": "Double Eagle", - "flag": "DO", - "isCrypto": false - }, - "DOP": { - "name": "Dominican Peso", - "flag": "DO", - "isCrypto": false - }, - "DZD": { - "name": "Algerian Dinar", - "flag": "DZ", - "isCrypto": false - }, - "EGP": { - "name": "Egyptian Pound", - "flag": "EG", - "isCrypto": false - }, - "ETB": { - "name": "Ethiopian Birr", - "flag": "ET", - "isCrypto": false - }, - "EUR": { - "name": "Euro", - "flag": "EU", - "isCrypto": false - }, - "FJD": { - "name": "Fiji Dollar", - "flag": "FJ", - "isCrypto": false - }, - "FRN": { - "name": "French Napoleon", - "flag": "FR", - "isCrypto": false - }, - "GBP": { - "name": "Pound Sterling", - "flag": "GB", - "isCrypto": false - }, - "GEL": { - "name": "Georgian Lari", - "flag": "GE", - "isCrypto": false - }, - "GHS": { - "name": "Ghanaian Cedi", - "flag": "GH", - "isCrypto": false - }, - "GMD": { - "name": "Gambian Dalasi", - "flag": "GM", - "isCrypto": false - }, - "GNF": { - "name": "Guinean Franc", - "flag": "GN", - "isCrypto": false - }, - "GTQ": { - "name": "Guatemalan Quetzal", - "flag": "GT", - "isCrypto": false - }, - "GYD": { - "name": "Guyanese Dollar", - "flag": "GY", - "isCrypto": false - }, - "HKD": { - "name": "Hong Kong Dollar", - "flag": "HK", - "isCrypto": false - }, - "HNL": { - "name": "Honduran Lempira", - "flag": "HN", - "isCrypto": false - }, - "HRK": { - "name": "Croatian Kuna", - "flag": "HR", - "isCrypto": false - }, - "HTG": { - "name": "Haitian Gourde", - "flag": "HT", - "isCrypto": false - }, - "HUF": { - "name": "Hungarian Forint", - "flag": "HU", - "isCrypto": false - }, - "IDR": { - "name": "Indonesian Rupiah", - "flag": "ID", - "isCrypto": false - }, - "ILS": { - "name": "Israeli New Shekel", - "flag": "IL", - "isCrypto": false - }, - "INR": { - "name": "Indian Rupee", - "flag": "IN", - "isCrypto": false - }, - "IQD": { - "name": "Iraqi Dinar", - "flag": "IQ", - "isCrypto": false - }, - "IRR": { - "name": "Iranian Rial", - "flag": "IR", - "isCrypto": false - }, - "ISK": { - "name": "Icelandic Króna", - "flag": "IS", - "isCrypto": false - }, - "JMD": { - "name": "Jamaican Dollar", - "flag": "JM", - "isCrypto": false - }, - "JOD": { - "name": "Jordanian Dinar", - "flag": "JO", - "isCrypto": false - }, - "JPY": { - "name": "Japanese Yen", - "flag": "JP", - "isCrypto": false - }, - "KES": { - "name": "Kenyan Shilling", - "flag": "KE", - "isCrypto": false - }, - "KHR": { - "name": "Cambodian Riel", - "flag": "KH", - "isCrypto": false - }, - "KMF": { - "name": "Comoro Franc", - "flag": "KM", - "isCrypto": false - }, - "KRU": { - "name": "South African Krugerrand", - "flag": "KR", - "isCrypto": false - }, - "KRW": { - "name": "South Korean Won", - "flag": "KR", - "isCrypto": false - }, - "KWD": { - "name": "Kuwaiti Dinar", - "flag": "KW", - "isCrypto": false - }, - "KYD": { - "name": "Cayman Islands Dollar", - "flag": "KY", - "isCrypto": false - }, - "KZT": { - "name": "Kazakhstani Tenge", - "flag": "KZ", - "isCrypto": false - }, - "LAK": { - "name": "Lao Kip", - "flag": "LA", - "isCrypto": false - }, - "LBP": { - "name": "Lebanese Pound", - "flag": "LB", - "isCrypto": false - }, - "LFX": { - "name": "Khazanah Sukuk", - "flag": "MY", - "isCrypto": false - }, - "LKR": { - "name": "Sri Lankan Rupee", - "flag": "LK", - "isCrypto": false - }, - "LRD": { - "name": "Liberian Dollar", - "flag": "LR", - "isCrypto": false - }, - "LSL": { - "name": "Lesotho Loti", - "flag": "LS", - "isCrypto": false - }, - "LTL": { - "name": "Lithuanian Litas", - "flag": "LT", - "isCrypto": false - }, - "LYD": { - "name": "Libyan Dinar", - "flag": "LY", - "isCrypto": false - }, - "M5P": { - "name": "Mexican 50 Peso", - "flag": "MX", - "isCrypto": false - }, - "MAD": { - "name": "Moroccan Dirham", - "flag": "MA", - "isCrypto": false - }, - "MAL": { - "name": "Maple Leaf", - "flag": "MA", - "isCrypto": false - }, - "MDL": { - "name": "Moldovan Leu", - "flag": "MD", - "isCrypto": false - }, - "MGA": { - "name": "Malagasy Ariary", - "flag": "MG", - "isCrypto": false - }, - "MKD": { - "name": "Macedonian Denar", - "flag": "MK", - "isCrypto": false - }, - "MMK": { - "name": "Myanma Kyat", - "flag": "MM", - "isCrypto": false - }, - "MOP": { - "name": "Macanese Pataca", - "flag": "MO", - "isCrypto": false - }, - "MRU": { - "name": "Mauritanian Ouguiya", - "flag": "MR", - "isCrypto": false - }, - "MTL": { - "name": "Maltese Lira", - "flag": "MT", - "isCrypto": false - }, - "MUR": { - "name": "Mauritian Rupee", - "flag": "MU", - "isCrypto": false - }, - "MVR": { - "name": "Maldivian Rufiyaa", - "flag": "MV", - "isCrypto": false - }, - "MWK": { - "name": "Malawian Kwacha", - "flag": "MW", - "isCrypto": false - }, - "MXN": { - "name": "Mexican Peso", - "flag": "MX", - "isCrypto": false - }, - "MYR": { - "name": "Malaysian Ringgit", - "flag": "MY", - "isCrypto": false - }, - "MZN": { - "name": "Mozambican Metical", - "flag": "MZ", - "isCrypto": false - }, - "NAD": { - "name": "Namibian Dollar", - "flag": "NA", - "isCrypto": false - }, - "NBL": { - "name": "Isle Of Man Noble", - "flag": "IM", - "isCrypto": false - }, - "NGN": { - "name": "Nigerian Naira", - "flag": "NG", - "isCrypto": false - }, - "NIO": { - "name": "Nicaraguan Córdoba", - "flag": "NI", - "isCrypto": false - }, - "NOK": { - "name": "Norwegian Krone", - "flag": "NO", - "isCrypto": false - }, - "NPR": { - "name": "Nepalese Rupee", - "flag": "NP", - "isCrypto": false - }, - "NSO": { - "name": "New Sovereign", - "flag": "GB", - "isCrypto": false - }, - "NZD": { - "name": "New Zealand Dollar", - "flag": "NZ", - "isCrypto": false - }, - "OMR": { - "name": "Omani Rial", - "flag": "OM", - "isCrypto": false - }, - "OSO": { - "name": "Old Sovereign", - "flag": "GB", - "isCrypto": false - }, - "PAB": { - "name": "Panamanian Balboa", - "flag": "PA", - "isCrypto": false - }, - "PEN": { - "name": "Peruvian Nuevo Sol", - "flag": "PE", - "isCrypto": false - }, - "PGK": { - "name": "Papua New Guinean Kina", - "flag": "PG", - "isCrypto": false - }, - "PHP": { - "name": "Philippine Peso", - "flag": "PH", - "isCrypto": false - }, - "PKR": { - "name": "Pakistani Rupee", - "flag": "PK", - "isCrypto": false - }, - "PLN": { - "name": "Polish Zloty", - "flag": "PL", - "isCrypto": false - }, - "PYG": { - "name": "Paraguayan Guaraní", - "flag": "PY", - "isCrypto": false - }, - "QAR": { - "name": "Qatari Riyal", - "flag": "QA", - "isCrypto": false - }, - "RON": { - "name": "Romanian New Leu", - "flag": "RO", - "isCrypto": false - }, - "RSD": { - "name": "Serbian Dinar", - "flag": "RS", - "isCrypto": false - }, - "RUB": { - "name": "Russian Rouble", - "flag": "RU", - "isCrypto": false - }, - "RWF": { - "name": "Rwandan Franc", - "flag": "RW", - "isCrypto": false - }, - "SAR": { - "name": "Saudi Riyal", - "flag": "SA", - "isCrypto": false - }, - "SBD": { - "name": "Solomon Islands Dollar", - "flag": "SB", - "isCrypto": false - }, - "SCR": { - "name": "Seychelles Rupee", - "flag": "SC", - "isCrypto": false - }, - "SDG": { - "name": "Sudanese Pound", - "flag": "SD", - "isCrypto": false - }, - "SEK": { - "name": "Swedish Krona", - "flag": "SE", - "isCrypto": false - }, - "SGD": { - "name": "Singapore Dollar", - "flag": "SG", - "isCrypto": false - }, - "SHP": { - "name": "Saint Helena Pound", - "flag": "SH", - "isCrypto": false - }, - "SLL": { - "name": "Sierra Leonean Leone", - "flag": "SL", - "isCrypto": false - }, - "SOS": { - "name": "Somali Shilling", - "flag": "SO", - "isCrypto": false - }, - "SRD": { - "name": "Surinamese Dollar", - "flag": "SR", - "isCrypto": false - }, - "STN": { - "name": "Sao Tomean Dobra", - "flag": "ST", - "isCrypto": false - }, - "SVC": { - "name": "Salvadoran Colón", - "flag": "SV", - "isCrypto": false - }, - "SZL": { - "name": "Swazi Lilangeni", - "flag": "SZ", - "isCrypto": false - }, - "THB": { - "name": "Thai Baht", - "flag": "TH", - "isCrypto": false - }, - "TJS": { - "name": "Tajikistani Somoni", - "flag": "TJ", - "isCrypto": false - }, - "TMT": { - "name": "Turkmenistani Manat", - "flag": "TM", - "isCrypto": false - }, - "TND": { - "name": "Tunisian Dinar", - "flag": "TN", - "isCrypto": false - }, - "TOP": { - "name": "Tongan PaʻAnga", - "flag": "TO", - "isCrypto": false - }, - "TRY": { - "name": "Turkish Lira", - "flag": "TR", - "isCrypto": false - }, - "TTD": { - "name": "Trinidad And Tobago Dollar", - "flag": "TT", - "isCrypto": false - }, - "TWD": { - "name": "New Taiwan Dollar", - "flag": "TW", - "isCrypto": false - }, - "TZS": { - "name": "Tanzanian Shilling", - "flag": "TZ", - "isCrypto": false - }, - "UAH": { - "name": "Ukrainian Hryvnia", - "flag": "UA", - "isCrypto": false - }, - "UGX": { - "name": "Ugandan Shilling", - "flag": "UG", - "isCrypto": false - }, - "USD": { - "name": "United States Dollar", - "flag": "US", - "isCrypto": false - }, - "UYU": { - "name": "Uruguayan Peso", - "flag": "UY", - "isCrypto": false - }, - "UZS": { - "name": "Uzbekistan Som", - "flag": "UZ", - "isCrypto": false - }, - "VES": { - "name": "Venezuelan Bolivar Soberano", - "flag": "VE", - "isCrypto": false - }, - "VND": { - "name": "Vietnamese Dong", - "flag": "VN", - "isCrypto": false - }, - "VRL": { - "name": "Vreneli 10F.", - "flag": "CH", - "isCrypto": false - }, - "VRN": { - "name": "Vreneli 20F", - "flag": "CH", - "isCrypto": false - }, - "XAG": { - "name": "Silver (One Troy Ounce)", - "flag": "XAG", - "isCrypto": false - }, - "XAGK": { - "name": "Silver (Kg)", - "flag": "XAG", - "isCrypto": false - }, - "XAU": { - "name": "Gold (One Troy Ounce)", - "flag": "XAU", - "isCrypto": false - }, - "XAUK": { - "name": "Gold (Kg)", - "flag": "XAU", - "isCrypto": false - }, - "XCD": { - "name": "East Caribbean Dollar", - "flag": "AI", - "isCrypto": false - }, - "XOF": { - "name": "Cfa Franc Bceao", - "flag": "SN", - "isCrypto": false - }, - "XPD": { - "name": "Palladium (One Troy Ounce)", - "flag": "XPD", - "isCrypto": false - }, - "XPDK": { - "name": "Palladium (Kg)", - "flag": "XPD", - "isCrypto": false - }, - "XPF": { - "name": "Cfp Franc", - "flag": "PF", - "isCrypto": false - }, - "XPT": { - "name": "Platinum (One Troy Ounce)", - "flag": "XPT", - "isCrypto": false - }, - "XPTK": { - "name": "Platinum (Kg)", - "flag": "XPT", - "isCrypto": false - }, - "YER": { - "name": "Yemeni Rial", - "flag": "YE", - "isCrypto": false - }, - "ZAR": { - "name": "South African Rand", - "flag": "ZA", - "isCrypto": false - }, - "ZMW": { - "name": "Zambian Kwacha", - "flag": "ZM", - "isCrypto": false - }, - "ZWD": { - "name": "Zimbabwe Dollar", - "flag": "ZW", - "isCrypto": false - }, - "DASH": { - "name": "Dash", - "flag": "DASH", - "isCrypto": true - }, - "BTC": { - "name": "Bitcoin", - "flag": "BTC", - "isCrypto": true - }, - "DSH": { - "name": "Dash Coin", - "flag": "DASH", - "isCrypto": true - }, - "LTC": { - "name": "Litecoin", - "flag": "LTC", - "isCrypto": true - }, - "ETH": { - "name": "Ethereum", - "flag": "ETH", - "isCrypto": true - }, - "BCH": { - "name": "Bitcoin Cash", - "flag": "BTC", - "isCrypto": true - }, - "XRP": { - "name": "Xrp Cryptocurrency", - "flag": "XRP", - "isCrypto": true - }, - "XLM": { - "name": "Stellar", - "flag": "XLM", - "isCrypto": true - }, - "ADA": { - "name": "Cardano", - "flag": "ADA", - "isCrypto": true - }, - "UKO": { - "name": "Brent Crude Oil", - "flag": "UKO", - "isCrypto": false - }, - "AAVE": { - "name": "Aave Token", - "flag": "AAVE", - "isCrypto": true - }, - "UNI": { - "name": "Uniswap", - "flag": "UNI", - "isCrypto": true - }, - "LUNA": { - "name": "Terra", - "flag": "LUNA", - "isCrypto": true - }, - "XMR": { - "name": "Monero", - "flag": "XMR", - "isCrypto": true - }, - "XDR": { - "name": "Special Drawing Rights", - "flag": "XDR", - "isCrypto": false - }, - "SOL": { - "name": "Solana", - "flag": "SOL", - "isCrypto": true - }, - "DOGE": { - "name": "Dogecoin", - "flag": "DOGE", - "isCrypto": true - }, - "VET": { - "name": "VeChain", - "flag": "VET", - "isCrypto": true - }, - "DOT": { - "name": "Polkadot", - "flag": "DOT", - "isCrypto": true - }, - "USDC": { - "name": "USD Coin", - "flag": "USDC", - "isCrypto": true - }, - "FIL": { - "name": "File Coin", - "flag": "FIL", - "isCrypto": true - }, - "LINK": { - "name": "Chainlink", - "flag": "LINK", - "isCrypto": true - }, - "MATIC": { - "name": "Polygon", - "flag": "MATIC", - "isCrypto": true - }, - "THETA": { - "name": "Theta", - "flag": "THETA", - "isCrypto": true - }, - "BNB": { - "name": "Binance Coin", - "flag": "BNB", - "isCrypto": true - } -}
--- a/Simoleon/Settings.swift Wed Aug 25 11:00:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,176 +0,0 @@ -// -// Settings.swift -// Simoleon -// -// Created by Dennis Concepción Martín on 19/07/2021. -// - -import SwiftUI -import Purchases - -struct Settings: View { - @Environment(\.managedObjectContext) private var viewContext - @FetchRequest(sortDescriptors: []) private var defaultCurrency: FetchedResults<DefaultCurrency> - - @State private var selectedDefaultCurrency = "" - @State private var showingSubscriptionPaywall = false - @State private var entitlementIsActive = false - @State private var alertTitle = "" - @State private var alertMessage = "" - @State private var showingAlert = false - @State private var searchCurrency = "" - - let fileController = FileController() - - /* - If searched currency string is empty: - * Show all currencies - else: - * Show filtered list of currencies containing searched currency string - */ - var searchResults: [String] { - let currencyPairsSupported: [String] = try! fileController.read(json: "CurrencyPairsSupported.json") - if searchCurrency.isEmpty { - return currencyPairsSupported.sorted() - } else { - return currencyPairsSupported.filter { $0.contains(searchCurrency.uppercased()) } - } - } - - var body: some View { - List { - Section(header: Text("Preferences")) { - if entitlementIsActive { - Picker("Default currency", selection: $selectedDefaultCurrency) { - SearchBar(placeholder: "Search...", text: $searchCurrency) - .padding(5) - - ForEach(searchResults, id: \.self) { currencyPairsSupported in - Text(currencyPairsSupported) - .tag(currencyPairsSupported) - } - } - } else { - LockedCurrencyPicker() - .contentShape(Rectangle()) - .onTapGesture { showingSubscriptionPaywall = true } - } - } - - Section(header: Text("Stay in touch")) { - Link(destination: URL(string: "https://itunes.apple.com/app/id1576390953?action=write-review")!) { - HStack { - Image(systemName: "heart.fill") - .foregroundColor(Color(.systemRed)) - .imageScale(.large) - - Text("Rate Simoleon") - } - } - - Link(destination: URL(string: "https://twitter.com/dennisconcep")!) { - HStack { - Image("TwitterLogo") - .resizable() - .frame(width: 30, height: 30) - - Text("Developer's Twitter") - } - } - - Link(destination: URL(string: "https://dennistech.io/contact")!) { - HStack { - Image(systemName: "envelope.fill") - .foregroundColor(Color(.systemIndigo)) - .imageScale(.large) - - Text("Contact") - } - } - } - - Section(header: Text("About")) { - Link(destination: URL(string: "https://dennistech.io")!) { - Text("Website") - } - - Link(destination: URL(string: "https://dennistech.io/simoleon-privacy-policy")!) { - Text("Privacy Policy") - } - - Link(destination: URL(string: "https://dennistech.io/simoleon-terms-of-use")!) { - Text("Terms of Use") - } - } - } - .alert(isPresented: $showingAlert) { - Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("Ok"))) - } - .onAppear { - checkEntitlement() - /* - if selectedDefaultCurrency is empty: - * View is appearing for the first time - * Set initial default curency for picker - else: - * View is appearing after user selected another default currency - * Save it to core data - */ -// if selectedDefaultCurrency == "" { -// selectedDefaultCurrency = defaultCurrency.first?.pair ?? "USD/GBP" -// } else { -// setCoreData() -// } - } - .listStyle(InsetGroupedListStyle()) - .navigationTitle("Settings") - .sheet(isPresented: $showingSubscriptionPaywall, onDismiss: checkEntitlement) { - SubscriptionPaywall(showingSubscriptionPaywall: $showingSubscriptionPaywall) - } - .if(UIDevice.current.userInterfaceIdiom == .phone) { content in - NavigationView { content } - } - } - - // Save default currency to core data -// private func setCoreData() { -// if defaultCurrency.isEmpty { // If it's empty -> add record -// let defaultCurrency = DefaultCurrency(context: viewContext) -// defaultCurrency.pair = selectedDefaultCurrency -// -// do { -// try viewContext.save() -// } catch { -// print(error.localizedDescription) -// } -// } else { // If not, update record -// defaultCurrency.first?.pair = selectedDefaultCurrency -// try? viewContext.save() -// } -// } - - // Check if user subscription is active - private func checkEntitlement() { - #if SCREENSHOTS - entitlementIsActive = true - #else - Purchases.shared.purchaserInfo { (purchaserInfo, error) in - if purchaserInfo?.entitlements["all"]?.isActive == true { - entitlementIsActive = true - } - - if let error = error as NSError? { - alertTitle = error.localizedDescription - alertMessage = error.localizedFailureReason ?? "" - showingAlert = true - } - } - #endif - } -} - -struct Settings_Previews: PreviewProvider { - static var previews: some View { - Settings() - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/SettingsView.swift Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,174 @@ +//// +//// SettingsView.swift +//// Simoleon +//// +//// Created by Dennis Concepción Martín on 19/07/2021. +//// +// +//import SwiftUI +//import Purchases +// +//struct SettingsView: View { +// @Environment(\.managedObjectContext) private var viewContext +// @FetchRequest(sortDescriptors: []) private var defaultCurrency: FetchedResults<DefaultCurrency> +// @State private var selectedDefaultCurrency = "" +// @State private var showingSubscriptionPaywall = false +// @State private var entitlementIsActive = false +// @State private var alertTitle = "" +// @State private var alertMessage = "" +// @State private var showingAlert = false +// @State private var searchCurrency = "" +// let file = File() +// +// /* +// If searched currency string is empty: +// * Show all currencies +// else: +// * Show filtered list of currencies containing searched currency string +// */ +// var searchResults: [String] { +// let currencyPairsSupported: [String] = try! file.read(json: "CurrencyPairsSupported.json") +// if searchCurrency.isEmpty { +// return currencyPairsSupported.sorted() +// } else { +// return currencyPairsSupported.filter { $0.contains(searchCurrency.uppercased()) } +// } +// } +// +// var body: some View { +// List { +// Section(header: Text("Preferences")) { +// if entitlementIsActive { +// Picker("Default currency", selection: $selectedDefaultCurrency) { +// SearchBar(placeholder: "Search...", text: $searchCurrency) +// .padding(5) +// +// ForEach(searchResults, id: \.self) { currencyPairsSupported in +// Text(currencyPairsSupported) +// .tag(currencyPairsSupported) +// } +// } +// } else { +// LockedCurrencyPicker() +// .contentShape(Rectangle()) +// .onTapGesture { showingSubscriptionPaywall = true } +// } +// } +// +// Section(header: Text("Stay in touch")) { +// Link(destination: URL(string: "https://itunes.apple.com/app/id1576390953?action=write-review")!) { +// HStack { +// Image(systemName: "heart.fill") +// .foregroundColor(Color(.systemRed)) +// .imageScale(.large) +// +// Text("Rate Simoleon") +// } +// } +// +// Link(destination: URL(string: "https://twitter.com/dennisconcep")!) { +// HStack { +// Image("TwitterLogo") +// .resizable() +// .frame(width: 30, height: 30) +// +// Text("Developer's Twitter") +// } +// } +// +// Link(destination: URL(string: "https://dennistech.io/contact")!) { +// HStack { +// Image(systemName: "envelope.fill") +// .foregroundColor(Color(.systemIndigo)) +// .imageScale(.large) +// +// Text("Contact") +// } +// } +// } +// +// Section(header: Text("About")) { +// Link(destination: URL(string: "https://dennistech.io")!) { +// Text("Website") +// } +// +// Link(destination: URL(string: "https://dennistech.io/simoleon-privacy-policy")!) { +// Text("Privacy Policy") +// } +// +// Link(destination: URL(string: "https://dennistech.io/simoleon-terms-of-use")!) { +// Text("Terms of Use") +// } +// } +// } +// .alert(isPresented: $showingAlert) { +// Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("Ok"))) +// } +// .onAppear { +// checkEntitlement() +// /* +// if selectedDefaultCurrency is empty: +// * View is appearing for the first time +// * Set initial default curency for picker +// else: +// * View is appearing after user selected another default currency +// * Save it to core data +// */ +//// if selectedDefaultCurrency == "" { +//// selectedDefaultCurrency = defaultCurrency.first?.pair ?? "USD/GBP" +//// } else { +//// setCoreData() +//// } +// } +// .listStyle(InsetGroupedListStyle()) +// .navigationTitle("Settings") +// .sheet(isPresented: $showingSubscriptionPaywall, onDismiss: checkEntitlement) { +// SubscriptionPaywall(showingSubscriptionPaywall: $showingSubscriptionPaywall) +// } +// .if(UIDevice.current.userInterfaceIdiom == .phone) { content in +// NavigationView { content } +// } +// } +// +// // Save default currency to core data +//// private func setCoreData() { +//// if defaultCurrency.isEmpty { // If it's empty -> add record +//// let defaultCurrency = DefaultCurrency(context: viewContext) +//// defaultCurrency.pair = selectedDefaultCurrency +//// +//// do { +//// try viewContext.save() +//// } catch { +//// print(error.localizedDescription) +//// } +//// } else { // If not, update record +//// defaultCurrency.first?.pair = selectedDefaultCurrency +//// try? viewContext.save() +//// } +//// } +// +// // Check if user subscription is active +// private func checkEntitlement() { +// #if SCREENSHOTS +// entitlementIsActive = true +// #else +// Purchases.shared.purchaserInfo { (purchaserInfo, error) in +// if purchaserInfo?.entitlements["all"]?.isActive == true { +// entitlementIsActive = true +// } +// +// if let error = error as NSError? { +// alertTitle = error.localizedDescription +// alertMessage = error.localizedFailureReason ?? "" +// showingAlert = true +// } +// } +// #endif +// } +//} +// +//struct SettingsView_Previews: PreviewProvider { +// static var previews: some View { +// SettingsView() +// } +//}
--- a/Simoleon/Simoleon.xcdatamodeld/Simoleon.xcdatamodel/contents Wed Aug 25 11:00:21 2021 +0100 +++ b/Simoleon/Simoleon.xcdatamodeld/Simoleon.xcdatamodel/contents Sat Aug 28 11:15:25 2021 +0100 @@ -1,13 +1,14 @@ <?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="18154" systemVersion="20G71" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier=""> +<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="18154" systemVersion="20G95" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier=""> <entity name="DefaultCurrency" representedClassName="DefaultCurrency" syncable="YES"> - <attribute name="pair" optional="YES" attributeType="String"/> + <attribute name="firstSymbol" optional="YES" attributeType="String"/> + <attribute name="secondSymbol" optional="YES" attributeType="String"/> </entity> <entity name="Favorite" representedClassName="Favorite" syncable="YES"> <attribute name="currencyPair" optional="YES" attributeType="String"/> </entity> <elements> - <element name="DefaultCurrency" positionX="0" positionY="0" width="128" height="44"/> + <element name="DefaultCurrency" positionX="0" positionY="0" width="128" height="59"/> <element name="Favorite" positionX="0" positionY="0" width="128" height="44"/> </elements> </model> \ No newline at end of file
--- a/Simoleon/SimoleonApp.swift Wed Aug 25 11:00:21 2021 +0100 +++ b/Simoleon/SimoleonApp.swift Sat Aug 28 11:15:25 2021 +0100 @@ -12,10 +12,9 @@ struct SimoleonApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate let persistenceController = PersistenceController.shared - let fileController = FileController() init() { - let apiKey = fileController.readConfigVariable(withKey: "PURCHASES_KEY")! + let apiKey = readConfigVariable(withKey: "PURCHASES_KEY")! Purchases.configure(withAPIKey: apiKey) }
--- a/Simoleon/SubscriptionPaywall.swift Wed Aug 25 11:00:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,95 +0,0 @@ -// -// SubscriptionPaywall.swift -// Simoleon -// -// Created by Dennis Concepción Martín on 22/07/2021. -// - -import SwiftUI - -struct SubscriptionPaywall: View { - @Binding var showingSubscriptionPaywall: Bool - - var body: some View { - NavigationView { - ScrollView { - VStack(alignment: .leading, spacing: 20) { - HStack { - Spacer() - VStack { - Image("Subscription") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 100, height: 100) - .cornerRadius(25) - .padding(.top) - - Text("Unlock All Access") - .font(.title) - .fontWeight(.semibold) - .fixedSize(horizontal: false, vertical: true) - .multilineTextAlignment(.center) - .padding(.top) - } - - Spacer() - } - - Divider() - - SubscriptionFeature( - symbol: "star.circle.fill", - colour: Color(.systemYellow), - title: "Favorite Forex Pairs", - description: "Save any currency pair to access them quickly." - ) - - SubscriptionFeature( - symbol: "flag.circle.fill", - colour: Color(.systemRed), - title: "Over 170 Currencies", - description: "Access almost every currency of the world." - ) - - SubscriptionFeature( - symbol: "icloud.circle.fill", - colour: Color(.systemBlue), - title: "Everything is Up-to-date", - description: "Your settings and favorite currencies in all your devices." - ) - - SubscriptionFeature( - symbol: "bitcoinsign.circle.fill", - colour: Color(.systemOrange), - title: "Cryptos and Commodities", - description: "Convert currency between cryptos, gold, and silver." - ) - - Spacer() - SubscribeButton(showingSubscriptionPaywall: $showingSubscriptionPaywall) - HStack { - Spacer() - RestoreButton(showingSubscriptionPaywall: $showingSubscriptionPaywall) - Spacer() - } - } - .padding(.bottom) - .padding(.horizontal, 40) - } - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button(action: { showingSubscriptionPaywall = false }) { - Text("Cancel") - } - } - } - } - } -} - -struct SubscriptionPaywall_Previews: PreviewProvider { - static var previews: some View { - SubscriptionPaywall(showingSubscriptionPaywall: .constant(true)) - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/Tests/ChildListResets.swift Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,44 @@ +// +// ModalSheetSelection.swift +// Simoleon +// +// Created by Dennis Concepción Martín on 26/8/21. +// + +import SwiftUI + +struct ParentView: View { + @State var selection: Int = 1 + @State private var showingList = false + + var body: some View { + VStack { + Button("Show list", action: {showingList = true}) + .sheet(isPresented: $showingList) { + ModalSheetSelection(selection: $selection) + } + + Text("My first var is: \(selection)") + } + } +} + +struct ModalSheetSelection: View { + @Binding var selection: Int + + var body: some View { + NavigationView { + List { + SearchBar(placeholder: "", text: .constant("")) + ForEach((1..<100), id: \.self) { number in + Button(action: {selection = number}) { + Text("\(number)") + } + } + } + .id(UUID()) + .navigationTitle("Currencies") + .navigationBarTitleDisplayMode(.inline) + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/UI/ConversionBox.swift Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,83 @@ +//// +//// ConversionBox.swift +//// Simoleon +//// +//// Created by Dennis Concepción Martín on 18/07/2021. +//// +// +//import SwiftUI +// +//struct ConversionBox: View { +// var currencyDetails: CurrencyDetailsModel +// @State var currencyPair: CurrencyPairModel +// +// var body: some View { +// VStack(alignment: .leading) { +// Text("\(baseName) (\(currencyPair.baseSymbol))") +// .font(.callout) +// .fontWeight(.semibold) +// .padding(.top, 40) +// +// ZStack(alignment: .trailing) { +// TextField("Enter amount", text: $amount) { startedEditing in +// if startedEditing { +// withAnimation { +// amountIsEditing = true +// } +// } +// } +// onCommit: { +// withAnimation { +// amountIsEditing = false +// } +// } +// .keyboardType(.decimalPad) +// .font(Font.title.weight(.semibold)) +// .lineLimit(1) +// .accessibilityIdentifier("ConversionTextField") +// } +// +// Divider() +// +// let quoteName = currencyDetails[currencyPair.quoteSymbol]!.name +// Text("\(quoteName) (\(currencyPair.quoteSymbol))") +// .font(.callout) +// .fontWeight(.semibold) +// .padding(.top, 10) +// +// if showingConversion { +// Text("\(makeConversion(), specifier: "%.2f")") +// .font(Font.title.weight(.semibold)) +// .lineLimit(1) +// .padding(.top, 5) +// } else { +// ProgressView() +// .padding(.top, 5) +// } +// } +// .onAppear(perform: request) +// } +// +// /* +// if the amount can be converted to Double: +// * Return amount +// else: +// * Return zero +// */ +// func makeConversion() -> Double { +// if let amountToConvert = Double(amount) { +// return amountToConvert * price // Conversion +// } else { +// return 0 +// } +// } +//} +// +// +//struct ConversionBox_Previews: PreviewProvider { +// static var previews: some View { +// let fileController = File() +// let currencyDetails: [String: CurrencyDetailsModel] = try! fileController.read(json: "CurrencyDetails.json") +// ConversionBox(currencyPair: CurrencyPair(), currencyDetails: currencyDetails) +// } +//}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/UI/CurrencyButton.swift Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,34 @@ +// +// CurrencyButton.swift +// Simoleon +// +// Created by Dennis Concepción Martín on 24/8/21. +// + +import SwiftUI + +struct CurrencyButton: View { + var selectedCurrency: String + let currencyDetails: [String: CurrencyModel] = try! read(json: "Currencies.json") + + var body: some View { + let currency = currencyDetails[selectedCurrency]! + RoundedRectangle(cornerRadius: 15) + .foregroundColor(Color(.secondarySystemBackground)) + .frame(height: 60) + .overlay( + HStack { + Flag(flag: currency.flag) + Text(currency.symbol) + .foregroundColor(.primary) + .font(.headline) + } + ) + } +} + +struct CurrencyButton_Previews: PreviewProvider { + static var previews: some View { + CurrencyButton(selectedCurrency: "USD") + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/UI/CurrencyRow.swift Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,41 @@ +// +// CurrencyRow.swift +// Simoleon +// +// Created by Dennis Concepción Martín on 15/07/2021. +// + +import SwiftUI + +struct CurrencyRow: View { + var currency: CurrencyModel + + var body: some View { + HStack { + Flag(flag: currency.flag) + VStack(alignment: .leading) { + Text(currency.symbol) + .font(.headline) + + Text(currency.name) + .font(.subheadline) + .lineLimit(1) + } + .padding(.horizontal) + } + } +} + +struct CurrencyRow_Previews: PreviewProvider { + static var previews: some View { + CurrencyRow( + currency: + CurrencyModel( + symbol: "USD", + name: "United States Dollar", + flag: "US", + isCrypto: false + ) + ) + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/UI/FavoriteButton.swift Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,108 @@ +// +// FavoriteButton.swift +// Simoleon +// +// Created by Dennis Concepción Martín on 19/07/2021. +// + +import SwiftUI + +struct FavoriteButton: View { + @State var currencyPair: CurrencyPairModel + @Environment(\.managedObjectContext) private var viewContext + @FetchRequest(sortDescriptors: []) private var favorites: FetchedResults<Favorite> + @State private var starSymbol = "star" + + var body: some View { + Button(action: favoriteAction) { + RoundedRectangle(cornerRadius: 15) + .foregroundColor(Color(.secondarySystemBackground)) + .frame(width: 60, height: 60) + .overlay( + Image(systemName: generateStar()) + .font(.system(size: 28)) + .foregroundColor(Color(.systemYellow)) + ) + } + .accessibilityIdentifier("AddToFavorites") + } + + /* + If currency pair is favorite: + * Button action is to remove from favorites + else: + * Button action is to add to favorites + */ + private func favoriteAction() { + let favoriteCurrencyPairs = favorites.map { $0.currencyPair } + let currencyPair = "(\(currencyPair.baseSymbol)/\(currencyPair.quoteSymbol)" + if favoriteCurrencyPairs.contains(currencyPair) { + removeFromFavorites() + } else { + addToFavorites() + } + + let haptics = Haptics() + haptics.simpleSuccess() + } + + /* + if currency pair is favorite: + * Return "star.fill" symbol + else: + * Return "star" + */ + private func generateStar() -> String { + let favoriteCurrencyPairs = favorites.map { $0.currencyPair } + let currencyPair = "(\(currencyPair.baseSymbol)/\(currencyPair.quoteSymbol)" + if favoriteCurrencyPairs.contains(currencyPair) { + return "star.fill" + } else { + return "star" + } + } + + /* + * Get first favorite core data object that matches the specified currency pair + * Delete it + */ + private func removeFromFavorites() { + let currencyPair = "(\(currencyPair.baseSymbol)/\(currencyPair.quoteSymbol)" + withAnimation { + let favoriteObject = favorites.first(where: { $0.currencyPair == currencyPair }) + viewContext.delete(favoriteObject ?? Favorite()) + + do { + try viewContext.save() + } catch { + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + } + } + + /* + * Create a favorite core data object + * Save it + */ + private func addToFavorites() { + let currencyPair = "(\(currencyPair.baseSymbol)/\(currencyPair.quoteSymbol)" + withAnimation { + let favorite = Favorite(context: viewContext) + favorite.currencyPair = currencyPair + + do { + try viewContext.save() + } catch { + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + } + } +} + +struct FavoriteButton_Previews: PreviewProvider { + static var previews: some View { + FavoriteButton(currencyPair: CurrencyPairModel(baseSymbol: "USD", quoteSymbol: "EUR")) + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/UI/Flag.swift Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,27 @@ +// +// Flag.swift +// Simoleon +// +// Created by Dennis Concepción Martín on 24/8/21. +// + +import SwiftUI + +struct Flag: View { + var flag: String + + var body: some View { + Image(flag) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 30, height: 30) + .clipShape(Circle()) + .overlay(Circle().stroke(Color(.secondaryLabel), lineWidth: 1)) + } +} + +struct Flag_Previews: PreviewProvider { + static var previews: some View { + Flag(flag: "GB") + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/UI/LockedCurrencyPicker.swift Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,31 @@ +// +// LockedCurrencyPicker.swift +// Simoleon +// +// Created by Dennis Concepción Martín on 22/07/2021. +// + +import SwiftUI + +struct LockedCurrencyPicker: View { + @Environment(\.managedObjectContext) private var viewContext + @FetchRequest(sortDescriptors: []) private var defaultCurrency: FetchedResults<DefaultCurrency> + + var body: some View { + HStack { + Text("Default currency") + Spacer() +// Text(defaultCurrency.first?.pair ?? "USD/GBP") +// .foregroundColor(.secondary) + + Image(systemName: "lock") + .foregroundColor(.secondary) + } + } +} + +struct LockedCurrencyPicker_Previews: PreviewProvider { + static var previews: some View { + LockedCurrencyPicker() + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/UI/RestoreButton.swift Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,57 @@ +// +// RestoreButton.swift +// Simoleon +// +// Created by Dennis Concepción Martín on 22/07/2021. +// + +import SwiftUI +import Purchases + +struct RestoreButton: View { + @Binding var showingSubscriptionPaywall: Bool + @State private var alertTitle: LocalizedStringKey = "" + @State private var alertMessage: LocalizedStringKey = "" + @State private var restoringPurchases = false + @State private var showingAlert = false + + var body: some View { + Button(action: restorePurchases) { + if restoringPurchases { + ProgressView() + } else { + Text("Restore purchases") + } + } + .alert(isPresented: $showingAlert) { + Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("Ok"))) + } + } + + private func restorePurchases() { + restoringPurchases = true + + Purchases.shared.restoreTransactions { purchaserInfo, error in + if purchaserInfo?.entitlements["all"]?.isActive == true { + showingSubscriptionPaywall = false + } else { + alertTitle = LocalizedStringKey("No subscriptions found") + alertMessage = LocalizedStringKey("You are not subscripted to Simoleon yet.") + restoringPurchases = false + showingAlert = true + } + + if let error = error as NSError? { + alertTitle = LocalizedStringKey(error.localizedDescription) + alertMessage = LocalizedStringKey(error.localizedFailureReason ?? "") + showingAlert = true + } + } + } +} + +struct RestoreButton_Previews: PreviewProvider { + static var previews: some View { + RestoreButton(showingSubscriptionPaywall: .constant(true)) + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/UI/SearchBar.swift Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,29 @@ +// +// SearchBar.swift +// Simoleon +// +// Created by Dennis Concepción Martín on 27/07/2021. +// + +import SwiftUI + +struct SearchBar: View { + var placeholder: LocalizedStringKey + @Binding var text: String + + var body: some View { + TextField(placeholder, text: $text) + .disableAutocorrection(true) + .padding(10) + .background( + RoundedRectangle(cornerRadius: 15) + .foregroundColor(Color(.tertiarySystemFill)) + ) + } +} + +struct SearchBar_Previews: PreviewProvider { + static var previews: some View { + SearchBar(placeholder: "Search ...", text: .constant("")) + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/UI/Sidebar.swift Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,43 @@ +//// +//// Sidebar.swift +//// Simoleon +//// +//// Created by Dennis Concepción Martín on 18/07/2021. +//// +// +//import SwiftUI +// +//struct Sidebar: View { +// @Environment(\.managedObjectContext) private var viewContext +// @FetchRequest(sortDescriptors: []) private var defaultCurrency: FetchedResults<DefaultCurrency> +// +// var body: some View { +// List { +// NavigationLink(destination: Conversion()) { +// Label("Convert", systemImage: "arrow.counterclockwise.circle") +// } +// .accessibilityIdentifier("NavigateToConversion") +// +// NavigationLink(destination: Favorites()) { +// Label("Favorites", systemImage: "star") +// } +// .accessibilityIdentifier("NavigateToFavorites") +// +// NavigationLink(destination: Settings()) { +// Label("Settings", systemImage: "gear") +// } +// .accessibilityIdentifier("NavigateToSettings") +// } +// .listStyle(SidebarListStyle()) +// .navigationTitle("Categories") +// .accessibilityIdentifier("Sidebar") +// } +//} +// +//struct Sidebar_Previews: PreviewProvider { +// static var previews: some View { +// NavigationView { +// Sidebar() +// } +// } +//}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/UI/SubscribeButton.swift Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,104 @@ +// +// SubscribeButton.swift +// Simoleon +// +// Created by Dennis Concepción Martín on 22/07/2021. +// + +import SwiftUI +import Purchases + +struct SubscribeButton: View { + @Binding var showingSubscriptionPaywall: Bool + @State private var price = "" + @State private var alertTitle = "" + @State private var alertMessage = "" + @State private var showingAlert = false + @State private var showingPrice = false + + var body: some View { + Button(action: purchaseMonthlySubscription) { + RoundedRectangle(cornerRadius: 15) + .frame(height: 60) + .overlay( + VStack { + if showingPrice { + Text("Subscribe for \(price) / month") + .foregroundColor(.white) + .fontWeight(.semibold) + } else { + ProgressView() + } + } + ) + } + .onAppear(perform: fetchMonthlySubscription) + .alert(isPresented: $showingAlert) { + Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("Ok"))) + } + } + + private func fetchMonthlySubscription() { + Purchases.shared.offerings { (offerings, error) in + if let product = offerings?.current?.monthly?.product { + price = formatCurrency(product.priceLocale, product.price) + showingPrice = true + } + + if let error = error as NSError? { + alertTitle = error.localizedDescription + alertMessage = error.localizedFailureReason ?? "" + price = "-" + showingPrice = true + showingAlert = true + } + } + } + + private func purchaseMonthlySubscription() { + showingPrice = false + + Purchases.shared.offerings { (offerings, error) in + if let package = offerings?.current?.monthly { + + Purchases.shared.purchasePackage(package) { (transaction, purchaserInfo, error, userCancelled) in + if purchaserInfo?.entitlements["all"]?.isActive == true { + showingPrice = true + showingSubscriptionPaywall = false + } + + if let error = error as NSError? { + alertTitle = error.localizedDescription + alertMessage = error.localizedFailureReason ?? "" + showingPrice = true + showingAlert = true + } + } + + if let error = error as NSError? { + alertTitle = error.localizedDescription + alertMessage = error.localizedFailureReason ?? "" + showingPrice = true + showingAlert = true + } + } + } + } + + private func formatCurrency(_ locale: Locale, _ amount: NSDecimalNumber) -> String { + let formatter = NumberFormatter() + formatter.locale = locale + formatter.numberStyle = .currency + + // It won't fail. Check unit test + let formattedAmount = formatter.string(from: amount as NSNumber)! + + return formattedAmount + } +} + +struct SubscribeButton_Previews: PreviewProvider { + static var previews: some View { + SubscribeButton(showingSubscriptionPaywall: .constant(true)) + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/UI/SubscriptionFeature.swift Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,41 @@ +// +// SubscriptionFeature.swift +// Simoleon +// +// Created by Dennis Concepción Martín on 26/07/2021. +// + +import SwiftUI + +struct SubscriptionFeature: View { + var symbol: String + var colour: Color + var title: LocalizedStringKey + var description: LocalizedStringKey + + var body: some View { + HStack(alignment:.top) { + Image(systemName: symbol) + .foregroundColor(colour) + .font(.title) + + VStack(alignment: .leading) { + Text(title) + .font(.headline) + + Text(description) + } + } + } +} + +struct SubscriptionFeature_Previews: PreviewProvider { + static var previews: some View { + SubscriptionFeature( + symbol: "star.circle.fill", + colour: Color(.systemYellow), + title: "Favorite Currencies", + description: "Save your favorite currencies to access them quickly." + ) + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/UI/SubscriptionPaywall.swift Sat Aug 28 11:15:25 2021 +0100 @@ -0,0 +1,95 @@ +// +// SubscriptionPaywall.swift +// Simoleon +// +// Created by Dennis Concepción Martín on 22/07/2021. +// + +import SwiftUI + +struct SubscriptionPaywall: View { + @Binding var showingSubscriptionPaywall: Bool + + var body: some View { + NavigationView { + ScrollView { + VStack(alignment: .leading, spacing: 20) { + HStack { + Spacer() + VStack { + Image("Subscription") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 100, height: 100) + .cornerRadius(25) + .padding(.top) + + Text("Unlock All Access") + .font(.title) + .fontWeight(.semibold) + .fixedSize(horizontal: false, vertical: true) + .multilineTextAlignment(.center) + .padding(.top) + } + + Spacer() + } + + Divider() + + SubscriptionFeature( + symbol: "star.circle.fill", + colour: Color(.systemYellow), + title: "Favorite Forex Pairs", + description: "Save any currency pair to access them quickly." + ) + + SubscriptionFeature( + symbol: "flag.circle.fill", + colour: Color(.systemRed), + title: "Over 170 Currencies", + description: "Access almost every currency of the world." + ) + + SubscriptionFeature( + symbol: "icloud.circle.fill", + colour: Color(.systemBlue), + title: "Everything is Up-to-date", + description: "Your settings and favorite currencies in all your devices." + ) + + SubscriptionFeature( + symbol: "bitcoinsign.circle.fill", + colour: Color(.systemOrange), + title: "Cryptos and Commodities", + description: "Convert currency between cryptos, gold, and silver." + ) + + Spacer() + SubscribeButton(showingSubscriptionPaywall: $showingSubscriptionPaywall) + HStack { + Spacer() + RestoreButton(showingSubscriptionPaywall: $showingSubscriptionPaywall) + Spacer() + } + } + .padding(.bottom) + .padding(.horizontal, 40) + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button(action: { showingSubscriptionPaywall = false }) { + Text("Cancel") + } + } + } + } + } +} + +struct SubscriptionPaywall_Previews: PreviewProvider { + static var previews: some View { + SubscriptionPaywall(showingSubscriptionPaywall: .constant(true)) + } +}
--- a/fastlane/SnapshotHelper.swift Wed Aug 25 11:00:21 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,309 +0,0 @@ -// -// SnapshotHelper.swift -// Example -// -// Created by Felix Krause on 10/8/15. -// - -// ----------------------------------------------------- -// IMPORTANT: When modifying this file, make sure to -// increment the version number at the very -// bottom of the file to notify users about -// the new SnapshotHelper.swift -// ----------------------------------------------------- - -import Foundation -import XCTest - -var deviceLanguage = "" -var locale = "" - -func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) { - Snapshot.setupSnapshot(app, waitForAnimations: waitForAnimations) -} - -func snapshot(_ name: String, waitForLoadingIndicator: Bool) { - if waitForLoadingIndicator { - Snapshot.snapshot(name) - } else { - Snapshot.snapshot(name, timeWaitingForIdle: 0) - } -} - -/// - Parameters: -/// - name: The name of the snapshot -/// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait. -func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { - Snapshot.snapshot(name, timeWaitingForIdle: timeout) -} - -enum SnapshotError: Error, CustomDebugStringConvertible { - case cannotFindSimulatorHomeDirectory - case cannotRunOnPhysicalDevice - - var debugDescription: String { - switch self { - case .cannotFindSimulatorHomeDirectory: - return "Couldn't find simulator home location. Please, check SIMULATOR_HOST_HOME env variable." - case .cannotRunOnPhysicalDevice: - return "Can't use Snapshot on a physical device." - } - } -} - -@objcMembers -open class Snapshot: NSObject { - static var app: XCUIApplication? - static var waitForAnimations = true - static var cacheDirectory: URL? - static var screenshotsDirectory: URL? { - return cacheDirectory?.appendingPathComponent("screenshots", isDirectory: true) - } - - open class func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) { - - Snapshot.app = app - Snapshot.waitForAnimations = waitForAnimations - - do { - let cacheDir = try getCacheDirectory() - Snapshot.cacheDirectory = cacheDir - setLanguage(app) - setLocale(app) - setLaunchArguments(app) - } catch let error { - NSLog(error.localizedDescription) - } - } - - class func setLanguage(_ app: XCUIApplication) { - guard let cacheDirectory = self.cacheDirectory else { - NSLog("CacheDirectory is not set - probably running on a physical device?") - return - } - - let path = cacheDirectory.appendingPathComponent("language.txt") - - do { - let trimCharacterSet = CharacterSet.whitespacesAndNewlines - deviceLanguage = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) - app.launchArguments += ["-AppleLanguages", "(\(deviceLanguage))"] - } catch { - NSLog("Couldn't detect/set language...") - } - } - - class func setLocale(_ app: XCUIApplication) { - guard let cacheDirectory = self.cacheDirectory else { - NSLog("CacheDirectory is not set - probably running on a physical device?") - return - } - - let path = cacheDirectory.appendingPathComponent("locale.txt") - - do { - let trimCharacterSet = CharacterSet.whitespacesAndNewlines - locale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) - } catch { - NSLog("Couldn't detect/set locale...") - } - - if locale.isEmpty && !deviceLanguage.isEmpty { - locale = Locale(identifier: deviceLanguage).identifier - } - - if !locale.isEmpty { - app.launchArguments += ["-AppleLocale", "\"\(locale)\""] - } - } - - class func setLaunchArguments(_ app: XCUIApplication) { - guard let cacheDirectory = self.cacheDirectory else { - NSLog("CacheDirectory is not set - probably running on a physical device?") - return - } - - let path = cacheDirectory.appendingPathComponent("snapshot-launch_arguments.txt") - app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"] - - do { - let launchArguments = try String(contentsOf: path, encoding: String.Encoding.utf8) - let regex = try NSRegularExpression(pattern: "(\\\".+?\\\"|\\S+)", options: []) - let matches = regex.matches(in: launchArguments, options: [], range: NSRange(location: 0, length: launchArguments.count)) - let results = matches.map { result -> String in - (launchArguments as NSString).substring(with: result.range) - } - app.launchArguments += results - } catch { - NSLog("Couldn't detect/set launch_arguments...") - } - } - - open class func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { - if timeout > 0 { - waitForLoadingIndicatorToDisappear(within: timeout) - } - - NSLog("snapshot: \(name)") // more information about this, check out https://docs.fastlane.tools/actions/snapshot/#how-does-it-work - - if Snapshot.waitForAnimations { - sleep(1) // Waiting for the animation to be finished (kind of) - } - - #if os(OSX) - guard let app = self.app else { - NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") - return - } - - app.typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: []) - #else - - guard self.app != nil else { - NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") - return - } - - let screenshot = XCUIScreen.main.screenshot() - #if os(iOS) - let image = XCUIDevice.shared.orientation.isLandscape ? fixLandscapeOrientation(image: screenshot.image) : screenshot.image - #else - let image = screenshot.image - #endif - - guard var simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return } - - do { - // The simulator name contains "Clone X of " inside the screenshot file when running parallelized UI Tests on concurrent devices - let regex = try NSRegularExpression(pattern: "Clone [0-9]+ of ") - let range = NSRange(location: 0, length: simulator.count) - simulator = regex.stringByReplacingMatches(in: simulator, range: range, withTemplate: "") - - let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png") - #if swift(<5.0) - UIImagePNGRepresentation(image)?.write(to: path, options: .atomic) - #else - try image.pngData()?.write(to: path, options: .atomic) - #endif - } catch let error { - NSLog("Problem writing screenshot: \(name) to \(screenshotsDir)/\(simulator)-\(name).png") - NSLog(error.localizedDescription) - } - #endif - } - - class func fixLandscapeOrientation(image: UIImage) -> UIImage { - #if os(watchOS) - return image - #else - if #available(iOS 10.0, *) { - let format = UIGraphicsImageRendererFormat() - format.scale = image.scale - let renderer = UIGraphicsImageRenderer(size: image.size, format: format) - return renderer.image { context in - image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)) - } - } else { - return image - } - #endif - } - - class func waitForLoadingIndicatorToDisappear(within timeout: TimeInterval) { - #if os(tvOS) - return - #endif - - guard let app = self.app else { - NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") - return - } - - let networkLoadingIndicator = app.otherElements.deviceStatusBars.networkLoadingIndicators.element - let networkLoadingIndicatorDisappeared = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == false"), object: networkLoadingIndicator) - _ = XCTWaiter.wait(for: [networkLoadingIndicatorDisappeared], timeout: timeout) - } - - class func getCacheDirectory() throws -> URL { - let cachePath = "Library/Caches/tools.fastlane" - // on OSX config is stored in /Users/<username>/Library - // and on iOS/tvOS/WatchOS it's in simulator's home dir - #if os(OSX) - let homeDir = URL(fileURLWithPath: NSHomeDirectory()) - return homeDir.appendingPathComponent(cachePath) - #elseif arch(i386) || arch(x86_64) || arch(arm64) - guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else { - throw SnapshotError.cannotFindSimulatorHomeDirectory - } - let homeDir = URL(fileURLWithPath: simulatorHostHome) - return homeDir.appendingPathComponent(cachePath) - #else - throw SnapshotError.cannotRunOnPhysicalDevice - #endif - } -} - -private extension XCUIElementAttributes { - var isNetworkLoadingIndicator: Bool { - if hasAllowListedIdentifier { return false } - - let hasOldLoadingIndicatorSize = frame.size == CGSize(width: 10, height: 20) - let hasNewLoadingIndicatorSize = frame.size.width.isBetween(46, and: 47) && frame.size.height.isBetween(2, and: 3) - - return hasOldLoadingIndicatorSize || hasNewLoadingIndicatorSize - } - - var hasAllowListedIdentifier: Bool { - let allowListedIdentifiers = ["GeofenceLocationTrackingOn", "StandardLocationTrackingOn"] - - return allowListedIdentifiers.contains(identifier) - } - - func isStatusBar(_ deviceWidth: CGFloat) -> Bool { - if elementType == .statusBar { return true } - guard frame.origin == .zero else { return false } - - let oldStatusBarSize = CGSize(width: deviceWidth, height: 20) - let newStatusBarSize = CGSize(width: deviceWidth, height: 44) - - return [oldStatusBarSize, newStatusBarSize].contains(frame.size) - } -} - -private extension XCUIElementQuery { - var networkLoadingIndicators: XCUIElementQuery { - let isNetworkLoadingIndicator = NSPredicate { (evaluatedObject, _) in - guard let element = evaluatedObject as? XCUIElementAttributes else { return false } - - return element.isNetworkLoadingIndicator - } - - return self.containing(isNetworkLoadingIndicator) - } - - var deviceStatusBars: XCUIElementQuery { - guard let app = Snapshot.app else { - fatalError("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") - } - - let deviceWidth = app.windows.firstMatch.frame.width - - let isStatusBar = NSPredicate { (evaluatedObject, _) in - guard let element = evaluatedObject as? XCUIElementAttributes else { return false } - - return element.isStatusBar(deviceWidth) - } - - return self.containing(isStatusBar) - } -} - -private extension CGFloat { - func isBetween(_ numberA: CGFloat, and numberB: CGFloat) -> Bool { - return numberA...numberB ~= self - } -} - -// Please don't remove the lines below -// They are used to detect outdated configuration files -// SnapshotHelperVersion [1.27]