diff --git a/SampleApps/PingExample/PingExample.xcodeproj/project.pbxproj b/SampleApps/PingExample/PingExample.xcodeproj/project.pbxproj index 2369cd4..54a18cd 100644 --- a/SampleApps/PingExample/PingExample.xcodeproj/project.pbxproj +++ b/SampleApps/PingExample/PingExample.xcodeproj/project.pbxproj @@ -10,7 +10,7 @@ 3A203D852BDB1A900020C995 /* PingDavinci.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A5442152BCE1F2900385131 /* PingDavinci.framework */; }; 3A203D862BDB1A900020C995 /* PingDavinci.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3A5442152BCE1F2900385131 /* PingDavinci.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 3A203D892BDB22B00020C995 /* PingOidc.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AB1CA252BD6F9AD003FCE3C /* PingOidc.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 3A3709962BEB038200AA7B51 /* AccessToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3709952BEB038200AA7B51 /* AccessToken.swift */; }; + 3A3709962BEB038200AA7B51 /* AccessTokenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3709952BEB038200AA7B51 /* AccessTokenView.swift */; }; 3A5441332BCDF10900385131 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5441322BCDF10900385131 /* ContentView.swift */; }; 3A5441352BCDF10B00385131 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A5441342BCDF10B00385131 /* Assets.xcassets */; }; 3A5441382BCDF10B00385131 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A5441372BCDF10B00385131 /* Preview Assets.xcassets */; }; @@ -24,12 +24,18 @@ 3ACD90E12BE586BC00DABCE6 /* DavinciViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACD90E02BE586BC00DABCE6 /* DavinciViewModel.swift */; }; 3AE945742BF27224007B3381 /* LoginUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE945732BF27223007B3381 /* LoginUtility.swift */; }; A588D0032BF6A00800F9052A /* PingOrchestrate.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3A5442272BCE1F3800385131 /* PingOrchestrate.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - A588D0062BF6A0E500F9052A /* StorageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A588D0042BF6A0E500F9052A /* StorageViewModel.swift */; }; - A588D0072BF6A0E500F9052A /* LoggerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A588D0052BF6A0E500F9052A /* LoggerViewModel.swift */; }; A588D0082BF6A1FF00F9052A /* PingLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A588CFF22BF69F1A00F9052A /* PingLogger.framework */; }; A588D0092BF6A1FF00F9052A /* PingLogger.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A588CFF22BF69F1A00F9052A /* PingLogger.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; A588D00A2BF6A20500F9052A /* PingStorage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A588CFFF2BF69F3300F9052A /* PingStorage.framework */; }; A588D00B2BF6A20500F9052A /* PingStorage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A588CFFF2BF69F3300F9052A /* PingStorage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + ECB75DC42C986312000517C9 /* ConfigurationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECB75DC32C986312000517C9 /* ConfigurationManager.swift */; }; + ECB75DC62C986663000517C9 /* ConfigurationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECB75DC52C986663000517C9 /* ConfigurationViewModel.swift */; }; + ECFB77CD2CAC14A400FF02BC /* DavinciView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFB77C22CAC149F00FF02BC /* DavinciView.swift */; }; + ECFB77D12CAC153300FF02BC /* UserInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFB77D02CAC152A00FF02BC /* UserInfoView.swift */; }; + ECFB77D82CAC168500FF02BC /* ConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFB77D72CAC168000FF02BC /* ConfigurationView.swift */; }; + ECFB77DA2CAC16A400FF02BC /* LogoutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFB77D92CAC169F00FF02BC /* LogoutView.swift */; }; + ECFB77DC2CAC16FC00FF02BC /* LogoutViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFB77DB2CAC16FA00FF02BC /* LogoutViewModel.swift */; }; + ECFB77DE2CAC172800FF02BC /* UserInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFB77DD2CAC172200FF02BC /* UserInfoViewModel.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -138,7 +144,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 3A3709952BEB038200AA7B51 /* AccessToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessToken.swift; sourceTree = ""; }; + 3A3709952BEB038200AA7B51 /* AccessTokenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessTokenView.swift; sourceTree = ""; }; 3A54412D2BCDF10900385131 /* PingExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PingExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 3A5441322BCDF10900385131 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 3A5441342BCDF10B00385131 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -159,8 +165,15 @@ 3AE945732BF27223007B3381 /* LoginUtility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginUtility.swift; sourceTree = ""; }; A588CFEA2BF69F1A00F9052A /* PingLogger.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = PingLogger.xcodeproj; path = ../../PingLogger/PingLogger.xcodeproj; sourceTree = ""; }; A588CFF92BF69F3300F9052A /* PingStorage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = PingStorage.xcodeproj; path = ../../PingStorage/PingStorage.xcodeproj; sourceTree = ""; }; - A588D0042BF6A0E500F9052A /* StorageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageViewModel.swift; sourceTree = ""; }; - A588D0052BF6A0E500F9052A /* LoggerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggerViewModel.swift; sourceTree = ""; }; + ECB75DC32C986312000517C9 /* ConfigurationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationManager.swift; sourceTree = ""; }; + ECB75DC52C986663000517C9 /* ConfigurationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigurationViewModel.swift; sourceTree = ""; }; + ECB75DC72C999368000517C9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + ECFB77C22CAC149F00FF02BC /* DavinciView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DavinciView.swift; sourceTree = ""; }; + ECFB77D02CAC152A00FF02BC /* UserInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInfoView.swift; sourceTree = ""; }; + ECFB77D72CAC168000FF02BC /* ConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationView.swift; sourceTree = ""; }; + ECFB77D92CAC169F00FF02BC /* LogoutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutView.swift; sourceTree = ""; }; + ECFB77DB2CAC16FA00FF02BC /* LogoutViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutViewModel.swift; sourceTree = ""; }; + ECFB77DD2CAC172200FF02BC /* UserInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInfoViewModel.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -220,16 +233,10 @@ 3A54412F2BCDF10900385131 /* PingExample */ = { isa = PBXGroup; children = ( - 3AE945732BF27223007B3381 /* LoginUtility.swift */, - 3A5441322BCDF10900385131 /* ContentView.swift */, - 3A8532322BE2D5D800F8619D /* LoginView.swift */, - 3A3709952BEB038200AA7B51 /* AccessToken.swift */, - 3AB1C9F32BD6C6F3003FCE3C /* LoginViewModel.swift */, - 3ACD90E02BE586BC00DABCE6 /* DavinciViewModel.swift */, - 3A8092F52BE9BB6B00AD667F /* TokenViewModel.swift */, - 3A8092F32BE99F0F00AD667F /* InputView.swift */, - A588D0052BF6A0E500F9052A /* LoggerViewModel.swift */, - A588D0042BF6A0E500F9052A /* StorageViewModel.swift */, + ECB75DC72C999368000517C9 /* Info.plist */, + ECFB77D22CAC157700FF02BC /* Utilities */, + ECFB77CF2CAC150200FF02BC /* ViewModels */, + ECFB77CE2CAC14F600FF02BC /* Views */, 3A5441342BCDF10B00385131 /* Assets.xcassets */, 3A5441362BCDF10B00385131 /* Preview Content */, ); @@ -313,6 +320,43 @@ name = Products; sourceTree = ""; }; + ECFB77CE2CAC14F600FF02BC /* Views */ = { + isa = PBXGroup; + children = ( + ECFB77D02CAC152A00FF02BC /* UserInfoView.swift */, + 3A5441322BCDF10900385131 /* ContentView.swift */, + 3A8532322BE2D5D800F8619D /* LoginView.swift */, + ECFB77C22CAC149F00FF02BC /* DavinciView.swift */, + 3A3709952BEB038200AA7B51 /* AccessTokenView.swift */, + 3A8092F32BE99F0F00AD667F /* InputView.swift */, + ECFB77D72CAC168000FF02BC /* ConfigurationView.swift */, + ECFB77D92CAC169F00FF02BC /* LogoutView.swift */, + ); + path = Views; + sourceTree = ""; + }; + ECFB77CF2CAC150200FF02BC /* ViewModels */ = { + isa = PBXGroup; + children = ( + ECB75DC52C986663000517C9 /* ConfigurationViewModel.swift */, + 3ACD90E02BE586BC00DABCE6 /* DavinciViewModel.swift */, + 3AB1C9F32BD6C6F3003FCE3C /* LoginViewModel.swift */, + 3A8092F52BE9BB6B00AD667F /* TokenViewModel.swift */, + ECFB77DB2CAC16FA00FF02BC /* LogoutViewModel.swift */, + ECFB77DD2CAC172200FF02BC /* UserInfoViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + ECFB77D22CAC157700FF02BC /* Utilities */ = { + isa = PBXGroup; + children = ( + ECB75DC32C986312000517C9 /* ConfigurationManager.swift */, + 3AE945732BF27223007B3381 /* LoginUtility.swift */, + ); + path = Utilities; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -539,16 +583,22 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - A588D0072BF6A0E500F9052A /* LoggerViewModel.swift in Sources */, + ECFB77D12CAC153300FF02BC /* UserInfoView.swift in Sources */, 3A8092F62BE9BB6B00AD667F /* TokenViewModel.swift in Sources */, 3ACD90E12BE586BC00DABCE6 /* DavinciViewModel.swift in Sources */, + ECFB77DC2CAC16FC00FF02BC /* LogoutViewModel.swift in Sources */, + ECFB77DA2CAC16A400FF02BC /* LogoutView.swift in Sources */, 3AE945742BF27224007B3381 /* LoginUtility.swift in Sources */, - A588D0062BF6A0E500F9052A /* StorageViewModel.swift in Sources */, 3A8532332BE2D5D800F8619D /* LoginView.swift in Sources */, 3A5441332BCDF10900385131 /* ContentView.swift in Sources */, + ECB75DC62C986663000517C9 /* ConfigurationViewModel.swift in Sources */, 3A8092F42BE99F0F00AD667F /* InputView.swift in Sources */, + ECB75DC42C986312000517C9 /* ConfigurationManager.swift in Sources */, + ECFB77CD2CAC14A400FF02BC /* DavinciView.swift in Sources */, + ECFB77D82CAC168500FF02BC /* ConfigurationView.swift in Sources */, + ECFB77DE2CAC172800FF02BC /* UserInfoViewModel.swift in Sources */, 3AB1C9F42BD6C6F3003FCE3C /* LoginViewModel.swift in Sources */, - 3A3709962BEB038200AA7B51 /* AccessToken.swift in Sources */, + 3A3709962BEB038200AA7B51 /* AccessTokenView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -715,10 +765,10 @@ DEVELOPMENT_TEAM = 9QSE66762D; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_CFBundleDisplayName = Davinci; + INFOPLIST_FILE = PingExample/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = DaVinci; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -746,10 +796,10 @@ DEVELOPMENT_TEAM = 9QSE66762D; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_CFBundleDisplayName = Davinci; + INFOPLIST_FILE = PingExample/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = DaVinci; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; IPHONEOS_DEPLOYMENT_TARGET = 16.0; diff --git a/SampleApps/PingExample/PingExample/ContentView.swift b/SampleApps/PingExample/PingExample/ContentView.swift deleted file mode 100644 index 13ada42..0000000 --- a/SampleApps/PingExample/PingExample/ContentView.swift +++ /dev/null @@ -1,161 +0,0 @@ -import SwiftUI - - -struct ContentView: View { - - - @State private var startDavinici = false - - @State private var path: [String] = [] - - var body: some View { - NavigationStack(path: $path) { - List { - NavigationLink(value: "Davinci") { - Text("Launch Davinci") - } - NavigationLink(value: "Token") { - Text("Access Token") - } - NavigationLink(value: "User") { - Text("User Info") - } - - NavigationLink(value: "Logout") { - Text("Logout") - } - - NavigationLink(value: "Logger") { - Text("Logger") - } - NavigationLink(value: "Storage") { - Text("Storage") - } - }.navigationDestination(for: String.self) { item in - switch item { - case "Davinci": - DavinciView(path: $path) - case "Token": - AccessTokenView() - case "User": - UserInfoView() - case "Logout": - LogOutView(path: $path) - case "Logger": - LoggerView() - case "Storage": - StorageView() - default: - EmptyView() - } - }.navigationBarTitle("Ping Davinci") - } - } -} - - -struct LogOutView: View { - - @Binding var path: [String] - - @StateObject private var viewmodel = LogOutViewModel() - - var body: some View { - - Text("Logout") - .font(.title) - .navigationBarTitle("Logout", displayMode: .inline) - - NextButton(title: "Procced to logout") { - Task { - await viewmodel.logout() - path.removeLast() - path.append("Davinci") - } - } - - } -} - - -struct SecondTabView: View { - var body: some View { - Text("LogOut") - .font(.title) - .navigationBarTitle("LogOut", displayMode: .inline) - - } -} - -struct Register: View { - var body: some View { - Text("Register") - .font(.title) - .navigationBarTitle("Register", displayMode: .inline) - } -} - -struct ForgotPassword: View { - var body: some View { - Text("ForgotPassword") - .font(.title) - .navigationBarTitle("ForgotPassword", displayMode: .inline) - } -} - -struct LoggerView: View { - var loggerViewModel = LoggerViewModel() - var body: some View { - Text("This View is for testing Logger functionality.\nPlease check the Console Logs") - .font(.title3) - .multilineTextAlignment(.center) - .navigationBarTitle("Logger", displayMode: .inline) - .onAppear() { - loggerViewModel.setupLogger() - } - } -} - -struct StorageView: View { - var storageViewModel = StorageViewModel() - var body: some View { - Text("This View is for testing Storage functionality.\nPlease check the Console Logs") - .font(.title3) - .multilineTextAlignment(.center) - .navigationBarTitle("Storage", displayMode: .inline) - .onAppear() { - Task { - await storageViewModel.setupMemoryStorage() - await storageViewModel.setupKeychainStorage() - } - } - } -} - -@main -struct MyApp: App { - var body: some Scene { - WindowGroup { - ContentView() - } - } -} - -struct ActivityIndicatorView: View { - @Binding var isAnimating: Bool - let style: UIActivityIndicatorView.Style - let color: Color - - var body: some View { - if isAnimating { - VStack { - Spacer() - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: color)) - .padding() - Spacer() - } - .background(Color.black.opacity(0.4).ignoresSafeArea()) - } - } -} diff --git a/SampleApps/PingExample/PingExample/DavinciViewModel.swift b/SampleApps/PingExample/PingExample/DavinciViewModel.swift deleted file mode 100644 index ff0a023..0000000 --- a/SampleApps/PingExample/PingExample/DavinciViewModel.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// DavinciViewModel.swift -// PingExample -// -// Copyright (c) 2024 Ping Identity. All rights reserved. -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -import Foundation -import PingDavinci -import PingOidc -import PingOrchestrate - -public let davinciStage = DaVinci.createDaVinci { config in - //config.debug = true - - config.module(OidcModule.config) { oidcValue in - oidcValue.clientId = "3172d977-8fdc-4e8b-b3c5-4f3a34cb7262" - oidcValue.scopes = ["openid", "email", "address", "phone", "profile"] - oidcValue.redirectUri = "org.forgerock.demo://oauth2redirect" - oidcValue.discoveryEndpoint = "https://auth.test-one-pingone.com/0c6851ed-0f12-4c9a-a174-9b1bf8b438ae/as/.well-known/openid-configuration" - } -} - -public let davinci = DaVinci.createDaVinci { config in - //config.debug = true - - config.module(OidcModule.config) { oidcValue in - oidcValue.clientId = "c12743f9-08e8-4420-a624-71bbb08e9fe1" - oidcValue.scopes = ["openid", "email", "address", "phone", "profile"] - oidcValue.redirectUri = "org.forgerock.demo://oauth2redirect" - oidcValue.discoveryEndpoint = "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration" - } -} - -class DavinciViewModel: ObservableObject { - - @Published public var data: StateNode = StateNode() - - @Published public var isLoading: Bool = false - - init() { - - Task { - await startDavinci() - } - } - - - private func startDavinci() async { - - await MainActor.run { - isLoading = true - } - - let node = await davinci.start() - - if let connector = node as? Connector { - let node = await connector.next() - await MainActor.run { - self.data = StateNode(currentNode: node, previousNode: node) - } - } else { - await MainActor.run { - self.data = StateNode(currentNode: node, previousNode: node) - } - } - - await MainActor.run { - isLoading = false - } - - } - - public func next(node: Node) async { - await MainActor.run { - isLoading = true - } - if let connector = node as? Connector { - let next = await connector.next() - await MainActor.run { - self.data = StateNode(currentNode: next, previousNode: node) - isLoading = false - } - } - } -} - -class StateNode { - var currentNode: Node? = nil - var previousNode: Node? = nil - - init(currentNode: Node? = nil, previousNode: Node? = nil) { - self.currentNode = currentNode - self.previousNode = previousNode - } -} diff --git a/SampleApps/PingExample/PingExample/Info.plist b/SampleApps/PingExample/PingExample/Info.plist new file mode 100644 index 0000000..5e86f97 --- /dev/null +++ b/SampleApps/PingExample/PingExample/Info.plist @@ -0,0 +1,11 @@ + + + + + UILaunchScreen + + UIImageName + Logo + + + diff --git a/SampleApps/PingExample/PingExample/LoggerViewModel.swift b/SampleApps/PingExample/PingExample/LoggerViewModel.swift deleted file mode 100644 index f6d2576..0000000 --- a/SampleApps/PingExample/PingExample/LoggerViewModel.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// LoggerViewModel.swift -// PingExample -// -// Copyright (c) 2024 Ping Identity. All rights reserved. -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -import Foundation -import PingLogger - -class LoggerViewModel { - - func setupLogger() { - - - var logger = LogManager.logger - logger.d("Debug") - logger.i("Info") - logger.w("Warning", error: TestError.success) - logger.e("Error", error: TestError.failure) - - logger = LogManager.standard - logger.d("Debug") - logger.i("Test log") - logger.w("Warning", error: TestError.success) - logger.e("Error", error: TestError.failure) - - logger = LogManager.warning - logger.d("Debug") - logger.i("Test log") - logger.w("Warning", error: TestError.success) - logger.e("Error", error: TestError.failure) - - } - - enum TestError: Error { - case success - case failure - } - -} diff --git a/SampleApps/PingExample/PingExample/LoginView.swift b/SampleApps/PingExample/PingExample/LoginView.swift deleted file mode 100644 index a9d2e13..0000000 --- a/SampleApps/PingExample/PingExample/LoginView.swift +++ /dev/null @@ -1,154 +0,0 @@ -// -// LoginView.swift -// PingExample -// -// Copyright (c) 2024 Ping Identity. All rights reserved. -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -import Foundation -import SwiftUI -import PingOrchestrate -import PingDavinci - -struct DavinciView: View { - - @StateObject private var viewmodel = DavinciViewModel() - @Binding var path: [String] - - var body: some View { - ZStack { - ScrollView { - VStack { - Spacer() - switch viewmodel.data.currentNode { - case let connector as Connector: - ConnectorView(viewmodel: viewmodel, connector: connector) - case is SuccessNode: - VStack{}.onAppear { - path.removeLast() - path.append("Token") - } - case let errorNode as ErrorNode: - if let connector = viewmodel.data.previousNode as? Connector { - ConnectorView(viewmodel: viewmodel, connector: connector) - } - ErrorView(name: errorNode.cause.localizedDescription) - case let failureNode as FailureNode: - if let connector = viewmodel.data.previousNode as? Connector { - ConnectorView(viewmodel: viewmodel, connector: connector) - } - ErrorView(name: failureNode.message) - default: - EmptyView() - } - } - } - - Spacer() - - // Activity indicator - if viewmodel.isLoading { - Color.black.opacity(0.4) - .edgesIgnoringSafeArea(.all) - - ProgressView() - .progressViewStyle(CircularProgressViewStyle()) - .scaleEffect(4) // Increase spinner size if needed - .foregroundColor(.white) // Set spinner color - } - } - } -} - -struct ConnectorView: View { - - @ObservedObject var viewmodel: DavinciViewModel - public var connector: Connector - - var body: some View { - VStack { - Image("Logo").resizable().scaledToFill().frame(width: 100, height: 100) - .padding(.vertical, 32) - HeaderView(name: connector.name) - NewLoginView( - davinciViewModel: viewmodel, - connector: connector, collectorsList: connector.collectors) - } - } -} - -struct ErrorView: View { - var name: String = "" - - var body: some View { - VStack { - Text("Oops! Something went wrong.\(name)") - .foregroundColor(.red).padding(.top, 20) - } - } -} - -struct HeaderView: View { - var name: String = "" - var body: some View { - VStack { - Text(name) - .font(.title) - } - } -} - -struct NewLoginView: View { - // MARK: - Propertiers - @ObservedObject var davinciViewModel: DavinciViewModel - - public var connector: Connector - - public var collectorsList: Collectors - - // MARK: - View - var body: some View { - - VStack { - - ForEach(collectorsList, id: \.id) { field in - - VStack { - if let text = field as? TextCollector { - InputView(text: text.value, placeholderString: text.label, field: text) - } - - if let password = field as? PasswordCollector { - InputView(placeholderString: password.label, secureField: true, field: password) - } - - if let submitButton = field as? SubmitCollector { - InputButton(title: submitButton.label, field: submitButton) { - Task { - await davinciViewModel.next(node: connector) - } - } - } - - }.padding(.horizontal, 5).padding(.top, 20) - - - if let flowButton = field as? FlowCollector { - Button(action: { - flowButton.value = "action" - Task { - await davinciViewModel.next(node: connector) - } - }) { - Text(flowButton.label) - .foregroundColor(.black) - } - } - } - } - } -} - diff --git a/SampleApps/PingExample/PingExample/StorageViewModel.swift b/SampleApps/PingExample/PingExample/StorageViewModel.swift deleted file mode 100644 index dddcf97..0000000 --- a/SampleApps/PingExample/PingExample/StorageViewModel.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// StorageViewModel.swift -// PingExample -// -// Copyright (c) 2024 Ping Identity. All rights reserved. -// -// This software may be modified and distributed under the terms -// of the MIT license. See the LICENSE file for details. -// - -import Foundation -import PingStorage -import PingLogger - -class StorageViewModel { - - func setupMemoryStorage() async { - do { - let memoryStorage1 = MemoryStorage() - try await memoryStorage1.save(item: "Andy") - let storedValue1 = try await memoryStorage1.get() - LogManager.standard.i("Memory Storage value: \(storedValue1!)") - } catch { - LogManager.standard.e("", error: error) - } - - } - - - func setupKeychainStorage() async { - do { - let keychainStorage = KeychainStorage(account: "token", encryptor: SecuredKeyEncryptor() ?? NoEncryptor()) - try await keychainStorage.save(item: "Jey") - let storedValue = try await keychainStorage.get() - LogManager.standard.i("Kechain Storage value: \(storedValue!)") - } catch { - LogManager.standard.e("", error: error) - } - } -} diff --git a/SampleApps/PingExample/PingExample/Utilities/ConfigurationManager.swift b/SampleApps/PingExample/PingExample/Utilities/ConfigurationManager.swift new file mode 100644 index 0000000..1bb1cc5 --- /dev/null +++ b/SampleApps/PingExample/PingExample/Utilities/ConfigurationManager.swift @@ -0,0 +1,107 @@ +// +// ConfigurationManager.swift +// PingExample +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import PingDavinci + +class ConfigurationManager: ObservableObject { + static let shared = ConfigurationManager() + public var davinci: DaVinci? + private var currentConfigurationViewModel: ConfigurationViewModel? + + public func loadConfigurationViewModel() -> ConfigurationViewModel { + if self.currentConfigurationViewModel == nil { + self.currentConfigurationViewModel = defaultConfigurationViewModel() + } + return self.currentConfigurationViewModel! + } + + public func createDavinciWorkflow() { + if let currentConfiguration = self.currentConfigurationViewModel { + //TODO: Integration Point. STEP 2 + /* + Based on the current `ConfigurationViewModel` create a DaVinci instance by calling the + `DaVinci.createDaVinci` SDK method. This will create a workflow, ready to be called + and start an Authorization/Authentication flow based on your configuration. + Example: + self.davinci = DaVinci.createDaVinci { config in + //config.debug = true + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = currentConfiguration.clientId + oidcValue.scopes = Set(currentConfiguration.scopes) + oidcValue.redirectUri = currentConfiguration.redirectUri + oidcValue.discoveryEndpoint = currentConfiguration.discoveryEndpoint + } + } + */ + self.davinci = DaVinci.createDaVinci { config in + //config.debug = true + config.module(OidcModule.config) { oidcValue in + oidcValue.clientId = currentConfiguration.clientId + oidcValue.scopes = Set(currentConfiguration.scopes) + oidcValue.redirectUri = currentConfiguration.redirectUri + oidcValue.discoveryEndpoint = currentConfiguration.discoveryEndpoint + } + } + } + } + + public func saveConfiguration() { + if let currentConfiguration = self.currentConfigurationViewModel { + let encoder = JSONEncoder() + let configuration = Configuration(clientId: currentConfiguration.clientId, scopes: currentConfiguration.scopes, redirectUri: currentConfiguration.redirectUri, discoveryEndpoint: currentConfiguration.discoveryEndpoint) + if let encoded = try? encoder.encode(configuration) { + let defaults = UserDefaults.standard + defaults.set(encoded, forKey: "CurrentConfiguration") + } + } + } + + public func deleteSavedConfiguration() { + let defaults = UserDefaults.standard + defaults.removeObject(forKey: "CurrentConfiguration") + } + + private func defaultConfigurationViewModel() -> ConfigurationViewModel { + let defaults = UserDefaults.standard + if let savedConfiguration = defaults.object(forKey: "CurrentConfiguration") as? Data { + let decoder = JSONDecoder() + if let loadedConfiguration = try? decoder.decode(Configuration.self, from: savedConfiguration) { + return ConfigurationViewModel(clientId: loadedConfiguration.clientId, scopes: loadedConfiguration.scopes, redirectUri: loadedConfiguration.redirectUri, discoveryEndpoint: loadedConfiguration.discoveryEndpoint) + } + } + //TODO: Integration Point. STEP 1 + /* + Create and return a ConfigurationViewModel. This will serve as the defaultConfigurationViewModel for the runtime of your application. + Any changes, by editing during runtime will be saved using the `saveConfiguration` method and loaded on start up using the `loadConfigurationViewModel` Method. + The developer needs to set up a configuration that the SDK will use to communicate the Davinci. These OAuth2.0 details will be used when making + authorization calls. The required properties are: `clientId`, `scopes`, `redirectUri`, `discoveryEndpoint`. In this Sample Application, + creating a ConfigurationViewModel, acts as the SDK configuration starting point. The `ConfigurationManager` is a Singleton object that + will remain in memory for the excecution of the application. To continue edit the values below between the '" "' symbols. + */ + return ConfigurationViewModel( + clientId: "[Client ID]", + scopes: ["openid", "email", "address", "phone", "profile"], + redirectUri: "[Redirect URI]", + discoveryEndpoint: "[Discovery Endpoint]" + ) + } +} + +/* + Example Values (Please create your own application as described in the documentation): + return ConfigurationViewModel( + clientId: "10a80cd7-a844-4cdf-b1c6-7dc2ccdb9769", + scopes: ["openid", "email", "address", "phone", "profile"], + redirectUri: "org.forgerock.demo://oauth2redirect", + discoveryEndpoint: "https://auth.pingone.com/5e508bc0-91e7-409b-8514-783bad6d1811/as/.well-known/openid-configuration" + ) + */ diff --git a/SampleApps/PingExample/PingExample/LoginUtility.swift b/SampleApps/PingExample/PingExample/Utilities/LoginUtility.swift similarity index 80% rename from SampleApps/PingExample/PingExample/LoginUtility.swift rename to SampleApps/PingExample/PingExample/Utilities/LoginUtility.swift index d03944e..4df0624 100644 --- a/SampleApps/PingExample/PingExample/LoginUtility.swift +++ b/SampleApps/PingExample/PingExample/Utilities/LoginUtility.swift @@ -83,4 +83,21 @@ struct RegisterNow: View { } } - +struct ActivityIndicatorView: View { + @Binding var isAnimating: Bool + let style: UIActivityIndicatorView.Style + let color: Color + + var body: some View { + if isAnimating { + VStack { + Spacer() + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: color)) + .padding() + Spacer() + } + .background(Color.black.opacity(0.4).ignoresSafeArea()) + } + } +} diff --git a/SampleApps/PingExample/PingExample/ViewModels/ConfigurationViewModel.swift b/SampleApps/PingExample/PingExample/ViewModels/ConfigurationViewModel.swift new file mode 100644 index 0000000..5a9ddc6 --- /dev/null +++ b/SampleApps/PingExample/PingExample/ViewModels/ConfigurationViewModel.swift @@ -0,0 +1,35 @@ +// +// ConfigurationViewModel.swift +// PingExample +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation + +class ConfigurationViewModel: ObservableObject { + + @Published public var clientId: String + @Published public var scopes: [String] + @Published public var redirectUri: String + @Published public var discoveryEndpoint: String + + public init(clientId: String, scopes: [String], redirectUri: String, discoveryEndpoint: String) { + self.clientId = clientId + self.scopes = scopes + self.redirectUri = redirectUri + self.discoveryEndpoint = discoveryEndpoint + } +} + +struct Configuration: Codable { + var clientId: String + var scopes: [String] + var redirectUri: String + var discoveryEndpoint: String +} + diff --git a/SampleApps/PingExample/PingExample/ViewModels/DavinciViewModel.swift b/SampleApps/PingExample/PingExample/ViewModels/DavinciViewModel.swift new file mode 100644 index 0000000..fd268ac --- /dev/null +++ b/SampleApps/PingExample/PingExample/ViewModels/DavinciViewModel.swift @@ -0,0 +1,98 @@ +// +// DavinciViewModel.swift +// PingExample +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import Foundation +import PingDavinci +import PingOidc +import PingOrchestrate + +class DavinciViewModel: ObservableObject { + + @Published public var data: StateNode = StateNode() + + @Published public var isLoading: Bool = false + + init() { + Task { + await startDavinci() + } + } + + private func startDavinci() async { + + await MainActor.run { + isLoading = true + } + //TODO: Integration Point. STEP 3 + /* + When the application is ready to start the Davinci flow, the SDK `start()` method needs to be called. + In this Sample app we have created a reference to the DaVinci Object inside the `ConfigurationManager` singleton class. + From any place in the app you can access this, by calling `ConfigurationManager.shared.davinci`. + Use the this reference and an `async/await` pattern to start the flow and receive the first node/connector. + + Starting a DaVinci flow, will return a Connector object. This will be assigned to a Published property of type: `StateNode`. + This will be consumed automatically, as this class is an `ObservableObject` from the DavinciView struct, that acts as the Flow view. + + Each connector can be either a `SuccessNode` or a `FailureNode` or an `ErrorNode` or finally a `Connector`. + A `Connector` contains the `collectors` array that is the `Inputs` and `Outputs` that developers need to present to collect the user inputs for the flow. + Notice that in order to move to the next connect in the flow we need to call `connector.next()`. + + Example: let node = await ConfigurationManager.shared.davinci?.start() + */ + + let node = await ConfigurationManager.shared.davinci?.start() + if let connector = node as? Connector { + let node = await connector.next() // <-- In the first step, calls DaVinci and starts the flow. On next steps, submits and returns the next `Connector`. + await MainActor.run { + self.data = StateNode(currentNode: node, previousNode: node) + } + } else { + await MainActor.run { + self.data = StateNode(currentNode: node, previousNode: node) + } + } + + await MainActor.run { + isLoading = false + } + + } + + public func next(node: Node) async { + await MainActor.run { + isLoading = true + } + if let connector = node as? Connector { + //TODO: Integration Point. STEP 4 + /* + At this point the application is ready to submit the Node/Connector and receive the "next step". + To submit the data call `connector.next()` + + Example: + let next = await connector.next() + */ + let next = await connector.next() + await MainActor.run { + self.data = StateNode(currentNode: next, previousNode: node) + isLoading = false + } + } + } +} + +class StateNode { + var currentNode: Node? = nil + var previousNode: Node? = nil + + init(currentNode: Node? = nil, previousNode: Node? = nil) { + self.currentNode = currentNode + self.previousNode = previousNode + } +} diff --git a/SampleApps/PingExample/PingExample/LoginViewModel.swift b/SampleApps/PingExample/PingExample/ViewModels/LoginViewModel.swift similarity index 99% rename from SampleApps/PingExample/PingExample/LoginViewModel.swift rename to SampleApps/PingExample/PingExample/ViewModels/LoginViewModel.swift index 614039a..cf0260b 100644 --- a/SampleApps/PingExample/PingExample/LoginViewModel.swift +++ b/SampleApps/PingExample/PingExample/ViewModels/LoginViewModel.swift @@ -18,10 +18,8 @@ import Observation class LoginViewModel: ObservableObject { @Published public var isLoading: Bool = false - @ObservedObject var davinciViewModel: DavinciViewModel - init( isLoading: Bool = false, davinciViewModel: DavinciViewModel) { diff --git a/SampleApps/PingExample/PingExample/ViewModels/LogoutViewModel.swift b/SampleApps/PingExample/PingExample/ViewModels/LogoutViewModel.swift new file mode 100644 index 0000000..8907022 --- /dev/null +++ b/SampleApps/PingExample/PingExample/ViewModels/LogoutViewModel.swift @@ -0,0 +1,31 @@ +// +// LogoutViewModel.swift +// PingExample +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import Foundation + +class LogoutViewModel: ObservableObject { + + @Published var logout: String = "" + + func logout() async { + //TODO: Integration Point. STEP 8 + /* + Use the ConfigurationManager shared class to retrieve the davinci class reference. + When wanting to logout the user, you need to call `user()?.logout()`. + + Example use: "await ConfigurationManager.shared.davinci?.user()?.logout()" + */ + await ConfigurationManager.shared.davinci?.user()?.logout() + await MainActor.run { + logout = "Logout completed" + } + + } +} diff --git a/SampleApps/PingExample/PingExample/ViewModels/TokenViewModel.swift b/SampleApps/PingExample/PingExample/ViewModels/TokenViewModel.swift new file mode 100644 index 0000000..f26bc07 --- /dev/null +++ b/SampleApps/PingExample/PingExample/ViewModels/TokenViewModel.swift @@ -0,0 +1,52 @@ +// +// TokenViewModel.swift +// PingExample +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import Foundation +import PingLogger + +class TokenViewModel: ObservableObject { + + @Published var accessToken: String = "" + + init() { + Task { + await accessToken() + } + } + + func accessToken() async { + //TODO: Integration Point. STEP 6 + /* + Use the ConfigurationManager shared class to retrieve the user token. + The ViewModel will save that in the `self.accessToken` variable and + the view will display the value. + + Example use: "let token = await ConfigurationManager.shared.davinci?.user()?.token()" + */ + let token = await ConfigurationManager.shared.davinci?.user()?.token() + switch token { + case .success(let accessToken): + await MainActor.run { + self.accessToken = String(describing: accessToken) + } + LogManager.standard.i("AccessToken: \(self.accessToken)") + case .failure(let error): + await MainActor.run { + self.accessToken = "Error: \(error.localizedDescription)" + } + LogManager.standard.e("", error: error) + case .none: + await MainActor.run { + self.accessToken = "Error: Token nil, need to log in" + } + } + + } +} diff --git a/SampleApps/PingExample/PingExample/TokenViewModel.swift b/SampleApps/PingExample/PingExample/ViewModels/UserInfoViewModel.swift similarity index 50% rename from SampleApps/PingExample/PingExample/TokenViewModel.swift rename to SampleApps/PingExample/PingExample/ViewModels/UserInfoViewModel.swift index 8bf970d..e0678ca 100644 --- a/SampleApps/PingExample/PingExample/TokenViewModel.swift +++ b/SampleApps/PingExample/PingExample/ViewModels/UserInfoViewModel.swift @@ -1,5 +1,5 @@ -// -// TokenViewModel.swift +// +// UserInfoViewModel.swift // PingExample // // Copyright (c) 2024 Ping Identity. All rights reserved. @@ -11,37 +11,6 @@ import Foundation import PingLogger -class TokenViewModel: ObservableObject { - - @Published var accessToken: String = "" - - - init() { - Task { - await accessToken() - } - } - - func accessToken() async { - let token = await davinci.user()?.token() - switch token { - case .success(let accessToken): - await MainActor.run { - self.accessToken = String(describing: accessToken) - } - LogManager.standard.i("AccessToken: \(self.accessToken)") - case .failure(let error): - await MainActor.run { - self.accessToken = "Error: \(error.localizedDescription)" - } - LogManager.standard.e("", error: error) - case .none: - break - } - - } -} - class UserInfoViewModel: ObservableObject { @Published var userInfo: String = "" @@ -53,7 +22,16 @@ class UserInfoViewModel: ObservableObject { } func fetchUserInfo() async { - let userInfo = await davinci.user()?.userinfo(cache: false) + /* + //TODO: Integration Point. STEP 7 + Use the ConfigurationManager shared class to retrieve the user info. + The ViewModel will save that in the `self.userInfo` variable and + the view will display the value. + + Example use: "let userInfo = await ConfigurationManager.shared.davinci?.user()?.userinfo(cache: false)" + */ + + let userInfo = await ConfigurationManager.shared.davinci?.user()?.userinfo(cache: false) switch userInfo { case .success(let userInfoDictionary): await MainActor.run { @@ -68,20 +46,9 @@ class UserInfoViewModel: ObservableObject { } LogManager.standard.e("", error: error) case .none: - break - } - } -} - -class LogOutViewModel: ObservableObject { - - @Published var logout: String = "" - - func logout() async { - await davinci.user()?.logout() - await MainActor.run { - logout = "Logout completed" + await MainActor.run { + self.userInfo = "Error: Userinfo nil, need to log in" + } } - } } diff --git a/SampleApps/PingExample/PingExample/Views/AccessTokenView.swift b/SampleApps/PingExample/PingExample/Views/AccessTokenView.swift new file mode 100644 index 0000000..66bc05e --- /dev/null +++ b/SampleApps/PingExample/PingExample/Views/AccessTokenView.swift @@ -0,0 +1,36 @@ +// +// AccessToken.swift +// PingExample +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import SwiftUI + +struct AccessTokenView: View { + + @Binding var path: [String] + + @StateObject var tokenViewModel = TokenViewModel() + + var body: some View { + VStack { + + TextEditor(text: $tokenViewModel.accessToken) + .foregroundStyle(.secondary) + .padding(.horizontal) + .navigationTitle("AccessToken") + NextButton(title: "Procced to logout") { + Task { + await ConfigurationManager.shared.davinci?.user()?.logout() + path.removeLast() + } + } + } + + + } +} diff --git a/SampleApps/PingExample/PingExample/Views/ConfigurationView.swift b/SampleApps/PingExample/PingExample/Views/ConfigurationView.swift new file mode 100644 index 0000000..d358558 --- /dev/null +++ b/SampleApps/PingExample/PingExample/Views/ConfigurationView.swift @@ -0,0 +1,53 @@ +// +// ConfigurationView.swift +// PingExample +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import SwiftUI + +struct ConfigurationView: View { + @Binding var viewmodel: ConfigurationViewModel + @State private var scopes: String = "" + + var body: some View { + Form { + Section { + Text("Client Id:") + TextField("Client Id", text: $viewmodel.clientId) + .disableAutocorrection(true) + .autocapitalization(.none) + } + Section { + Text("Redirect URI:") + TextField("Redirect URI", text: $viewmodel.redirectUri) + .disableAutocorrection(true) + .autocapitalization(.none) + } + Section { + Text("Discovery Endpoint:") + TextField("Discovery Endpoint", text: $viewmodel.discoveryEndpoint) + .disableAutocorrection(true) + .autocapitalization(.none) + } + Section { + Text("Scopes:") + TextField("scopes:", text: $scopes) + .disableAutocorrection(true) + .autocapitalization(.none) + } + } + .navigationTitle("Edit Configuration") + .onAppear{ + scopes = $viewmodel.scopes.wrappedValue.joined(separator: ",") + } + .onDisappear{ + viewmodel.scopes = scopes.components(separatedBy: ",") + ConfigurationManager.shared.saveConfiguration() + } + } +} diff --git a/SampleApps/PingExample/PingExample/Views/ContentView.swift b/SampleApps/PingExample/PingExample/Views/ContentView.swift new file mode 100644 index 0000000..2cabf46 --- /dev/null +++ b/SampleApps/PingExample/PingExample/Views/ContentView.swift @@ -0,0 +1,62 @@ +import SwiftUI + + +struct ContentView: View { + + @State private var startDavinici = false + + @State private var path: [String] = [] + + @State private var configurationViewModel: ConfigurationViewModel = ConfigurationManager.shared.loadConfigurationViewModel() + + var body: some View { + NavigationStack(path: $path) { + List { + NavigationLink(value: "Configuration") { + Text("Edit configuration") + } + NavigationLink(value: "DaVinci") { + Text("Launch DaVinci") + } + NavigationLink(value: "Token") { + Text("Access Token") + } + NavigationLink(value: "User") { + Text("User Info") + } + NavigationLink(value: "Logout") { + Text("Logout") + } + }.navigationDestination(for: String.self) { item in + switch item { + case "Configuration": + ConfigurationView(viewmodel: $configurationViewModel) + case "DaVinci": + DavinciView(path: $path) + case "Token": + AccessTokenView(path: $path) + case "User": + UserInfoView(path: $path) + case "Logout": + LogoutView(path: $path) + default: + EmptyView() + } + }.navigationBarTitle("Ping DaVinci") + Image(uiImage: UIImage(named: "Logo")!) + .resizable() + .frame(width: 180.0, height: 180.0).clipped() + }.onAppear{ + ConfigurationManager.shared.createDavinciWorkflow() + } + } +} + +@main +struct MyApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/SampleApps/PingExample/PingExample/Views/DavinciView.swift b/SampleApps/PingExample/PingExample/Views/DavinciView.swift new file mode 100644 index 0000000..e3a99ea --- /dev/null +++ b/SampleApps/PingExample/PingExample/Views/DavinciView.swift @@ -0,0 +1,63 @@ +// +// DavinciView.swift +// PingExample +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import SwiftUI +import PingOrchestrate +import PingDavinci + +struct DavinciView: View { + + @StateObject var viewmodel = DavinciViewModel() + @Binding var path: [String] + + var body: some View { + ZStack { + ScrollView { + VStack { + Spacer() + switch viewmodel.data.currentNode { + case let connector as Connector: + ConnectorView(viewmodel: viewmodel, connector: connector) + case is SuccessNode: + VStack{}.onAppear { + path.removeLast() + path.append("Token") + } + case let errorNode as ErrorNode: + if let connector = viewmodel.data.previousNode as? Connector { + ConnectorView(viewmodel: viewmodel, connector: connector) + } + ErrorView(name: errorNode.cause.localizedDescription) + case let failureNode as FailureNode: + if let connector = viewmodel.data.previousNode as? Connector { + ConnectorView(viewmodel: viewmodel, connector: connector) + } + ErrorView(name: failureNode.message) + default: + EmptyView() + } + } + } + + Spacer() + + // Activity indicator + if viewmodel.isLoading { + Color.black.opacity(0.4) + .edgesIgnoringSafeArea(.all) + + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .scaleEffect(4) // Increase spinner size if needed + .foregroundColor(.white) // Set spinner color + } + } + } +} diff --git a/SampleApps/PingExample/PingExample/InputView.swift b/SampleApps/PingExample/PingExample/Views/InputView.swift similarity index 100% rename from SampleApps/PingExample/PingExample/InputView.swift rename to SampleApps/PingExample/PingExample/Views/InputView.swift diff --git a/SampleApps/PingExample/PingExample/Views/LoginView.swift b/SampleApps/PingExample/PingExample/Views/LoginView.swift new file mode 100644 index 0000000..7255c2e --- /dev/null +++ b/SampleApps/PingExample/PingExample/Views/LoginView.swift @@ -0,0 +1,152 @@ +// +// LoginView.swift +// PingExample +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import Foundation +import SwiftUI +import PingOrchestrate +import PingDavinci + +struct ConnectorView: View { + + @ObservedObject var viewmodel: DavinciViewModel + public var connector: Connector + + var body: some View { + VStack { + Image("Logo").resizable().scaledToFill().frame(width: 100, height: 100) + .padding(.vertical, 32) + HeaderView(name: connector.name) + LoginView( + davinciViewModel: viewmodel, + connector: connector, collectorsList: connector.collectors) + } + } +} + +struct ErrorView: View { + var name: String = "" + + var body: some View { + VStack { + Text("Oops! Something went wrong.\(name)") + .foregroundColor(.red).padding(.top, 20) + } + } +} + +struct HeaderView: View { + var name: String = "" + var body: some View { + VStack { + Text(name) + .font(.title) + } + } +} + +struct LoginView: View { + // MARK: - Propertiers + @ObservedObject var davinciViewModel: DavinciViewModel + + public var connector: Connector + + public var collectorsList: Collectors + + // MARK: - View + var body: some View { + + VStack { + //TODO: Integration Point. STEP 5 + /* + At this point the DaVinci flow has returned Collectors to present. + As a developer you will need to go though the returned collectors (`collectorsList` in this case) + and build your view. Notice that Flows, similar to PingAM Journeys can be dynamic and you views + will need to adapt to what the server sends. + + As mentioned on previous steps the `Connector` object contains a list of `Collectors`. Those `Collectors` + contain input and outputs. In this ViewModel, a conviniece property to access the collectors directly has been set up. + + To use this loop through the `collectorsList` and create `InputViews` for each collector based on its type. + The SDK at the moment supports the following collectors returned from DaVinci through the use of an HTML Connector: + 1. `TextCollector` + 2. `PasswordCollector` + 3. `SubmitCollector` + 4. `FlowCollector` + + Example for `TextCollector`: + if let text = field as? TextCollector { + InputView(text: text.value, placeholderString: text.label, field: text) + } + + Example for `PasswordCollector` + if let password = field as? PasswordCollector { + InputView(placeholderString: password.label, secureField: true, field: password) + } + + Example for `SubmitCollector` + if let submitButton = field as? SubmitCollector { + InputButton(title: submitButton.label, field: submitButton) { + Task { + /* + Call next + */ + await davinciViewModel.next(node: connector) + } + } + } + + If the type is `FlowCollector` you will need to submit to DaVinci by calling `.next()` + Example: + if let flowButton = field as? FlowCollector { + Button(action: { + flowButton.value = "action" + Task { + await davinciViewModel.next(node: connector) + } + }) { + Text(flowButton.label) + .foregroundColor(.black) + } + } + */ + ForEach(collectorsList, id: \.id) { field in + if let text = field as? TextCollector { + InputView(text: text.value, placeholderString: text.label, field: text) + } + if let password = field as? PasswordCollector { + InputView(placeholderString: password.label, secureField: true, field: password) + } + if let submitButton = field as? SubmitCollector { + InputButton(title: submitButton.label, field: submitButton) { + Task { + /* + Call next + */ + await davinciViewModel.next(node: connector) + } + } + } + if let flowButton = field as? FlowCollector { + Button(action: { + flowButton.value = "action" + Task { + await davinciViewModel.next(node: connector) + } + }) { + Text(flowButton.label) + .foregroundColor(.black) + } + } + } + + } + } +} + diff --git a/SampleApps/PingExample/PingExample/Views/LogoutView.swift b/SampleApps/PingExample/PingExample/Views/LogoutView.swift new file mode 100644 index 0000000..1565571 --- /dev/null +++ b/SampleApps/PingExample/PingExample/Views/LogoutView.swift @@ -0,0 +1,33 @@ +// +// LogoutView.swift +// PingExample +// +// Copyright (c) 2024 Ping Identity. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import SwiftUI + +struct LogoutView: View { + + @Binding var path: [String] + + @StateObject private var viewmodel = LogoutViewModel() + + var body: some View { + + Text("Logout") + .font(.title) + .navigationBarTitle("Logout", displayMode: .inline) + + NextButton(title: "Procced to logout") { + Task { + await viewmodel.logout() + path.removeLast() + } + } + + } +} diff --git a/SampleApps/PingExample/PingExample/AccessToken.swift b/SampleApps/PingExample/PingExample/Views/UserInfoView.swift similarity index 57% rename from SampleApps/PingExample/PingExample/AccessToken.swift rename to SampleApps/PingExample/PingExample/Views/UserInfoView.swift index 1cbdd75..02d80a8 100644 --- a/SampleApps/PingExample/PingExample/AccessToken.swift +++ b/SampleApps/PingExample/PingExample/Views/UserInfoView.swift @@ -1,5 +1,5 @@ -// -// AccessToken.swift +// +// UserInfoView.swift // PingExample // // Copyright (c) 2024 Ping Identity. All rights reserved. @@ -10,23 +10,8 @@ import SwiftUI -struct AccessTokenView: View { - - @StateObject var accessToken = TokenViewModel() - - var body: some View { - VStack { - - TextEditor(text: $accessToken.accessToken) - .foregroundStyle(.secondary) - .padding(.horizontal) - .navigationTitle("AccessToken") - } - - } -} - struct UserInfoView: View { + @Binding var path: [String] @StateObject var vm = UserInfoViewModel() @@ -37,6 +22,11 @@ struct UserInfoView: View { .padding(.horizontal) .navigationTitle("User Info") - + NextButton(title: "Procced to logout") { + Task { + await ConfigurationManager.shared.davinci?.user()?.logout() + path.removeLast() + } + } } }