From 931c8438e4603d25f264d8149135f623abf2f8a8 Mon Sep 17 00:00:00 2001 From: goncalo-frade-iohk <87179681+goncalo-frade-iohk@users.noreply.github.com> Date: Mon, 15 Apr 2024 12:32:16 +0100 Subject: [PATCH] feat(demo): update demo app with new features (#133) --- .../CreateAnoncredCredentialRequest.swift | 4 +- .../PrismAgent/Sources/PrismAgent.swift | 4 +- Core/Sources/Helpers/Map+AsyncAwait.swift | 22 ++ .../project.pbxproj | 366 ++++++++++++++++-- .../CommonUI/ActivityView.swift | 16 + .../CommonUI/AtalaButton.swift | 61 +++ .../CommonUI/ButtonNavigationLink.swift | 35 ++ .../CommonUI/CheckButton.swift | 22 ++ .../CommonUI/ClosableSheet.swift | 25 ++ .../CommonUI/DeleteView.swift | 106 +++++ .../CommonUI}/EmptyNavigationLink.swift | 0 .../CommonUI/ErrorDialogView.swift | 77 ++++ .../CommonUI/ErrorTextBox.swift | 22 ++ .../CommonUI/FlowSuccessfulView.swift | 60 +++ .../CommonUI/HyperlinkText.swift | 207 ++++++++++ .../CommonUI}/LazyView.swift | 0 .../CommonUI/PinInsertView.swift | 107 +++++ .../CommonUI/SearchBoxView.swift | 35 ++ .../CommonUI/SelectCheckButton.swift | 33 ++ .../CommonUI/WebView.swift | 87 +++++ .../CommonUI/WordTagEditable.swift | 90 +++++ .../CommonUI/WordTagGrid.swift | 85 ++++ .../CommonUI/WordsTextFieldView.swift | 87 +++++ .../IsLoadingEnvironmentValue.swift | 12 + .../Environment}/RootPresentationMode.swift | 0 .../FancyToast.swift | 0 .../FancyToastModifier.swift | 0 .../Modifiers/AtalaNavigationBackButton.swift | 60 +++ .../Modifiers/Button+Configurations.swift | 67 ++++ .../Modifiers/ClearFullCoverModifier.swift | 149 +++++++ .../Modifiers/LoadingModifier.swift | 40 ++ .../NavigationBarUtilModifiers.swift | 37 ++ .../Modifiers/RoundedRectBorderWithText.swift | 39 ++ .../NavigationUtils.swift | 58 +++ .../DisablePreferenceKey.swift | 35 ++ .../IntrinsicSizePreferenceKey.swift | 41 ++ .../Shapes/SpecificRoundedRect.swift | 15 + .../Array+Helpers.swift} | 0 .../Helper/{ => SwiftTools}/Builder.swift | 0 .../Helper/SwiftTools/Combine+Drop.swift | 40 ++ .../{ => SwiftTools}/ComponentContainer.swift | 0 .../Helper/{ => SwiftTools}/DIContainer.swift | 0 .../{ => SwiftTools}/PrintObjects.swift | 0 .../{ => SwiftTools}/String+extensions.swift | 0 .../SwiftTools/URL+StringLiterals.swift | 12 + .../AddNewContact/AddNewContactBuilder.swift | 22 ++ .../AddNewContact/AddNewContactState.swift | 20 + .../AddNewContact/AddNewContactView.swift | 110 ++++++ .../AddNewContactViewModel.swift | 78 ++++ .../UI/AlreadyConnectedView.swift | 39 ++ .../UI/ConfirmConnectionView.swift | 51 +++ .../AddNewContact/UI/InsertCodeView.swift | 57 +++ .../Common/DisplayErrorState.swift | 12 + .../Connections/ConnectionsListRouter.swift | 16 + .../Connections/ConnectionsListView.swift | 66 ++-- .../CredentialDetailView.swift | 61 +-- .../CredentialsList/CredentialListView.swift | 146 ++++++- .../CredentialListViewModel.swift | 112 +++++- .../CredentialListViewState.swift | 46 +++ .../RequestDetail/RequestDetailRouter.swift | 1 + .../RequestDetail/RequestDetailView.swift | 24 ++ .../RequestDetailViewModel.swift | 1 + .../RequestDetailViewState.swift | 26 ++ .../WalletDemo2/Main/Main2Router.swift | 21 +- .../Modules/WalletDemo2/Main/Main2View.swift | 17 +- .../MediatorPage/MediatorPageView.swift | 56 ++- .../MediatorPage/MediatorPageViewModel.swift | 9 + .../MessagesList/MessagesListView.swift | 2 +- .../QRCode/QRCodeScannerBuilder.swift | 18 + .../QRCode/QRCodeScannerRouter.swift | 14 + .../QRCode/QRCodeScannerView.swift | 102 +++++ .../QRCode/QRCodeScannerViewModel.swift | 16 + .../QRCode/UI/CameraViewController.swift | 144 +++++++ .../WalletDemo2/QRCode/UI/QRScannerView.swift | 53 +++ .../WalletDemo2/Settings/SettingsView.swift | 47 +++ .../Settings/SettingsViewRouter.swift | 25 ++ .../Settings/SettingsViewState.swift | 12 + 77 files changed, 3426 insertions(+), 154 deletions(-) create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/ActivityView.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/AtalaButton.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/ButtonNavigationLink.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/CheckButton.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/ClosableSheet.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/DeleteView.swift rename Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/{ => AtalaSwiftUIComponents/CommonUI}/EmptyNavigationLink.swift (100%) create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/ErrorDialogView.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/ErrorTextBox.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/FlowSuccessfulView.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/HyperlinkText.swift rename Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/{ => AtalaSwiftUIComponents/CommonUI}/LazyView.swift (100%) create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/PinInsertView.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/SearchBoxView.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/SelectCheckButton.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/WebView.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/WordTagEditable.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/WordTagGrid.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/WordsTextFieldView.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Environment/IsLoadingEnvironmentValue.swift rename Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/{ => AtalaSwiftUIComponents/Environment}/RootPresentationMode.swift (100%) rename Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/{ => AtalaSwiftUIComponents}/FancyToast.swift (100%) rename Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/{ => AtalaSwiftUIComponents}/FancyToastModifier.swift (100%) create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/AtalaNavigationBackButton.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/Button+Configurations.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/ClearFullCoverModifier.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/LoadingModifier.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/NavigationBarUtilModifiers.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/RoundedRectBorderWithText.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/NavigationUtils.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/PreferenceHelpers/DisablePreferenceKey.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/PreferenceHelpers/IntrinsicSizePreferenceKey.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Shapes/SpecificRoundedRect.swift rename Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/{Array+Zipped.swift => SwiftTools/Array+Helpers.swift} (100%) rename Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/{ => SwiftTools}/Builder.swift (100%) create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/SwiftTools/Combine+Drop.swift rename Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/{ => SwiftTools}/ComponentContainer.swift (100%) rename Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/{ => SwiftTools}/DIContainer.swift (100%) rename Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/{ => SwiftTools}/PrintObjects.swift (100%) rename Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/{ => SwiftTools}/String+extensions.swift (100%) create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/SwiftTools/URL+StringLiterals.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactBuilder.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactState.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactView.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactViewModel.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/UI/AlreadyConnectedView.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/UI/ConfirmConnectionView.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/UI/InsertCodeView.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Common/DisplayErrorState.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Connections/ConnectionsListRouter.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/RequestDetail/RequestDetailRouter.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/RequestDetail/RequestDetailView.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/RequestDetail/RequestDetailViewModel.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/RequestDetail/RequestDetailViewState.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/QRCode/QRCodeScannerBuilder.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/QRCode/QRCodeScannerRouter.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/QRCode/QRCodeScannerView.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/QRCode/QRCodeScannerViewModel.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/QRCode/UI/CameraViewController.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/QRCode/UI/QRScannerView.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Settings/SettingsView.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Settings/SettingsViewRouter.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Settings/SettingsViewState.swift diff --git a/AtalaPrismSDK/Pollux/Sources/Operation/Anoncreds/CreateAnoncredCredentialRequest.swift b/AtalaPrismSDK/Pollux/Sources/Operation/Anoncreds/CreateAnoncredCredentialRequest.swift index 8f5412f2..e6736c47 100644 --- a/AtalaPrismSDK/Pollux/Sources/Operation/Anoncreds/CreateAnoncredCredentialRequest.swift +++ b/AtalaPrismSDK/Pollux/Sources/Operation/Anoncreds/CreateAnoncredCredentialRequest.swift @@ -39,7 +39,7 @@ struct CreateAnoncredCredentialRequest { let linkSecretObj = try LinkSecret.newFromValue(valueString: linkSecret) let offer = try CredentialOffer(jsonString: String(data: offerData, encoding: .utf8)!) let credDefId = offer.getCredDefId() - + let credentialDefinitionData = try await credentialDefinitionDownloader.downloadFromEndpoint(urlOrDID: credDefId) let credentialDefinitionJson = try credentialDefinitionData.toString() let credentialDefinition = try CredentialDefinition(jsonString: credentialDefinitionJson) @@ -53,7 +53,7 @@ struct CreateAnoncredCredentialRequest { credentialOffer: offer ) - guard + guard let metadata = try requestData.metadata.getJson().data(using: .utf8) else { throw CommonError.invalidCoding(message: "Could not decode to data") diff --git a/AtalaPrismSDK/PrismAgent/Sources/PrismAgent.swift b/AtalaPrismSDK/PrismAgent/Sources/PrismAgent.swift index 9d440af5..e77c1e57 100644 --- a/AtalaPrismSDK/PrismAgent/Sources/PrismAgent.swift +++ b/AtalaPrismSDK/PrismAgent/Sources/PrismAgent.swift @@ -111,7 +111,7 @@ public class PrismAgent { castor: castor, secretsStream: secretsStream ).build() - + let seed = seedData.map { Seed(value: $0) } ?? apollo.createRandomSeed().seed self.init( apollo: apollo, @@ -193,7 +193,7 @@ public class PrismAgent { state = .stoped logger.info(message: "Agent not running") } - + private func firstLinkSecretSetup() async throws { if try await pluto.getLinkSecret().first().await() == nil { let secret = try apollo.createNewLinkSecret() diff --git a/Core/Sources/Helpers/Map+AsyncAwait.swift b/Core/Sources/Helpers/Map+AsyncAwait.swift index dd1437a2..434667f3 100644 --- a/Core/Sources/Helpers/Map+AsyncAwait.swift +++ b/Core/Sources/Helpers/Map+AsyncAwait.swift @@ -10,4 +10,26 @@ public extension Sequence { return values } + + func asyncCompactMap( + _ transform: (Element) async throws -> T? + ) async rethrows -> [T] { + var values = [T]() + + for element in self { + if let value = try await transform(element) { + values.append(value) + } + } + + return values + } + + func asyncForEach( + _ process: (Element) async throws -> Void + ) async rethrows { + for element in self { + try await process(element) + } + } } diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo.xcodeproj/project.pbxproj b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo.xcodeproj/project.pbxproj index e74efbc1..dfccd27b 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo.xcodeproj/project.pbxproj +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo.xcodeproj/project.pbxproj @@ -29,10 +29,6 @@ EE6C38F029468196006CD2D3 /* DIContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE6C38EF29468196006CD2D3 /* DIContainer.swift */; }; EE6C39012946827B006CD2D3 /* ComponentContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE6C39002946827B006CD2D3 /* ComponentContainer.swift */; }; EE6C390329468288006CD2D3 /* Builder.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE6C390229468288006CD2D3 /* Builder.swift */; }; - EE6C390529468309006CD2D3 /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE6C390429468309006CD2D3 /* LazyView.swift */; }; - EE6C39072946834F006CD2D3 /* RootPresentationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE6C39062946834F006CD2D3 /* RootPresentationMode.swift */; }; - EE6C393C29468614006CD2D3 /* EmptyNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE6C393B29468614006CD2D3 /* EmptyNavigationLink.swift */; }; - EE6C39402946931E006CD2D3 /* Array+Zipped.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE6C393F2946931E006CD2D3 /* Array+Zipped.swift */; }; EE6C7F4B29C2357A00D866AD /* MediatorPageStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE6C7F4A29C2357A00D866AD /* MediatorPageStateView.swift */; }; EE6C7F4D29C2367400D866AD /* MediatorPageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE6C7F4C29C2367400D866AD /* MediatorPageViewModel.swift */; }; EE6C7F5129C23A3400D866AD /* MediatorPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE6C7F5029C23A3400D866AD /* MediatorPageView.swift */; }; @@ -44,6 +40,62 @@ EE75148029C378DB00FFFAA4 /* DIDDetailViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE75147F29C378DB00FFFAA4 /* DIDDetailViewState.swift */; }; EE75A2F729479488007D4405 /* FancyToast.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE75A2F629479488007D4405 /* FancyToast.swift */; }; EE75A2F9294794F9007D4405 /* FancyToastModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE75A2F8294794F8007D4405 /* FancyToastModifier.swift */; }; + EE8F37492B87498700EC0638 /* RequestDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE8F37482B87498700EC0638 /* RequestDetailView.swift */; }; + EE8F374B2B87499400EC0638 /* RequestDetailViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE8F374A2B87499400EC0638 /* RequestDetailViewState.swift */; }; + EE8F374D2B8749A400EC0638 /* RequestDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE8F374C2B8749A400EC0638 /* RequestDetailViewModel.swift */; }; + EE8F374F2B8749AF00EC0638 /* RequestDetailRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE8F374E2B8749AF00EC0638 /* RequestDetailRouter.swift */; }; + EE8F37522B87739D00EC0638 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE8F37512B87739D00EC0638 /* SettingsView.swift */; }; + EE8F37562B8773F100EC0638 /* SettingsViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE8F37552B8773F100EC0638 /* SettingsViewState.swift */; }; + EE8F37582B87743D00EC0638 /* SettingsViewRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE8F37572B87743D00EC0638 /* SettingsViewRouter.swift */; }; + EE92C3CE2B7E1C0D00FC0B6E /* QRCodeScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3C72B7E1C0D00FC0B6E /* QRCodeScannerView.swift */; }; + EE92C3CF2B7E1C0D00FC0B6E /* QRScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3C92B7E1C0D00FC0B6E /* QRScannerView.swift */; }; + EE92C3D02B7E1C0D00FC0B6E /* CameraViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3CA2B7E1C0D00FC0B6E /* CameraViewController.swift */; }; + EE92C3D12B7E1C0D00FC0B6E /* QRCodeScannerBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3CB2B7E1C0D00FC0B6E /* QRCodeScannerBuilder.swift */; }; + EE92C3D22B7E1C0D00FC0B6E /* QRCodeScannerRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3CC2B7E1C0D00FC0B6E /* QRCodeScannerRouter.swift */; }; + EE92C3D32B7E1C0D00FC0B6E /* QRCodeScannerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3CD2B7E1C0D00FC0B6E /* QRCodeScannerViewModel.swift */; }; + EE92C4072B7E1CA200FC0B6E /* RoundedRectBorderWithText.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3D72B7E1CA200FC0B6E /* RoundedRectBorderWithText.swift */; }; + EE92C4082B7E1CA200FC0B6E /* Button+Configurations.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3D82B7E1CA200FC0B6E /* Button+Configurations.swift */; }; + EE92C40A2B7E1CA200FC0B6E /* NavigationBarUtilModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3DA2B7E1CA200FC0B6E /* NavigationBarUtilModifiers.swift */; }; + EE92C40C2B7E1CA200FC0B6E /* ClearFullCoverModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3DC2B7E1CA200FC0B6E /* ClearFullCoverModifier.swift */; }; + EE92C40D2B7E1CA200FC0B6E /* AtalaNavigationBackButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3DD2B7E1CA200FC0B6E /* AtalaNavigationBackButton.swift */; }; + EE92C40F2B7E1CA200FC0B6E /* LoadingModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3DF2B7E1CA200FC0B6E /* LoadingModifier.swift */; }; + EE92C4102B7E1CA200FC0B6E /* SpecificRoundedRect.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3E12B7E1CA200FC0B6E /* SpecificRoundedRect.swift */; }; + EE92C4112B7E1CA200FC0B6E /* ClosableSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3E32B7E1CA200FC0B6E /* ClosableSheet.swift */; }; + EE92C4122B7E1CA200FC0B6E /* ErrorDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3E42B7E1CA200FC0B6E /* ErrorDialogView.swift */; }; + EE92C4142B7E1CA200FC0B6E /* EmptyNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3E62B7E1CA200FC0B6E /* EmptyNavigationLink.swift */; }; + EE92C4152B7E1CA200FC0B6E /* CheckButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3E72B7E1CA200FC0B6E /* CheckButton.swift */; }; + EE92C4162B7E1CA200FC0B6E /* SelectCheckButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3E82B7E1CA200FC0B6E /* SelectCheckButton.swift */; }; + EE92C4172B7E1CA200FC0B6E /* FlowSuccessfulView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3E92B7E1CA200FC0B6E /* FlowSuccessfulView.swift */; }; + EE92C4182B7E1CA200FC0B6E /* PinInsertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3EA2B7E1CA200FC0B6E /* PinInsertView.swift */; }; + EE92C41A2B7E1CA200FC0B6E /* ButtonNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3EC2B7E1CA200FC0B6E /* ButtonNavigationLink.swift */; }; + EE92C41B2B7E1CA200FC0B6E /* WordTagGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3ED2B7E1CA200FC0B6E /* WordTagGrid.swift */; }; + EE92C41C2B7E1CA200FC0B6E /* HyperlinkText.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3EE2B7E1CA200FC0B6E /* HyperlinkText.swift */; }; + EE92C41D2B7E1CA200FC0B6E /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3EF2B7E1CA200FC0B6E /* LazyView.swift */; }; + EE92C41E2B7E1CA200FC0B6E /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3F02B7E1CA200FC0B6E /* WebView.swift */; }; + EE92C4202B7E1CA200FC0B6E /* WordsTextFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3F22B7E1CA200FC0B6E /* WordsTextFieldView.swift */; }; + EE92C4212B7E1CA200FC0B6E /* ErrorTextBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3F32B7E1CA200FC0B6E /* ErrorTextBox.swift */; }; + EE92C4222B7E1CA200FC0B6E /* WordTagEditable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3F42B7E1CA200FC0B6E /* WordTagEditable.swift */; }; + EE92C4232B7E1CA200FC0B6E /* DeleteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3F52B7E1CA200FC0B6E /* DeleteView.swift */; }; + EE92C4242B7E1CA200FC0B6E /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3F62B7E1CA200FC0B6E /* ActivityView.swift */; }; + EE92C4252B7E1CA200FC0B6E /* AtalaButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3F72B7E1CA200FC0B6E /* AtalaButton.swift */; }; + EE92C4262B7E1CA200FC0B6E /* SearchBoxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3F82B7E1CA200FC0B6E /* SearchBoxView.swift */; }; + EE92C4282B7E1CA200FC0B6E /* RootPresentationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3FB2B7E1CA200FC0B6E /* RootPresentationMode.swift */; }; + EE92C4292B7E1CA200FC0B6E /* IsLoadingEnvironmentValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3FC2B7E1CA200FC0B6E /* IsLoadingEnvironmentValue.swift */; }; + EE92C42A2B7E1CA200FC0B6E /* IntrinsicSizePreferenceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3FE2B7E1CA200FC0B6E /* IntrinsicSizePreferenceKey.swift */; }; + EE92C42B2B7E1CA200FC0B6E /* DisablePreferenceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C3FF2B7E1CA200FC0B6E /* DisablePreferenceKey.swift */; }; + EE92C42C2B7E1CA200FC0B6E /* NavigationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C4002B7E1CA200FC0B6E /* NavigationUtils.swift */; }; + EE92C42E2B7E1CA200FC0B6E /* URL+StringLiterals.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C4032B7E1CA200FC0B6E /* URL+StringLiterals.swift */; }; + EE92C42F2B7E1CA200FC0B6E /* Combine+Drop.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C4042B7E1CA200FC0B6E /* Combine+Drop.swift */; }; + EE92C4302B7E1CA200FC0B6E /* Array+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C4052B7E1CA200FC0B6E /* Array+Helpers.swift */; }; + EE92C43B2B7E1F6A00FC0B6E /* AddNewContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C4332B7E1F6A00FC0B6E /* AddNewContactView.swift */; }; + EE92C43C2B7E1F6A00FC0B6E /* AddNewContactViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C4342B7E1F6A00FC0B6E /* AddNewContactViewModel.swift */; }; + EE92C43D2B7E1F6A00FC0B6E /* AlreadyConnectedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C4362B7E1F6A00FC0B6E /* AlreadyConnectedView.swift */; }; + EE92C43E2B7E1F6A00FC0B6E /* ConfirmConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C4372B7E1F6A00FC0B6E /* ConfirmConnectionView.swift */; }; + EE92C43F2B7E1F6A00FC0B6E /* InsertCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C4382B7E1F6A00FC0B6E /* InsertCodeView.swift */; }; + EE92C4402B7E1F6A00FC0B6E /* AddNewContactState.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C4392B7E1F6A00FC0B6E /* AddNewContactState.swift */; }; + EE92C4412B7E1F6A00FC0B6E /* AddNewContactBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C43A2B7E1F6A00FC0B6E /* AddNewContactBuilder.swift */; }; + EE92C4442B7E2BAB00FC0B6E /* DisplayErrorState.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C4432B7E2BAB00FC0B6E /* DisplayErrorState.swift */; }; + EE92C4462B7E2D8500FC0B6E /* ConnectionsListRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE92C4452B7E2D8500FC0B6E /* ConnectionsListRouter.swift */; }; EEB7D32229420180006E076D /* SetupPrismAgentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB7D32129420180006E076D /* SetupPrismAgentView.swift */; }; EEB7D3242942018C006E076D /* SetupPrismAgentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB7D3232942018C006E076D /* SetupPrismAgentViewModel.swift */; }; EEBC938D29C730FA0015A36E /* CredentialListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBC938C29C730FA0015A36E /* CredentialListView.swift */; }; @@ -91,10 +143,6 @@ EE6C38EF29468196006CD2D3 /* DIContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DIContainer.swift; sourceTree = ""; }; EE6C39002946827B006CD2D3 /* ComponentContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentContainer.swift; sourceTree = ""; }; EE6C390229468288006CD2D3 /* Builder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Builder.swift; sourceTree = ""; }; - EE6C390429468309006CD2D3 /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = ""; }; - EE6C39062946834F006CD2D3 /* RootPresentationMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootPresentationMode.swift; sourceTree = ""; }; - EE6C393B29468614006CD2D3 /* EmptyNavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyNavigationLink.swift; sourceTree = ""; }; - EE6C393F2946931E006CD2D3 /* Array+Zipped.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Zipped.swift"; sourceTree = ""; }; EE6C7F4A29C2357A00D866AD /* MediatorPageStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediatorPageStateView.swift; sourceTree = ""; }; EE6C7F4C29C2367400D866AD /* MediatorPageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediatorPageViewModel.swift; sourceTree = ""; }; EE6C7F5029C23A3400D866AD /* MediatorPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediatorPageView.swift; sourceTree = ""; }; @@ -106,6 +154,62 @@ EE75147F29C378DB00FFFAA4 /* DIDDetailViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DIDDetailViewState.swift; sourceTree = ""; }; EE75A2F629479488007D4405 /* FancyToast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FancyToast.swift; sourceTree = ""; }; EE75A2F8294794F8007D4405 /* FancyToastModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FancyToastModifier.swift; sourceTree = ""; }; + EE8F37482B87498700EC0638 /* RequestDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestDetailView.swift; sourceTree = ""; }; + EE8F374A2B87499400EC0638 /* RequestDetailViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestDetailViewState.swift; sourceTree = ""; }; + EE8F374C2B8749A400EC0638 /* RequestDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestDetailViewModel.swift; sourceTree = ""; }; + EE8F374E2B8749AF00EC0638 /* RequestDetailRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestDetailRouter.swift; sourceTree = ""; }; + EE8F37512B87739D00EC0638 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + EE8F37552B8773F100EC0638 /* SettingsViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewState.swift; sourceTree = ""; }; + EE8F37572B87743D00EC0638 /* SettingsViewRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewRouter.swift; sourceTree = ""; }; + EE92C3C72B7E1C0D00FC0B6E /* QRCodeScannerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRCodeScannerView.swift; sourceTree = ""; }; + EE92C3C92B7E1C0D00FC0B6E /* QRScannerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScannerView.swift; sourceTree = ""; }; + EE92C3CA2B7E1C0D00FC0B6E /* CameraViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraViewController.swift; sourceTree = ""; }; + EE92C3CB2B7E1C0D00FC0B6E /* QRCodeScannerBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRCodeScannerBuilder.swift; sourceTree = ""; }; + EE92C3CC2B7E1C0D00FC0B6E /* QRCodeScannerRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRCodeScannerRouter.swift; sourceTree = ""; }; + EE92C3CD2B7E1C0D00FC0B6E /* QRCodeScannerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRCodeScannerViewModel.swift; sourceTree = ""; }; + EE92C3D72B7E1CA200FC0B6E /* RoundedRectBorderWithText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoundedRectBorderWithText.swift; sourceTree = ""; }; + EE92C3D82B7E1CA200FC0B6E /* Button+Configurations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Button+Configurations.swift"; sourceTree = ""; }; + EE92C3DA2B7E1CA200FC0B6E /* NavigationBarUtilModifiers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationBarUtilModifiers.swift; sourceTree = ""; }; + EE92C3DC2B7E1CA200FC0B6E /* ClearFullCoverModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClearFullCoverModifier.swift; sourceTree = ""; }; + EE92C3DD2B7E1CA200FC0B6E /* AtalaNavigationBackButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AtalaNavigationBackButton.swift; sourceTree = ""; }; + EE92C3DF2B7E1CA200FC0B6E /* LoadingModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingModifier.swift; sourceTree = ""; }; + EE92C3E12B7E1CA200FC0B6E /* SpecificRoundedRect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpecificRoundedRect.swift; sourceTree = ""; }; + EE92C3E32B7E1CA200FC0B6E /* ClosableSheet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClosableSheet.swift; sourceTree = ""; }; + EE92C3E42B7E1CA200FC0B6E /* ErrorDialogView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorDialogView.swift; sourceTree = ""; }; + EE92C3E62B7E1CA200FC0B6E /* EmptyNavigationLink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyNavigationLink.swift; sourceTree = ""; }; + EE92C3E72B7E1CA200FC0B6E /* CheckButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckButton.swift; sourceTree = ""; }; + EE92C3E82B7E1CA200FC0B6E /* SelectCheckButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectCheckButton.swift; sourceTree = ""; }; + EE92C3E92B7E1CA200FC0B6E /* FlowSuccessfulView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlowSuccessfulView.swift; sourceTree = ""; }; + EE92C3EA2B7E1CA200FC0B6E /* PinInsertView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinInsertView.swift; sourceTree = ""; }; + EE92C3EC2B7E1CA200FC0B6E /* ButtonNavigationLink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonNavigationLink.swift; sourceTree = ""; }; + EE92C3ED2B7E1CA200FC0B6E /* WordTagGrid.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WordTagGrid.swift; sourceTree = ""; }; + EE92C3EE2B7E1CA200FC0B6E /* HyperlinkText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HyperlinkText.swift; sourceTree = ""; }; + EE92C3EF2B7E1CA200FC0B6E /* LazyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = ""; }; + EE92C3F02B7E1CA200FC0B6E /* WebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; + EE92C3F22B7E1CA200FC0B6E /* WordsTextFieldView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WordsTextFieldView.swift; sourceTree = ""; }; + EE92C3F32B7E1CA200FC0B6E /* ErrorTextBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorTextBox.swift; sourceTree = ""; }; + EE92C3F42B7E1CA200FC0B6E /* WordTagEditable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WordTagEditable.swift; sourceTree = ""; }; + EE92C3F52B7E1CA200FC0B6E /* DeleteView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteView.swift; sourceTree = ""; }; + EE92C3F62B7E1CA200FC0B6E /* ActivityView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; + EE92C3F72B7E1CA200FC0B6E /* AtalaButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AtalaButton.swift; sourceTree = ""; }; + EE92C3F82B7E1CA200FC0B6E /* SearchBoxView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchBoxView.swift; sourceTree = ""; }; + EE92C3FB2B7E1CA200FC0B6E /* RootPresentationMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootPresentationMode.swift; sourceTree = ""; }; + EE92C3FC2B7E1CA200FC0B6E /* IsLoadingEnvironmentValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IsLoadingEnvironmentValue.swift; sourceTree = ""; }; + EE92C3FE2B7E1CA200FC0B6E /* IntrinsicSizePreferenceKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntrinsicSizePreferenceKey.swift; sourceTree = ""; }; + EE92C3FF2B7E1CA200FC0B6E /* DisablePreferenceKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisablePreferenceKey.swift; sourceTree = ""; }; + EE92C4002B7E1CA200FC0B6E /* NavigationUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationUtils.swift; sourceTree = ""; }; + EE92C4032B7E1CA200FC0B6E /* URL+StringLiterals.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+StringLiterals.swift"; sourceTree = ""; }; + EE92C4042B7E1CA200FC0B6E /* Combine+Drop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Combine+Drop.swift"; sourceTree = ""; }; + EE92C4052B7E1CA200FC0B6E /* Array+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Helpers.swift"; sourceTree = ""; }; + EE92C4332B7E1F6A00FC0B6E /* AddNewContactView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddNewContactView.swift; sourceTree = ""; }; + EE92C4342B7E1F6A00FC0B6E /* AddNewContactViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddNewContactViewModel.swift; sourceTree = ""; }; + EE92C4362B7E1F6A00FC0B6E /* AlreadyConnectedView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlreadyConnectedView.swift; sourceTree = ""; }; + EE92C4372B7E1F6A00FC0B6E /* ConfirmConnectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfirmConnectionView.swift; sourceTree = ""; }; + EE92C4382B7E1F6A00FC0B6E /* InsertCodeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsertCodeView.swift; sourceTree = ""; }; + EE92C4392B7E1F6A00FC0B6E /* AddNewContactState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddNewContactState.swift; sourceTree = ""; }; + EE92C43A2B7E1F6A00FC0B6E /* AddNewContactBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddNewContactBuilder.swift; sourceTree = ""; }; + EE92C4432B7E2BAB00FC0B6E /* DisplayErrorState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayErrorState.swift; sourceTree = ""; }; + EE92C4452B7E2D8500FC0B6E /* ConnectionsListRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionsListRouter.swift; sourceTree = ""; }; EEB7D32129420180006E076D /* SetupPrismAgentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupPrismAgentView.swift; sourceTree = ""; }; EEB7D3232942018C006E076D /* SetupPrismAgentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupPrismAgentViewModel.swift; sourceTree = ""; }; EEBC938C29C730FA0015A36E /* CredentialListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialListView.swift; sourceTree = ""; }; @@ -210,6 +314,10 @@ EE6C7F4729C234C200D866AD /* WalletDemo2 */ = { isa = PBXGroup; children = ( + EE8F37502B87738F00EC0638 /* Settings */, + EE92C4422B7E2B9D00FC0B6E /* Common */, + EE92C4322B7E1F2E00FC0B6E /* AddNewContact */, + EE92C3C62B7E1BB400FC0B6E /* QRCode */, EEBC938929C730900015A36E /* Credentials */, EEECB28929C298DC00BBB4B9 /* Messages */, EEECB28429C28A6E00BBB4B9 /* Main */, @@ -267,6 +375,173 @@ path = DIDList; sourceTree = ""; }; + EE8F37472B87496900EC0638 /* RequestDetail */ = { + isa = PBXGroup; + children = ( + EE8F37482B87498700EC0638 /* RequestDetailView.swift */, + EE8F374A2B87499400EC0638 /* RequestDetailViewState.swift */, + EE8F374C2B8749A400EC0638 /* RequestDetailViewModel.swift */, + EE8F374E2B8749AF00EC0638 /* RequestDetailRouter.swift */, + ); + path = RequestDetail; + sourceTree = ""; + }; + EE8F37502B87738F00EC0638 /* Settings */ = { + isa = PBXGroup; + children = ( + EE8F37512B87739D00EC0638 /* SettingsView.swift */, + EE8F37552B8773F100EC0638 /* SettingsViewState.swift */, + EE8F37572B87743D00EC0638 /* SettingsViewRouter.swift */, + ); + path = Settings; + sourceTree = ""; + }; + EE92C3C62B7E1BB400FC0B6E /* QRCode */ = { + isa = PBXGroup; + children = ( + EE92C3CB2B7E1C0D00FC0B6E /* QRCodeScannerBuilder.swift */, + EE92C3CC2B7E1C0D00FC0B6E /* QRCodeScannerRouter.swift */, + EE92C3C72B7E1C0D00FC0B6E /* QRCodeScannerView.swift */, + EE92C3CD2B7E1C0D00FC0B6E /* QRCodeScannerViewModel.swift */, + EE92C3C82B7E1C0D00FC0B6E /* UI */, + ); + path = QRCode; + sourceTree = ""; + }; + EE92C3C82B7E1C0D00FC0B6E /* UI */ = { + isa = PBXGroup; + children = ( + EE92C3C92B7E1C0D00FC0B6E /* QRScannerView.swift */, + EE92C3CA2B7E1C0D00FC0B6E /* CameraViewController.swift */, + ); + path = UI; + sourceTree = ""; + }; + EE92C3D52B7E1CA200FC0B6E /* AtalaSwiftUIComponents */ = { + isa = PBXGroup; + children = ( + EE75A2F629479488007D4405 /* FancyToast.swift */, + EE75A2F8294794F8007D4405 /* FancyToastModifier.swift */, + EE92C3D62B7E1CA200FC0B6E /* Modifiers */, + EE92C3E02B7E1CA200FC0B6E /* Shapes */, + EE92C3E22B7E1CA200FC0B6E /* CommonUI */, + EE92C3FA2B7E1CA200FC0B6E /* Environment */, + EE92C3FD2B7E1CA200FC0B6E /* PreferenceHelpers */, + EE92C4002B7E1CA200FC0B6E /* NavigationUtils.swift */, + ); + path = AtalaSwiftUIComponents; + sourceTree = ""; + }; + EE92C3D62B7E1CA200FC0B6E /* Modifiers */ = { + isa = PBXGroup; + children = ( + EE92C3D72B7E1CA200FC0B6E /* RoundedRectBorderWithText.swift */, + EE92C3D82B7E1CA200FC0B6E /* Button+Configurations.swift */, + EE92C3DA2B7E1CA200FC0B6E /* NavigationBarUtilModifiers.swift */, + EE92C3DC2B7E1CA200FC0B6E /* ClearFullCoverModifier.swift */, + EE92C3DD2B7E1CA200FC0B6E /* AtalaNavigationBackButton.swift */, + EE92C3DF2B7E1CA200FC0B6E /* LoadingModifier.swift */, + ); + path = Modifiers; + sourceTree = ""; + }; + EE92C3E02B7E1CA200FC0B6E /* Shapes */ = { + isa = PBXGroup; + children = ( + EE92C3E12B7E1CA200FC0B6E /* SpecificRoundedRect.swift */, + ); + path = Shapes; + sourceTree = ""; + }; + EE92C3E22B7E1CA200FC0B6E /* CommonUI */ = { + isa = PBXGroup; + children = ( + EE92C3E32B7E1CA200FC0B6E /* ClosableSheet.swift */, + EE92C3E42B7E1CA200FC0B6E /* ErrorDialogView.swift */, + EE92C3E62B7E1CA200FC0B6E /* EmptyNavigationLink.swift */, + EE92C3E72B7E1CA200FC0B6E /* CheckButton.swift */, + EE92C3E82B7E1CA200FC0B6E /* SelectCheckButton.swift */, + EE92C3E92B7E1CA200FC0B6E /* FlowSuccessfulView.swift */, + EE92C3EA2B7E1CA200FC0B6E /* PinInsertView.swift */, + EE92C3EC2B7E1CA200FC0B6E /* ButtonNavigationLink.swift */, + EE92C3ED2B7E1CA200FC0B6E /* WordTagGrid.swift */, + EE92C3EE2B7E1CA200FC0B6E /* HyperlinkText.swift */, + EE92C3EF2B7E1CA200FC0B6E /* LazyView.swift */, + EE92C3F02B7E1CA200FC0B6E /* WebView.swift */, + EE92C3F22B7E1CA200FC0B6E /* WordsTextFieldView.swift */, + EE92C3F32B7E1CA200FC0B6E /* ErrorTextBox.swift */, + EE92C3F42B7E1CA200FC0B6E /* WordTagEditable.swift */, + EE92C3F52B7E1CA200FC0B6E /* DeleteView.swift */, + EE92C3F62B7E1CA200FC0B6E /* ActivityView.swift */, + EE92C3F72B7E1CA200FC0B6E /* AtalaButton.swift */, + EE92C3F82B7E1CA200FC0B6E /* SearchBoxView.swift */, + ); + path = CommonUI; + sourceTree = ""; + }; + EE92C3FA2B7E1CA200FC0B6E /* Environment */ = { + isa = PBXGroup; + children = ( + EE92C3FB2B7E1CA200FC0B6E /* RootPresentationMode.swift */, + EE92C3FC2B7E1CA200FC0B6E /* IsLoadingEnvironmentValue.swift */, + ); + path = Environment; + sourceTree = ""; + }; + EE92C3FD2B7E1CA200FC0B6E /* PreferenceHelpers */ = { + isa = PBXGroup; + children = ( + EE92C3FE2B7E1CA200FC0B6E /* IntrinsicSizePreferenceKey.swift */, + EE92C3FF2B7E1CA200FC0B6E /* DisablePreferenceKey.swift */, + ); + path = PreferenceHelpers; + sourceTree = ""; + }; + EE92C4012B7E1CA200FC0B6E /* SwiftTools */ = { + isa = PBXGroup; + children = ( + EEE620122937F1D40053AE52 /* PrintObjects.swift */, + EE6C38DB294626E1006CD2D3 /* String+extensions.swift */, + EE6C38EF29468196006CD2D3 /* DIContainer.swift */, + EE6C39002946827B006CD2D3 /* ComponentContainer.swift */, + EE6C390229468288006CD2D3 /* Builder.swift */, + EE92C4032B7E1CA200FC0B6E /* URL+StringLiterals.swift */, + EE92C4042B7E1CA200FC0B6E /* Combine+Drop.swift */, + EE92C4052B7E1CA200FC0B6E /* Array+Helpers.swift */, + ); + path = SwiftTools; + sourceTree = ""; + }; + EE92C4322B7E1F2E00FC0B6E /* AddNewContact */ = { + isa = PBXGroup; + children = ( + EE92C43A2B7E1F6A00FC0B6E /* AddNewContactBuilder.swift */, + EE92C4392B7E1F6A00FC0B6E /* AddNewContactState.swift */, + EE92C4332B7E1F6A00FC0B6E /* AddNewContactView.swift */, + EE92C4342B7E1F6A00FC0B6E /* AddNewContactViewModel.swift */, + EE92C4352B7E1F6A00FC0B6E /* UI */, + ); + path = AddNewContact; + sourceTree = ""; + }; + EE92C4352B7E1F6A00FC0B6E /* UI */ = { + isa = PBXGroup; + children = ( + EE92C4362B7E1F6A00FC0B6E /* AlreadyConnectedView.swift */, + EE92C4372B7E1F6A00FC0B6E /* ConfirmConnectionView.swift */, + EE92C4382B7E1F6A00FC0B6E /* InsertCodeView.swift */, + ); + path = UI; + sourceTree = ""; + }; + EE92C4422B7E2B9D00FC0B6E /* Common */ = { + isa = PBXGroup; + children = ( + EE92C4432B7E2BAB00FC0B6E /* DisplayErrorState.swift */, + ); + path = Common; + sourceTree = ""; + }; EEB7D32029420155006E076D /* SetupPrismAgent */ = { isa = PBXGroup; children = ( @@ -279,6 +554,7 @@ EEBC938929C730900015A36E /* Credentials */ = { isa = PBXGroup; children = ( + EE8F37472B87496900EC0638 /* RequestDetail */, EE549F452ACC1F400038ED1D /* CredentialDetail */, EEBC939229C7354E0015A36E /* CredentialsList */, ); @@ -377,17 +653,8 @@ EEE620112937F1C30053AE52 /* Helper */ = { isa = PBXGroup; children = ( - EEE620122937F1D40053AE52 /* PrintObjects.swift */, - EE6C38DB294626E1006CD2D3 /* String+extensions.swift */, - EE6C38EF29468196006CD2D3 /* DIContainer.swift */, - EE6C39002946827B006CD2D3 /* ComponentContainer.swift */, - EE6C390229468288006CD2D3 /* Builder.swift */, - EE6C390429468309006CD2D3 /* LazyView.swift */, - EE6C39062946834F006CD2D3 /* RootPresentationMode.swift */, - EE6C393B29468614006CD2D3 /* EmptyNavigationLink.swift */, - EE6C393F2946931E006CD2D3 /* Array+Zipped.swift */, - EE75A2F629479488007D4405 /* FancyToast.swift */, - EE75A2F8294794F8007D4405 /* FancyToastModifier.swift */, + EE92C4012B7E1CA200FC0B6E /* SwiftTools */, + EE92C3D52B7E1CA200FC0B6E /* AtalaSwiftUIComponents */, ); path = Helper; sourceTree = ""; @@ -416,6 +683,7 @@ EEECB27E29C282A800BBB4B9 /* ConnectionsListView.swift */, EEECB28029C282D500BBB4B9 /* ConnectionsViewState.swift */, EEECB28229C2831E00BBB4B9 /* ConnectionsListViewModel.swift */, + EE92C4452B7E2D8500FC0B6E /* ConnectionsListRouter.swift */, ); path = Connections; sourceTree = ""; @@ -548,40 +816,80 @@ buildActionMask = 2147483647; files = ( EEECB29029C29F0700BBB4B9 /* MessagesListViewModel.swift in Sources */, + EE8F374F2B8749AF00EC0638 /* RequestDetailRouter.swift in Sources */, + EE92C42C2B7E1CA200FC0B6E /* NavigationUtils.swift in Sources */, + EE92C40F2B7E1CA200FC0B6E /* LoadingModifier.swift in Sources */, + EE92C4282B7E1CA200FC0B6E /* RootPresentationMode.swift in Sources */, EEBC939729C737DE0015A36E /* CredentialListRouter.swift in Sources */, + EE92C42F2B7E1CA200FC0B6E /* Combine+Drop.swift in Sources */, EEB7D3242942018C006E076D /* SetupPrismAgentViewModel.swift in Sources */, + EE92C43C2B7E1F6A00FC0B6E /* AddNewContactViewModel.swift in Sources */, EEE620132937F1D40053AE52 /* PrintObjects.swift in Sources */, EEECB29929C309FC00BBB4B9 /* MessageDetailViewModel.swift in Sources */, + EE92C42E2B7E1CA200FC0B6E /* URL+StringLiterals.swift in Sources */, EEECB28C29C29A5400BBB4B9 /* MessagesListView.swift in Sources */, + EE8F37562B8773F100EC0638 /* SettingsViewState.swift in Sources */, EEE620302937FDE50053AE52 /* AuthenticateWalletViewModel.swift in Sources */, - EE6C390529468309006CD2D3 /* LazyView.swift in Sources */, + EE92C40C2B7E1CA200FC0B6E /* ClearFullCoverModifier.swift in Sources */, + EE92C41B2B7E1CA200FC0B6E /* WordTagGrid.swift in Sources */, EE75147E29C376E700FFFAA4 /* DIDDetailView.swift in Sources */, EEE61FE02937CEAA0053AE52 /* SeedViewModel.swift in Sources */, + EE92C41E2B7E1CA200FC0B6E /* WebView.swift in Sources */, EEBC939529C735910015A36E /* CredentialListViewModel.swift in Sources */, EE6C38DC294626E1006CD2D3 /* String+extensions.swift in Sources */, + EE92C40A2B7E1CA200FC0B6E /* NavigationBarUtilModifiers.swift in Sources */, + EE92C4262B7E1CA200FC0B6E /* SearchBoxView.swift in Sources */, + EE92C4462B7E2D8500FC0B6E /* ConnectionsListRouter.swift in Sources */, + EE92C41A2B7E1CA200FC0B6E /* ButtonNavigationLink.swift in Sources */, + EE92C42A2B7E1CA200FC0B6E /* IntrinsicSizePreferenceKey.swift in Sources */, + EE92C40D2B7E1CA200FC0B6E /* AtalaNavigationBackButton.swift in Sources */, + EE92C4172B7E1CA200FC0B6E /* FlowSuccessfulView.swift in Sources */, + EE92C43F2B7E1F6A00FC0B6E /* InsertCodeView.swift in Sources */, EEE61FE82937D7EE0053AE52 /* DIDFuncionalitiesView.swift in Sources */, EEE61FBC2937CA280053AE52 /* FuncionalitiesList.swift in Sources */, EEE61FDD2937CD7A0053AE52 /* SeedFuncionalitiesView.swift in Sources */, EE75A2F9294794F9007D4405 /* FancyToastModifier.swift in Sources */, EE549F492ACC1F7D0038ED1D /* CredentialDetailViewState.swift in Sources */, + EE92C43E2B7E1F6A00FC0B6E /* ConfirmConnectionView.swift in Sources */, + EE8F37522B87739D00EC0638 /* SettingsView.swift in Sources */, + EE92C3D12B7E1C0D00FC0B6E /* QRCodeScannerBuilder.swift in Sources */, + EE92C3D02B7E1C0D00FC0B6E /* CameraViewController.swift in Sources */, + EE92C3CE2B7E1C0D00FC0B6E /* QRCodeScannerView.swift in Sources */, + EE8F37582B87743D00EC0638 /* SettingsViewRouter.swift in Sources */, EE6C7F5429C2754500D866AD /* DIDListView.swift in Sources */, EE6C7F4B29C2357A00D866AD /* MediatorPageStateView.swift in Sources */, EEECB29229C2A37700BBB4B9 /* MessagesListRouter.swift in Sources */, + EE92C4182B7E1CA200FC0B6E /* PinInsertView.swift in Sources */, EE75148029C378DB00FFFAA4 /* DIDDetailViewState.swift in Sources */, + EE92C42B2B7E1CA200FC0B6E /* DisablePreferenceKey.swift in Sources */, EE6C7F5129C23A3400D866AD /* MediatorPageView.swift in Sources */, + EE92C43D2B7E1F6A00FC0B6E /* AlreadyConnectedView.swift in Sources */, EE75A2F729479488007D4405 /* FancyToast.swift in Sources */, EE75147C29C3766800FFFAA4 /* DIDDetailViewModel.swift in Sources */, + EE92C4302B7E1CA200FC0B6E /* Array+Helpers.swift in Sources */, + EE92C3D32B7E1C0D00FC0B6E /* QRCodeScannerViewModel.swift in Sources */, EE6C7F5629C2758100D866AD /* DIDListViewState.swift in Sources */, - EE6C393C29468614006CD2D3 /* EmptyNavigationLink.swift in Sources */, + EE92C4162B7E1CA200FC0B6E /* SelectCheckButton.swift in Sources */, + EE92C4072B7E1CA200FC0B6E /* RoundedRectBorderWithText.swift in Sources */, EE6C39012946827B006CD2D3 /* ComponentContainer.swift in Sources */, EEE61FE42937D5560053AE52 /* DIDFuncionalitiesViewModel.swift in Sources */, EEE6202F2937FDE50053AE52 /* AuthenticateWalletView.swift in Sources */, EEE620182937F6870053AE52 /* SigningVerificationView.swift in Sources */, + EE92C3CF2B7E1C0D00FC0B6E /* QRScannerView.swift in Sources */, EEE61FBA2937CA280053AE52 /* AtalaPrismWalletDemoApp.swift in Sources */, + EE92C4122B7E1CA200FC0B6E /* ErrorDialogView.swift in Sources */, + EE92C4152B7E1CA200FC0B6E /* CheckButton.swift in Sources */, + EE92C41D2B7E1CA200FC0B6E /* LazyView.swift in Sources */, EE2D40972ACC470100CF9446 /* CredentialDetailViewModel.swift in Sources */, + EE92C4202B7E1CA200FC0B6E /* WordsTextFieldView.swift in Sources */, + EE92C4442B7E2BAB00FC0B6E /* DisplayErrorState.swift in Sources */, EEECB28129C282D500BBB4B9 /* ConnectionsViewState.swift in Sources */, - EE6C39402946931E006CD2D3 /* Array+Zipped.swift in Sources */, + EE92C3D22B7E1C0D00FC0B6E /* QRCodeScannerRouter.swift in Sources */, + EE92C4212B7E1CA200FC0B6E /* ErrorTextBox.swift in Sources */, EEECB27F29C282A800BBB4B9 /* ConnectionsListView.swift in Sources */, + EE92C4232B7E1CA200FC0B6E /* DeleteView.swift in Sources */, + EE8F374D2B8749A400EC0638 /* RequestDetailViewModel.swift in Sources */, + EE8F374B2B87499400EC0638 /* RequestDetailViewState.swift in Sources */, EEECB29529C2A48B00BBB4B9 /* MessageDetailView.swift in Sources */, EE6C390329468288006CD2D3 /* Builder.swift in Sources */, EEECB29729C2A4AA00BBB4B9 /* MessageDetailViewState.swift in Sources */, @@ -589,15 +897,27 @@ EE6C7F5829C275C400D866AD /* DIDListViewModel.swift in Sources */, EE6C7F4D29C2367400D866AD /* MediatorPageViewModel.swift in Sources */, EEBC938D29C730FA0015A36E /* CredentialListView.swift in Sources */, + EE92C4402B7E1F6A00FC0B6E /* AddNewContactState.swift in Sources */, EEECB28329C2831E00BBB4B9 /* ConnectionsListViewModel.swift in Sources */, + EE8F37492B87498700EC0638 /* RequestDetailView.swift in Sources */, + EE92C41C2B7E1CA200FC0B6E /* HyperlinkText.swift in Sources */, EEBC938F29C7311C0015A36E /* CredentialListViewState.swift in Sources */, + EE92C4242B7E1CA200FC0B6E /* ActivityView.swift in Sources */, + EE92C4112B7E1CA200FC0B6E /* ClosableSheet.swift in Sources */, + EE92C4082B7E1CA200FC0B6E /* Button+Configurations.swift in Sources */, + EE92C43B2B7E1F6A00FC0B6E /* AddNewContactView.swift in Sources */, EEECB28829C28CA100BBB4B9 /* Main2Router.swift in Sources */, + EE92C4222B7E1CA200FC0B6E /* WordTagEditable.swift in Sources */, + EE92C4252B7E1CA200FC0B6E /* AtalaButton.swift in Sources */, + EE92C4292B7E1CA200FC0B6E /* IsLoadingEnvironmentValue.swift in Sources */, + EE92C4102B7E1CA200FC0B6E /* SpecificRoundedRect.swift in Sources */, EEB7D32229420180006E076D /* SetupPrismAgentView.swift in Sources */, EEECB28E29C29A7300BBB4B9 /* MessagesListViewState.swift in Sources */, + EE92C4142B7E1CA200FC0B6E /* EmptyNavigationLink.swift in Sources */, EE6C38F029468196006CD2D3 /* DIContainer.swift in Sources */, EEECB28629C28A8400BBB4B9 /* Main2View.swift in Sources */, EEE620162937F3110053AE52 /* SigningVerificationViewModel.swift in Sources */, - EE6C39072946834F006CD2D3 /* RootPresentationMode.swift in Sources */, + EE92C4412B7E1F6A00FC0B6E /* AddNewContactBuilder.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/ActivityView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/ActivityView.swift new file mode 100644 index 00000000..5165c810 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/ActivityView.swift @@ -0,0 +1,16 @@ +import SwiftUI + +struct ActivityView: UIViewControllerRepresentable { + let activityItems: [Any] + let applicationActivities: [UIActivity]? + + func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIActivityViewController { + UIActivityViewController(activityItems: activityItems, + applicationActivities: applicationActivities) + } + + func updateUIViewController( + _ uiViewController: UIActivityViewController, + context: UIViewControllerRepresentableContext + ) {} +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/AtalaButton.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/AtalaButton.swift new file mode 100644 index 00000000..9ebe2346 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/AtalaButton.swift @@ -0,0 +1,61 @@ +import SwiftUI + +struct AtalaButton: View { + enum Configuration { + case primary + case secondary + } + + let configuration: Configuration + let loading: Bool + let action: () -> Void + @ViewBuilder var label: () -> Label + + init( + configuration: Configuration = .primary, + loading: Bool = false, + action: @escaping () -> Void, + @ViewBuilder label: @escaping () -> Label + ) { + self.configuration = configuration + self.loading = loading + self.action = action + self.label = label + } + + @Environment(\.isEnabled) var isEnabled: Bool + + var body: some View { + let button = Button(action: action, label: { + HStack(spacing: 6) { + label() + if loading { + ProgressView() + .progressViewStyle( + CircularProgressViewStyle( + tint: .white + ) + ) + } + } + }) + if configuration == .primary { + button + .primeButtonConfiguration() + .environment(\.isLoading, loading) + } else { + button + .secondaryButtonConfiguration() + .environment(\.isLoading, loading) + } + } +} + +struct AtalaButton_Previews: PreviewProvider { + static var previews: some View { + AtalaButton(loading: true, action: {}, label: { + Text("Something") + }) + .disabled(true) + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/ButtonNavigationLink.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/ButtonNavigationLink.swift new file mode 100644 index 00000000..60edd183 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/ButtonNavigationLink.swift @@ -0,0 +1,35 @@ +import SwiftUI + +struct PrimeButtonNavidationLink: View { + let text: String + let destination: Destination + + var body: some View { + NavigationLink( + destination: destination, + label: { + Text(text) + .bold() + .frame(maxWidth: .infinity) + .primeButtonModifier() + } + ) + } +} + +struct SecondaryButtonNavidationLink: View { + let text: String + let destination: Destination + + var body: some View { + NavigationLink( + destination: destination, + label: { + Text(text) + .bold() + .frame(maxWidth: .infinity) + .secondaryButtonModifier() + } + ) + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/CheckButton.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/CheckButton.swift new file mode 100644 index 00000000..3ad4382b --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/CheckButton.swift @@ -0,0 +1,22 @@ +import SwiftUI + +struct CheckButton: View { + @Binding var isSelected: Bool + + var body: some View { + Button(action: { + withAnimation { + self.isSelected = !isSelected + } + }, label: { + if isSelected { + Image("ico_check_on") + .resizable() + } else { + Image("ico_check_off") + .resizable() + } + }) + .frame(width: 40, height: 40) + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/ClosableSheet.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/ClosableSheet.swift new file mode 100644 index 00000000..1f1fd6ab --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/ClosableSheet.swift @@ -0,0 +1,25 @@ +import SwiftUI + +struct ClosableSheet: View { + @Environment(\.presentationMode) var presentationMode + @ViewBuilder let content: () -> SheetContent + + var body: some View { + VStack(spacing: 16) { + content() + Button(action: { + self.presentationMode.wrappedValue.dismiss() + }, label: { + Image("ico_close_red") + }) + } + } +} + +struct ClosableSheet_Previews: PreviewProvider { + static var previews: some View { + ClosableSheet(content: { + Text("") + }) + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/DeleteView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/DeleteView.swift new file mode 100644 index 00000000..4a66725a --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/DeleteView.swift @@ -0,0 +1,106 @@ +import SwiftUI + +struct DeleteView: View { + private let deleteAction: () -> Void + private let showInfoView: Bool + private var loading: Bool + + @ViewBuilder private var content: () -> Content + @ViewBuilder private var info: () -> Info + + @Environment(\.presentationMode) var presentationMode + + init( + showInfoView: Bool = true, + loading: Bool = false, + deleteAction: @escaping () -> Void, + @ViewBuilder content: @escaping () -> Content, + @ViewBuilder info: @escaping () -> Info + ) { + self.deleteAction = deleteAction + self.content = content + self.info = info + self.showInfoView = showInfoView + self.loading = loading + } + + var body: some View { + VStack(spacing: 15) { + VStack(spacing: 9) { + Image("icon_delete") + VStack(spacing: 4) { + Text("contacts_delete_confrimation_title".localize()) + .font(.body) + .fontWeight(.heavy) + .bold() + .foregroundColor(Color(.red)) + Text("contacts_delete_confrimation_message".localize()) + .font(.body) + .foregroundColor(.gray) + } + } + Divider() + content() + Divider() + if showInfoView { + info() + Divider() + } + HStack { + Button(action: { + self.presentationMode.wrappedValue.dismiss() + }, label: { + Text("cancel".localize()) + .frame(maxWidth: .infinity) + .secondaryButtonModifier() + }) + + AtalaButton(loading: self.loading) { + self.deleteAction() + } label: { + Text("delete".localize()) + .frame(maxWidth: .infinity) + } + } + } + .padding(24) + .background(Color.white) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .padding() + } +} + +struct DeleteView_Previews: PreviewProvider { + static var previews: some View { + DeleteView(showInfoView: true) {} content: { + HStack(spacing: 16) { + Image("ico_placeholder_credential") + .resizable() + .frame(width: 40, height: 40) + .clipShape(RoundedRectangle(cornerRadius: 10)) + Text("Atala KYC") + .font(.title3) + .fontWeight(.heavy) + .bold() + .foregroundColor(.black) + Spacer() + } + } info: { + VStack(alignment: .leading, spacing: 9) { + Text("contacts_delete_description".localize()) + .font(.body) + .foregroundColor(.gray) + VStack(alignment: .leading, spacing: 6) { + Text(". ID Credential") + .bold() + .font(.body) + .foregroundColor(.black) + Text(". University Credential") + .bold() + .font(.body) + .foregroundColor(.black) + } + } + } + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/EmptyNavigationLink.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/EmptyNavigationLink.swift similarity index 100% rename from Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/EmptyNavigationLink.swift rename to Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/EmptyNavigationLink.swift diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/ErrorDialogView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/ErrorDialogView.swift new file mode 100644 index 00000000..379371b8 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/ErrorDialogView.swift @@ -0,0 +1,77 @@ +import SwiftUI + +protocol DisplayError: Error { + var message: String { get } + var debugMessage: String? { get } +} + +struct ErrorDialogView: View { + // The idea in here is to be able to nil the error + // once it is presented. It seems more streamlined. + // But open to discussion. + @Binding var error: DisplayError? + private let errorMessage: String + private let debugMessage: String? + private let action: () -> Void + + init( + error: Binding, + action: @escaping () -> Void + ) { + _error = error + errorMessage = error.wrappedValue?.message ?? "" + debugMessage = error.wrappedValue?.debugMessage + self.action = action + } + + var body: some View { + VStack(spacing: 30) { + VStack(spacing: 8) { + Image("img_verifyId_error") + Text("error".localize()) + .font(.system(size: 18, weight: .bold)) + .foregroundColor(.black) + Text(errorMessage) + .font(.system(size: 16, weight: .regular)) + .foregroundColor(.gray) + if let debugError = debugMessage { + Text("DEBUG:\n\(debugError)") + .font(.system(size: 16, weight: .regular)) + .multilineTextAlignment(.leading) + } + } + AtalaButton { + self.error = nil + self.action() + } label: { + Text("accept".localize()) + } + } + .padding(24) + .background(Color.white) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .padding() + } +} + +private struct MockError: DisplayError { + var message: String = "Something went wrong." + var debugMessage: String? +} + +struct ErrorDialogView_Previews: PreviewProvider { + static var previews: some View { + ErrorDialogView(error: .constant(MockError())) {} + } +} + +extension View { + func showErrorDialog(error: Binding) -> some View { + clearFullScreenCover( + isPresented: .init(get: { + error.wrappedValue != nil + }, set: { _ in })) { + ErrorDialogView(error: error) {} + } + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/ErrorTextBox.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/ErrorTextBox.swift new file mode 100644 index 00000000..c0fd0c6e --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/ErrorTextBox.swift @@ -0,0 +1,22 @@ +import SwiftUI + +struct ErrorTextBox: View { + let errorText: String + + var body: some View { + HStack(spacing: 16) { + Image("ico_err") + .resizable() + .frame(width: 14, height: 14) + Text(errorText) + .font(.caption) + .bold() + .multilineTextAlignment(.leading) + .foregroundColor(Color(.red)) + Spacer() + } + .padding() + .background(Color(.red).opacity(0.3)) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/FlowSuccessfulView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/FlowSuccessfulView.swift new file mode 100644 index 00000000..71f8c569 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/FlowSuccessfulView.swift @@ -0,0 +1,60 @@ +import SwiftUI + +struct FlowSuccessfulView: View { + let imageName: String + let titleText: String? + let subtitleText: String? + let infoText: String? + let buttonText: String + let action: () -> Void + + var body: some View { + VStack(alignment: .center, spacing: 20) { + Spacer() + Image(imageName) + if let text = titleText { + Text(text) + .font(.title) + .bold() + .foregroundColor(.black) + } + if let text = subtitleText { + Text(text) + .font(.title2) + .fontWeight(.medium) + .multilineTextAlignment(.center) + .foregroundColor(.black) + } + if let text = infoText { + Text(text) + .font(.body) + .multilineTextAlignment(.center) + .foregroundColor(.gray) + } + Spacer() + Button(action: { + self.action() + }, label: { + Text(buttonText) + .bold() + .frame(maxWidth: .infinity) + .primeButtonModifier() + }) + } + .padding() + .navigationBarHidden(true) + .navigationBarBackButtonHidden(true) + } +} + +struct FlowSuccessfulView_Previews: PreviewProvider { + static var previews: some View { + FlowSuccessfulView( + imageName: "img_success", + titleText: "Welcome!", + subtitleText: "Your account has been successfuly restored", + infoText: nil, + buttonText: "Continue" + ) {} + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/HyperlinkText.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/HyperlinkText.swift new file mode 100644 index 00000000..dd7c4c82 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/HyperlinkText.swift @@ -0,0 +1,207 @@ +import SwiftUI + +struct HyperlinkTextView: View { + let text: String + let selectionText: String + @ViewBuilder let textBuilder: (String) -> Text + @ViewBuilder let hyperlinkBuilder: (String) -> Text + let action: () -> Void + + init( + text: String, + selectionText: String, + @ViewBuilder textBuilder: @escaping (String) -> Text, + @ViewBuilder hyperlinkBuilder: @escaping (String) -> Text, + action: @escaping () -> Void + ) { + self.text = text + self.selectionText = selectionText + self.textBuilder = textBuilder + self.hyperlinkBuilder = hyperlinkBuilder + self.action = action + } + + var body: some View { + HyperlinkView( + text: text, + selectionText: selectionText, + textBuilder: textBuilder + ) { + hyperlinkBuilder($0) + .onTapGesture { + self.action() + } + } + } +} + +struct HyperlinkNavigationView: View { + let text: String + let selectionText: String + @ViewBuilder let destination: () -> Destination + @ViewBuilder let textBuilder: (String) -> Text + @ViewBuilder let hyperlinkBuilder: (String) -> Text + + init( + text: String, + selectionText: String, + @ViewBuilder destination: @escaping () -> Destination, + @ViewBuilder textBuilder: @escaping (String) -> Text, + @ViewBuilder hyperlinkBuilder: @escaping (String) -> Text + ) { + self.text = text + self.selectionText = selectionText + self.destination = destination + self.textBuilder = textBuilder + self.hyperlinkBuilder = hyperlinkBuilder + } + + var body: some View { + HyperlinkView( + text: text, + selectionText: selectionText, + textBuilder: textBuilder + ) { text in + NavigationLink( + destination: destination(), + label: { + hyperlinkBuilder(text) + } + ) + } + } +} + +struct HyperlinkSheetView: View { + let text: String + let selectionText: String + @ViewBuilder let destination: () -> Destination + @ViewBuilder let textBuilder: (String) -> Text + @ViewBuilder let hyperlinkBuilder: (String) -> Text + + @State private var presentingSheet = false + + init( + text: String, + selectionText: String, + @ViewBuilder destination: @escaping () -> Destination, + @ViewBuilder textBuilder: @escaping (String) -> Text, + @ViewBuilder hyperlinkBuilder: @escaping (String) -> Text + ) { + self.text = text + self.selectionText = selectionText + self.destination = destination + self.textBuilder = textBuilder + self.hyperlinkBuilder = hyperlinkBuilder + } + + var body: some View { + HyperlinkView( + text: text, + selectionText: selectionText, + textBuilder: textBuilder + ) { + hyperlinkBuilder($0) + .onTapGesture { + self.presentingSheet = true + } + .sheet(isPresented: $presentingSheet, content: destination) + } + } +} + +struct HyperlinkView: View { + let text: String + let selectionText: String + @ViewBuilder let textBuilder: (String) -> Text + @ViewBuilder let hyperlinkBuilder: (String) -> HyperlinkView + + private let separatedStrings: [String] + + init( + text: String, + selectionText: String, + @ViewBuilder textBuilder: @escaping (String) -> Text, + @ViewBuilder hyperlinkBuilder: @escaping (String) -> HyperlinkView + ) { + self.text = text + self.selectionText = selectionText + self.textBuilder = textBuilder + self.hyperlinkBuilder = hyperlinkBuilder + separatedStrings = text.components(separatedBy: selectionText) + } + + var body: some View { + HStack(spacing: 0) { + ForEach(Array(self.separatedStrings.enumerated()), id: \.offset) { + textBuilder($0.element) + if $0.offset != self.separatedStrings.count - 1 { + hyperlinkBuilder(selectionText) + } + } + } + } +} + +struct HyperlinkText_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + VStack { + HyperlinkTextView( + text: "Hello, World", + selectionText: "World", + textBuilder: { + Text($0) + .font(.body) + .foregroundColor(.black) + }, + hyperlinkBuilder: { + Text($0) + .font(.body) + .bold() + .foregroundColor(Color(.red)) + }, + action: {} + ) + + HyperlinkNavigationView( + text: "Hello, World", + selectionText: "World", + destination: { + Text("Next") + }, + textBuilder: { + Text($0) + .font(.body) + .foregroundColor(.black) + }, + hyperlinkBuilder: { + Text($0) + .font(.body) + .bold() + .foregroundColor(Color(.red)) + } + ) + + HyperlinkSheetView( + text: "Hello, World", + selectionText: "World", + destination: { + Text("Next") + }, + textBuilder: { + Text($0) + .font(.body) + .foregroundColor(.black) + }, + hyperlinkBuilder: { + Text($0) + .font(.body) + .bold() + .foregroundColor(Color(.red)) + } + ) + } + } + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/LazyView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/LazyView.swift similarity index 100% rename from Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/LazyView.swift rename to Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/LazyView.swift diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/PinInsertView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/PinInsertView.swift new file mode 100644 index 00000000..ad717306 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/PinInsertView.swift @@ -0,0 +1,107 @@ +import Combine +import SwiftUI + +struct PinInsertView: View { + let title: String? + var centered = false + @Binding var code: String + @State private var isVisible = false + @State private var isFocused = false + + private let maxLength = 4 + + var body: some View { + VStack(alignment: centered ? .center : .leading) { + if title != nil { + Text(title ?? "") + .font(.body) + .foregroundColor(Color(UIColor.gray)) + } + HStack(spacing: 10) { + ZStack { + pinDots + backgroundField + } + Button(action: { + isVisible.toggle() + }, label: { + Image(isVisible ? "ico_visibility_off" : "ico_visibility_on") + }) + } + .frame(maxWidth: .infinity, alignment: centered ? .center : .leading) + } + } + + private var pinDots: some View { + HStack(spacing: 10) { + ForEach(0 ..< maxLength) { index in + ZStack { + RoundedRectangle(cornerRadius: 6) + .foregroundColor(Color(UIColor.lightGray)) + .frame(width: 44, height: 44) + + Text(self.getDigits(at: index)) + .font(.title) + } + } + } + } + + private var backgroundField: some View { + TextField("", text: $code, onEditingChanged: { editingChanged in + isFocused = editingChanged + }) + .accentColor(.clear) + .foregroundColor(.clear) + .frame(width: 210, height: 60) + .keyboardType(.numberPad) + .onReceive(Just(code)) { _ in limitText(maxLength) } + } + + // Function to keep text length in limits + func limitText(_ upper: Int) { + if code.count > upper { + code = String(code.prefix(upper)) + } + } + + private func getDigits(at index: Int) -> String { + if index > code.count { + return "" + } + if index == code.count { + return isFocused ? "|" : "" + } + + return isVisible ? code.digits[index].numberString : "•" + } +} + +private extension String { + var digits: [Int] { + var result = [Int]() + + for char in self { + if let number = Int(String(char)) { + result.append(number) + } + } + + return result + } +} + +private extension Int { + var numberString: String { + guard self < 10 else { return "0" } + + return String(self) + } +} + +struct PinInsertView_Previews: PreviewProvider { + @State static var code = "" + static var previews: some View { + PinInsertView(title: "TITLE", code: $code) + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/SearchBoxView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/SearchBoxView.swift new file mode 100644 index 00000000..5e4609b4 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/SearchBoxView.swift @@ -0,0 +1,35 @@ +import SwiftUI + +struct SearchBoxView: View { + let placeholder: String + @Binding var searchString: String + + var body: some View { + HStack { + Image(systemName: "magnifyingglass") + .foregroundColor(.gray) + TextField(placeholder, text: $searchString) + Spacer() + } + .padding(.horizontal) + .frame(height: 40) + .background( + RoundedRectangle(cornerRadius: 6) + .strokeBorder( + Color(.gray), + lineWidth: 1 + ) + .background(Color.white) + ) + } +} + +struct SearchBoxView_Previews: PreviewProvider { + static var previews: some View { + SearchBoxView( + placeholder: "connections_search_contacts".localize(), + searchString: .constant("") + ) + .padding() + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/SelectCheckButton.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/SelectCheckButton.swift new file mode 100644 index 00000000..c148212e --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/SelectCheckButton.swift @@ -0,0 +1,33 @@ +import SwiftUI + +struct SelectCheckButton: View { + @Binding var isSelected: Bool + + var body: some View { + Button(action: { + withAnimation { + self.isSelected = !isSelected + } + }, label: { + if isSelected { + Image(systemName: "checkmark.circle.fill") + .resizable() + .foregroundColor(Color(.red)) + } else { + Circle() + .strokeBorder( + Color(.gray), + lineWidth: 1, + antialiased: true + ) + } + }) + .frame(width: 24, height: 24) + } +} + +struct SelectCheckButtonPreviewView: PreviewProvider { + static var previews: some View { + SelectCheckButton(isSelected: .constant(false)) + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/WebView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/WebView.swift new file mode 100644 index 00000000..038bbe38 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/WebView.swift @@ -0,0 +1,87 @@ +import SwiftUI +import UIKit +import WebKit + +struct WebView: UIViewRepresentable { + enum Source { + case url(URL) + case html(String) + } + + class Coordinator: NSObject, WKNavigationDelegate { + var view: WKWebView? { + didSet { + view?.navigationDelegate = self + } + } + + var didLoad = false + + func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { + webView.reload() + } + } + + let source: Source + @Environment(\.presentationMode) var presentationMode + + init(source: Source) { + self.source = source + } + + func makeCoordinator() -> Coordinator { + return Coordinator() + } + + func makeUIView(context: Context) -> UIView { + guard let view = context.coordinator.view else { + switch source { + case let .url(url): + let view = WKWebView() + context.coordinator.view = view + let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad) + view.load(request) + return view + case let .html(html): + let view = ResizableWebView() + context.coordinator.view = view + view.loadHTMLString(html, baseURL: Bundle.main.bundleURL) + return view + } + } + return view + } + + func updateUIView(_ uiView: UIView, context: Context) { + guard + presentationMode.wrappedValue.isPresented, + !context.coordinator.didLoad, + let view = context.coordinator.view + else { return } + switch source { + case let .url(url): + let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad) + view.load(request) + case let .html(html): + DispatchQueue.main.async { + view.loadHTMLString(html, baseURL: Bundle.main.bundleURL) + } + } + } +} + +class ResizableWebView: WKWebView { + init() { + let configuration = WKWebViewConfiguration() + super.init(frame: .zero, configuration: configuration) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var intrinsicContentSize: CGSize { + return scrollView.contentSize + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/WordTagEditable.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/WordTagEditable.swift new file mode 100644 index 00000000..bb79a35d --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/WordTagEditable.swift @@ -0,0 +1,90 @@ +import SwiftUI + +struct WordTagEditable: View { + let words: [String] + let selectedToDelete: Int? + let spacing: CGFloat + let onDelete: (Int) -> Void + + @State private var intrinsicWidth: CGFloat = 0 + + var body: some View { + // ScrollView(.vertical, showsIndicators: true) { + VStack(alignment: .leading) { + let rows = calculateRows(words: words) + ForEach(Array(rows.enumerated()), id: \.offset) { row in + HStack { + ForEach(Array(row.element.enumerated()), id: \.offset) { + TagView( + index: $0.element.index, + word: $0.element.word, + isSelected: $0.element.index == selectedToDelete, + onDelete: onDelete + ) + } + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .intrinsicContentWidth(to: $intrinsicWidth) + // } + // .fixedSize(horizontal: false, vertical: true) + } + + private func calculateRows(words: [String]) -> [[(index: Int, word: String)]] { + words.enumerated().reduce([[(Int, String)]]()) { rows, word in + var newRows = rows + if var row = rows.last, calculateRowSize(words: row) <= intrinsicWidth { + if calculateRowSize(words: row + [word]) <= intrinsicWidth { + newRows.removeLast() + row.append(word) + newRows.append(row) + return newRows + } + } + newRows.append([word]) + return newRows + } + } + + private func calculateRowSize(words: [(Int, String)]) -> CGFloat { + words.reduce(CGFloat(0)) { + let view = TagView( + index: $1.0, + word: $1.1, + isSelected: $1.0 == selectedToDelete + ) { _ in } + return $0 + UIHostingController(rootView: view) + .view + .intrinsicContentSize + .width + } + spacing * CGFloat(words.count) + } +} + +private struct TagView: View { + let index: Int + let word: String + let isSelected: Bool + let onDelete: (Int) -> Void + + var body: some View { + HStack(spacing: 6) { + Text("\(index + 1). \(word)") + .bold() + .minimumScaleFactor(0.5) + .lineLimit(1) + .foregroundColor(Color(.red)) + .padding(.leading) + .padding(.vertical, 2) + .fixedSize(horizontal: false, vertical: true) + Image("ico_delete_x") + .onTapGesture { + onDelete(index) + } + .padding(.trailing) + } + .background(isSelected ? Color(.red).opacity(0.6) : Color(.red).opacity(0.3)) + .clipShape(Capsule()) + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/WordTagGrid.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/WordTagGrid.swift new file mode 100644 index 00000000..84e1b61c --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/WordTagGrid.swift @@ -0,0 +1,85 @@ +import SwiftUI + +struct WordTagsGrid: View { + private struct WidthPreferenceKey: PreferenceKey { + static var defaultValue: CGFloat = .zero + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {} + } + + let words: [String] + let onDelete: (Int) -> Void + + private let gridSpacing: CGFloat = 16 + @State private var itemsWidth: CGFloat = 1 + + @Environment(\.editMode) var editMode + + var isEditing: Bool { editMode?.wrappedValue == .active } + + var body: some View { + LazyVGrid( + columns: grids, + spacing: 20 + ) { + ForEach(Array(words.enumerated()), id: \.offset) { + tagView( + index: $0.offset, + word: $0.element, + width: itemsWidth, + editing: editMode?.wrappedValue == .active + ) + } + } + .background(GeometryReader { geometry in + Color.clear + .preference(key: WidthPreferenceKey.self, value: geometry.size.width) + }) + .onPreferenceChange(WidthPreferenceKey.self, perform: { value in + let div = value - (gridSpacing * 2) + if div >= 0 { + itemsWidth = div / 3 + } + }) + } + + private var grids: [GridItem] { + if isEditing { + return [ + GridItem(.flexible(), spacing: gridSpacing), + GridItem(.flexible(), spacing: gridSpacing), + GridItem(.flexible()) + ] + } else { + return [ + GridItem(.fixed(itemsWidth), spacing: gridSpacing), + GridItem(.fixed(itemsWidth), spacing: gridSpacing), + GridItem(.fixed(itemsWidth)) + ] + } + } + + @ViewBuilder + private func tagView(index: Int, word: String, width: CGFloat, editing: Bool) -> some View { + HStack(spacing: 6) { + Text("\(index + 1). \(word)") + .bold() + .minimumScaleFactor(0.5) + .lineLimit(1) + .foregroundColor(Color(.red)) + .padding(editing ? .leading : .horizontal) + .padding(.vertical, 2) + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: true) + if editing { + Image("ico_delete_x") + .onTapGesture { + onDelete(index) + } + .padding(.trailing) + } + } + .frame(width: width > 0 ? width : 0, height: 30) + .background(Color(.red).opacity(0.1)) + .clipShape(Capsule()) + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/WordsTextFieldView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/WordsTextFieldView.swift new file mode 100644 index 00000000..1f6b246f --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/CommonUI/WordsTextFieldView.swift @@ -0,0 +1,87 @@ +import SwiftUI +import UIKit + +struct WordsTextFieldView: UIViewRepresentable { + enum Key { + case backspace + case enter + } + + let placeholder: String + @Binding var text: String + let keyPressedAction: (Key) -> Void + + func makeCoordinator() -> TextFieldUIKitViewCoordinator { + return TextFieldUIKitViewCoordinator(text: $text, keyPressed: keyPressedAction) + } + + func makeUIView(context: Context) -> UITextField { + context.coordinator.textField.placeholder = placeholder + return context.coordinator.textField + } + + func updateUIView(_ uiView: UITextField, context: Context) { + uiView.text = text + } +} + +class TextFieldUIKitViewCoordinator: NSObject { + let textField: UITextField + @Binding var text: String + let keyPressed: (WordsTextFieldView.Key) -> Void + + init(text: Binding, keyPressed: @escaping (WordsTextFieldView.Key) -> Void) { + _text = text + self.keyPressed = keyPressed + textField = TextField(keyPressed: keyPressed) + super.init() + textField.delegate = self + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.spellCheckingType = .no + } + + private class TextField: UITextField { + let keyPressed: (WordsTextFieldView.Key) -> Void + + init(keyPressed: @escaping (WordsTextFieldView.Key) -> Void) { + self.keyPressed = keyPressed + super.init(frame: .zero) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func deleteBackward() { + super.deleteBackward() + keyPressed(.backspace) + } + } +} + +extension TextFieldUIKitViewCoordinator: UITextFieldDelegate { + func textFieldDidChangeSelection(_ textField: UITextField) { + guard text != textField.text else { return } + text = textField.text ?? "" + } + + func textField( + _ textField: UITextField, + shouldChangeCharactersIn range: NSRange, + replacementString string: String + ) -> Bool { + guard string.isEmpty else { + return string.rangeOfCharacter( + from: CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLKMNOPQRSTUVWXYZ ").inverted + ) == nil + } + return true + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + keyPressed(.enter) + return false + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Environment/IsLoadingEnvironmentValue.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Environment/IsLoadingEnvironmentValue.swift new file mode 100644 index 00000000..df716490 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Environment/IsLoadingEnvironmentValue.swift @@ -0,0 +1,12 @@ +import SwiftUI + +private struct LoadingKey: EnvironmentKey { + static let defaultValue = false +} + +extension EnvironmentValues { + var isLoading: Bool { + get { self[LoadingKey.self] } + set { self[LoadingKey.self] = newValue } + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/RootPresentationMode.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Environment/RootPresentationMode.swift similarity index 100% rename from Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/RootPresentationMode.swift rename to Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Environment/RootPresentationMode.swift diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/FancyToast.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/FancyToast.swift similarity index 100% rename from Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/FancyToast.swift rename to Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/FancyToast.swift diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/FancyToastModifier.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/FancyToastModifier.swift similarity index 100% rename from Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/FancyToastModifier.swift rename to Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/FancyToastModifier.swift diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/AtalaNavigationBackButton.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/AtalaNavigationBackButton.swift new file mode 100644 index 00000000..3766482f --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/AtalaNavigationBackButton.swift @@ -0,0 +1,60 @@ +import SwiftUI + +struct AtalaNavigationBackButtonModifier: ViewModifier { + let tintColor: Color + let divider: Bool + @ViewBuilder var title: () -> Text + @ViewBuilder var trailing: () -> Trailing + @Environment(\.presentationMode) var presentationMode + + func body(content: Content) -> some View { + VStack(spacing: 0) { + if divider { + Divider() + } + content + Spacer() + } + .navigationBarBackButtonHidden(true) + .navigationBarItems(leading: leadingView, trailing: trailing()) + } + + private var leadingView: some View { + HStack { + Button(action: { + self.presentationMode.wrappedValue.dismiss() + }, label: { + Image("ico_backarrow") + .renderingMode(.template) + .aspectRatio(contentMode: .fit) + .foregroundColor(tintColor) + }) + title() + .font(.system(size: 18)) + .foregroundColor(tintColor) + } + } +} + +extension View { + func atalaNavigationBackButton(title: String = "", divider: Bool = true) -> some View { + atalaNavigationBackButton(title: title, divider: divider) { + EmptyView() + } + } + + func atalaNavigationBackButton( + title: String = "", + divider: Bool = true, + tintColor: Color = Color(.black), + @ViewBuilder trailing: @escaping () -> Trailing + ) -> some View { + modifier(AtalaNavigationBackButtonModifier( + tintColor: tintColor, + divider: divider, + title: { + Text(title) + }, trailing: trailing + )) + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/Button+Configurations.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/Button+Configurations.swift new file mode 100644 index 00000000..0f55bb92 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/Button+Configurations.swift @@ -0,0 +1,67 @@ +import SwiftUI + +struct AtalaPrimeButtonViewModifier: ViewModifier { + @Environment(\.isEnabled) var isEnabled: Bool + @Environment(\.isLoading) var isLoading: Bool + + func body(content: Content) -> some View { + content + .padding(.horizontal) + .frame(maxWidth: .infinity) + .frame(height: 45) + .background((isEnabled || isLoading) ? Color(.red) : Color(.lightGray)) + .foregroundColor((isEnabled || isLoading) ? .white : Color(.gray)) + .accentColor((isEnabled || isLoading) ? .white : Color(.gray)) + .clipShape(Capsule()) + } +} + +struct AtalaSecondaryButtonViewModifier: ViewModifier { + func body(content: Content) -> some View { + content + .padding(.horizontal) + .frame(maxWidth: .infinity) + .frame(height: 45) + .background(Color.white) + .foregroundColor(Color(.red)) + .accentColor(Color(.red)) + .overlay( + Capsule() + .stroke(Color(.red), lineWidth: 3) + ) + } +} + +struct AtalaPrimaryButtonConfiguration: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .modifier(AtalaPrimeButtonViewModifier()) + } +} + +struct AtalaSecondaryButtonConfiguration: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .modifier(AtalaSecondaryButtonViewModifier()) + } +} + +extension View { + func primeButtonModifier() -> some View { + modifier(AtalaPrimeButtonViewModifier()) + } + + func secondaryButtonModifier() -> some View { + modifier(AtalaSecondaryButtonViewModifier()) + } +} + +extension Button { + func primeButtonConfiguration() -> some View { + buttonStyle(AtalaPrimaryButtonConfiguration()) + } + + func secondaryButtonConfiguration() -> some View { + buttonStyle(AtalaSecondaryButtonConfiguration()) + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/ClearFullCoverModifier.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/ClearFullCoverModifier.swift new file mode 100644 index 00000000..a59a3266 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/ClearFullCoverModifier.swift @@ -0,0 +1,149 @@ +import Combine +import SwiftUI + +private struct ClearFullCoverModifier: ViewModifier { + let animated: Bool + @Binding var isPresented: Bool + @ViewBuilder var presenting: () -> Presenting + @State var contentDisabled = false + + init( + animated: Bool, + isPresented: Binding, + @ViewBuilder presenting: @escaping () -> Presenting + ) { + self.animated = animated + _isPresented = isPresented + self.presenting = presenting + } + + func body(content: Content) -> some View { + content + .fullScreenCover(isPresented: $isPresented) { + ZStack { + presenting() + .disablePreference($contentDisabled) + .onTapGesture { + // Funny "hack" so the dismissal tap on the background view + // doesnt affect the presented. + // Just having this clean tapGesture works + } + } + .ignoresSafeArea(.container) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background( + BackgroundClearView(isPresented: $isPresented, isDisabled: contentDisabled) + ) + } + } +} + +extension View { + func clearFullScreenCover( + isPresented: Binding, + animated: Bool = true, + @ViewBuilder presenting: @escaping () -> Content + ) -> some View { + modifier(ClearFullCoverModifier( + animated: animated, + isPresented: isPresented, + presenting: presenting + )) + } +} + +private struct BackgroundClearView: UIViewRepresentable { + let isPresented: Binding + let isDisabled: Bool + + @Environment(\.isEnabled) var isEnabled + + class ViewCoordinator { + let view: BackgroundClearUIView + init(view: BackgroundClearUIView) { + self.view = view + } + } + + func makeCoordinator() -> ViewCoordinator { + ViewCoordinator(view: BackgroundClearUIView(action: { + self.isPresented.wrappedValue = false + })) + } + + func makeUIView(context: Context) -> BackgroundClearUIView { + return context.coordinator.view + } + + func updateUIView(_ uiView: BackgroundClearUIView, context: Context) { + uiView.gesture?.isEnabled = !isDisabled + } +} + +private class BackgroundClearUIView: UIView { + let action: () -> Void + var gesture: UITapGestureRecognizer? + var animating = false + var cancellables = Set() + var didAppear = false + + init(action: @escaping () -> Void) { + self.action = action + super.init(frame: .zero) + gesture = UITapGestureRecognizer(target: self, action: #selector(didTouchBackground(_:))) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func didMoveToWindow() { + super.didMoveToWindow() + superview?.superview?.backgroundColor = .clear + if !UIView.areAnimationsEnabled { + superview? + .superview? + .superview? + .backgroundColor = .init(red: 0, green: 0, blue: 0, alpha: 0.6) + } + + superview?.superview?.layer.publisher(for: \.position) + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] _ in + guard + let self = self, + let animation = self.superview?.superview?.layer.animation(forKey: "position") + else { return } + + if let gesture = self.gesture, gesture.view == nil { + self.superview?.superview?.superview?.addGestureRecognizer(gesture) + self.superview?.superview?.superview?.isUserInteractionEnabled = true + } + + UIView.animate(withDuration: animation.duration) { [weak self] in + guard let self = self else { return } + self.animating = true + if self.didAppear { + self.superview? + .superview? + .superview? + .backgroundColor = .init(red: 0, green: 0, blue: 0, alpha: 0.0) + } else { + self.superview? + .superview? + .superview? + .backgroundColor = .init(red: 0, green: 0, blue: 0, alpha: 0.6) + } + } completion: { [weak self] _ in + self?.didAppear = true + self?.animating = false + } + }) + .store(in: &cancellables) + } + + @objc func didTouchBackground(_ sender: Any?) { + action() + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/LoadingModifier.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/LoadingModifier.swift new file mode 100644 index 00000000..a0831702 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/LoadingModifier.swift @@ -0,0 +1,40 @@ +import SwiftUI + +private struct LoadingModifier: ViewModifier { + @Binding var enabled: Bool + + func body(content: Content) -> some View { + content + .clearFullScreenCover(isPresented: $enabled) { + ProgressView("loading_title".localize()) + .progressViewStyle(CircularProgressViewStyle()) + .frame(alignment: .center) + .padding() + .background( + Color(.lightGray) + .opacity(0.8) + .blur(radius: 2.0, opaque: true) + .clipShape(RoundedRectangle(cornerRadius: 20)) + ) + .onAppear { + UIView.setAnimationsEnabled(true) + } + .onDisappear { + UIView.setAnimationsEnabled(true) + } + } + } +} + +extension View { + func addLoading(enabled: Bool) -> some View { + modifier(LoadingModifier( + enabled: .init( + get: { + if enabled {} + return enabled + }, set: { _ in } + ) + )) + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/NavigationBarUtilModifiers.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/NavigationBarUtilModifiers.swift new file mode 100644 index 00000000..5c740dba --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/NavigationBarUtilModifiers.swift @@ -0,0 +1,37 @@ +import SwiftUI + +private struct NavigationControllerIntrospect: UIViewControllerRepresentable { + var configure: (UINavigationController) -> Void = { _ in } + + func makeUIViewController( + context: UIViewControllerRepresentableContext + ) -> UIViewController { + UIViewController() + } + + func updateUIViewController( + _ uiViewController: UIViewController, + context: UIViewControllerRepresentableContext + ) { + guard + let navigationContoller = uiViewController.navigationController + else { return } + configure(navigationContoller) + } +} + +extension View { + func configureNavigationBar(_ configure: @escaping (UINavigationBar) -> Void) -> some View { + modifier(NavigationConfigurationViewModifier(configure: configure)) + } +} + +struct NavigationConfigurationViewModifier: ViewModifier { + let configure: (UINavigationBar) -> Void + + func body(content: Content) -> some View { + content.background(NavigationControllerIntrospect(configure: { + configure($0.navigationBar) + })) + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/RoundedRectBorderWithText.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/RoundedRectBorderWithText.swift new file mode 100644 index 00000000..92fab7e9 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Modifiers/RoundedRectBorderWithText.swift @@ -0,0 +1,39 @@ +import SwiftUI + +private struct RoundedRectBorderWithText: ViewModifier { + let text: String + let borderColor: Color + + func body(content: Content) -> some View { + content + .background( + border.overlay( + Text(text) + .font(.footnote) + .bold() + .foregroundColor(.gray) + .background(Color(.white)) + .padding(.leading, 16) + .padding(.top, -7), + alignment: .topLeading + ) + ) + } + + var border: some View { + RoundedRectangle(cornerRadius: 10) + .strokeBorder( + borderColor, + lineWidth: 1 + ) + } +} + +extension View { + func roundedRectBorderWithText( + _ text: String, + borderColor: Color = Color(.gray).opacity(0.7) + ) -> some View { + modifier(RoundedRectBorderWithText(text: text, borderColor: borderColor)) + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/NavigationUtils.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/NavigationUtils.swift new file mode 100644 index 00000000..c9a2e588 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/NavigationUtils.swift @@ -0,0 +1,58 @@ +import UIKit + +struct NavigationUtil { + static func popToRootView( + navigationVC: UINavigationController, + animated: Bool = true + ) { + navigationVC.popToRootViewController(animated: animated) + } + + static func findNavigationControllerFromRoot() -> UINavigationController? { + guard + let rootVc = UIApplication + .shared + .windows + .filter({ $0.isKeyWindow }) + .first? + .rootViewController + else { return nil } + + return findNavigationController(viewController: rootVc) + } + + static func findNavigationController(viewController: UIViewController?) -> UINavigationController? { + guard let viewController = viewController else { + return nil + } + + if let navigationController = viewController as? UINavigationController { + return navigationController + } + + for childViewController in viewController.children { + return findNavigationController(viewController: childViewController) + } + + return nil + } + + static func findTopViewController() -> UIViewController? { + guard + let rootVc = UIApplication + .shared + .windows + .filter({ $0.isKeyWindow }) + .first? + .rootViewController + else { return nil } + + var topVC = rootVc + + while let presentedVC = topVC.presentedViewController { + topVC = presentedVC + } + + return topVC + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/PreferenceHelpers/DisablePreferenceKey.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/PreferenceHelpers/DisablePreferenceKey.swift new file mode 100644 index 00000000..b532f5c6 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/PreferenceHelpers/DisablePreferenceKey.swift @@ -0,0 +1,35 @@ +import SwiftUI + +struct DisablePreferenceKey: PreferenceKey { + static let defaultValue = false + + static func reduce(value: inout Bool, nextValue: () -> Bool) { + let next = nextValue() + if next != value { + value = next + } + } +} + +private struct CommitDisablePreferenceModifier: ViewModifier { + @Environment(\.isEnabled) var isEnabled + + func body(content: Content) -> some View { + content + .preference(key: DisablePreferenceKey.self, value: isEnabled) + } +} + +extension View { + func disablePreference(_ toBinder: Binding) -> some View { + onPreferenceChange(DisablePreferenceKey.self, perform: { value in + DispatchQueue.main.async { + toBinder.wrappedValue = !value + } + }) + } + + func commitDisablePreference() -> some View { + modifier(CommitDisablePreferenceModifier()) + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/PreferenceHelpers/IntrinsicSizePreferenceKey.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/PreferenceHelpers/IntrinsicSizePreferenceKey.swift new file mode 100644 index 00000000..60b38ce6 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/PreferenceHelpers/IntrinsicSizePreferenceKey.swift @@ -0,0 +1,41 @@ +import SwiftUI + +struct IntrinsicContentWidthPreferenceKey: PreferenceKey { + static let defaultValue: CGFloat = 0 + + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {} +} + +extension View { + func intrinsicContentWidth(to width: Binding) -> some View { + background(GeometryReader { geometry in + Color.clear.preference( + key: IntrinsicContentWidthPreferenceKey.self, + value: geometry.size.width + ) + }) + .onPreferenceChange(IntrinsicContentWidthPreferenceKey.self) { + width.wrappedValue = $0 + } + } +} + +struct IntrinsicContentSizePreferenceKey: PreferenceKey { + static let defaultValue: CGSize = .zero + + static func reduce(value: inout CGSize, nextValue: () -> CGSize) {} +} + +extension View { + func intrinsicContentSize(to size: Binding) -> some View { + background(GeometryReader { geometry in + Color.clear.preference( + key: IntrinsicContentSizePreferenceKey.self, + value: geometry.size + ) + }) + .onPreferenceChange(IntrinsicContentSizePreferenceKey.self) { + size.wrappedValue = $0 + } + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Shapes/SpecificRoundedRect.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Shapes/SpecificRoundedRect.swift new file mode 100644 index 00000000..92fa8984 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/Shapes/SpecificRoundedRect.swift @@ -0,0 +1,15 @@ +import SwiftUI + +struct SpecificRoundedRect: Shape { + let radius: CGFloat + let corners: UIRectCorner + + func path(in rect: CGRect) -> Path { + let path = UIBezierPath( + roundedRect: rect, + byRoundingCorners: corners, + cornerRadii: CGSize(width: radius, height: radius) + ) + return Path(path.cgPath) + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/Array+Zipped.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/SwiftTools/Array+Helpers.swift similarity index 100% rename from Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/Array+Zipped.swift rename to Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/SwiftTools/Array+Helpers.swift diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/Builder.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/SwiftTools/Builder.swift similarity index 100% rename from Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/Builder.swift rename to Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/SwiftTools/Builder.swift diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/SwiftTools/Combine+Drop.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/SwiftTools/Combine+Drop.swift new file mode 100644 index 00000000..ba64a73f --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/SwiftTools/Combine+Drop.swift @@ -0,0 +1,40 @@ +import Combine + +extension Publisher { + func dropCast(_: T.Type) -> AnyPublisher { + map { $0 as? T } + .drop { $0 == nil } + .eraseToAnyPublisher() + } + + func dropNil() -> AnyPublisher where Output == T? { + drop { $0 == nil } + .map { + guard let value = $0 else { fatalError("This will never happen") } + return value + } + .eraseToAnyPublisher() + } +} + +extension Publishers { + struct MissingOutputError: Error {} +} + +public extension Publishers.First where Failure == Error { + func await() async throws -> Output { + for try await output in values { + return output + } + throw Publishers.MissingOutputError() + } +} + +public extension Publishers.FirstWhere where Failure == Error { + func await() async throws -> Output { + for try await output in values { + return output + } + throw Publishers.MissingOutputError() + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/ComponentContainer.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/SwiftTools/ComponentContainer.swift similarity index 100% rename from Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/ComponentContainer.swift rename to Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/SwiftTools/ComponentContainer.swift diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/DIContainer.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/SwiftTools/DIContainer.swift similarity index 100% rename from Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/DIContainer.swift rename to Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/SwiftTools/DIContainer.swift diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/PrintObjects.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/SwiftTools/PrintObjects.swift similarity index 100% rename from Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/PrintObjects.swift rename to Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/SwiftTools/PrintObjects.swift diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/String+extensions.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/SwiftTools/String+extensions.swift similarity index 100% rename from Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/String+extensions.swift rename to Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/SwiftTools/String+extensions.swift diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/SwiftTools/URL+StringLiterals.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/SwiftTools/URL+StringLiterals.swift new file mode 100644 index 00000000..0828fc74 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/SwiftTools/URL+StringLiterals.swift @@ -0,0 +1,12 @@ +import Foundation + +extension URL: ExpressibleByStringLiteral { + public init(stringLiteral value: StaticString) { + guard let url = URL(string: "\(value)") else { + assert(URL(string: "\(value)") == nil, "Invalid literal URL") + self = URL(fileURLWithPath: "") + return + } + self = url + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactBuilder.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactBuilder.swift new file mode 100644 index 00000000..d0b97224 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactBuilder.swift @@ -0,0 +1,22 @@ +import PrismAgent +import SwiftUI + +struct AddNewContactComponent: ComponentContainer { + let container: DIContainer + let token: String? +} + +struct AddNewContactBuilder: Builder { + func build(component: AddNewContactComponent) -> some View { + let viewModel = getViewModel(component: component) { + AddNewContactViewModelImpl( + token: component.token ?? "", + agent: component.container.resolve(type: PrismAgent.self)! + ) + } + return AddNewContactView(viewModel: viewModel) + .onDisappear { + component.container.unregister(type: AddNewContactViewModelImpl.self) + } + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactState.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactState.swift new file mode 100644 index 00000000..97b8cfa3 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactState.swift @@ -0,0 +1,20 @@ +import Foundation + +struct AddNewContactState { + enum AddContacFlowStep { + case getCode + case checkDuplication + case alreadyConnected + case confirmConnection + case error(DisplayError) + } + + struct Contact { + enum Icon { + case data(Data) + case name(String) + } + + let text: String + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactView.swift new file mode 100644 index 00000000..482269f0 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactView.swift @@ -0,0 +1,110 @@ +import SwiftUI + +protocol AddNewContactViewModel: ObservableObject { + var flowStep: AddNewContactState.AddContacFlowStep { get } + var contactInfo: AddNewContactState.Contact? { get } + var loading: Bool { get } + var dismiss: Bool { get } + var dismissRoot: Bool { get } + var code: String { get set } + func isContactAlreadyAdded() + func addContact() +} + +struct AddNewContactView: View { + @StateObject var viewModel: ViewModel + @Environment(\.presentationMode) var presentationMode + @Environment(\.rootPresentationMode) var modalPresentation + + var body: some View { + VStack { + switch viewModel.flowStep { + case .getCode: + InsertCodeView( + textField: $viewModel.code, + loading: viewModel.loading + ) { + self.viewModel.isContactAlreadyAdded() + } cancelAction: { + self.presentationMode.wrappedValue.dismiss() + } + .commitDisablePreference() + .disabled(viewModel.loading) + case .checkDuplication: + ProgressView() + .progressViewStyle( + CircularProgressViewStyle() + ) + .onAppear(perform: { + viewModel.isContactAlreadyAdded() + }) + .frame(maxWidth: .infinity, maxHeight: 55) + .commitDisablePreference() + .disabled(true) + case .alreadyConnected: + if let contact = viewModel.contactInfo { + AlreadyConnectedView( + name: contact.text + ) { + self.presentationMode.wrappedValue.dismiss() + } + .commitDisablePreference() + .disabled(viewModel.loading) + } + + case .confirmConnection: + if let contact = viewModel.contactInfo { + ConfirmConnectionView( + name: contact.text, + loading: viewModel.loading + ) { + self.viewModel.addContact() + } cancelAction: { + self.presentationMode.wrappedValue.dismiss() + } + .commitDisablePreference() + .disabled(viewModel.loading) + } + case let .error(error): + ErrorDialogView( + error: .constant(error) + ) { + self.presentationMode.wrappedValue.dismiss() + } + } + } + .background(Color.white) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .padding() + .animation(.default) + .onChange(of: viewModel.dismiss, perform: { value in + if value { + self.presentationMode.wrappedValue.dismiss() + } + }) + .onChange(of: viewModel.dismissRoot, perform: { value in + self.modalPresentation.wrappedValue = value + }) + } +} + +struct AddNewContactView_Previews: PreviewProvider { + static var previews: some View { + AddNewContactView(viewModel: MockViewModel()) + } +} + +private class MockViewModel: AddNewContactViewModel { + var contactInfo: AddNewContactState.Contact? + var flowStep: AddNewContactState.AddContacFlowStep = .getCode + var loading = false + var dismiss = false + var dismissRoot = false + var code = "" + func isContactAlreadyAdded() { + } + + func addContact() { + dismiss = true + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactViewModel.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactViewModel.swift new file mode 100644 index 00000000..66203456 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactViewModel.swift @@ -0,0 +1,78 @@ +import Combine +import Foundation +import PrismAgent + +final class AddNewContactViewModelImpl: AddNewContactViewModel { + @Published var flowStep: AddNewContactState.AddContacFlowStep + @Published var code = "" + @Published var dismiss = false + @Published var dismissRoot = false + @Published var loading = false + @Published var contactInfo: AddNewContactState.Contact? + + private let agent: PrismAgent + private var cancellables = Set() + + init( + token: String = "", + agent: PrismAgent + ) { + code = token + self.agent = agent + flowStep = token.isEmpty ? .getCode : .checkDuplication + } + + func isContactAlreadyAdded() { + guard !loading else { return } + loading = true + Task { [weak self] in + guard + let self + else { return } + + do { + let connection = try agent.parseOOBInvitation(url: self.code) + let didPairs = try await agent.getAllDIDPairs().first().await() + + await MainActor.run { [weak self] in + guard didPairs.first(where: { $0.other.string == connection.from }) == nil else { + self?.flowStep = .alreadyConnected + self?.loading = false + return + } + + self?.contactInfo = .init(text: connection.from) + self?.flowStep = .confirmConnection + self?.loading = false + } + } catch { + await MainActor.run { [weak self] in + self?.flowStep = .error(DisplayErrorState(error: error)) + self?.loading = false + } + } + } + } + + func addContact() { + guard contactInfo != nil, !loading else { return } + loading = true + Task { [weak self] in + guard let self else { return } + do { + let connection = try agent.parseOOBInvitation(url: self.code) + try await self.agent.acceptDIDCommInvitation(invitation: connection) + await MainActor.run { [weak self] in + self?.dismiss = true + self?.dismissRoot = true + self?.loading = false + } + } catch { + await MainActor.run { [weak self] in + self?.flowStep = .error(DisplayErrorState(error: error)) + self?.loading = false + } + } + } + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/UI/AlreadyConnectedView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/UI/AlreadyConnectedView.swift new file mode 100644 index 00000000..06a12722 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/UI/AlreadyConnectedView.swift @@ -0,0 +1,39 @@ +import SwiftUI + +struct AlreadyConnectedView: View { + let name: String + let doneAction: () -> Void + + var body: some View { + VStack(spacing: 20) { + HStack { + VStack(alignment: .leading, spacing: 5) { + Text("contacts_add_new_already_connected_caption".localize()) + .font(.footnote) + .foregroundColor(.gray) + Text(name) + .font(.title3) + .fontWeight(.heavy) + .foregroundColor(.black) + } + } + + Divider() + + AtalaButton { + self.doneAction() + } label: { + Text("ok".localize()) + } + } + .padding(24) + } +} + +struct AlreadyConnectedView_Previews: PreviewProvider { + static var previews: some View { + AlreadyConnectedView( + name: "Atala KYC" + ) {} + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/UI/ConfirmConnectionView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/UI/ConfirmConnectionView.swift new file mode 100644 index 00000000..d55f20a9 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/UI/ConfirmConnectionView.swift @@ -0,0 +1,51 @@ +import SwiftUI + +struct ConfirmConnectionView: View { + let name: String + let loading: Bool + let doneAction: () -> Void + let cancelAction: () -> Void + + var body: some View { + VStack(spacing: 20) { + HStack { + VStack(alignment: .leading, spacing: 5) { + Text("contacts_add_new_confirm_connection_caption".localize()) + .font(.footnote) + .foregroundColor(.gray) + Text(name) + .font(.title3) + .fontWeight(.heavy) + .foregroundColor(.black) + } + Spacer() + } + + Divider() + + HStack { + AtalaButton(configuration: .secondary) { + self.cancelAction() + } label: { + Text("cancel".localize()) + } + + AtalaButton(loading: loading) { + self.doneAction() + } label: { + Text("contacts_add_new_confirm_connection_confirm_bt".localize()) + } + } + } + .padding(24) + } +} + +struct ConfirmConnectionView_Previews: PreviewProvider { + static var previews: some View { + ConfirmConnectionView( + name: "Atala KYC", + loading: false + ) {} cancelAction: {} + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/UI/InsertCodeView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/UI/InsertCodeView.swift new file mode 100644 index 00000000..b7248e16 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/UI/InsertCodeView.swift @@ -0,0 +1,57 @@ +import SwiftUI + +struct InsertCodeView: View { + @Binding var textField: String + let loading: Bool + let doneAction: () -> Void + let cancelAction: () -> Void + + var body: some View { + VStack(spacing: 35) { + VStack(spacing: 0) { + Image("img_qr_red") + Text("connections_enter_code".localize()) + .font(.title2) + .fontWeight(.heavy) + } + + TextField("", text: $textField) + .padding() + .roundedRectBorderWithText("connections_enter_code_placeholder".localize()) + + HStack { + AtalaButton( + configuration: .secondary, + action: { + self.cancelAction() + }, + label: { + Text("cancel".localize()) + } + ) + + AtalaButton( + loading: loading, + action: { + self.doneAction() + }, + label: { + Text("confirm".localize()) + } + ) + } + } + .padding(24) + } +} + +struct InsertCodeView_Previews: PreviewProvider { + static var previews: some View { + InsertCodeView( + textField: .constant(""), + loading: false, + doneAction: {}, + cancelAction: {} + ) + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Common/DisplayErrorState.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Common/DisplayErrorState.swift new file mode 100644 index 00000000..bbddaff8 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Common/DisplayErrorState.swift @@ -0,0 +1,12 @@ +import PrismAgent +import Foundation + +struct DisplayErrorState: DisplayError { + let message: String + let debugMessage: String? + + init(error: Error) { + message = "default_error_message".localize() + debugMessage = error.localizedDescription + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Connections/ConnectionsListRouter.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Connections/ConnectionsListRouter.swift new file mode 100644 index 00000000..eb261149 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Connections/ConnectionsListRouter.swift @@ -0,0 +1,16 @@ +import SwiftUI + +struct ConnectionsListRouterImpl: ConnectionsListRouter { + let container: DIContainer + + func routeToAddNewConnection() -> some View { +#if targetEnvironment(simulator) + AddNewContactBuilder().build(component: .init( + container: container, + token: nil + )) +#else + QRCodeScannerBuilder().build(component: .init(container: container)) +#endif + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Connections/ConnectionsListView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Connections/ConnectionsListView.swift index 3e297048..b7c0d2d2 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Connections/ConnectionsListView.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Connections/ConnectionsListView.swift @@ -7,7 +7,13 @@ protocol ConnectionsListViewModel: ObservableObject { func addConnection(invitation: String, alias: String) } -struct ConnectionsListView: View { +protocol ConnectionsListRouter { + associatedtype AddNewConnectionV: View + func routeToAddNewConnection() -> AddNewConnectionV +} + +struct ConnectionsListView: View { + let router: Router @StateObject var viewModel: ViewModel @State var showAddConnection = false @State var aliasInput = "" @@ -15,50 +21,28 @@ struct ConnectionsListView: View { var body: some View { NavigationStack { - VStack { - if showAddConnection { - VStack(spacing: 16) { - TextField("Alias", text: $aliasInput) - TextField("DID or OOB", text: $newConnectionInput) - HStack(spacing: 8) { - Button { - viewModel.addConnection(invitation: newConnectionInput, alias: aliasInput) - showAddConnection = false - } label: { - Text("Add") - } - Button { - showAddConnection = false - } label: { - Text("Cancel") - } + List { + ForEach(viewModel.connections) { connection in + VStack(spacing: 8) { + if let alias = connection.alias, !alias.isEmpty { + Text(alias) + } + HStack { + Text("Host: ") + Text(connection.hostDID) + .lineLimit(1) + .truncationMode(.middle) } - } - } - List { - ForEach(viewModel.connections) { connection in - VStack(spacing: 8) { - if let alias = connection.alias { - Text(alias) - } - HStack { - Text("Host: ") - Text(connection.hostDID) - .lineLimit(1) - .truncationMode(.middle) - } - HStack { - Text("Recipient: ") - Text(connection.recipientDID) - .lineLimit(1) - .truncationMode(.middle) - } + HStack { + Text("Recipient: ") + Text(connection.recipientDID) + .lineLimit(1) + .truncationMode(.middle) } } } } - .padding() .toolbar(content: { ToolbarItem(placement: .navigationBarTrailing) { Button(action: { @@ -68,6 +52,10 @@ struct ConnectionsListView: View { }) } }) + .clearFullScreenCover(isPresented: $showAddConnection) { + router.routeToAddNewConnection() + } + .navigationTitle("My Connections") } } } diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialDetail/CredentialDetailView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialDetail/CredentialDetailView.swift index f6ea7b98..2addf727 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialDetail/CredentialDetailView.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialDetail/CredentialDetailView.swift @@ -9,42 +9,47 @@ struct CredentialDetailView: View { @StateObject var viewModel: ViewModel var body: some View { - VStack(alignment: .leading, spacing: 16) { - HStack { - Text("Issuer:") - .font(.title2) - Text(viewModel.credential.issuer) - .lineLimit(nil) - } - - if let id = viewModel.credential.credentialDefinitionId { + VStack { + VStack(alignment: .leading, spacing: 16) { HStack { - Text("Credential Definition ID:") + Text("Issuer: ") .font(.title2) - Text(id) - .lineLimit(nil) + Text(viewModel.credential.issuer) + .lineLimit(3) + .truncationMode(.middle) } - } - if let id = viewModel.credential.schemaId { - HStack { - Text("Schema ID:") - .font(.title2) - Text(id) - .lineLimit(nil) + + if let id = viewModel.credential.credentialDefinitionId { + HStack { + Text("Credential Definition ID: ") + .font(.title2) + Text(id) + .lineLimit(3) + .truncationMode(.middle) + } + } + if let id = viewModel.credential.schemaId { + HStack { + Text("Schema ID:") + .font(.title2) + Text(id) + .lineLimit(3) + .truncationMode(.middle) + } } } - Text("Claims") - .font(.title2) - VStack(spacing: 8) { - ForEach(viewModel.credential.claims.sorted(by: >), id: \.key) { key, value in - Text("\(key) - \(value)") + .padding() + + List { + Section("Claims") { + ForEach(viewModel.credential.claims.sorted(by: >), id: \.key) { key, value in + Text("\(key): \(value)") + .lineLimit(3) + .truncationMode(.middle) + } } } - .padding(.leading) - - Spacer() } - .padding() } } diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListView.swift index 8467d9d2..bcb240b9 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListView.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListView.swift @@ -1,7 +1,12 @@ import SwiftUI protocol CredentialListViewModel: ObservableObject { + var requests: [CredentialListViewState.Requests] { get } + var responses: [CredentialListViewState.Responses] { get } var credentials: [CredentialListViewState.Credential] { get } + var requestId: String? { get set } + func acceptRequest(id: String, credentialId: String?) + func rejectRequest(id: String) } protocol CredentialListRouter { @@ -17,24 +22,98 @@ struct CredentialListView< @StateObject var viewModel: ViewModel let router: Router + @State var showCredentialList = false + var body: some View { NavigationStack { - List(viewModel.credentials, id: \.id) { credential in - NavigationLink(value: credential) { - VStack(alignment: .leading) { - Text(credential.id) - .font(.headline) - Text(credential.issuer) - .lineLimit(1) - .truncationMode(.middle) - .font(.subheadline) - .foregroundColor(.secondary) - Text(credential.issuanceDate) - .font(.subheadline) - .foregroundColor(.secondary) - Text(credential.type) - .font(.subheadline) - .foregroundColor(.secondary) + List { + Section("New Requests") { + ForEach(viewModel.requests) { flow in + VStack(alignment: .leading) { + Text(flow.textName) + .font(.headline) + Text(flow.thid) + .lineLimit(1) + .truncationMode(.middle) + .font(.subheadline) + .foregroundColor(.secondary) + HStack { + Button(action: { + switch flow { + case .presentationRequest: + viewModel.requestId = flow.id + self.showCredentialList = true + case .proposal: + viewModel.acceptRequest(id: flow.id, credentialId: nil) + } + }) { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + .font(.largeTitle) + } + + Button(action: { + viewModel.rejectRequest(id: flow.id) + }) { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.red) + .font(.largeTitle) + } + } + } + } + } + Section("Credentials") { + ForEach(viewModel.credentials, id: \.id) { credential in + NavigationLink(value: credential) { + VStack(alignment: .leading) { +// Text(credential.id) +// .lineLimit(1) +// .font(.headline) +// .truncationMode(.middle) + Text(credential.issuer) + .lineLimit(1) + .truncationMode(.middle) + .font(.headline) +// .foregroundColor(.secondary) + Text(credential.issuanceDate) + .font(.subheadline) + .foregroundColor(.secondary) + Text(credential.type) + .font(.subheadline) + .foregroundColor(.secondary) + } + } + } + } + Section("Responses") { + ForEach(viewModel.responses) { flow in + switch flow { + case .credentialRequest(let id): + NavigationLink(value: flow) { + VStack(alignment: .leading) { + Text("Credential Request") + .font(.headline) + Text(id) + .lineLimit(1) + .truncationMode(.middle) + .font(.subheadline) + .foregroundColor(.secondary) + } + } + case .presentation(let id): + NavigationLink(value: flow) { + VStack(alignment: .leading) { + Text("Presentation") + .font(.headline) + Text(id) + .lineLimit(1) + .truncationMode(.middle) + .font(.subheadline) + .foregroundColor(.secondary) + } + } + } } } } @@ -42,6 +121,41 @@ struct CredentialListView< router.routeToCredentialDetail(id: $0.id) } .navigationTitle("My Credentials") + .sheet(isPresented: $showCredentialList) { + CredentialSelectionListView() + .environmentObject(viewModel) + .presentationDetents([.medium]) + .presentationDragIndicator(.visible) + } + } + } +} + +struct CredentialSelectionListView: View { + @EnvironmentObject var viewModel: ViewModel + @Environment(\.dismiss) var dismiss + + var body: some View { + List { + Section("Select Credential") { + ForEach(viewModel.credentials, id: \.id) { credential in + Button(action: { + viewModel.acceptRequest(id: viewModel.requestId ?? "", credentialId: credential.id) + dismiss() + }) { + VStack(alignment: .leading) { + Text(credential.issuer) + .font(.headline) + .foregroundStyle(.black) + .lineLimit(1) + .truncationMode(.middle) + Text(credential.type) + .font(.subheadline) + .foregroundColor(.secondary) + } + } + } + } } } } diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListViewModel.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListViewModel.swift index cf22521e..0d771ac4 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListViewModel.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListViewModel.swift @@ -4,12 +4,20 @@ import Foundation import PrismAgent final class CredentialListViewModelImpl: CredentialListViewModel { + @Published var requests = [CredentialListViewState.Requests]() + @Published var responses = [CredentialListViewState.Responses]() @Published var credentials = [CredentialListViewState.Credential]() + @Published var requestId: String? = nil private let agent: PrismAgent + private let pluto: Pluto - init(agent: PrismAgent) { + init( + agent: PrismAgent, + pluto: Pluto + ) { self.agent = agent + self.pluto = pluto bind() } @@ -28,5 +36,107 @@ final class CredentialListViewModelImpl: CredentialListViewModel { } .replaceError(with: []) .assign(to: &$credentials) + + finalThreadFlowRequests() + finalThreadFlowResponses() + } + + private func finalThreadFlowRequests() { + pluto.getAllMessages() + .map { + getRequests(messages: $0) + } + .replaceError(with: []) + .assign(to: &$requests) + } + + private func finalThreadFlowResponses() { + pluto.getAllMessages() + .map { + getResponses(messages: $0) + } + .replaceError(with: []) + .assign(to: &$responses) + } + + func acceptRequest(id: String, credentialId: String?) { + Task.detached { [weak self] in + do { + guard + let self, + let message = try await self.pluto.getMessage(id: id).first().await() + else { + return + } + switch message.piuri { + case ProtocolTypes.didcommOfferCredential3_0.rawValue: + let newPrismDID = try await self.agent.createNewPrismDID() + guard let requestCredential = try await self.agent.prepareRequestCredentialWithIssuer( + did: newPrismDID, + offer: try OfferCredential3_0(fromMessage: message) + ) else { throw UnknownError.somethingWentWrongError() } + _ = try await self.agent.sendMessage(message: try requestCredential.makeMessage()) + + case ProtocolTypes.didcommRequestPresentation.rawValue: + let credential = try await self.agent.verifiableCredentials() + .map { $0.first { $0.id == credentialId } } + .first() + .await() + guard let credential else { + throw UnknownError.somethingWentWrongError() + } + let presentation = try await self.agent.createPresentationForRequestProof( + request: try RequestPresentation(fromMessage: message), + credential: credential + ) + _ = try await self.agent.sendMessage(message: try presentation.makeMessage()) + default: + throw UnknownError.somethingWentWrongError(customMessage: nil, underlyingErrors: nil) + } + } catch { + print(error.localizedDescription) + } + } + } + + func rejectRequest(id: String) { + } +} + +private func getRequests(messages: [Message]) -> [CredentialListViewState.Requests] { + let groupedByThreadId = Dictionary(grouping: messages) { $0.thid ?? "" } + let sortedValues = groupedByThreadId.mapValues { $0.sorted(by: { $0.createdTime < $1.createdTime }) } + return sortedValues.compactMap { dicRow -> CredentialListViewState.Requests? in + guard let last = dicRow.value.last else { + return nil + } + switch last.piuri { + case "https://didcomm.org/issue-credential/3.0/offer-credential": +// print(try! (last.attachments.first!.data as! AttachmentJsonData).data.tryToString()) + return CredentialListViewState.Requests.proposal(id: last.id, thid: dicRow.key) + case "https://didcomm.atalaprism.io/present-proof/3.0/request-presentation": +// print(try! (last.attachments.first!.data as! AttachmentJsonData).data.tryToString()) + return CredentialListViewState.Requests.presentationRequest(id: last.id, thid: dicRow.key) + default: + return nil + } + } +} + +private func getResponses(messages: [Message]) -> [CredentialListViewState.Responses] { + let groupedByThreadId = Dictionary(grouping: messages) { $0.thid ?? "" } + let sortedValues = groupedByThreadId.mapValues { $0.sorted(by: { $0.createdTime < $1.createdTime }) } + return sortedValues.compactMap { dicRow -> CredentialListViewState.Responses? in + guard let last = dicRow.value.last else { + return nil + } + switch last.piuri { + case "https://didcomm.org/issue-credential/3.0/request-credential": + return CredentialListViewState.Responses.credentialRequest(id: dicRow.key) + case "https://didcomm.atalaprism.io/present-proof/3.0/presentation": + return CredentialListViewState.Responses.presentation(id: dicRow.key) + default: + return nil + } } } diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListViewState.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListViewState.swift index 7df7a71a..64856181 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListViewState.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListViewState.swift @@ -1,4 +1,50 @@ struct CredentialListViewState { + enum Requests: Identifiable, Hashable { + case proposal(id: String, thid: String) + case presentationRequest(id: String, thid: String) + + var id: String { + switch self { + case .proposal(let id, _): + return id + case .presentationRequest(let id, _): + return id + } + } + + var textName: String { + switch self { + case .proposal: + return "Credential Proposal" + case .presentationRequest: + return "Presentation Request" + } + } + + var thid: String { + switch self { + case .proposal(_, let thid): + return thid + case .presentationRequest(_, let thid): + return thid + } + } + } + + enum Responses: Identifiable, Hashable { + case credentialRequest(id: String) + case presentation(id: String) + + var id: String { + switch self { + case .credentialRequest(let id): + return id + case .presentation(let id): + return id + } + } + } + struct Credential: Hashable { let id: String let issuer: String diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/RequestDetail/RequestDetailRouter.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/RequestDetail/RequestDetailRouter.swift new file mode 100644 index 00000000..fecc4ab4 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/RequestDetail/RequestDetailRouter.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/RequestDetail/RequestDetailView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/RequestDetail/RequestDetailView.swift new file mode 100644 index 00000000..7bdd01b4 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/RequestDetail/RequestDetailView.swift @@ -0,0 +1,24 @@ +import SwiftUI + +protocol RequestDetailViewModel: ObservableObject { + var request: RequestDetailViewState.RequestType { get } + + func acceptProposal(id: String) + func refuseProposal(id: String) + func acceptPresentation(id: String, credentialId: String) + func refusePresentation(id: String) +} + +protocol RequestDetailViewRouter { + associatedtype CredentialDetailV: View + + func routeToCredentialDetail() -> CredentialDetailV +} + +//struct RequestDetailView: View { +// @StateObject var viewModel: ViewModel +// let router: Router +// +// var body: some View { +// } +//} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/RequestDetail/RequestDetailViewModel.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/RequestDetail/RequestDetailViewModel.swift new file mode 100644 index 00000000..fecc4ab4 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/RequestDetail/RequestDetailViewModel.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/RequestDetail/RequestDetailViewState.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/RequestDetail/RequestDetailViewState.swift new file mode 100644 index 00000000..137ef682 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/RequestDetail/RequestDetailViewState.swift @@ -0,0 +1,26 @@ +import Foundation + +struct RequestDetailViewState { + struct CredentialProposal { + let thid: String + let issuer: String + let claims: [String: String] + } + + struct PresentationRequest { + struct CredentialPicker { + let id: String + let type: String + let issuer: String + } + + let thid: String + let verifier: String + let credentialPicker: [CredentialPicker] + } + + enum RequestType { + case credentialProposal(CredentialProposal) + case presentationRequest(PresentationRequest) + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Main/Main2Router.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Main/Main2Router.swift index d97ddf06..a080076b 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Main/Main2Router.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Main/Main2Router.swift @@ -20,12 +20,18 @@ final class Main2RouterImpl: Main2ViewRouter { castor: castor ) ).build() + + let mnemonics = ["pig", "fork", "educate", "gun", "entire", "scatter", "satoshi", "laugh", "project", "buffalo", "race", "enroll", "shiver", "theme", "similar", "thought", "prepare", "velvet", "wild", "mention", "jelly", "match", "document", "rapid"] + + let seed = try! apollo.createSeed(mnemonics: mnemonics, passphrase: "") + let agent = PrismAgent( apollo: apollo, castor: castor, pluto: pluto, pollux: pollux, - mercury: mercury + mercury: mercury, + seed: seed ) container.register(type: Apollo.self, component: apollo) container.register(type: Castor.self, component: castor) @@ -60,7 +66,10 @@ final class Main2RouterImpl: Main2ViewRouter { agent: container.resolve(type: PrismAgent.self)! ) - return ConnectionsListView(viewModel: viewModel) + return ConnectionsListView( + router: ConnectionsListRouterImpl(container: container), + viewModel: viewModel + ) } func routeToMessages() -> some View { @@ -76,7 +85,8 @@ final class Main2RouterImpl: Main2ViewRouter { func routeToCredentials() -> some View { let viewModel = CredentialListViewModelImpl( - agent: container.resolve(type: PrismAgent.self)! + agent: container.resolve(type: PrismAgent.self)!, + pluto: container.resolve(type: Pluto.self)! ) return CredentialListView( @@ -84,6 +94,11 @@ final class Main2RouterImpl: Main2ViewRouter { router: CredentialListRouterImpl(container: container) ) } + + func routeToSettings() -> some View { + let router = SettingsViewRouterImpl(container: container) + return SettingsView(viewModel: SettingsViewModelImpl(), router: router) + } } private func createSecretsStream( diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Main/Main2View.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Main/Main2View.swift index fb3269d9..2c8132c2 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Main/Main2View.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Main/Main2View.swift @@ -6,12 +6,14 @@ protocol Main2ViewRouter { associatedtype ConnectionsV: View associatedtype MessagesV: View associatedtype CredentialsV: View + associatedtype SettingsV: View func routeToMediator() -> MediatorV func routeToDids() -> DidsV func routeToConnections() -> ConnectionsV func routeToMessages() -> MessagesV func routeToCredentials() -> CredentialsV + func routeToSettings() -> SettingsV } struct Main2View: View { @@ -19,16 +21,6 @@ struct Main2View: View { var body: some View { TabView { - router.routeToMediator() - .tabItem { - Text("Mediator") - } - - router.routeToDids() - .tabItem { - Text("DID") - } - router.routeToConnections() .tabItem { Text("Connections") @@ -43,6 +35,11 @@ struct Main2View: View { .tabItem { Text("Credentials") } + + router.routeToSettings() + .tabItem { + Text("Settings") + } } } } diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Mediator/MediatorPage/MediatorPageView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Mediator/MediatorPage/MediatorPageView.swift index b6f8b076..8a2ebf42 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Mediator/MediatorPage/MediatorPageView.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Mediator/MediatorPage/MediatorPageView.swift @@ -15,40 +15,38 @@ struct MediatorPageView: View { @State var didInput = "did:peer:2.Ez6LSghwSE437wnDE1pt3X6hVDUQzSjsHzinpX3XFvMjRAm7y.Vz6Mkhh1e5CEYYq6JBUcTZ6Cp2ranCWRrv7Yax3Le4N59R6dd.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHBzOi8vc2l0LXByaXNtLW1lZGlhdG9yLmF0YWxhcHJpc20uaW8iLCJhIjpbImRpZGNvbW0vdjIiXX19.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6IndzczovL3NpdC1wcmlzbS1tZWRpYXRvci5hdGFsYXByaXNtLmlvL3dzIiwiYSI6WyJkaWRjb21tL3YyIl19fQ" var body: some View { - NavigationStack { - VStack { - if let mediator = viewModel.mediator { - VStack(spacing: 16) { - Text("Mediator DID:") - Text(mediator.mediatorDID) - Text("Routing DID:") - Text(mediator.routingDID) - Text("State of agent: \(viewModel.agentRunning ? "Running" : "Stoped")") - Button { - if viewModel.agentRunning { - viewModel.stopAgent() - } else { - viewModel.startAgent(mediatorDID: mediator.mediatorDID) - } - } label: { - Text(viewModel.agentRunning ? "Stop" : "Start") - } - .disabled(viewModel.loading) - } - } else { - VStack(spacing: 16) { - TextField("Mediator DID", text: $didInput) - Button { - viewModel.startAgent(mediatorDID: didInput) - } label: { - Text("Start") + VStack { + if let mediator = viewModel.mediator { + VStack(spacing: 16) { + Text("Mediator DID:") + Text(mediator.mediatorDID) + Text("Routing DID:") + Text(mediator.routingDID) + Text("State of agent: \(viewModel.agentRunning ? "Running" : "Stoped")") + Button { + if viewModel.agentRunning { + viewModel.stopAgent() + } else { + viewModel.startAgent(mediatorDID: mediator.mediatorDID) } + } label: { + Text(viewModel.agentRunning ? "Stop" : "Start") } .disabled(viewModel.loading) } + } else { + VStack(spacing: 16) { + TextField("Mediator DID", text: $didInput) + Button { + viewModel.startAgent(mediatorDID: didInput) + } label: { + Text("Start") + } + } + .disabled(viewModel.loading) } - .padding() - .toastView(toast: $viewModel.error) } + .padding() + .toastView(toast: $viewModel.error) } } diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Mediator/MediatorPage/MediatorPageViewModel.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Mediator/MediatorPage/MediatorPageViewModel.swift index 4eb583c3..887867ea 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Mediator/MediatorPage/MediatorPageViewModel.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Mediator/MediatorPage/MediatorPageViewModel.swift @@ -40,6 +40,15 @@ final class MediatorViewModelImpl: MediatorPageViewModel { } .replaceError(with: nil) .assign(to: &$mediator) + + Task.detached { [weak self] in + if + let self, + let mediator = try await self.pluto.getAllMediators().map({ $0.first}).first().await() + { + self.startAgent(mediatorDID: mediator.did.string) + } + } } func startAgent(mediatorDID: String) { diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Messages/MessagesList/MessagesListView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Messages/MessagesList/MessagesListView.swift index 280ef9dd..4d1e6c8b 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Messages/MessagesList/MessagesListView.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Messages/MessagesList/MessagesListView.swift @@ -41,7 +41,7 @@ struct MessagesListView some View { + let viewModel = getViewModel(component: component) { + QRCodeScannerViewModelImpl() + } + let router = QRCodeScannerRouterImpl(container: component.container) + return QRCodeScannerView(router: router, viewModel: viewModel) + .onDisappear { + component.container.unregister(type: QRCodeScannerViewModelImpl.self) + } + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/QRCode/QRCodeScannerRouter.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/QRCode/QRCodeScannerRouter.swift new file mode 100644 index 00000000..3fcfee2b --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/QRCode/QRCodeScannerRouter.swift @@ -0,0 +1,14 @@ +import SwiftUI + +struct QRCodeScannerRouterImpl: QRCodeScannerRouter { + let container: DIContainer + + func routeToAddNewContact(token: String?) -> some View { + AddNewContactBuilder().build( + component: .init( + container: container, + token: token + ) + ) + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/QRCode/QRCodeScannerView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/QRCode/QRCodeScannerView.swift new file mode 100644 index 00000000..dfdebbfc --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/QRCode/QRCodeScannerView.swift @@ -0,0 +1,102 @@ +import SwiftUI + +protocol QRCodeScannerViewModel: ObservableObject { + var token: String? { get } + var showInfo: Bool { get set } + var dismiss: Bool { get set } + func qrCodeFound(_ qrCode: String) + func cameraError(_ error: Error) +} + +protocol QRCodeScannerRouter { + associatedtype AddNewContact: View + func routeToAddNewContact(token: String?) -> AddNewContact +} + +struct QRCodeScannerView< + ViewModel: QRCodeScannerViewModel, + Router: QRCodeScannerRouter +>: View { + let router: Router + @StateObject var viewModel: ViewModel + @Environment(\.presentationMode) var presentationMode + + var body: some View { + ZStack { + ZStack { + QRScannerView { + self.viewModel.qrCodeFound($0) + } onCameraError: { + self.viewModel.cameraError($0) + } + LinearGradient( + gradient: Gradient(colors: [ + .black.opacity(0.6), + .clear, + .clear, + .black.opacity(0.6) + ]), + startPoint: .top, + endPoint: .bottom + ) + } + .ignoresSafeArea() + + VStack { + HStack { + Button { + self.viewModel.dismiss = true + } label: { + Image(systemName: "xmark.circle.fill") + .resizable() + .foregroundColor(.white) + .frame(width: 40, height: 40) + } + Spacer() + } + Spacer() + } + .padding(.horizontal, 26) + .padding(.vertical) + } + .preferredColorScheme(.dark) + .clearFullScreenCover( + isPresented: $viewModel.showInfo + ) { + LazyView { + router.routeToAddNewContact(token: viewModel.token) + .environment(\.rootPresentationMode, $viewModel.dismiss) + } + } + .onChange(of: viewModel.dismiss, perform: { value in + if value { + self.presentationMode.wrappedValue.dismiss() + } + }) + } +} + +struct QRCodeScannerView_Previews: PreviewProvider { + static var previews: some View { + QRCodeScannerView(router: MockRouter(), viewModel: MockViewModel()) + } +} + +private class MockViewModel: QRCodeScannerViewModel { + var token: String? = "" + + var showInfo = false + + var dismiss = false + + func qrCodeFound(_ qrCode: String) {} + + func cameraError(_ error: Error) {} +} + +private struct MockRouter: QRCodeScannerRouter { + func routeToAddNewContact(token: String?) -> some View { + Text("token") + .foregroundColor(.white) + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/QRCode/QRCodeScannerViewModel.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/QRCode/QRCodeScannerViewModel.swift new file mode 100644 index 00000000..8b43b6a4 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/QRCode/QRCodeScannerViewModel.swift @@ -0,0 +1,16 @@ +import Combine +import Foundation + +final class QRCodeScannerViewModelImpl: QRCodeScannerViewModel { + @Published var token: String? + @Published var showInfo = false + @Published var dismiss = false + + func qrCodeFound(_ qrCode: String) { + guard !showInfo, !dismiss else { return } + token = qrCode + showInfo = true + } + + func cameraError(_ error: Error) {} +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/QRCode/UI/CameraViewController.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/QRCode/UI/CameraViewController.swift new file mode 100644 index 00000000..ebf35c5e --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/QRCode/UI/CameraViewController.swift @@ -0,0 +1,144 @@ +import AVFoundation +import Combine +import UIKit + +protocol CameraViewDelegate: AVCaptureMetadataOutputObjectsDelegate { + func didFail(error: Error) +} + +final class CameraViewController: UIViewController { + enum CameraError: Error { + case invalidInput + case invalidOutput + } + + let codeTypes: [AVMetadataObject.ObjectType] + let videoCaptureDevice = AVCaptureDevice.default(for: .video) + var captureSession: AVCaptureSession! + var previewLayer: AVCaptureVideoPreviewLayer! + weak var delegate: CameraViewDelegate? + var cancellables = Set() + + init(codeTypes: [AVMetadataObject.ObjectType], delegate: CameraViewDelegate? = nil) { + self.codeTypes = codeTypes + self.delegate = delegate + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + NotificationCenter.default + .publisher(for: Notification.Name("UIDeviceOrientationDidChangeNotification")) + .sink { [weak self] _ in + self?.updateOrientation() + } + .store(in: &cancellables) + + view.backgroundColor = UIColor.black + captureSession = AVCaptureSession() + + guard let videoCaptureDevice = videoCaptureDevice else { + return + } + + guard let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice) else { return } + + if captureSession.canAddInput(videoInput) { + captureSession.addInput(videoInput) + } else { + delegate?.didFail(error: CameraError.invalidInput) + return + } + + let metadataOutput = AVCaptureMetadataOutput() + + if captureSession.canAddOutput(metadataOutput) { + captureSession.addOutput(metadataOutput) + + metadataOutput.setMetadataObjectsDelegate(delegate, queue: DispatchQueue.main) + metadataOutput.metadataObjectTypes = codeTypes + } else { + delegate?.didFail(error: CameraError.invalidOutput) + return + } + + let gesture = UITapGestureRecognizer(target: self, action: #selector(tap(sender:))) + view.addGestureRecognizer(gesture) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if previewLayer == nil { + previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + } + previewLayer.frame = view.layer.bounds + previewLayer.videoGravity = .resizeAspectFill + view.layer.addSublayer(previewLayer) + + if captureSession?.isRunning == false { + captureSession.startRunning() + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + updateOrientation() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + if captureSession?.isRunning == true { + captureSession.stopRunning() + } + } + + override func viewWillLayoutSubviews() { + previewLayer?.frame = view.layer.bounds + } + + @objc func tap(sender: UITapGestureRecognizer) { + guard let touchView = sender.view else { return } + if sender.state == .ended { + let screenSize = touchView.bounds.size + let touchPoint = sender.location(in: touchView) + let xPoint = touchPoint.y / screenSize.height + let yPoint = 1.0 - touchPoint.x / screenSize.width + let focusPoint = CGPoint(x: xPoint, y: yPoint) + + focus(point: focusPoint) + } + } + + private func updateOrientation() { + guard let connection = captureSession.connections.last, connection.isVideoOrientationSupported else { return } + view.window?.windowScene.map { + connection.videoOrientation = AVCaptureVideoOrientation( + rawValue: $0.interfaceOrientation.rawValue + ) ?? .portrait + } + } + + private func focus(point: CGPoint) { + guard let device = videoCaptureDevice else { return } + + do { + try device.lockForConfiguration() + } catch { + return + } + + device.focusPointOfInterest = point + device.focusMode = .continuousAutoFocus + device.exposurePointOfInterest = point + device.exposureMode = AVCaptureDevice.ExposureMode.continuousAutoExposure + device.unlockForConfiguration() + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/QRCode/UI/QRScannerView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/QRCode/UI/QRScannerView.swift new file mode 100644 index 00000000..73d74ae8 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/QRCode/UI/QRScannerView.swift @@ -0,0 +1,53 @@ +import AVFoundation +import SwiftUI + +struct QRScannerView: UIViewControllerRepresentable { + class Coordinator: NSObject, CameraViewDelegate { + let onQRCodeFound: (String) -> Void + let onError: (Error) -> Void + + init( + onQRCodeFound: @escaping (String) -> Void, + onError: @escaping (Error) -> Void + ) { + self.onQRCodeFound = onQRCodeFound + self.onError = onError + } + + func metadataOutput( + _ output: AVCaptureMetadataOutput, + didOutput metadataObjects: [AVMetadataObject], + from connection: AVCaptureConnection + ) { + guard + let metadataObject = metadataObjects.first, + let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject, + let stringValue = readableObject.stringValue + else { return } + onQRCodeFound(stringValue) + } + + func reset() {} + + func didFail(error: Error) { + onError(error) + } + } + + let onQRCodeFound: (String) -> Void + let onCameraError: (Error) -> Void + + func makeCoordinator() -> Coordinator { + return Coordinator( + onQRCodeFound: onQRCodeFound, + onError: onCameraError + ) + } + + func makeUIViewController(context: Context) -> CameraViewController { + let viewController = CameraViewController(codeTypes: [.qr], delegate: context.coordinator) + return viewController + } + + func updateUIViewController(_ uiViewController: CameraViewController, context: Context) {} +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Settings/SettingsView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Settings/SettingsView.swift new file mode 100644 index 00000000..63393ff3 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Settings/SettingsView.swift @@ -0,0 +1,47 @@ +import SwiftUI + +protocol SettingsViewModel: ObservableObject { + var menu: [SettingsViewState.Menu] { get } +} + +protocol SettingsViewRouter { + associatedtype MediatorV: View + associatedtype DIDsV: View + + func routeToMediator() -> MediatorV + func routeToDIDs() -> DIDsV +} + +class SettingsViewModelImpl: SettingsViewModel { + @Published var menu = [ + SettingsViewState.Menu.mediator, + SettingsViewState.Menu.dids + ] +} + +struct SettingsView: View { + + @StateObject var viewModel: ViewModel + let router: Router + + var body: some View { + NavigationStack { + List(viewModel.menu) { menu in + NavigationLink(value: menu) { + Text(menu.rawValue) + .lineLimit(1) + .font(.headline) + .truncationMode(.middle) + } + } + .navigationDestination(for: SettingsViewState.Menu.self) { menu in + switch menu { + case .dids: + router.routeToDIDs() + case .mediator: + router.routeToMediator() + } + } + } + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Settings/SettingsViewRouter.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Settings/SettingsViewRouter.swift new file mode 100644 index 00000000..eac484aa --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Settings/SettingsViewRouter.swift @@ -0,0 +1,25 @@ +import Domain +import PrismAgent +import SwiftUI + +struct SettingsViewRouterImpl: SettingsViewRouter { + let container: DIContainer + + func routeToDIDs() -> some View { + let viewModel = DIDListViewModelImpl( + pluto: container.resolve(type: Pluto.self)!, + agent: container.resolve(type: PrismAgent.self)! + ) + + return DIDListView(viewModel: viewModel) + } + + func routeToMediator() -> some View { + let viewModel = MediatorViewModelImpl( + castor: container.resolve(type: Castor.self)!, + pluto: container.resolve(type: Pluto.self)!, + agent: container.resolve(type: PrismAgent.self)! + ) + return MediatorPageView(viewModel: viewModel) + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Settings/SettingsViewState.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Settings/SettingsViewState.swift new file mode 100644 index 00000000..84b6514b --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Settings/SettingsViewState.swift @@ -0,0 +1,12 @@ +import Foundation + +struct SettingsViewState { + enum Menu: String, Identifiable { + case dids = "DIDs" + case mediator = "Mediator" + + var id: String { + self.rawValue + } + } +}