From ff26d73987c1d50665e4edbd9cb6ada996c5ab0f Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Mon, 20 Oct 2025 18:16:36 -0700 Subject: [PATCH 01/19] Basic methods to override consumer key / revert to bootconfig --- .../Classes/Common/SalesforceSDKManager.h | 13 ++++++++++++ .../Classes/Common/SalesforceSDKManager.m | 20 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.h b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.h index 5a9bfa2a47..04457a1534 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.h +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.h @@ -334,6 +334,19 @@ NS_SWIFT_NAME(SalesforceManager) nativeLoginViewController:(nonnull UIViewController *)nativeLoginViewController scene:(nullable UIScene *)scene; +/** + * Call this method before initiating a login if the application needs to revert back to the consumer key/callback url statically configured in bootconfig.plist + */ +- (void) revertToBootConfig; + +/** + * Call this method before initiating a login if the application needs to use a consumer key/callback url different from the one statically configured in bootconfig.plist + * @param consumerKey The Connected App consumer key. + * @param callbackUrl The Connected App redirect URI. + */ +- (void) overrideBootConfigWithConsumerKey:(nonnull NSString *)consumerKey + callbackUrl:(nonnull NSString *)callbackUrl NS_SWIFT_NAME(overrideBootConfig(consumerKey:callbackUrl:)); + /** * Returns The NativeLoginManager instance. * diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.m b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.m index ac7ae05152..80fefaac45 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.m +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.m @@ -954,6 +954,26 @@ - (void)biometricAuthenticationFlowDidComplete:(NSNotification *)notification { return nativeLogin; } +#pragma - Dynamic boot config + +- (void) revertToBootConfig { + _appConfig = nil; // next access will read from default bootconfig + [self setupServiceConfiguration]; +} + +- (void) overrideBootConfigWithConsumerKey:(nonnull NSString *)consumerKey + callbackUrl:(nonnull NSString *)callbackUrl { + + NSDictionary *dict = @{ + @"remoteAccessConsumerKey": consumerKey, + @"oauthRedirectURI": callbackUrl + }; + + SFSDKAppConfig *config = [[SFSDKAppConfig alloc] initWithDict:dict]; + _appConfig = config; + [self setupServiceConfiguration]; +} + @end NSString *SFAppTypeGetDescription(SFAppType appType){ From 7356c3282b429011f1e28471034d58f54e323c7e Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Mon, 20 Oct 2025 18:17:07 -0700 Subject: [PATCH 02/19] Showing buttons before login to pick between static bootconfig or some dynamic config (WIP) --- .../RestAPIExplorer/SceneDelegate.swift | 13 +- .../InitialViewController.swift | 119 ++++++++++++++---- 2 files changed, 107 insertions(+), 25 deletions(-) diff --git a/native/SampleApps/RestAPIExplorer/RestAPIExplorer/SceneDelegate.swift b/native/SampleApps/RestAPIExplorer/RestAPIExplorer/SceneDelegate.swift index 2ca9acda69..386db9940e 100644 --- a/native/SampleApps/RestAPIExplorer/RestAPIExplorer/SceneDelegate.swift +++ b/native/SampleApps/RestAPIExplorer/RestAPIExplorer/SceneDelegate.swift @@ -28,6 +28,7 @@ import Foundation import UIKit import SalesforceSDKCore +import SwiftUI class SceneDelegate: UIResponder, UIWindowSceneDelegate { @@ -44,9 +45,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } } self.initializeAppViewState() - AuthHelper.loginIfRequired(scene) { - self.setupRootViewController() - } } func sceneDidDisconnect(_ scene: UIScene) { @@ -93,7 +91,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { return } - self.window?.rootViewController = InitialViewController(nibName: nil, bundle: nil) + self.window?.rootViewController = UIHostingController(rootView: InitialView(onConfigurationCompleted: onConfigurationCompleted)) self.window?.makeKeyAndVisible() } @@ -112,4 +110,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } postResetBlock() } + + func onConfigurationCompleted() { + guard let windowScene = self.window?.windowScene else { return } + AuthHelper.loginIfRequired(windowScene) { + self.setupRootViewController() + } + } } diff --git a/native/SampleApps/RestAPIExplorer/RestAPIExplorer/ViewControllers/InitialViewController.swift b/native/SampleApps/RestAPIExplorer/RestAPIExplorer/ViewControllers/InitialViewController.swift index 87f789e74a..680f9aa84e 100644 --- a/native/SampleApps/RestAPIExplorer/RestAPIExplorer/ViewControllers/InitialViewController.swift +++ b/native/SampleApps/RestAPIExplorer/RestAPIExplorer/ViewControllers/InitialViewController.swift @@ -27,32 +27,109 @@ WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -import UIKit -import SalesforceSDKCore.UIColor_SFColors +import SwiftUI +import SalesforceSDKCore -class InitialViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() +struct InitialView: View { + @State private var isLoading = false + let onConfigurationCompleted: () -> Void + + var body: some View { + VStack(spacing: 30) { + // App name label + Text(appName) + .font(.largeTitle) + .fontWeight(.bold) + .foregroundColor(.primary) + .padding(.top, 100) + + Spacer() + + // Buttons container + VStack(spacing: 20) { + // Static bootconfig button + Button(action: handleStaticBootconfig) { + Text("Use static bootconfig") + .font(.headline) + .foregroundColor(.white) + .frame(width: 200, height: 44) + .background(Color.blue) + .cornerRadius(8) + } + .disabled(isLoading) + + // Dynamic bootconfig button + Button(action: handleDynamicBootconfig) { + Text("Use dynamic bootconfig") + .font(.headline) + .foregroundColor(.white) + .frame(width: 200, height: 44) + .background(Color.green) + .cornerRadius(8) + } + .disabled(isLoading) + } + + Spacer() + + // Loading indicator + if isLoading { + ProgressView("Authenticating...") + .padding() + } + } + .background(Color(.systemBackground)) + .onAppear { + // Any setup that needs to happen when the view appears + } + } + + // MARK: - Computed Properties + + private var appName: String { + guard let info = Bundle.main.infoDictionary, + let name = info[kCFBundleNameKey as String] as? String else { + return "RestAPIExplorer" + } + return name + } + + // MARK: - Button Actions + + private func handleStaticBootconfig() { + isLoading = true - self.view.backgroundColor = UIColor.salesforceSystemBackground + SalesforceManager.shared.revertToBootConfig() + + // Use static bootconfig - no additional setup needed + onConfigurationCompleted() + } + + private func handleDynamicBootconfig() { + isLoading = true - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - guard let info = Bundle.main.infoDictionary, let name = info[kCFBundleNameKey as String] else { return } - label.font = UIFont.systemFont(ofSize: 29) - label.textColor = UIColor.black - label.text = name as? String + // Use the dynamic bootconfig method + SalesforceManager.shared.overrideBootConfig( + // ECA without refresh scope + consumerKey: "3MVG9SemV5D80oBcXZ2EUzbcJw.BPBV7Nd7htOt2IMVa3r5Zb_UgI92gVmxnVoCLfysf3.tIkrYAJF8mHsJxB", + callbackUrl: "testsfdc:///mobilesdk/detect/oauth/done" + ) - self.view.addSubview(label) - label.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true - label.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true - // Do any additional setup after loading the view, typically from a nib. + // Proceed with login + onConfigurationCompleted() } +} - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } +// MARK: - UIViewControllerRepresentable +struct InitialViewController: UIViewControllerRepresentable { + let onConfigurationCompleted: () -> Void + + func makeUIViewController(context: Context) -> UIHostingController { + return UIHostingController(rootView: InitialView(onConfigurationCompleted: onConfigurationCompleted)) + } + + func updateUIViewController(_ uiViewController: UIHostingController, context: Context) { + // No updates needed + } } From 825f2e8fea69ced479b6801b7ae7bcc298e4b037 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Tue, 21 Oct 2025 15:26:36 -0700 Subject: [PATCH 03/19] Revert "Showing buttons before login to pick between static bootconfig or some dynamic config (WIP)" This reverts commit 7356c3282b429011f1e28471034d58f54e323c7e. --- .../RestAPIExplorer/SceneDelegate.swift | 13 +- .../InitialViewController.swift | 119 ++++-------------- 2 files changed, 25 insertions(+), 107 deletions(-) diff --git a/native/SampleApps/RestAPIExplorer/RestAPIExplorer/SceneDelegate.swift b/native/SampleApps/RestAPIExplorer/RestAPIExplorer/SceneDelegate.swift index 386db9940e..2ca9acda69 100644 --- a/native/SampleApps/RestAPIExplorer/RestAPIExplorer/SceneDelegate.swift +++ b/native/SampleApps/RestAPIExplorer/RestAPIExplorer/SceneDelegate.swift @@ -28,7 +28,6 @@ import Foundation import UIKit import SalesforceSDKCore -import SwiftUI class SceneDelegate: UIResponder, UIWindowSceneDelegate { @@ -45,6 +44,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } } self.initializeAppViewState() + AuthHelper.loginIfRequired(scene) { + self.setupRootViewController() + } } func sceneDidDisconnect(_ scene: UIScene) { @@ -91,7 +93,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { return } - self.window?.rootViewController = UIHostingController(rootView: InitialView(onConfigurationCompleted: onConfigurationCompleted)) + self.window?.rootViewController = InitialViewController(nibName: nil, bundle: nil) self.window?.makeKeyAndVisible() } @@ -110,11 +112,4 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } postResetBlock() } - - func onConfigurationCompleted() { - guard let windowScene = self.window?.windowScene else { return } - AuthHelper.loginIfRequired(windowScene) { - self.setupRootViewController() - } - } } diff --git a/native/SampleApps/RestAPIExplorer/RestAPIExplorer/ViewControllers/InitialViewController.swift b/native/SampleApps/RestAPIExplorer/RestAPIExplorer/ViewControllers/InitialViewController.swift index 680f9aa84e..87f789e74a 100644 --- a/native/SampleApps/RestAPIExplorer/RestAPIExplorer/ViewControllers/InitialViewController.swift +++ b/native/SampleApps/RestAPIExplorer/RestAPIExplorer/ViewControllers/InitialViewController.swift @@ -27,109 +27,32 @@ WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -import SwiftUI -import SalesforceSDKCore +import UIKit +import SalesforceSDKCore.UIColor_SFColors -struct InitialView: View { - @State private var isLoading = false - let onConfigurationCompleted: () -> Void - - var body: some View { - VStack(spacing: 30) { - // App name label - Text(appName) - .font(.largeTitle) - .fontWeight(.bold) - .foregroundColor(.primary) - .padding(.top, 100) - - Spacer() - - // Buttons container - VStack(spacing: 20) { - // Static bootconfig button - Button(action: handleStaticBootconfig) { - Text("Use static bootconfig") - .font(.headline) - .foregroundColor(.white) - .frame(width: 200, height: 44) - .background(Color.blue) - .cornerRadius(8) - } - .disabled(isLoading) - - // Dynamic bootconfig button - Button(action: handleDynamicBootconfig) { - Text("Use dynamic bootconfig") - .font(.headline) - .foregroundColor(.white) - .frame(width: 200, height: 44) - .background(Color.green) - .cornerRadius(8) - } - .disabled(isLoading) - } - - Spacer() - - // Loading indicator - if isLoading { - ProgressView("Authenticating...") - .padding() - } - } - .background(Color(.systemBackground)) - .onAppear { - // Any setup that needs to happen when the view appears - } - } - - // MARK: - Computed Properties - - private var appName: String { - guard let info = Bundle.main.infoDictionary, - let name = info[kCFBundleNameKey as String] as? String else { - return "RestAPIExplorer" - } - return name - } - - // MARK: - Button Actions - - private func handleStaticBootconfig() { - isLoading = true - - SalesforceManager.shared.revertToBootConfig() +class InitialViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() - // Use static bootconfig - no additional setup needed - onConfigurationCompleted() - } - - private func handleDynamicBootconfig() { - isLoading = true + self.view.backgroundColor = UIColor.salesforceSystemBackground - // Use the dynamic bootconfig method - SalesforceManager.shared.overrideBootConfig( - // ECA without refresh scope - consumerKey: "3MVG9SemV5D80oBcXZ2EUzbcJw.BPBV7Nd7htOt2IMVa3r5Zb_UgI92gVmxnVoCLfysf3.tIkrYAJF8mHsJxB", - callbackUrl: "testsfdc:///mobilesdk/detect/oauth/done" - ) + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + guard let info = Bundle.main.infoDictionary, let name = info[kCFBundleNameKey as String] else { return } + label.font = UIFont.systemFont(ofSize: 29) + label.textColor = UIColor.black + label.text = name as? String - // Proceed with login - onConfigurationCompleted() + self.view.addSubview(label) + label.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true + label.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true + // Do any additional setup after loading the view, typically from a nib. } -} - -// MARK: - UIViewControllerRepresentable -struct InitialViewController: UIViewControllerRepresentable { - let onConfigurationCompleted: () -> Void - - func makeUIViewController(context: Context) -> UIHostingController { - return UIHostingController(rootView: InitialView(onConfigurationCompleted: onConfigurationCompleted)) - } - - func updateUIViewController(_ uiViewController: UIHostingController, context: Context) { - // No updates needed + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. } + } From c5954683fc61a40fa06698fbf66edea2b244b548 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Tue, 21 Oct 2025 15:28:15 -0700 Subject: [PATCH 04/19] New sample app AuthFlowTester --- .../AuthFlowTester.xcodeproj/project.pbxproj | 405 ++++++++++++++++++ .../xcschemes/AuthFlowTester.xcscheme | 78 ++++ .../AuthFlowTester/AppDelegate.swift | 105 +++++ .../AuthFlowTester.entitlements | 10 + .../AuthFlowTester/AuthFlowTester/Info.plist | 78 ++++ .../AuthFlowTester/PrivacyInfo.xcprivacy | 8 + .../AuthFlowTester/SceneDelegate.swift | 120 ++++++ .../InitialViewController.swift | 133 ++++++ .../ViewControllers/RootViewController.swift | 57 +++ .../AuthFlowTester/bootconfig.plist | 12 + 10 files changed, 1006 insertions(+) create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/xcshareddata/xcschemes/AuthFlowTester.xcscheme create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/AppDelegate.swift create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/AuthFlowTester.entitlements create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/Info.plist create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/PrivacyInfo.xcprivacy create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/SceneDelegate.swift create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/InitialViewController.swift create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/RootViewController.swift create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/bootconfig.plist diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj b/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..41d9ce0a85 --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj @@ -0,0 +1,405 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 90; + objects = { + +/* Begin PBXBuildFile section */ + 4F95A89C2EA806E700C98D18 /* SalesforceAnalytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F95A8962EA801DC00C98D18 /* SalesforceAnalytics.framework */; }; + 4F95A89D2EA806E700C98D18 /* SalesforceAnalytics.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4F95A8962EA801DC00C98D18 /* SalesforceAnalytics.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 4F95A89F2EA806E900C98D18 /* SalesforceSDKCommon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F95A8982EA801DC00C98D18 /* SalesforceSDKCommon.framework */; }; + 4F95A8A02EA806E900C98D18 /* SalesforceSDKCommon.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4F95A8982EA801DC00C98D18 /* SalesforceSDKCommon.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 4F95A8A12EA806EA00C98D18 /* SalesforceSDKCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F95A89A2EA801DC00C98D18 /* SalesforceSDKCore.framework */; }; + 4F95A8A22EA806EA00C98D18 /* SalesforceSDKCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4F95A89A2EA801DC00C98D18 /* SalesforceSDKCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + AUTH001 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH002 /* AppDelegate.swift */; }; + AUTH003 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH004 /* SceneDelegate.swift */; }; + AUTH005 /* InitialViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH006 /* InitialViewController.swift */; }; + AUTH007 /* RootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH008 /* RootViewController.swift */; }; + AUTH009 /* bootconfig.plist in Resources */ = {isa = PBXBuildFile; fileRef = AUTH010 /* bootconfig.plist */; }; + AUTH013 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = AUTH014 /* PrivacyInfo.xcprivacy */; }; + AUTH033 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AUTH034 /* Images.xcassets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 4F95A89E2EA806E700C98D18 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + dstPath = ""; + dstSubfolder = Frameworks; + files = ( + 4F95A8A22EA806EA00C98D18 /* SalesforceSDKCore.framework in Embed Frameworks */, + 4F95A89D2EA806E700C98D18 /* SalesforceAnalytics.framework in Embed Frameworks */, + 4F95A8A02EA806E900C98D18 /* SalesforceSDKCommon.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 4F95A8962EA801DC00C98D18 /* SalesforceAnalytics.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SalesforceAnalytics.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4F95A8982EA801DC00C98D18 /* SalesforceSDKCommon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SalesforceSDKCommon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4F95A89A2EA801DC00C98D18 /* SalesforceSDKCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SalesforceSDKCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AUTH002 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + AUTH004 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + AUTH006 /* InitialViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialViewController.swift; sourceTree = ""; }; + AUTH008 /* RootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewController.swift; sourceTree = ""; }; + AUTH010 /* bootconfig.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = bootconfig.plist; sourceTree = ""; }; + AUTH012 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AUTH014 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + AUTH016 /* AuthFlowTester.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AuthFlowTester.entitlements; sourceTree = ""; }; + AUTH017 /* AuthFlowTester.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AuthFlowTester.app; sourceTree = BUILT_PRODUCTS_DIR; }; + AUTH034 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = ../../../../shared/resources/Images.xcassets; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + AUTH018 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + files = ( + 4F95A8A12EA806EA00C98D18 /* SalesforceSDKCore.framework in Frameworks */, + 4F95A89C2EA806E700C98D18 /* SalesforceAnalytics.framework in Frameworks */, + 4F95A89F2EA806E900C98D18 /* SalesforceSDKCommon.framework in Frameworks */, + ); + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 4F95A8952EA801DC00C98D18 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4F95A8962EA801DC00C98D18 /* SalesforceAnalytics.framework */, + 4F95A8982EA801DC00C98D18 /* SalesforceSDKCommon.framework */, + 4F95A89A2EA801DC00C98D18 /* SalesforceSDKCore.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + AUTH019 = { + isa = PBXGroup; + children = ( + AUTH020 /* AuthFlowTester */, + 4F95A8952EA801DC00C98D18 /* Frameworks */, + AUTH021 /* Products */, + ); + sourceTree = ""; + }; + AUTH020 /* AuthFlowTester */ = { + isa = PBXGroup; + children = ( + AUTH002 /* AppDelegate.swift */, + AUTH004 /* SceneDelegate.swift */, + AUTH022 /* ViewControllers */, + AUTH035 /* Resources */, + AUTH010 /* bootconfig.plist */, + AUTH012 /* Info.plist */, + AUTH014 /* PrivacyInfo.xcprivacy */, + AUTH016 /* AuthFlowTester.entitlements */, + ); + path = AuthFlowTester; + sourceTree = ""; + }; + AUTH021 /* Products */ = { + isa = PBXGroup; + children = ( + AUTH017 /* AuthFlowTester.app */, + ); + name = Products; + sourceTree = ""; + }; + AUTH022 /* ViewControllers */ = { + isa = PBXGroup; + children = ( + AUTH006 /* InitialViewController.swift */, + AUTH008 /* RootViewController.swift */, + ); + path = ViewControllers; + sourceTree = ""; + }; + AUTH035 /* Resources */ = { + isa = PBXGroup; + children = ( + AUTH034 /* Images.xcassets */, + ); + name = Resources; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + AUTH023 /* AuthFlowTester */ = { + isa = PBXNativeTarget; + buildConfigurationList = AUTH024 /* Build configuration list for PBXNativeTarget "AuthFlowTester" */; + buildPhases = ( + AUTH025 /* Sources */, + AUTH018 /* Frameworks */, + AUTH026 /* Resources */, + 4F95A89E2EA806E700C98D18 /* Embed Frameworks */, + ); + buildRules = ( + ); + name = AuthFlowTester; + productName = AuthFlowTester; + productReference = AUTH017 /* AuthFlowTester.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + AUTH027 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 2600; + TargetAttributes = { + AUTH023 = { + CreatedOnToolsVersion = 15.0; + }; + }; + }; + buildConfigurationList = AUTH028 /* Build configuration list for PBXProject "AuthFlowTester" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = AUTH019; + preferredProjectObjectVersion = 90; + productRefGroup = AUTH021 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AUTH023 /* AuthFlowTester */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + AUTH026 /* Resources */ = { + isa = PBXResourcesBuildPhase; + files = ( + AUTH009 /* bootconfig.plist in Resources */, + AUTH013 /* PrivacyInfo.xcprivacy in Resources */, + AUTH033 /* Images.xcassets in Resources */, + ); + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + AUTH025 /* Sources */ = { + isa = PBXSourcesBuildPhase; + files = ( + AUTH001 /* AppDelegate.swift in Sources */, + AUTH003 /* SceneDelegate.swift in Sources */, + AUTH005 /* InitialViewController.swift in Sources */, + AUTH007 /* RootViewController.swift in Sources */, + ); + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + AUTH029 /* Debug configuration for PBXProject "AuthFlowTester" */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + AUTH030 /* Release configuration for PBXProject "AuthFlowTester" */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + AUTH031 /* Debug configuration for PBXNativeTarget "AuthFlowTester" */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = AuthFlowTester/AuthFlowTester.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = AuthFlowTester/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.salesforce.mobilesdk.AuthFlowTester; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AUTH032 /* Release configuration for PBXNativeTarget "AuthFlowTester" */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = AuthFlowTester/AuthFlowTester.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = AuthFlowTester/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.salesforce.mobilesdk.AuthFlowTester; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + AUTH024 /* Build configuration list for PBXNativeTarget "AuthFlowTester" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AUTH031 /* Debug configuration for PBXNativeTarget "AuthFlowTester" */, + AUTH032 /* Release configuration for PBXNativeTarget "AuthFlowTester" */, + ); + defaultConfigurationName = Release; + }; + AUTH028 /* Build configuration list for PBXProject "AuthFlowTester" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AUTH029 /* Debug configuration for PBXProject "AuthFlowTester" */, + AUTH030 /* Release configuration for PBXProject "AuthFlowTester" */, + ); + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = AUTH027 /* Project object */; +} diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/xcshareddata/xcschemes/AuthFlowTester.xcscheme b/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/xcshareddata/xcschemes/AuthFlowTester.xcscheme new file mode 100644 index 0000000000..fdff2a002a --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/xcshareddata/xcschemes/AuthFlowTester.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/AppDelegate.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/AppDelegate.swift new file mode 100644 index 0000000000..5fba38f5a6 --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/AppDelegate.swift @@ -0,0 +1,105 @@ +/* + AppDelegate.swift + AuthFlowTester + + Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. + + Redistribution and use of this software in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions + and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior written + permission of salesforce.com, inc. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import UIKit +import SalesforceSDKCommon +import SalesforceSDKCore +import MobileCoreServices +import UniformTypeIdentifiers + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + override init() { + + super.init() + + SalesforceManager.initializeSDK() + SalesforceManager.shared.appDisplayName = "Auth Flow Tester" + UserAccountManager.shared.navigationPolicyForAction = { webView, action in + if let url = action.request.url, url.absoluteString == "https://www.salesforce.com/us/company/privacy" { + SFApplicationHelper.open(url, options: [:], completionHandler: nil) + return .cancel + } + return .allow + } + } + + // MARK: - App delegate lifecycle + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + self.window = UIWindow(frame: UIScreen.main.bounds) + + // If you wish to register for push notifications, uncomment the line below. Note that, + // if you want to receive push notifications from Salesforce, you will also need to + // implement the application:didRegisterForRemoteNotificationsWithDeviceToken: method (below). + // self.registerForRemotePushNotifications() + + return true + } + + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + // Uncomment the code below to register your device token with the push notification manager + // didRegisterForRemoteNotifications(deviceToken) + } + + func didRegisterForRemoteNotifications(_ deviceToken: Data) { + PushNotificationManager.sharedInstance().didRegisterForRemoteNotifications(withDeviceToken: deviceToken) + if let _ = UserAccountManager.shared.currentUserAccount?.credentials.accessToken { + PushNotificationManager.sharedInstance().registerForSalesforceNotifications { (result) in + switch (result) { + case .success(let successFlag): + SalesforceLogger.d(AppDelegate.self, message: "Registration for Salesforce notifications status: \(successFlag)") + case .failure(let error): + SalesforceLogger.e(AppDelegate.self, message: "Registration for Salesforce notifications failed \(error)") + } + } + } + } + + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error ) { + // Respond to any push notification registration errors here. + } + + // MARK: - Private methods + func registerForRemotePushNotifications() { + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in + if granted { + DispatchQueue.main.async { + PushNotificationManager.sharedInstance().registerForRemoteNotifications() + } + } else { + SalesforceLogger.d(AppDelegate.self, message: "Push notification authorization denied") + } + + if let error = error { + SalesforceLogger.e(AppDelegate.self, message: "Push notification authorization error: \(error)") + } + } + } +} diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/AuthFlowTester.entitlements b/native/SampleApps/AuthFlowTester/AuthFlowTester/AuthFlowTester.entitlements new file mode 100644 index 0000000000..b9d14fee5b --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/AuthFlowTester.entitlements @@ -0,0 +1,10 @@ + + + + + keychain-access-groups + + $(AppIdentifierPrefix)com.salesforce.mobilesdk.sample.authflowtester + + + diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Info.plist b/native/SampleApps/AuthFlowTester/AuthFlowTester/Info.plist new file mode 100644 index 0000000000..c208cf45da --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Info.plist @@ -0,0 +1,78 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + com.salesforce.mobilesdk.sample.authflowtester + CFBundleURLSchemes + + com.salesforce.mobilesdk.sample.authflowtester + + + + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSFaceIDUsageDescription + "Use FaceID" + SFDCOAuthLoginHost + login.salesforce.com + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortraitUpsideDown + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/PrivacyInfo.xcprivacy b/native/SampleApps/AuthFlowTester/AuthFlowTester/PrivacyInfo.xcprivacy new file mode 100644 index 0000000000..cfbe279c7b --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/PrivacyInfo.xcprivacy @@ -0,0 +1,8 @@ + + + + + NSPrivacyTracking + + + diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/SceneDelegate.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/SceneDelegate.swift new file mode 100644 index 0000000000..7e1801fb43 --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/SceneDelegate.swift @@ -0,0 +1,120 @@ +// +// SceneDelegate.swift +// AuthFlowTester +// +// Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. +// +// Redistribution and use of this software in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// * Redistributions of source code must retain the above copyright notice, this list of conditions +// and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, this list of +// conditions and the following disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission of salesforce.com, inc. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY +// WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + +import Foundation +import UIKit +import SalesforceSDKCore +import SwiftUI + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + public var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = (scene as? UIWindowScene) else { return } + self.window = UIWindow(frame: windowScene.coordinateSpace.bounds) + self.window?.windowScene = windowScene + + AuthHelper.registerBlock(forCurrentUserChangeNotifications: scene) { + self.resetViewState { + self.setupRootViewController() + } + } + self.initializeAppViewState() + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + // Uncomment following block to enable IDP Login flow +// if let urlContext = URLContexts.first { +// UserAccountManager.shared.handleIdentityProviderResponse(from: urlContext.url, with: [UserAccountManager.IDPSceneKey: scene.session.persistentIdentifier]) +// } + } + + // MARK: - Private methods + func initializeAppViewState() { + if (!Thread.isMainThread) { + DispatchQueue.main.async { + self.initializeAppViewState() + } + return + } + + self.window?.rootViewController = UIHostingController(rootView: InitialView(onConfigurationCompleted: onConfigurationCompleted)) + self.window?.makeKeyAndVisible() + } + + func setupRootViewController() { + let rootVC = RootViewController() + let navVC = UINavigationController(rootViewController: rootVC) + self.window!.rootViewController = navVC + } + + func resetViewState(_ postResetBlock: @escaping () -> ()) { + if let rootViewController = self.window?.rootViewController { + if let _ = rootViewController.presentedViewController { + rootViewController.dismiss(animated: false, completion: postResetBlock) + return + } + } + postResetBlock() + } + + func onConfigurationCompleted() { + guard let windowScene = self.window?.windowScene else { return } + AuthHelper.loginIfRequired(windowScene) { + self.setupRootViewController() + } + } +} diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/InitialViewController.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/InitialViewController.swift new file mode 100644 index 0000000000..f65711ba2b --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/InitialViewController.swift @@ -0,0 +1,133 @@ +/* + InitialViewController.swift + AuthFlowTester + + Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. + + Redistribution and use of this software in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions + and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior written + permission of salesforce.com, inc. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import SwiftUI +import SalesforceSDKCore + +struct InitialView: View { + @State private var isLoading = false + let onConfigurationCompleted: () -> Void + + var body: some View { + VStack(spacing: 30) { + // App name label + Text(appName) + .font(.largeTitle) + .fontWeight(.bold) + .foregroundColor(.primary) + .padding(.top, 100) + + Spacer() + + // Buttons container + VStack(spacing: 20) { + // Static bootconfig button + Button(action: handleStaticBootconfig) { + Text("Use static bootconfig") + .font(.headline) + .foregroundColor(.white) + .frame(width: 200, height: 44) + .background(Color.blue) + .cornerRadius(8) + } + .disabled(isLoading) + + // Dynamic bootconfig button + Button(action: handleDynamicBootconfig) { + Text("Use dynamic bootconfig") + .font(.headline) + .foregroundColor(.white) + .frame(width: 200, height: 44) + .background(Color.green) + .cornerRadius(8) + } + .disabled(isLoading) + } + + Spacer() + + // Loading indicator + if isLoading { + ProgressView("Authenticating...") + .padding() + } + } + .background(Color(.systemBackground)) + .onAppear { + // Any setup that needs to happen when the view appears + } + } + + // MARK: - Computed Properties + + private var appName: String { + guard let info = Bundle.main.infoDictionary, + let name = info[kCFBundleNameKey as String] as? String else { + return "AuthFlowTester" + } + return name + } + + // MARK: - Button Actions + + private func handleStaticBootconfig() { + isLoading = true + + SalesforceManager.shared.revertToBootConfig() + + // Use static bootconfig - no additional setup needed + onConfigurationCompleted() + } + + private func handleDynamicBootconfig() { + isLoading = true + + // Use the dynamic bootconfig method + SalesforceManager.shared.overrideBootConfig( + // ECA without refresh scope + consumerKey: "3MVG9SemV5D80oBcXZ2EUzbcJw.BPBV7Nd7htOt2IMVa3r5Zb_UgI92gVmxnVoCLfysf3.tIkrYAJF8mHsJxB", + callbackUrl: "testsfdc:///mobilesdk/detect/oauth/done" + ) + + // Proceed with login + onConfigurationCompleted() + } +} + +// MARK: - UIViewControllerRepresentable + +struct InitialViewController: UIViewControllerRepresentable { + let onConfigurationCompleted: () -> Void + + func makeUIViewController(context: Context) -> UIHostingController { + return UIHostingController(rootView: InitialView(onConfigurationCompleted: onConfigurationCompleted)) + } + + func updateUIViewController(_ uiViewController: UIHostingController, context: Context) { + // No updates needed + } +} diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/RootViewController.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/RootViewController.swift new file mode 100644 index 0000000000..0ba0572fc5 --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/RootViewController.swift @@ -0,0 +1,57 @@ +/* + RootViewController.swift + AuthFlowTester + + Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. + + Redistribution and use of this software in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions + and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior written + permission of salesforce.com, inc. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import SwiftUI +import SalesforceSDKCore + +struct RootView: View { + var body: some View { + VStack { + Spacer() + + Text("Authentication successful!") + .font(.headline) + .foregroundColor(.secondary) + .padding(.top, 8) + + Spacer() + } + .background(Color(.systemBackground)) + .navigationTitle("AuthFlowTester") + .navigationBarTitleDisplayMode(.large) + } +} + +class RootViewController: UIHostingController { + init() { + super.init(rootView: RootView()) + } + + @objc required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/bootconfig.plist b/native/SampleApps/AuthFlowTester/AuthFlowTester/bootconfig.plist new file mode 100644 index 0000000000..42bef06d94 --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/bootconfig.plist @@ -0,0 +1,12 @@ + + + + + remoteAccessConsumerKey + 3MVG98dostKihXN53TYStBIiS8NRTXcbDzn9nHPb3piMElfQDD.kTyHeXjKV9JNUbe5sZeSQ4CVY1Onzpq21N + oauthRedirectURI + com.salesforce.mobilesdk.sample.authflowtester://oauth/success + shouldAuthenticate + + + From 4aa7a0967dc9927b26e44022c257a7eebe082a27 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Tue, 21 Oct 2025 17:19:00 -0700 Subject: [PATCH 05/19] AuthFlowTester now showing session details --- .../contents.xcworkspacedata | 3 + .../AuthFlowTester.xcodeproj/project.pbxproj | 56 +-- .../{ => Classes}/AppDelegate.swift | 0 .../Classes/ConfigPickerViewController.swift | 213 +++++++++++ .../{ => Classes}/SceneDelegate.swift | 11 +- .../Classes/SessionDetailViewController.swift | 350 ++++++++++++++++++ .../AuthFlowTester.entitlements | 0 .../{ => Supporting Files}/Info.plist | 0 .../PrivacyInfo.xcprivacy | 0 .../{ => Supporting Files}/bootconfig.plist | 4 +- .../Supporting Files/bootconfig2.plist | 13 + .../InitialViewController.swift | 133 ------- .../ViewControllers/RootViewController.swift | 57 --- 13 files changed, 624 insertions(+), 216 deletions(-) rename native/SampleApps/AuthFlowTester/AuthFlowTester/{ => Classes}/AppDelegate.swift (100%) create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/Classes/ConfigPickerViewController.swift rename native/SampleApps/AuthFlowTester/AuthFlowTester/{ => Classes}/SceneDelegate.swift (91%) create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/Classes/SessionDetailViewController.swift rename native/SampleApps/AuthFlowTester/AuthFlowTester/{ => Supporting Files}/AuthFlowTester.entitlements (100%) rename native/SampleApps/AuthFlowTester/AuthFlowTester/{ => Supporting Files}/Info.plist (100%) rename native/SampleApps/AuthFlowTester/AuthFlowTester/{ => Supporting Files}/PrivacyInfo.xcprivacy (100%) rename native/SampleApps/AuthFlowTester/AuthFlowTester/{ => Supporting Files}/bootconfig.plist (61%) create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig2.plist delete mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/InitialViewController.swift delete mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/RootViewController.swift diff --git a/SalesforceMobileSDK.xcworkspace/contents.xcworkspacedata b/SalesforceMobileSDK.xcworkspace/contents.xcworkspacedata index 4b34180074..6ae303fa27 100644 --- a/SalesforceMobileSDK.xcworkspace/contents.xcworkspacedata +++ b/SalesforceMobileSDK.xcworkspace/contents.xcworkspacedata @@ -48,6 +48,9 @@ + + Void + + var body: some View { + ScrollView { + VStack(spacing: 30) { + // App name label + Text(appName) + .font(.largeTitle) + .fontWeight(.bold) + .foregroundColor(.primary) + .padding(.top, 40) + + // Default config section + VStack(alignment: .leading, spacing: 12) { + Text("Default Configuration") + .font(.headline) + .padding(.horizontal) + + VStack(alignment: .leading, spacing: 8) { + Text("Consumer Key:") + .font(.caption) + .foregroundColor(.secondary) + Text(defaultConsumerKey) + .font(.system(.caption, design: .monospaced)) + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(.systemGray6)) + .cornerRadius(4) + + Text("Callback URL:") + .font(.caption) + .foregroundColor(.secondary) + Text(defaultCallbackUrl) + .font(.system(.caption, design: .monospaced)) + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(.systemGray6)) + .cornerRadius(4) + } + .padding(.horizontal) + + Button(action: handleDefaultConfig) { + Text("Use default config") + .font(.headline) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 44) + .background(Color.blue) + .cornerRadius(8) + } + .disabled(isLoading) + .padding(.horizontal) + } + .padding(.vertical) + + Divider() + + // Dynamic config section + VStack(alignment: .leading, spacing: 12) { + Text("Dynamic Configuration") + .font(.headline) + .padding(.horizontal) + + VStack(alignment: .leading, spacing: 8) { + Text("Consumer Key:") + .font(.caption) + .foregroundColor(.secondary) + TextField("Consumer Key", text: $dynamicConsumerKey) + .font(.system(.caption, design: .monospaced)) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .autocapitalization(.none) + .disableAutocorrection(true) + + Text("Callback URL:") + .font(.caption) + .foregroundColor(.secondary) + TextField("Callback URL", text: $dynamicCallbackUrl) + .font(.system(.caption, design: .monospaced)) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .autocapitalization(.none) + .disableAutocorrection(true) + } + .padding(.horizontal) + + Button(action: handleDynamicBootconfig) { + Text("Use dynamic bootconfig") + .font(.headline) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 44) + .background(Color.green) + .cornerRadius(8) + } + .disabled(isLoading) + .padding(.horizontal) + } + .padding(.vertical) + + // Loading indicator + if isLoading { + ProgressView("Authenticating...") + .padding() + } + } + .padding(.bottom, 40) + } + .background(Color(.systemBackground)) + .onAppear { + loadDynamicConfigDefaults() + } + } + + // MARK: - Computed Properties + + private var appName: String { + guard let info = Bundle.main.infoDictionary, + let name = info[kCFBundleNameKey as String] as? String else { + return "AuthFlowTester" + } + return name + } + + private var defaultConsumerKey: String { + return SalesforceManager.shared.bootConfig?.remoteAccessConsumerKey ?? "" + } + + private var defaultCallbackUrl: String { + return SalesforceManager.shared.bootConfig?.oauthRedirectURI ?? "" + } + + // MARK: - Helper Methods + + private func loadDynamicConfigDefaults() { + // Load initial values from bootconfig2.plist + if let config = BootConfig("/bootconfig2.plist") { + dynamicConsumerKey = config.remoteAccessConsumerKey ?? "" + dynamicCallbackUrl = config.oauthRedirectURI ?? "" + } + } + + // MARK: - Button Actions + + private func handleDefaultConfig() { + isLoading = true + + SalesforceManager.shared.revertToBootConfig() + + // Use default bootconfig - no additional setup needed + onConfigurationCompleted() + } + + private func handleDynamicBootconfig() { + isLoading = true + + // Use the values from the text fields + SalesforceManager.shared.overrideBootConfig( + consumerKey: dynamicConsumerKey, + callbackUrl: dynamicCallbackUrl + ) + + // Proceed with login + onConfigurationCompleted() + } +} + +// MARK: - UIViewControllerRepresentable + +struct ConfigPickerViewController: UIViewControllerRepresentable { + let onConfigurationCompleted: () -> Void + + func makeUIViewController(context: Context) -> UIHostingController { + return UIHostingController(rootView: ConfigPickerView(onConfigurationCompleted: onConfigurationCompleted)) + } + + func updateUIViewController(_ uiViewController: UIHostingController, context: Context) { + // No updates needed + } +} diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/SceneDelegate.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Classes/SceneDelegate.swift similarity index 91% rename from native/SampleApps/AuthFlowTester/AuthFlowTester/SceneDelegate.swift rename to native/SampleApps/AuthFlowTester/AuthFlowTester/Classes/SceneDelegate.swift index 7e1801fb43..66501360e2 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/SceneDelegate.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Classes/SceneDelegate.swift @@ -91,12 +91,19 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { return } - self.window?.rootViewController = UIHostingController(rootView: InitialView(onConfigurationCompleted: onConfigurationCompleted)) + // Check if user is already logged in + if UserAccountManager.shared.currentUserAccount != nil { + // User is already logged in, go directly to session detail + self.setupRootViewController() + } else { + // User is not logged in, show config picker + self.window?.rootViewController = UIHostingController(rootView: ConfigPickerView(onConfigurationCompleted: onConfigurationCompleted)) + } self.window?.makeKeyAndVisible() } func setupRootViewController() { - let rootVC = RootViewController() + let rootVC = SessionDetailViewController() let navVC = UINavigationController(rootViewController: rootVC) self.window!.rootViewController = navVC } diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Classes/SessionDetailViewController.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Classes/SessionDetailViewController.swift new file mode 100644 index 0000000000..7b6c167088 --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Classes/SessionDetailViewController.swift @@ -0,0 +1,350 @@ +/* + SessionDetailViewController.swift + AuthFlowTester + + Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. + + Redistribution and use of this software in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions + and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior written + permission of salesforce.com, inc. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import SwiftUI +import SalesforceSDKCore + +struct SessionDetailView: View { + @State private var isLoading = false + @State private var lastRequestResult: String = "" + @State private var isResultExpanded = false + @State private var refreshTrigger = UUID() + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 20) { + // REST API Request Section (moved to top) + VStack(alignment: .leading, spacing: 12) { + Text("REST API Test") + .font(.title2) + .fontWeight(.bold) + + Button(action: { + Task { + await makeRestRequest() + } + }) { + HStack { + if isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .scaleEffect(0.8) + } + Text(isLoading ? "Making Request..." : "Make REST API Request") + .font(.headline) + } + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 44) + .background(isLoading ? Color.gray : Color.blue) + .cornerRadius(8) + } + .disabled(isLoading) + + // Collapsible result section + if !lastRequestResult.isEmpty { + VStack(alignment: .leading, spacing: 8) { + Button(action: { + withAnimation { + isResultExpanded.toggle() + } + }) { + HStack { + Text("Last Request Result:") + .font(.caption) + .foregroundColor(.secondary) + Spacer() + Image(systemName: isResultExpanded ? "chevron.down" : "chevron.right") + .font(.caption) + .foregroundColor(.secondary) + } + } + + if isResultExpanded { + ScrollView([.vertical, .horizontal], showsIndicators: true) { + Text(lastRequestResult) + .font(.system(.caption, design: .monospaced)) + .foregroundColor(lastRequestResult.hasPrefix("✓") ? .green : .red) + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + } + .frame(height: 300) + .background(Color(.systemGray6)) + .cornerRadius(4) + } + } + .padding(.vertical, 4) + } + } + .padding() + .background(Color(.secondarySystemBackground)) + .cornerRadius(8) + + // OAuth Configuration Section + VStack(alignment: .leading, spacing: 12) { + Text("OAuth Configuration") + .font(.title2) + .fontWeight(.bold) + + InfoRow(label: configuredConsumerKeyLabel, + value: configuredConsumerKey) + + InfoRow(label: "Configured Callback URL:", + value: configuredCallbackUrl) + } + .padding() + .background(Color(.secondarySystemBackground)) + .cornerRadius(8) + + // Access Scopes Section + if !accessScopes.isEmpty { + VStack(alignment: .leading, spacing: 12) { + Text("Access Scopes") + .font(.title2) + .fontWeight(.bold) + + InfoRow(label: "Scopes:", value: accessScopes) + } + .padding() + .background(Color(.secondarySystemBackground)) + .cornerRadius(8) + } + + // User Credentials Section + VStack(alignment: .leading, spacing: 12) { + Text("User Credentials") + .font(.title2) + .fontWeight(.bold) + + InfoRow(label: "Client ID:", value: clientId) + InfoRow(label: "Redirect URI:", value: redirectUri) + InfoRow(label: "Instance URL:", value: instanceUrl) + InfoRow(label: "Organization ID:", value: organizationId) + InfoRow(label: "User ID:", value: userId) + InfoRow(label: "Username:", value: username) + InfoRow(label: "Scopes:", value: credentialsScopes) + InfoRow(label: "Token Format:", value: tokenFormat) + InfoRow(label: "Beacon Child Consumer Key:", value: beaconChildConsumerKey) + } + .padding() + .background(Color(.secondarySystemBackground)) + .cornerRadius(8) + + // Token Section (collapsed/sensitive) + VStack(alignment: .leading, spacing: 12) { + Text("Tokens") + .font(.title2) + .fontWeight(.bold) + + InfoRow(label: "Access Token:", value: accessToken, isSensitive: true) + InfoRow(label: "Refresh Token:", value: refreshToken, isSensitive: true) + } + .padding() + .background(Color(.secondarySystemBackground)) + .cornerRadius(8) + } + .padding() + } + .background(Color(.systemBackground)) + .navigationTitle("AuthFlowTester") + .navigationBarTitleDisplayMode(.large) + .id(refreshTrigger) + } + + // MARK: - REST API Request + + private func makeRestRequest() async { + isLoading = true + lastRequestResult = "" + + do { + let request = RestClient.shared.cheapRequest("v63.0") + let response = try await RestClient.shared.send(request: request) + + // Request succeeded - pretty print the JSON + let prettyJSON = prettyPrintJSON(response.asString()) + lastRequestResult = "✓ Success:\n\n\(prettyJSON)" + isResultExpanded = true // Auto-expand on new result + + // Force refresh of all fields + refreshTrigger = UUID() + } catch { + // Request failed + lastRequestResult = "✗ Error: \(error.localizedDescription)" + isResultExpanded = true // Auto-expand on error + } + + isLoading = false + } + + private func prettyPrintJSON(_ jsonString: String) -> String { + guard let jsonData = jsonString.data(using: .utf8) else { + return jsonString + } + + do { + let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) + let prettyData = try JSONSerialization.data(withJSONObject: jsonObject, options: [.prettyPrinted, .sortedKeys]) + + if let prettyString = String(data: prettyData, encoding: .utf8) { + return prettyString + } + } catch { + // If parsing fails, return original string + return jsonString + } + + return jsonString + } + + // MARK: - Computed Properties + + private var configuredConsumerKeyLabel: String { + let label = "Configured Consumer Key:" + if let defaultKey = SalesforceManager.shared.bootConfig?.remoteAccessConsumerKey, + configuredConsumerKey == defaultKey { + return "\(label) (default)" + } + return label + } + + private var configuredConsumerKey: String { + return UserAccountManager.shared.oauthClientID ?? "" + } + + private var configuredCallbackUrl: String { + return UserAccountManager.shared.oauthCompletionURL ?? "" + } + + private var accessScopes: String { + guard let scopes = UserAccountManager.shared.currentUserAccount?.accessScopes else { + return "" + } + return scopes.joined(separator: " ") + } + + private var clientId: String { + return UserAccountManager.shared.currentUserAccount?.credentials.clientId ?? "" + } + + private var redirectUri: String { + return UserAccountManager.shared.currentUserAccount?.credentials.redirectUri ?? "" + } + + private var refreshToken: String { + return UserAccountManager.shared.currentUserAccount?.credentials.refreshToken ?? "" + } + + private var accessToken: String { + return UserAccountManager.shared.currentUserAccount?.credentials.accessToken ?? "" + } + + private var tokenFormat: String { + return UserAccountManager.shared.currentUserAccount?.credentials.tokenFormat ?? "" + } + + private var beaconChildConsumerKey: String { + return UserAccountManager.shared.currentUserAccount?.credentials.beaconChildConsumerKey ?? "" + } + + private var instanceUrl: String { + return UserAccountManager.shared.currentUserAccount?.credentials.instanceUrl?.absoluteString ?? "" + } + + private var organizationId: String { + return UserAccountManager.shared.currentUserAccount?.credentials.organizationId ?? "" + } + + private var userId: String { + return UserAccountManager.shared.currentUserAccount?.credentials.userId ?? "" + } + + private var username: String { + return UserAccountManager.shared.currentUserAccount?.idData.username ?? "" + } + + private var credentialsScopes: String { + guard let scopes = UserAccountManager.shared.currentUserAccount?.credentials.scopes else { + return "" + } + return scopes.joined(separator: " ") + } +} + +// MARK: - Helper Views + +struct InfoRow: View { + let label: String + let value: String + var isSensitive: Bool = false + + @State private var isRevealed = false + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(label) + .font(.caption) + .foregroundColor(.secondary) + + if isSensitive && !isRevealed { + HStack { + Text("••••••••") + .font(.system(.body, design: .monospaced)) + Spacer() + Button(action: { isRevealed.toggle() }) { + Image(systemName: "eye") + .foregroundColor(.blue) + } + } + } else { + HStack { + Text(value.isEmpty ? "(empty)" : value) + .font(.system(.body, design: .monospaced)) + .foregroundColor(value.isEmpty ? .secondary : .primary) + Spacer() + if isSensitive { + Button(action: { isRevealed.toggle() }) { + Image(systemName: "eye.slash") + .foregroundColor(.blue) + } + } + } + } + } + .padding(.vertical, 4) + } +} + +class SessionDetailViewController: UIHostingController { + init() { + super.init(rootView: SessionDetailView()) + } + + @objc required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/AuthFlowTester.entitlements b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/AuthFlowTester.entitlements similarity index 100% rename from native/SampleApps/AuthFlowTester/AuthFlowTester/AuthFlowTester.entitlements rename to native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/AuthFlowTester.entitlements diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Info.plist b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/Info.plist similarity index 100% rename from native/SampleApps/AuthFlowTester/AuthFlowTester/Info.plist rename to native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/Info.plist diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/PrivacyInfo.xcprivacy b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/PrivacyInfo.xcprivacy similarity index 100% rename from native/SampleApps/AuthFlowTester/AuthFlowTester/PrivacyInfo.xcprivacy rename to native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/PrivacyInfo.xcprivacy diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/bootconfig.plist b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig.plist similarity index 61% rename from native/SampleApps/AuthFlowTester/AuthFlowTester/bootconfig.plist rename to native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig.plist index 42bef06d94..7e66324c5f 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/bootconfig.plist +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig.plist @@ -3,9 +3,9 @@ remoteAccessConsumerKey - 3MVG98dostKihXN53TYStBIiS8NRTXcbDzn9nHPb3piMElfQDD.kTyHeXjKV9JNUbe5sZeSQ4CVY1Onzpq21N + 3MVG98dostKihXN53TYStBIiS8FC2a3tE3XhGId0hQ37iQjF0xe4fxMSb2mFaWZn9e3GiLs1q67TNlyRji.Xw oauthRedirectURI - com.salesforce.mobilesdk.sample.authflowtester://oauth/success + testsfdc:///mobilesdk/detect/oauth/done shouldAuthenticate diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig2.plist b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig2.plist new file mode 100644 index 0000000000..89b45fa1df --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig2.plist @@ -0,0 +1,13 @@ + + + + + remoteAccessConsumerKey + 3MVG9SemV5D80oBcXZ2EUzbcJw.BPBV7Nd7htOt2IMVa3r5Zb_UgI92gVmxnVoCLfysf3.tIkrYAJF8mHsJxB + oauthRedirectURI + testsfdc:///mobilesdk/detect/oauth/done + shouldAuthenticate + + + + diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/InitialViewController.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/InitialViewController.swift deleted file mode 100644 index f65711ba2b..0000000000 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/InitialViewController.swift +++ /dev/null @@ -1,133 +0,0 @@ -/* - InitialViewController.swift - AuthFlowTester - - Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. - - Redistribution and use of this software in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list of conditions - and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of - conditions and the following disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to - endorse or promote products derived from this software without specific prior written - permission of salesforce.com, inc. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR - IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY - WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import SwiftUI -import SalesforceSDKCore - -struct InitialView: View { - @State private var isLoading = false - let onConfigurationCompleted: () -> Void - - var body: some View { - VStack(spacing: 30) { - // App name label - Text(appName) - .font(.largeTitle) - .fontWeight(.bold) - .foregroundColor(.primary) - .padding(.top, 100) - - Spacer() - - // Buttons container - VStack(spacing: 20) { - // Static bootconfig button - Button(action: handleStaticBootconfig) { - Text("Use static bootconfig") - .font(.headline) - .foregroundColor(.white) - .frame(width: 200, height: 44) - .background(Color.blue) - .cornerRadius(8) - } - .disabled(isLoading) - - // Dynamic bootconfig button - Button(action: handleDynamicBootconfig) { - Text("Use dynamic bootconfig") - .font(.headline) - .foregroundColor(.white) - .frame(width: 200, height: 44) - .background(Color.green) - .cornerRadius(8) - } - .disabled(isLoading) - } - - Spacer() - - // Loading indicator - if isLoading { - ProgressView("Authenticating...") - .padding() - } - } - .background(Color(.systemBackground)) - .onAppear { - // Any setup that needs to happen when the view appears - } - } - - // MARK: - Computed Properties - - private var appName: String { - guard let info = Bundle.main.infoDictionary, - let name = info[kCFBundleNameKey as String] as? String else { - return "AuthFlowTester" - } - return name - } - - // MARK: - Button Actions - - private func handleStaticBootconfig() { - isLoading = true - - SalesforceManager.shared.revertToBootConfig() - - // Use static bootconfig - no additional setup needed - onConfigurationCompleted() - } - - private func handleDynamicBootconfig() { - isLoading = true - - // Use the dynamic bootconfig method - SalesforceManager.shared.overrideBootConfig( - // ECA without refresh scope - consumerKey: "3MVG9SemV5D80oBcXZ2EUzbcJw.BPBV7Nd7htOt2IMVa3r5Zb_UgI92gVmxnVoCLfysf3.tIkrYAJF8mHsJxB", - callbackUrl: "testsfdc:///mobilesdk/detect/oauth/done" - ) - - // Proceed with login - onConfigurationCompleted() - } -} - -// MARK: - UIViewControllerRepresentable - -struct InitialViewController: UIViewControllerRepresentable { - let onConfigurationCompleted: () -> Void - - func makeUIViewController(context: Context) -> UIHostingController { - return UIHostingController(rootView: InitialView(onConfigurationCompleted: onConfigurationCompleted)) - } - - func updateUIViewController(_ uiViewController: UIHostingController, context: Context) { - // No updates needed - } -} diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/RootViewController.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/RootViewController.swift deleted file mode 100644 index 0ba0572fc5..0000000000 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/RootViewController.swift +++ /dev/null @@ -1,57 +0,0 @@ -/* - RootViewController.swift - AuthFlowTester - - Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. - - Redistribution and use of this software in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list of conditions - and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of - conditions and the following disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to - endorse or promote products derived from this software without specific prior written - permission of salesforce.com, inc. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR - IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY - WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import SwiftUI -import SalesforceSDKCore - -struct RootView: View { - var body: some View { - VStack { - Spacer() - - Text("Authentication successful!") - .font(.headline) - .foregroundColor(.secondary) - .padding(.top, 8) - - Spacer() - } - .background(Color(.systemBackground)) - .navigationTitle("AuthFlowTester") - .navigationBarTitleDisplayMode(.large) - } -} - -class RootViewController: UIHostingController { - init() { - super.init(rootView: RootView()) - } - - @objc required dynamic init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} From cf2033dd91eb23052a9a1c1e614073aed4f567ce Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Wed, 22 Oct 2025 17:36:31 -0700 Subject: [PATCH 06/19] Enriching AuthFlowTester application --- .../Classes/OAuth/JwtAccessToken.swift | 36 +- .../AuthFlowTester.xcodeproj/project.pbxproj | 50 ++- .../Classes/ConfigPickerViewController.swift | 213 ----------- .../Classes/SessionDetailViewController.swift | 350 ------------------ .../Supporting Files/bootconfig.plist | 4 +- .../Supporting Files/bootconfig2.plist | 5 +- .../ConfigPickerViewController.swift | 139 +++++++ .../SessionDetailViewController.swift | 155 ++++++++ .../Views/DefaultConfigView.swift | 104 ++++++ .../Views/DynamicConfigView.swift | 94 +++++ .../AuthFlowTester/Views/FlowTypesView.swift | 69 ++++ .../AuthFlowTester/Views/InfoRowView.swift | 71 ++++ .../AuthFlowTester/Views/JwtAccessView.swift | 137 +++++++ .../Views/OAuthConfigurationView.swift | 68 ++++ .../Views/RestApiTestView.swift | 166 +++++++++ .../Views/UserCredentialsView.swift | 100 +++++ 16 files changed, 1174 insertions(+), 587 deletions(-) delete mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/Classes/ConfigPickerViewController.swift delete mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/Classes/SessionDetailViewController.swift create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/ConfigPickerViewController.swift create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/SessionDetailViewController.swift create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DefaultConfigView.swift create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DynamicConfigView.swift create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/Views/FlowTypesView.swift create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/Views/InfoRowView.swift create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/Views/JwtAccessView.swift create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/Views/OAuthConfigurationView.swift create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/Views/RestApiTestView.swift create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/Views/UserCredentialsView.swift diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/JwtAccessToken.swift b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/JwtAccessToken.swift index 75c3cdb588..7f141c0929 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/JwtAccessToken.swift +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/JwtAccessToken.swift @@ -30,12 +30,12 @@ import Foundation /// Struct representing a JWT Header public struct JwtHeader: Codable { - let algorithm: String? - let type: String? - let keyId: String? - let tokenType: String? - let tenantKey: String? - let version: String? + public let algorithm: String? + public let type: String? + public let keyId: String? + public let tokenType: String? + public let tenantKey: String? + public let version: String? enum CodingKeys: String, CodingKey { case algorithm = "alg" @@ -49,13 +49,13 @@ public struct JwtHeader: Codable { /// Struct representing a JWT Payload public struct JwtPayload: Codable { - let audience: [String]? - let expirationTime: Int? - let issuer: String? - let notBeforeTime: Int? - let subject: String? - let scopes: String? - let clientId: String? + public let audience: [String]? + public let expirationTime: Int? + public let issuer: String? + public let notBeforeTime: Int? + public let subject: String? + public let scopes: String? + public let clientId: String? enum CodingKeys: String, CodingKey { case audience = "aud" @@ -71,9 +71,9 @@ public struct JwtPayload: Codable { /// Class representing a JWT Access Token @objc(SFSDKJwtAccessToken) public class JwtAccessToken : NSObject { - let rawJwt: String - let header: JwtHeader - let payload: JwtPayload + public let rawJwt: String + public let header: JwtHeader + public let payload: JwtPayload /// Initializer to parse and decode the JWT string @objc public init(jwt: String) throws { @@ -116,7 +116,7 @@ public class JwtAccessToken : NSObject { } /// Helper method to decode Base64 URL-encoded strings - private static func decodeBase64Url(_ string: String) throws -> String { + public static func decodeBase64Url(_ string: String) throws -> String { var base64 = string .replacingOccurrences(of: "-", with: "+") .replacingOccurrences(of: "_", with: "/") @@ -133,7 +133,7 @@ public class JwtAccessToken : NSObject { } /// Custom errors for JWT decoding - enum JwtError: Error { + public enum JwtError: Error { case invalidFormat case invalidBase64 } diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj b/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj index 7765332490..75365ef020 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj @@ -18,7 +18,15 @@ AUTH005 /* ConfigPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH006 /* ConfigPickerViewController.swift */; }; AUTH007 /* SessionDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH008 /* SessionDetailViewController.swift */; }; AUTH009 /* bootconfig.plist in Resources */ = {isa = PBXBuildFile; fileRef = AUTH010 /* bootconfig.plist */; }; + AUTH037 /* UserCredentialsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH038 /* UserCredentialsView.swift */; }; AUTH011 /* bootconfig2.plist in Resources */ = {isa = PBXBuildFile; fileRef = AUTH036 /* bootconfig2.plist */; }; + AUTH040 /* RestApiTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH041 /* RestApiTestView.swift */; }; + AUTH042 /* OAuthConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH043 /* OAuthConfigurationView.swift */; }; + AUTH045 /* DefaultConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH046 /* DefaultConfigView.swift */; }; + AUTH047 /* DynamicConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH048 /* DynamicConfigView.swift */; }; + AUTH049 /* JwtAccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH050 /* JwtAccessView.swift */; }; + AUTH051 /* InfoRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH052 /* InfoRowView.swift */; }; + AUTH053 /* FlowTypesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH054 /* FlowTypesView.swift */; }; AUTH013 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = AUTH014 /* PrivacyInfo.xcprivacy */; }; AUTH033 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AUTH034 /* Images.xcassets */; }; /* End PBXBuildFile section */ @@ -46,6 +54,14 @@ AUTH006 /* ConfigPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigPickerViewController.swift; sourceTree = ""; }; AUTH008 /* SessionDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDetailViewController.swift; sourceTree = ""; }; AUTH010 /* bootconfig.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = bootconfig.plist; sourceTree = ""; }; + AUTH038 /* UserCredentialsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCredentialsView.swift; sourceTree = ""; }; + AUTH041 /* RestApiTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestApiTestView.swift; sourceTree = ""; }; + AUTH043 /* OAuthConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthConfigurationView.swift; sourceTree = ""; }; + AUTH046 /* DefaultConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultConfigView.swift; sourceTree = ""; }; + AUTH048 /* DynamicConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicConfigView.swift; sourceTree = ""; }; + AUTH050 /* JwtAccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JwtAccessView.swift; sourceTree = ""; }; + AUTH052 /* InfoRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoRowView.swift; sourceTree = ""; }; + AUTH054 /* FlowTypesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowTypesView.swift; sourceTree = ""; }; AUTH012 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AUTH014 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; AUTH016 /* AuthFlowTester.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AuthFlowTester.entitlements; sourceTree = ""; }; @@ -101,6 +117,8 @@ isa = PBXGroup; children = ( AUTH022 /* Classes */, + AUTH044 /* ViewControllers */, + AUTH039 /* Views */, AUTH035 /* Resources */, 4FF0EF9E2EA8568C005E4474 /* Supporting Files */, ); @@ -120,10 +138,17 @@ children = ( AUTH002 /* AppDelegate.swift */, AUTH004 /* SceneDelegate.swift */, + ); + path = Classes; + sourceTree = ""; + }; + AUTH044 /* ViewControllers */ = { + isa = PBXGroup; + children = ( AUTH006 /* ConfigPickerViewController.swift */, AUTH008 /* SessionDetailViewController.swift */, ); - path = Classes; + path = ViewControllers; sourceTree = ""; }; AUTH035 /* Resources */ = { @@ -134,6 +159,21 @@ name = Resources; sourceTree = ""; }; + AUTH039 /* Views */ = { + isa = PBXGroup; + children = ( + AUTH038 /* UserCredentialsView.swift */, + AUTH041 /* RestApiTestView.swift */, + AUTH043 /* OAuthConfigurationView.swift */, + AUTH046 /* DefaultConfigView.swift */, + AUTH048 /* DynamicConfigView.swift */, + AUTH050 /* JwtAccessView.swift */, + AUTH052 /* InfoRowView.swift */, + AUTH054 /* FlowTypesView.swift */, + ); + path = Views; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -206,6 +246,14 @@ AUTH003 /* SceneDelegate.swift in Sources */, AUTH005 /* ConfigPickerViewController.swift in Sources */, AUTH007 /* SessionDetailViewController.swift in Sources */, + AUTH037 /* UserCredentialsView.swift in Sources */, + AUTH040 /* RestApiTestView.swift in Sources */, + AUTH042 /* OAuthConfigurationView.swift in Sources */, + AUTH045 /* DefaultConfigView.swift in Sources */, + AUTH047 /* DynamicConfigView.swift in Sources */, + AUTH049 /* JwtAccessView.swift in Sources */, + AUTH051 /* InfoRowView.swift in Sources */, + AUTH053 /* FlowTypesView.swift in Sources */, ); }; /* End PBXSourcesBuildPhase section */ diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Classes/ConfigPickerViewController.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Classes/ConfigPickerViewController.swift deleted file mode 100644 index 08f8b884bd..0000000000 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/Classes/ConfigPickerViewController.swift +++ /dev/null @@ -1,213 +0,0 @@ -/* - ConfigPickerViewController.swift - AuthFlowTester - - Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. - - Redistribution and use of this software in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list of conditions - and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of - conditions and the following disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to - endorse or promote products derived from this software without specific prior written - permission of salesforce.com, inc. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR - IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY - WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import SwiftUI -import SalesforceSDKCore - -struct ConfigPickerView: View { - @State private var isLoading = false - @State private var dynamicConsumerKey = "" - @State private var dynamicCallbackUrl = "" - - let onConfigurationCompleted: () -> Void - - var body: some View { - ScrollView { - VStack(spacing: 30) { - // App name label - Text(appName) - .font(.largeTitle) - .fontWeight(.bold) - .foregroundColor(.primary) - .padding(.top, 40) - - // Default config section - VStack(alignment: .leading, spacing: 12) { - Text("Default Configuration") - .font(.headline) - .padding(.horizontal) - - VStack(alignment: .leading, spacing: 8) { - Text("Consumer Key:") - .font(.caption) - .foregroundColor(.secondary) - Text(defaultConsumerKey) - .font(.system(.caption, design: .monospaced)) - .padding(8) - .frame(maxWidth: .infinity, alignment: .leading) - .background(Color(.systemGray6)) - .cornerRadius(4) - - Text("Callback URL:") - .font(.caption) - .foregroundColor(.secondary) - Text(defaultCallbackUrl) - .font(.system(.caption, design: .monospaced)) - .padding(8) - .frame(maxWidth: .infinity, alignment: .leading) - .background(Color(.systemGray6)) - .cornerRadius(4) - } - .padding(.horizontal) - - Button(action: handleDefaultConfig) { - Text("Use default config") - .font(.headline) - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .frame(height: 44) - .background(Color.blue) - .cornerRadius(8) - } - .disabled(isLoading) - .padding(.horizontal) - } - .padding(.vertical) - - Divider() - - // Dynamic config section - VStack(alignment: .leading, spacing: 12) { - Text("Dynamic Configuration") - .font(.headline) - .padding(.horizontal) - - VStack(alignment: .leading, spacing: 8) { - Text("Consumer Key:") - .font(.caption) - .foregroundColor(.secondary) - TextField("Consumer Key", text: $dynamicConsumerKey) - .font(.system(.caption, design: .monospaced)) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .autocapitalization(.none) - .disableAutocorrection(true) - - Text("Callback URL:") - .font(.caption) - .foregroundColor(.secondary) - TextField("Callback URL", text: $dynamicCallbackUrl) - .font(.system(.caption, design: .monospaced)) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .autocapitalization(.none) - .disableAutocorrection(true) - } - .padding(.horizontal) - - Button(action: handleDynamicBootconfig) { - Text("Use dynamic bootconfig") - .font(.headline) - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .frame(height: 44) - .background(Color.green) - .cornerRadius(8) - } - .disabled(isLoading) - .padding(.horizontal) - } - .padding(.vertical) - - // Loading indicator - if isLoading { - ProgressView("Authenticating...") - .padding() - } - } - .padding(.bottom, 40) - } - .background(Color(.systemBackground)) - .onAppear { - loadDynamicConfigDefaults() - } - } - - // MARK: - Computed Properties - - private var appName: String { - guard let info = Bundle.main.infoDictionary, - let name = info[kCFBundleNameKey as String] as? String else { - return "AuthFlowTester" - } - return name - } - - private var defaultConsumerKey: String { - return SalesforceManager.shared.bootConfig?.remoteAccessConsumerKey ?? "" - } - - private var defaultCallbackUrl: String { - return SalesforceManager.shared.bootConfig?.oauthRedirectURI ?? "" - } - - // MARK: - Helper Methods - - private func loadDynamicConfigDefaults() { - // Load initial values from bootconfig2.plist - if let config = BootConfig("/bootconfig2.plist") { - dynamicConsumerKey = config.remoteAccessConsumerKey ?? "" - dynamicCallbackUrl = config.oauthRedirectURI ?? "" - } - } - - // MARK: - Button Actions - - private func handleDefaultConfig() { - isLoading = true - - SalesforceManager.shared.revertToBootConfig() - - // Use default bootconfig - no additional setup needed - onConfigurationCompleted() - } - - private func handleDynamicBootconfig() { - isLoading = true - - // Use the values from the text fields - SalesforceManager.shared.overrideBootConfig( - consumerKey: dynamicConsumerKey, - callbackUrl: dynamicCallbackUrl - ) - - // Proceed with login - onConfigurationCompleted() - } -} - -// MARK: - UIViewControllerRepresentable - -struct ConfigPickerViewController: UIViewControllerRepresentable { - let onConfigurationCompleted: () -> Void - - func makeUIViewController(context: Context) -> UIHostingController { - return UIHostingController(rootView: ConfigPickerView(onConfigurationCompleted: onConfigurationCompleted)) - } - - func updateUIViewController(_ uiViewController: UIHostingController, context: Context) { - // No updates needed - } -} diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Classes/SessionDetailViewController.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Classes/SessionDetailViewController.swift deleted file mode 100644 index 7b6c167088..0000000000 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/Classes/SessionDetailViewController.swift +++ /dev/null @@ -1,350 +0,0 @@ -/* - SessionDetailViewController.swift - AuthFlowTester - - Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. - - Redistribution and use of this software in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list of conditions - and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of - conditions and the following disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to - endorse or promote products derived from this software without specific prior written - permission of salesforce.com, inc. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR - IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY - WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import SwiftUI -import SalesforceSDKCore - -struct SessionDetailView: View { - @State private var isLoading = false - @State private var lastRequestResult: String = "" - @State private var isResultExpanded = false - @State private var refreshTrigger = UUID() - - var body: some View { - ScrollView { - VStack(alignment: .leading, spacing: 20) { - // REST API Request Section (moved to top) - VStack(alignment: .leading, spacing: 12) { - Text("REST API Test") - .font(.title2) - .fontWeight(.bold) - - Button(action: { - Task { - await makeRestRequest() - } - }) { - HStack { - if isLoading { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .scaleEffect(0.8) - } - Text(isLoading ? "Making Request..." : "Make REST API Request") - .font(.headline) - } - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .frame(height: 44) - .background(isLoading ? Color.gray : Color.blue) - .cornerRadius(8) - } - .disabled(isLoading) - - // Collapsible result section - if !lastRequestResult.isEmpty { - VStack(alignment: .leading, spacing: 8) { - Button(action: { - withAnimation { - isResultExpanded.toggle() - } - }) { - HStack { - Text("Last Request Result:") - .font(.caption) - .foregroundColor(.secondary) - Spacer() - Image(systemName: isResultExpanded ? "chevron.down" : "chevron.right") - .font(.caption) - .foregroundColor(.secondary) - } - } - - if isResultExpanded { - ScrollView([.vertical, .horizontal], showsIndicators: true) { - Text(lastRequestResult) - .font(.system(.caption, design: .monospaced)) - .foregroundColor(lastRequestResult.hasPrefix("✓") ? .green : .red) - .padding(8) - .frame(maxWidth: .infinity, alignment: .leading) - } - .frame(height: 300) - .background(Color(.systemGray6)) - .cornerRadius(4) - } - } - .padding(.vertical, 4) - } - } - .padding() - .background(Color(.secondarySystemBackground)) - .cornerRadius(8) - - // OAuth Configuration Section - VStack(alignment: .leading, spacing: 12) { - Text("OAuth Configuration") - .font(.title2) - .fontWeight(.bold) - - InfoRow(label: configuredConsumerKeyLabel, - value: configuredConsumerKey) - - InfoRow(label: "Configured Callback URL:", - value: configuredCallbackUrl) - } - .padding() - .background(Color(.secondarySystemBackground)) - .cornerRadius(8) - - // Access Scopes Section - if !accessScopes.isEmpty { - VStack(alignment: .leading, spacing: 12) { - Text("Access Scopes") - .font(.title2) - .fontWeight(.bold) - - InfoRow(label: "Scopes:", value: accessScopes) - } - .padding() - .background(Color(.secondarySystemBackground)) - .cornerRadius(8) - } - - // User Credentials Section - VStack(alignment: .leading, spacing: 12) { - Text("User Credentials") - .font(.title2) - .fontWeight(.bold) - - InfoRow(label: "Client ID:", value: clientId) - InfoRow(label: "Redirect URI:", value: redirectUri) - InfoRow(label: "Instance URL:", value: instanceUrl) - InfoRow(label: "Organization ID:", value: organizationId) - InfoRow(label: "User ID:", value: userId) - InfoRow(label: "Username:", value: username) - InfoRow(label: "Scopes:", value: credentialsScopes) - InfoRow(label: "Token Format:", value: tokenFormat) - InfoRow(label: "Beacon Child Consumer Key:", value: beaconChildConsumerKey) - } - .padding() - .background(Color(.secondarySystemBackground)) - .cornerRadius(8) - - // Token Section (collapsed/sensitive) - VStack(alignment: .leading, spacing: 12) { - Text("Tokens") - .font(.title2) - .fontWeight(.bold) - - InfoRow(label: "Access Token:", value: accessToken, isSensitive: true) - InfoRow(label: "Refresh Token:", value: refreshToken, isSensitive: true) - } - .padding() - .background(Color(.secondarySystemBackground)) - .cornerRadius(8) - } - .padding() - } - .background(Color(.systemBackground)) - .navigationTitle("AuthFlowTester") - .navigationBarTitleDisplayMode(.large) - .id(refreshTrigger) - } - - // MARK: - REST API Request - - private func makeRestRequest() async { - isLoading = true - lastRequestResult = "" - - do { - let request = RestClient.shared.cheapRequest("v63.0") - let response = try await RestClient.shared.send(request: request) - - // Request succeeded - pretty print the JSON - let prettyJSON = prettyPrintJSON(response.asString()) - lastRequestResult = "✓ Success:\n\n\(prettyJSON)" - isResultExpanded = true // Auto-expand on new result - - // Force refresh of all fields - refreshTrigger = UUID() - } catch { - // Request failed - lastRequestResult = "✗ Error: \(error.localizedDescription)" - isResultExpanded = true // Auto-expand on error - } - - isLoading = false - } - - private func prettyPrintJSON(_ jsonString: String) -> String { - guard let jsonData = jsonString.data(using: .utf8) else { - return jsonString - } - - do { - let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) - let prettyData = try JSONSerialization.data(withJSONObject: jsonObject, options: [.prettyPrinted, .sortedKeys]) - - if let prettyString = String(data: prettyData, encoding: .utf8) { - return prettyString - } - } catch { - // If parsing fails, return original string - return jsonString - } - - return jsonString - } - - // MARK: - Computed Properties - - private var configuredConsumerKeyLabel: String { - let label = "Configured Consumer Key:" - if let defaultKey = SalesforceManager.shared.bootConfig?.remoteAccessConsumerKey, - configuredConsumerKey == defaultKey { - return "\(label) (default)" - } - return label - } - - private var configuredConsumerKey: String { - return UserAccountManager.shared.oauthClientID ?? "" - } - - private var configuredCallbackUrl: String { - return UserAccountManager.shared.oauthCompletionURL ?? "" - } - - private var accessScopes: String { - guard let scopes = UserAccountManager.shared.currentUserAccount?.accessScopes else { - return "" - } - return scopes.joined(separator: " ") - } - - private var clientId: String { - return UserAccountManager.shared.currentUserAccount?.credentials.clientId ?? "" - } - - private var redirectUri: String { - return UserAccountManager.shared.currentUserAccount?.credentials.redirectUri ?? "" - } - - private var refreshToken: String { - return UserAccountManager.shared.currentUserAccount?.credentials.refreshToken ?? "" - } - - private var accessToken: String { - return UserAccountManager.shared.currentUserAccount?.credentials.accessToken ?? "" - } - - private var tokenFormat: String { - return UserAccountManager.shared.currentUserAccount?.credentials.tokenFormat ?? "" - } - - private var beaconChildConsumerKey: String { - return UserAccountManager.shared.currentUserAccount?.credentials.beaconChildConsumerKey ?? "" - } - - private var instanceUrl: String { - return UserAccountManager.shared.currentUserAccount?.credentials.instanceUrl?.absoluteString ?? "" - } - - private var organizationId: String { - return UserAccountManager.shared.currentUserAccount?.credentials.organizationId ?? "" - } - - private var userId: String { - return UserAccountManager.shared.currentUserAccount?.credentials.userId ?? "" - } - - private var username: String { - return UserAccountManager.shared.currentUserAccount?.idData.username ?? "" - } - - private var credentialsScopes: String { - guard let scopes = UserAccountManager.shared.currentUserAccount?.credentials.scopes else { - return "" - } - return scopes.joined(separator: " ") - } -} - -// MARK: - Helper Views - -struct InfoRow: View { - let label: String - let value: String - var isSensitive: Bool = false - - @State private var isRevealed = false - - var body: some View { - VStack(alignment: .leading, spacing: 4) { - Text(label) - .font(.caption) - .foregroundColor(.secondary) - - if isSensitive && !isRevealed { - HStack { - Text("••••••••") - .font(.system(.body, design: .monospaced)) - Spacer() - Button(action: { isRevealed.toggle() }) { - Image(systemName: "eye") - .foregroundColor(.blue) - } - } - } else { - HStack { - Text(value.isEmpty ? "(empty)" : value) - .font(.system(.body, design: .monospaced)) - .foregroundColor(value.isEmpty ? .secondary : .primary) - Spacer() - if isSensitive { - Button(action: { isRevealed.toggle() }) { - Image(systemName: "eye.slash") - .foregroundColor(.blue) - } - } - } - } - } - .padding(.vertical, 4) - } -} - -class SessionDetailViewController: UIHostingController { - init() { - super.init(rootView: SessionDetailView()) - } - - @objc required dynamic init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig.plist b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig.plist index 7e66324c5f..927fefb46a 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig.plist +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig.plist @@ -3,9 +3,9 @@ remoteAccessConsumerKey - 3MVG98dostKihXN53TYStBIiS8FC2a3tE3XhGId0hQ37iQjF0xe4fxMSb2mFaWZn9e3GiLs1q67TNlyRji.Xw + 3MVG9SemV5D80oBcXZ2EUzbcJw6aYF3RcTY1FgYlgnWAA72zHubsit4NKIA.DNcINzbDjz23yUJP3ucnY99F6 oauthRedirectURI - testsfdc:///mobilesdk/detect/oauth/done + testerjwt://mobilesdk/done shouldAuthenticate diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig2.plist b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig2.plist index 89b45fa1df..16e10f7aec 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig2.plist +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig2.plist @@ -3,11 +3,10 @@ remoteAccessConsumerKey - 3MVG9SemV5D80oBcXZ2EUzbcJw.BPBV7Nd7htOt2IMVa3r5Zb_UgI92gVmxnVoCLfysf3.tIkrYAJF8mHsJxB + 3MVG9SemV5D80oBcXZ2EUzbcJwzJCe2n4LaHH_Z2JSpIJqJ1MzFK_XRlHrupqNdeus8.NRonkpx0sAAWKzfK8 oauthRedirectURI - testsfdc:///mobilesdk/detect/oauth/done + testeropaque://mobilesdk/done shouldAuthenticate - diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/ConfigPickerViewController.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/ConfigPickerViewController.swift new file mode 100644 index 0000000000..dd1a9e4610 --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/ConfigPickerViewController.swift @@ -0,0 +1,139 @@ +/* + ConfigPickerViewController.swift + AuthFlowTester + + Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. + + Redistribution and use of this software in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions + and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior written + permission of salesforce.com, inc. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import SwiftUI +import SalesforceSDKCore + +struct ConfigPickerView: View { + @State private var isLoading = false + @State private var dynamicConsumerKey = "" + @State private var dynamicCallbackUrl = "" + + let onConfigurationCompleted: () -> Void + + var body: some View { + NavigationView { + ScrollView { + VStack(spacing: 30) { + // Flow types section + FlowTypesView() + .padding(.top, 20) + + Divider() + + // Default config section + DefaultConfigView( + isLoading: isLoading, + onUseConfig: handleDefaultConfig + ) + + Divider() + + // Dynamic config section + DynamicConfigView( + consumerKey: $dynamicConsumerKey, + callbackUrl: $dynamicCallbackUrl, + isLoading: isLoading, + onUseConfig: handleDynamicBootconfig + ) + + // Loading indicator + if isLoading { + ProgressView("Authenticating...") + .padding() + } + } + .padding(.bottom, 40) + } + .background(Color(.systemBackground)) + .navigationTitle(appName) + .navigationBarTitleDisplayMode(.large) + } + .navigationViewStyle(.stack) + .onAppear { + loadDynamicConfigDefaults() + } + } + + // MARK: - Computed Properties + + private var appName: String { + guard let info = Bundle.main.infoDictionary, + let name = info[kCFBundleNameKey as String] as? String else { + return "AuthFlowTester" + } + return name + } + + // MARK: - Helper Methods + + private func loadDynamicConfigDefaults() { + // Load initial values from bootconfig2.plist + if let config = BootConfig("/bootconfig2.plist") { + dynamicConsumerKey = config.remoteAccessConsumerKey ?? "" + dynamicCallbackUrl = config.oauthRedirectURI ?? "" + } + } + + // MARK: - Button Actions + + private func handleDefaultConfig() { + isLoading = true + + SalesforceManager.shared.revertToBootConfig() + + // Use default bootconfig - no additional setup needed + onConfigurationCompleted() + } + + private func handleDynamicBootconfig() { + isLoading = true + + // Use the values from the text fields + SalesforceManager.shared.overrideBootConfig( + consumerKey: dynamicConsumerKey, + callbackUrl: dynamicCallbackUrl + ) + + // Proceed with login + onConfigurationCompleted() + } +} + +// MARK: - UIViewControllerRepresentable + +struct ConfigPickerViewController: UIViewControllerRepresentable { + let onConfigurationCompleted: () -> Void + + func makeUIViewController(context: Context) -> UIHostingController { + return UIHostingController(rootView: ConfigPickerView(onConfigurationCompleted: onConfigurationCompleted)) + } + + func updateUIViewController(_ uiViewController: UIHostingController, context: Context) { + // No updates needed + } +} diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/SessionDetailViewController.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/SessionDetailViewController.swift new file mode 100644 index 0000000000..a187008995 --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/SessionDetailViewController.swift @@ -0,0 +1,155 @@ +/* + SessionDetailViewController.swift + AuthFlowTester + + Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. + + Redistribution and use of this software in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions + and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior written + permission of salesforce.com, inc. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import SwiftUI +import SalesforceSDKCore + +struct SessionDetailView: View { + @State private var refreshTrigger = UUID() + @State private var showNotImplementedAlert = false + @State private var showLogoutConfigPicker = false + + var onChangeConsumerKey: () -> Void + var onSwitchUser: () -> Void + var onLogout: () -> Void + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 20) { + // REST API Request Section (moved to top) + RestApiTestView(onRequestCompleted: { + refreshTrigger = UUID() + }) + + // OAuth Configuration Section + OAuthConfigurationView() + .id(refreshTrigger) + + // User Credentials Section + UserCredentialsView() + .id(refreshTrigger) + } + .padding() + } + .background(Color(.systemBackground)) + .navigationTitle("AuthFlowTester") + .navigationBarTitleDisplayMode(.large) + .toolbar { + ToolbarItemGroup(placement: .bottomBar) { + Button(action: { + showNotImplementedAlert = true + }) { + Label("Change Key", systemImage: "key.horizontal.fill") + } + + Spacer() + + Button(action: { + onSwitchUser() + }) { + Label("Switch User", systemImage: "person.2.fill") + } + + Spacer() + + Button(action: { + showLogoutConfigPicker = true + }) { + Label("Logout", systemImage: "rectangle.portrait.and.arrow.right") + } + } + } + .sheet(isPresented: $showLogoutConfigPicker) { + NavigationView { + ConfigPickerView(onConfigurationCompleted: { + showLogoutConfigPicker = false + onLogout() + }) + .navigationTitle("Select Config for Re-login") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + showLogoutConfigPicker = false + } + } + } + } + } + .alert("Change Consumer Key", isPresented: $showNotImplementedAlert) { + Button("OK", role: .cancel) { } + } message: { + Text("Not implemented yet!") + } + } +} + +class SessionDetailViewController: UIHostingController { + + init() { + super.init(rootView: SessionDetailView( + onChangeConsumerKey: {}, + onSwitchUser: {}, + onLogout: {} + )) + + // Update the rootView with actual closures after init + self.rootView = SessionDetailView( + onChangeConsumerKey: { [weak self] in + // Alert is handled in SwiftUI + }, + onSwitchUser: { [weak self] in + self?.handleSwitchUser() + }, + onLogout: { [weak self] in + self?.handleLogout() + } + ) + } + + @objc required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + // Show the toolbar so the bottom bar is visible + navigationController?.isToolbarHidden = false + } + + private func handleSwitchUser() { + let umvc = SalesforceUserManagementViewController.init(completionBlock: { [weak self] _ in + self?.dismiss(animated: true, completion: nil) + }) + self.present(umvc, animated: true, completion: nil) + } + + private func handleLogout() { + // Perform the actual logout - config has already been selected by the user + UserAccountManager.shared.logout(SFLogoutReason.userInitiated) + } +} diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DefaultConfigView.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DefaultConfigView.swift new file mode 100644 index 0000000000..75c1fc76ac --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DefaultConfigView.swift @@ -0,0 +1,104 @@ +/* + DefaultConfigView.swift + AuthFlowTester + + Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. + + Redistribution and use of this software in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions + and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior written + permission of salesforce.com, inc. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import SwiftUI +import SalesforceSDKCore + +struct DefaultConfigView: View { + let isLoading: Bool + let onUseConfig: () -> Void + @State private var isExpanded: Bool = false + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Button(action: { + withAnimation { + isExpanded.toggle() + } + }) { + HStack { + Text("Default Configuration") + .font(.headline) + .foregroundColor(.primary) + Spacer() + Image(systemName: isExpanded ? "chevron.up" : "chevron.down") + .foregroundColor(.secondary) + } + .padding(.horizontal) + } + + if isExpanded { + VStack(alignment: .leading, spacing: 8) { + Text("Consumer Key:") + .font(.caption) + .foregroundColor(.secondary) + Text(defaultConsumerKey) + .font(.system(.caption, design: .monospaced)) + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(.systemGray6)) + .cornerRadius(4) + + Text("Callback URL:") + .font(.caption) + .foregroundColor(.secondary) + Text(defaultCallbackUrl) + .font(.system(.caption, design: .monospaced)) + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(.systemGray6)) + .cornerRadius(4) + } + .padding(.horizontal) + } + + Button(action: onUseConfig) { + Text("Use default config") + .font(.headline) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 44) + .background(Color.blue) + .cornerRadius(8) + } + .disabled(isLoading) + .padding(.horizontal) + } + .padding(.vertical) + } + + // MARK: - Computed Properties + + private var defaultConsumerKey: String { + return SalesforceManager.shared.bootConfig?.remoteAccessConsumerKey ?? "" + } + + private var defaultCallbackUrl: String { + return SalesforceManager.shared.bootConfig?.oauthRedirectURI ?? "" + } +} + diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DynamicConfigView.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DynamicConfigView.swift new file mode 100644 index 0000000000..d311fc5337 --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DynamicConfigView.swift @@ -0,0 +1,94 @@ +/* + DynamicConfigView.swift + AuthFlowTester + + Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. + + Redistribution and use of this software in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions + and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior written + permission of salesforce.com, inc. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import SwiftUI +import SalesforceSDKCore + +struct DynamicConfigView: View { + @Binding var consumerKey: String + @Binding var callbackUrl: String + let isLoading: Bool + let onUseConfig: () -> Void + @State private var isExpanded: Bool = false + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Button(action: { + withAnimation { + isExpanded.toggle() + } + }) { + HStack { + Text("Dynamic Configuration") + .font(.headline) + .foregroundColor(.primary) + Spacer() + Image(systemName: isExpanded ? "chevron.up" : "chevron.down") + .foregroundColor(.secondary) + } + .padding(.horizontal) + } + + if isExpanded { + VStack(alignment: .leading, spacing: 8) { + Text("Consumer Key:") + .font(.caption) + .foregroundColor(.secondary) + TextField("Consumer Key", text: $consumerKey) + .font(.system(.caption, design: .monospaced)) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .autocapitalization(.none) + .disableAutocorrection(true) + + Text("Callback URL:") + .font(.caption) + .foregroundColor(.secondary) + TextField("Callback URL", text: $callbackUrl) + .font(.system(.caption, design: .monospaced)) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .autocapitalization(.none) + .disableAutocorrection(true) + } + .padding(.horizontal) + } + + Button(action: onUseConfig) { + Text("Use dynamic bootconfig") + .font(.headline) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 44) + .background(Color.green) + .cornerRadius(8) + } + .disabled(isLoading) + .padding(.horizontal) + } + .padding(.vertical) + } +} + diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/FlowTypesView.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/FlowTypesView.swift new file mode 100644 index 0000000000..6003dfbb0d --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/FlowTypesView.swift @@ -0,0 +1,69 @@ +/* + FlowTypesView.swift + AuthFlowTester + + Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. + + Redistribution and use of this software in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions + and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior written + permission of salesforce.com, inc. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import SwiftUI +import SalesforceSDKCore + +struct FlowTypesView: View { + @State private var useWebServerFlow: Bool + @State private var useHybridFlow: Bool + + init() { + _useWebServerFlow = State(initialValue: SalesforceManager.shared.useWebServerAuthentication) + _useHybridFlow = State(initialValue: SalesforceManager.shared.useHybridAuthentication) + } + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text("Authentication Flow Types") + .font(.headline) + .padding(.horizontal) + + VStack(alignment: .leading, spacing: 8) { + Toggle(isOn: $useWebServerFlow) { + Text("Use Web Server Flow") + .font(.body) + } + .onChange(of: useWebServerFlow) { newValue in + SalesforceManager.shared.useWebServerAuthentication = newValue + } + .padding(.horizontal) + + Toggle(isOn: $useHybridFlow) { + Text("Use Hybrid Flow") + .font(.body) + } + .onChange(of: useHybridFlow) { newValue in + SalesforceManager.shared.useHybridAuthentication = newValue + } + .padding(.horizontal) + } + } + .padding(.vertical) + } +} + diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/InfoRowView.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/InfoRowView.swift new file mode 100644 index 0000000000..31377fe19d --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/InfoRowView.swift @@ -0,0 +1,71 @@ +/* + InfoRowView.swift + AuthFlowTester + + Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. + + Redistribution and use of this software in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions + and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior written + permission of salesforce.com, inc. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import SwiftUI + +struct InfoRowView: View { + let label: String + let value: String + var isSensitive: Bool = false + + @State private var isRevealed = false + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(label) + .font(.caption) + .foregroundColor(.secondary) + + if isSensitive && !isRevealed { + HStack { + Text("••••••••") + .font(.system(.body, design: .monospaced)) + Spacer() + Button(action: { isRevealed.toggle() }) { + Image(systemName: "eye") + .foregroundColor(.blue) + } + } + } else { + HStack { + Text(value.isEmpty ? "(empty)" : value) + .font(.system(.body, design: .monospaced)) + .foregroundColor(value.isEmpty ? .secondary : .primary) + Spacer() + if isSensitive { + Button(action: { isRevealed.toggle() }) { + Image(systemName: "eye.slash") + .foregroundColor(.blue) + } + } + } + } + } + .padding(.vertical, 4) + } +} + diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/JwtAccessView.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/JwtAccessView.swift new file mode 100644 index 0000000000..1d8689b274 --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/JwtAccessView.swift @@ -0,0 +1,137 @@ +/* + JwtAccessView.swift + AuthFlowTester + + Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. + + Redistribution and use of this software in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions + and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior written + permission of salesforce.com, inc. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import SwiftUI +import SalesforceSDKCore + +struct JwtAccessView: View { + let jwtToken: JwtAccessToken + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Divider() + + Text("JWT Access Token Details") + .font(.headline) + .padding(.top, 8) + + // JWT Header + Text("Header:") + .font(.subheadline) + .fontWeight(.semibold) + .padding(.top, 4) + + JwtHeaderView(token: jwtToken) + + // JWT Payload + Text("Payload:") + .font(.subheadline) + .fontWeight(.semibold) + .padding(.top, 8) + + JwtPayloadView(token: jwtToken) + } + } +} + +// MARK: - JWT Header View + +struct JwtHeaderView: View { + let token: JwtAccessToken + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + let header = token.header + + JwtFieldRow(label: "Algorithm (alg):", value: header.algorithm) + JwtFieldRow(label: "Type (typ):", value: header.type) + JwtFieldRow(label: "Key ID (kid):", value: header.keyId) + JwtFieldRow(label: "Token Type (tty):", value: header.tokenType) + JwtFieldRow(label: "Tenant Key (tnk):", value: header.tenantKey) + JwtFieldRow(label: "Version (ver):", value: header.version) + } + .padding(8) + .background(Color(.systemGray6)) + .cornerRadius(4) + } +} + +// MARK: - JWT Payload View + +struct JwtPayloadView: View { + let token: JwtAccessToken + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + let payload = token.payload + + if let audience = payload.audience { + JwtFieldRow(label: "Audience (aud):", value: audience.joined(separator: ", ")) + } + + if let expirationDate = token.expirationDate() { + JwtFieldRow(label: "Expiration Date (exp):", value: formatDate(expirationDate)) + } + + JwtFieldRow(label: "Issuer (iss):", value: payload.issuer) + JwtFieldRow(label: "Subject (sub):", value: payload.subject) + JwtFieldRow(label: "Scopes (scp):", value: payload.scopes) + JwtFieldRow(label: "Client ID (client_id):", value: payload.clientId) + } + .padding(8) + .background(Color(.systemGray6)) + .cornerRadius(4) + } + + private func formatDate(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .medium + return formatter.string(from: date) + } +} + +// MARK: - Helper Views + +struct JwtFieldRow: View { + let label: String + let value: String? + + var body: some View { + if let value = value, !value.isEmpty { + VStack(alignment: .leading, spacing: 2) { + Text(label) + .font(.caption) + .foregroundColor(.secondary) + Text(value) + .font(.system(.caption, design: .monospaced)) + .foregroundColor(.primary) + } + } + } +} + diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/OAuthConfigurationView.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/OAuthConfigurationView.swift new file mode 100644 index 0000000000..213cd9843c --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/OAuthConfigurationView.swift @@ -0,0 +1,68 @@ +/* + OAuthConfigurationView.swift + AuthFlowTester + + Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. + + Redistribution and use of this software in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions + and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior written + permission of salesforce.com, inc. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import SwiftUI +import SalesforceSDKCore + +struct OAuthConfigurationView: View { + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text("OAuth Configuration") + .font(.title2) + .fontWeight(.bold) + + InfoRowView(label: configuredConsumerKeyLabel, + value: configuredConsumerKey) + + InfoRowView(label: "Configured Callback URL:", + value: configuredCallbackUrl) + } + .padding() + .background(Color(.secondarySystemBackground)) + .cornerRadius(8) + } + + // MARK: - Computed Properties + + private var configuredConsumerKeyLabel: String { + let label = "Configured Consumer Key:" + if let defaultKey = SalesforceManager.shared.bootConfig?.remoteAccessConsumerKey, + configuredConsumerKey == defaultKey { + return "\(label) (default)" + } + return label + } + + private var configuredConsumerKey: String { + return UserAccountManager.shared.oauthClientID ?? "" + } + + private var configuredCallbackUrl: String { + return UserAccountManager.shared.oauthCompletionURL ?? "" + } +} + diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/RestApiTestView.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/RestApiTestView.swift new file mode 100644 index 0000000000..33bfa8b25c --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/RestApiTestView.swift @@ -0,0 +1,166 @@ +/* + RestApiTestView.swift + AuthFlowTester + + Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. + + Redistribution and use of this software in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions + and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior written + permission of salesforce.com, inc. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import SwiftUI +import SalesforceSDKCore + +struct RestApiTestView: View { + @State private var isLoading = false + @State private var lastRequestResult: String = "" + @State private var isResultExpanded = false + + let onRequestCompleted: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text("REST API Test") + .font(.title2) + .fontWeight(.bold) + + Button(action: { + Task { + await makeRestRequest() + } + }) { + HStack { + if isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .scaleEffect(0.8) + } + Text(isLoading ? "Making Request..." : "Make REST API Request") + .font(.headline) + } + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 44) + .background(isLoading ? Color.gray : Color.blue) + .cornerRadius(8) + } + .disabled(isLoading) + + // Result section - always visible + VStack(alignment: .leading, spacing: 8) { + Button(action: { + if !lastRequestResult.isEmpty { + withAnimation { + isResultExpanded.toggle() + } + } + }) { + HStack { + Text("Last Request Result:") + .font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.primary) + Spacer() + if !lastRequestResult.isEmpty { + Image(systemName: isResultExpanded ? "chevron.up" : "chevron.down") + .font(.caption) + .foregroundColor(.secondary) + } + } + } + .disabled(lastRequestResult.isEmpty) + + if lastRequestResult.isEmpty { + Text("No request made yet") + .font(.system(.caption, design: .monospaced)) + .foregroundColor(.secondary) + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(.systemGray6)) + .cornerRadius(4) + } else if isResultExpanded { + ScrollView([.vertical, .horizontal], showsIndicators: true) { + Text(lastRequestResult) + .font(.system(.caption, design: .monospaced)) + .foregroundColor(lastRequestResult.hasPrefix("✓") ? .green : .red) + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + .textSelection(.enabled) + } + .frame(minHeight: 200, maxHeight: 400) + .background(Color(.systemGray6)) + .cornerRadius(4) + } + } + .padding(.vertical, 4) + } + .padding() + .background(Color(.secondarySystemBackground)) + .cornerRadius(8) + } + + // MARK: - REST API Request + + @MainActor + private func makeRestRequest() async { + isLoading = true + lastRequestResult = "" + + do { + let request = RestClient.shared.cheapRequest("v63.0") + let response = try await RestClient.shared.send(request: request) + + // Request succeeded - pretty print the JSON + let prettyJSON = prettyPrintJSON(response.asString()) + lastRequestResult = "✓ Success:\n\n\(prettyJSON)" + isResultExpanded = true // Auto-expand on new result + + // Notify parent to refresh fields + onRequestCompleted() + } catch { + // Request failed + lastRequestResult = "✗ Error: \(error.localizedDescription)" + isResultExpanded = true // Auto-expand on error + } + + isLoading = false + } + + private func prettyPrintJSON(_ jsonString: String) -> String { + guard let jsonData = jsonString.data(using: .utf8) else { + return jsonString + } + + do { + let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) + let prettyData = try JSONSerialization.data(withJSONObject: jsonObject, options: [.prettyPrinted, .sortedKeys]) + + if let prettyString = String(data: prettyData, encoding: .utf8) { + return prettyString + } + } catch { + // If parsing fails, return original string + return jsonString + } + + return jsonString + } +} + diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/UserCredentialsView.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/UserCredentialsView.swift new file mode 100644 index 0000000000..07700b8640 --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/UserCredentialsView.swift @@ -0,0 +1,100 @@ +/* + UserCredentialsView.swift + AuthFlowTester + + Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. + + Redistribution and use of this software in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions + and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior written + permission of salesforce.com, inc. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import SwiftUI +import SalesforceSDKCore + +struct UserCredentialsView: View { + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text("User Credentials") + .font(.title2) + .fontWeight(.bold) + + InfoRowView(label: "Client ID:", value: clientId) + InfoRowView(label: "Redirect URI:", value: redirectUri) + InfoRowView(label: "Instance URL:", value: instanceUrl) + InfoRowView(label: "Username:", value: username) + InfoRowView(label: "Scopes:", value: credentialsScopes) + InfoRowView(label: "Beacon Child Consumer Key:", value: beaconChildConsumerKey) + InfoRowView(label: "Refresh Token:", value: refreshToken, isSensitive: true) + InfoRowView(label: "Token Format:", value: tokenFormat) + InfoRowView(label: "Access Token:", value: accessToken, isSensitive: true) + + // JWT Access Token Details (if applicable) + if tokenFormat.lowercased() == "jwt", let jwtToken = try? JwtAccessToken(jwt: accessToken) { + JwtAccessView(jwtToken: jwtToken) + } + } + .padding() + .background(Color(.secondarySystemBackground)) + .cornerRadius(8) + } + + // MARK: - Computed Properties + + private var clientId: String { + return UserAccountManager.shared.currentUserAccount?.credentials.clientId ?? "" + } + + private var redirectUri: String { + return UserAccountManager.shared.currentUserAccount?.credentials.redirectUri ?? "" + } + + private var instanceUrl: String { + return UserAccountManager.shared.currentUserAccount?.credentials.instanceUrl?.absoluteString ?? "" + } + + private var username: String { + return UserAccountManager.shared.currentUserAccount?.idData.username ?? "" + } + + private var credentialsScopes: String { + guard let scopes = UserAccountManager.shared.currentUserAccount?.credentials.scopes else { + return "" + } + return scopes.joined(separator: " ") + } + + private var tokenFormat: String { + return UserAccountManager.shared.currentUserAccount?.credentials.tokenFormat ?? "" + } + + private var beaconChildConsumerKey: String { + return UserAccountManager.shared.currentUserAccount?.credentials.beaconChildConsumerKey ?? "" + } + + private var accessToken: String { + return UserAccountManager.shared.currentUserAccount?.credentials.accessToken ?? "" + } + + private var refreshToken: String { + return UserAccountManager.shared.currentUserAccount?.credentials.refreshToken ?? "" + } + +} + From d8971ab2671f81faa29cc67bace122c18d4f1292 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Wed, 22 Oct 2025 17:37:07 -0700 Subject: [PATCH 07/19] Removing duplicate in RestAPIExplorer project --- .../RestAPIExplorer.xcodeproj/project.pbxproj | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/native/SampleApps/RestAPIExplorer/RestAPIExplorer.xcodeproj/project.pbxproj b/native/SampleApps/RestAPIExplorer/RestAPIExplorer.xcodeproj/project.pbxproj index 1cfe13b779..0db5d29bdd 100644 --- a/native/SampleApps/RestAPIExplorer/RestAPIExplorer.xcodeproj/project.pbxproj +++ b/native/SampleApps/RestAPIExplorer/RestAPIExplorer.xcodeproj/project.pbxproj @@ -38,13 +38,6 @@ remoteGlobalIDString = B716A3E4218F6EEA009D407F; remoteInfo = SalesforceSDKCommonTestApp; }; - A3D2307B219749C3009F433D /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = B7D641C1218F429C006DF5F0 /* SalesforceSDKCommon.xcodeproj */; - proxyType = 1; - remoteGlobalIDString = B7136CFD216684A700F6A221; - remoteInfo = SalesforceSDKCommon; - }; B79F06E420EA931200BC7D6F /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = B79F06DC20EA931200BC7D6F /* MobileSync.xcodeproj */; @@ -391,7 +384,6 @@ B7D641DA218F42C0006DF5F0 /* PBXTargetDependency */, B79F072A20EA938300BC7D6F /* PBXTargetDependency */, B79F07CE20EA943400BC7D6F /* PBXTargetDependency */, - A3D2307C219749C3009F433D /* PBXTargetDependency */, ); name = RestAPIExplorer; productName = RestAPIExplorer; @@ -598,11 +590,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - A3D2307C219749C3009F433D /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = SalesforceSDKCommon; - targetProxy = A3D2307B219749C3009F433D /* PBXContainerItemProxy */; - }; B79F072A20EA938300BC7D6F /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = SalesforceAnalytics; From 414d43226be303647591a9e95feb4c336fb89668 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Wed, 22 Oct 2025 19:34:07 -0700 Subject: [PATCH 08/19] Added revoke access token button Made sections more readable / collapsable etc --- .../AuthFlowTester.xcodeproj/project.pbxproj | 40 +++--- .../SessionDetailViewController.swift | 28 +++- .../AuthFlowTester/Views/InfoRowView.swift | 18 ++- .../AuthFlowTester/Views/JwtAccessView.swift | 58 +++++--- .../Views/OAuthConfigurationView.swift | 35 +++-- .../Views/RestApiTestView.swift | 4 - .../AuthFlowTester/Views/RevokeView.swift | 135 ++++++++++++++++++ .../Views/UserCredentialsView.swift | 46 +++--- 8 files changed, 287 insertions(+), 77 deletions(-) create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/Views/RevokeView.swift diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj b/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj index 75365ef020..98b06f34e2 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj @@ -13,13 +13,16 @@ 4F95A8A02EA806E900C98D18 /* SalesforceSDKCommon.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4F95A8982EA801DC00C98D18 /* SalesforceSDKCommon.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4F95A8A12EA806EA00C98D18 /* SalesforceSDKCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F95A89A2EA801DC00C98D18 /* SalesforceSDKCore.framework */; }; 4F95A8A22EA806EA00C98D18 /* SalesforceSDKCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4F95A89A2EA801DC00C98D18 /* SalesforceSDKCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 4FEBAF292EA9B91500D4880A /* RevokeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FEBAF282EA9B91500D4880A /* RevokeView.swift */; }; AUTH001 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH002 /* AppDelegate.swift */; }; AUTH003 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH004 /* SceneDelegate.swift */; }; AUTH005 /* ConfigPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH006 /* ConfigPickerViewController.swift */; }; AUTH007 /* SessionDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH008 /* SessionDetailViewController.swift */; }; AUTH009 /* bootconfig.plist in Resources */ = {isa = PBXBuildFile; fileRef = AUTH010 /* bootconfig.plist */; }; - AUTH037 /* UserCredentialsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH038 /* UserCredentialsView.swift */; }; AUTH011 /* bootconfig2.plist in Resources */ = {isa = PBXBuildFile; fileRef = AUTH036 /* bootconfig2.plist */; }; + AUTH013 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = AUTH014 /* PrivacyInfo.xcprivacy */; }; + AUTH033 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AUTH034 /* Images.xcassets */; }; + AUTH037 /* UserCredentialsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH038 /* UserCredentialsView.swift */; }; AUTH040 /* RestApiTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH041 /* RestApiTestView.swift */; }; AUTH042 /* OAuthConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH043 /* OAuthConfigurationView.swift */; }; AUTH045 /* DefaultConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH046 /* DefaultConfigView.swift */; }; @@ -27,8 +30,6 @@ AUTH049 /* JwtAccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH050 /* JwtAccessView.swift */; }; AUTH051 /* InfoRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH052 /* InfoRowView.swift */; }; AUTH053 /* FlowTypesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH054 /* FlowTypesView.swift */; }; - AUTH013 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = AUTH014 /* PrivacyInfo.xcprivacy */; }; - AUTH033 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AUTH034 /* Images.xcassets */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -49,11 +50,18 @@ 4F95A8962EA801DC00C98D18 /* SalesforceAnalytics.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SalesforceAnalytics.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4F95A8982EA801DC00C98D18 /* SalesforceSDKCommon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SalesforceSDKCommon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4F95A89A2EA801DC00C98D18 /* SalesforceSDKCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SalesforceSDKCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4FEBAF282EA9B91500D4880A /* RevokeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevokeView.swift; sourceTree = ""; }; AUTH002 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; AUTH004 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; AUTH006 /* ConfigPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigPickerViewController.swift; sourceTree = ""; }; AUTH008 /* SessionDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDetailViewController.swift; sourceTree = ""; }; AUTH010 /* bootconfig.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = bootconfig.plist; sourceTree = ""; }; + AUTH012 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AUTH014 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + AUTH016 /* AuthFlowTester.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AuthFlowTester.entitlements; sourceTree = ""; }; + AUTH017 /* AuthFlowTester.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AuthFlowTester.app; sourceTree = BUILT_PRODUCTS_DIR; }; + AUTH034 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = ../../../../shared/resources/Images.xcassets; sourceTree = ""; }; + AUTH036 /* bootconfig2.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = bootconfig2.plist; sourceTree = ""; }; AUTH038 /* UserCredentialsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCredentialsView.swift; sourceTree = ""; }; AUTH041 /* RestApiTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestApiTestView.swift; sourceTree = ""; }; AUTH043 /* OAuthConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthConfigurationView.swift; sourceTree = ""; }; @@ -62,12 +70,6 @@ AUTH050 /* JwtAccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JwtAccessView.swift; sourceTree = ""; }; AUTH052 /* InfoRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoRowView.swift; sourceTree = ""; }; AUTH054 /* FlowTypesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowTypesView.swift; sourceTree = ""; }; - AUTH012 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - AUTH014 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; - AUTH016 /* AuthFlowTester.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AuthFlowTester.entitlements; sourceTree = ""; }; - AUTH017 /* AuthFlowTester.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AuthFlowTester.app; sourceTree = BUILT_PRODUCTS_DIR; }; - AUTH034 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = ../../../../shared/resources/Images.xcassets; sourceTree = ""; }; - AUTH036 /* bootconfig2.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = bootconfig2.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -142,15 +144,6 @@ path = Classes; sourceTree = ""; }; - AUTH044 /* ViewControllers */ = { - isa = PBXGroup; - children = ( - AUTH006 /* ConfigPickerViewController.swift */, - AUTH008 /* SessionDetailViewController.swift */, - ); - path = ViewControllers; - sourceTree = ""; - }; AUTH035 /* Resources */ = { isa = PBXGroup; children = ( @@ -162,6 +155,7 @@ AUTH039 /* Views */ = { isa = PBXGroup; children = ( + 4FEBAF282EA9B91500D4880A /* RevokeView.swift */, AUTH038 /* UserCredentialsView.swift */, AUTH041 /* RestApiTestView.swift */, AUTH043 /* OAuthConfigurationView.swift */, @@ -174,6 +168,15 @@ path = Views; sourceTree = ""; }; + AUTH044 /* ViewControllers */ = { + isa = PBXGroup; + children = ( + AUTH006 /* ConfigPickerViewController.swift */, + AUTH008 /* SessionDetailViewController.swift */, + ); + path = ViewControllers; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -251,6 +254,7 @@ AUTH042 /* OAuthConfigurationView.swift in Sources */, AUTH045 /* DefaultConfigView.swift in Sources */, AUTH047 /* DynamicConfigView.swift in Sources */, + 4FEBAF292EA9B91500D4880A /* RevokeView.swift in Sources */, AUTH049 /* JwtAccessView.swift in Sources */, AUTH051 /* InfoRowView.swift in Sources */, AUTH053 /* FlowTypesView.swift in Sources */, diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/SessionDetailViewController.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/SessionDetailViewController.swift index a187008995..ca50b06792 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/SessionDetailViewController.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/SessionDetailViewController.swift @@ -32,6 +32,9 @@ struct SessionDetailView: View { @State private var refreshTrigger = UUID() @State private var showNotImplementedAlert = false @State private var showLogoutConfigPicker = false + @State private var isUserCredentialsExpanded = false + @State private var isJwtDetailsExpanded = false + @State private var isOAuthConfigExpanded = false var onChangeConsumerKey: () -> Void var onSwitchUser: () -> Void @@ -40,18 +43,33 @@ struct SessionDetailView: View { var body: some View { ScrollView { VStack(alignment: .leading, spacing: 20) { - // REST API Request Section (moved to top) + // Revoke Access Token Section + RevokeView(onRevokeCompleted: { + refreshTrigger = UUID() + }) + + // REST API Request Section RestApiTestView(onRequestCompleted: { refreshTrigger = UUID() }) + // User Credentials Section + UserCredentialsView(isExpanded: $isUserCredentialsExpanded) + .id(refreshTrigger) + + // JWT Access Token Details Section (if applicable) + if let credentials = UserAccountManager.shared.currentUserAccount?.credentials, + credentials.tokenFormat?.lowercased() == "jwt", + let accessToken = credentials.accessToken, + let jwtToken = try? JwtAccessToken(jwt: accessToken) { + JwtAccessView(jwtToken: jwtToken, isExpanded: $isJwtDetailsExpanded) + .id(refreshTrigger) + } + // OAuth Configuration Section - OAuthConfigurationView() + OAuthConfigurationView(isExpanded: $isOAuthConfigExpanded) .id(refreshTrigger) - // User Credentials Section - UserCredentialsView() - .id(refreshTrigger) } .padding() } diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/InfoRowView.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/InfoRowView.swift index 31377fe19d..6c3057ce62 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/InfoRowView.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/InfoRowView.swift @@ -42,8 +42,8 @@ struct InfoRowView: View { if isSensitive && !isRevealed { HStack { - Text("••••••••") - .font(.system(.body, design: .monospaced)) + Text(maskedValue) + .font(.system(.caption, design: .monospaced)) Spacer() Button(action: { isRevealed.toggle() }) { Image(systemName: "eye") @@ -53,7 +53,7 @@ struct InfoRowView: View { } else { HStack { Text(value.isEmpty ? "(empty)" : value) - .font(.system(.body, design: .monospaced)) + .font(.system(.caption, design: .monospaced)) .foregroundColor(value.isEmpty ? .secondary : .primary) Spacer() if isSensitive { @@ -67,5 +67,17 @@ struct InfoRowView: View { } .padding(.vertical, 4) } + + // MARK: - Computed Properties + + private var maskedValue: String { + guard value.count >= 10 else { + return "••••••••" + } + + let firstFive = value.prefix(5) + let lastFive = value.suffix(5) + return "\(firstFive)...\(lastFive)" + } } diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/JwtAccessView.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/JwtAccessView.swift index 1d8689b274..4e05932fc4 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/JwtAccessView.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/JwtAccessView.swift @@ -30,31 +30,47 @@ import SalesforceSDKCore struct JwtAccessView: View { let jwtToken: JwtAccessToken + @Binding var isExpanded: Bool var body: some View { - VStack(alignment: .leading, spacing: 12) { - Divider() - - Text("JWT Access Token Details") - .font(.headline) - .padding(.top, 8) - - // JWT Header - Text("Header:") - .font(.subheadline) - .fontWeight(.semibold) - .padding(.top, 4) - - JwtHeaderView(token: jwtToken) - - // JWT Payload - Text("Payload:") - .font(.subheadline) - .fontWeight(.semibold) - .padding(.top, 8) + VStack(alignment: .leading, spacing: 8) { + Button(action: { + withAnimation { + isExpanded.toggle() + } + }) { + HStack { + Text("JWT Access Token Details") + .font(.headline) + .foregroundColor(.primary) + Spacer() + Image(systemName: isExpanded ? "chevron.up" : "chevron.down") + .font(.caption) + .foregroundColor(.secondary) + } + } - JwtPayloadView(token: jwtToken) + if isExpanded { + // JWT Header + Text("Header:") + .font(.subheadline) + .fontWeight(.semibold) + .padding(.top, 4) + + JwtHeaderView(token: jwtToken) + + // JWT Payload + Text("Payload:") + .font(.subheadline) + .fontWeight(.semibold) + .padding(.top, 8) + + JwtPayloadView(token: jwtToken) + } } + .padding() + .background(Color(.secondarySystemBackground)) + .cornerRadius(8) } } diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/OAuthConfigurationView.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/OAuthConfigurationView.swift index 213cd9843c..a951f87955 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/OAuthConfigurationView.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/OAuthConfigurationView.swift @@ -29,17 +29,34 @@ import SwiftUI import SalesforceSDKCore struct OAuthConfigurationView: View { + @Binding var isExpanded: Bool + var body: some View { - VStack(alignment: .leading, spacing: 12) { - Text("OAuth Configuration") - .font(.title2) - .fontWeight(.bold) - - InfoRowView(label: configuredConsumerKeyLabel, - value: configuredConsumerKey) + VStack(alignment: .leading, spacing: 8) { + Button(action: { + withAnimation { + isExpanded.toggle() + } + }) { + HStack { + Text("OAuth Configuration") + .font(.headline) + .fontWeight(.semibold) + .foregroundColor(.primary) + Spacer() + Image(systemName: isExpanded ? "chevron.up" : "chevron.down") + .font(.caption) + .foregroundColor(.secondary) + } + } - InfoRowView(label: "Configured Callback URL:", - value: configuredCallbackUrl) + if isExpanded { + InfoRowView(label: configuredConsumerKeyLabel, + value: configuredConsumerKey, isSensitive: true) + + InfoRowView(label: "Configured Callback URL:", + value: configuredCallbackUrl) + } } .padding() .background(Color(.secondarySystemBackground)) diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/RestApiTestView.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/RestApiTestView.swift index 33bfa8b25c..d550c669df 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/RestApiTestView.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/RestApiTestView.swift @@ -37,10 +37,6 @@ struct RestApiTestView: View { var body: some View { VStack(alignment: .leading, spacing: 12) { - Text("REST API Test") - .font(.title2) - .fontWeight(.bold) - Button(action: { Task { await makeRestRequest() diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/RevokeView.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/RevokeView.swift new file mode 100644 index 0000000000..44ddb461cc --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/RevokeView.swift @@ -0,0 +1,135 @@ +/* + RevokeView.swift + AuthFlowTester + + Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. + + Redistribution and use of this software in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions + and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior written + permission of salesforce.com, inc. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import SwiftUI +import SalesforceSDKCore + +struct RevokeView: View { + enum AlertType { + case success + case error(String) + } + + @State private var isRevoking = false + @State private var alertType: AlertType? + + let onRevokeCompleted: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Button(action: { + Task { + await revokeAccessToken() + } + }) { + HStack { + if isRevoking { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .scaleEffect(0.8) + } + Text(isRevoking ? "Revoking..." : "Revoke Access Token") + .font(.headline) + } + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 44) + .background(isRevoking ? Color.gray : Color.red) + .cornerRadius(8) + } + .disabled(isRevoking) + } + .padding() + .background(Color(.secondarySystemBackground)) + .cornerRadius(8) + .alert(item: Binding( + get: { alertType.map { AlertItem(type: $0) } }, + set: { alertType = $0?.type } + )) { alertItem in + switch alertItem.type { + case .success: + return Alert( + title: Text("Access Token Revoked"), + message: Text("The access token has been successfully revoked. You may need to make a REST API request to trigger a token refresh."), + dismissButton: .default(Text("OK")) + ) + case .error(let message): + return Alert( + title: Text("Revoke Failed"), + message: Text(message), + dismissButton: .default(Text("OK")) + ) + } + } + } + + struct AlertItem: Identifiable { + let id = UUID() + let type: AlertType + } + + // MARK: - Revoke Token + + @MainActor + private func revokeAccessToken() async { + guard let credentials = UserAccountManager.shared.currentUserAccount?.credentials else { + alertType = .error("No credentials found") + return + } + + guard let accessToken = credentials.accessToken, + let encodedToken = accessToken.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { + alertType = .error("Invalid access token") + return + } + + isRevoking = true + + do { + // Create POST request to revoke endpoint + let request = RestRequest(method: .POST, path: "/services/oauth2/revoke", queryParams: nil) + request.endpoint = "" + + // Set the request body with URL-encoded token + let bodyString = "token=\(encodedToken)" + request.setCustomRequestBodyString(bodyString, contentType: "application/x-www-form-urlencoded") + + // Send the request + _ = try await RestClient.shared.send(request: request) + + alertType = .success + + // Notify parent to refresh fields + onRevokeCompleted() + } catch { + alertType = .error(error.localizedDescription) + } + + isRevoking = false + } +} + diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/UserCredentialsView.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/UserCredentialsView.swift index 07700b8640..2f5937c4b3 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/UserCredentialsView.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/UserCredentialsView.swift @@ -29,25 +29,37 @@ import SwiftUI import SalesforceSDKCore struct UserCredentialsView: View { + @Binding var isExpanded: Bool + var body: some View { - VStack(alignment: .leading, spacing: 12) { - Text("User Credentials") - .font(.title2) - .fontWeight(.bold) - - InfoRowView(label: "Client ID:", value: clientId) - InfoRowView(label: "Redirect URI:", value: redirectUri) - InfoRowView(label: "Instance URL:", value: instanceUrl) - InfoRowView(label: "Username:", value: username) - InfoRowView(label: "Scopes:", value: credentialsScopes) - InfoRowView(label: "Beacon Child Consumer Key:", value: beaconChildConsumerKey) - InfoRowView(label: "Refresh Token:", value: refreshToken, isSensitive: true) - InfoRowView(label: "Token Format:", value: tokenFormat) - InfoRowView(label: "Access Token:", value: accessToken, isSensitive: true) + VStack(alignment: .leading, spacing: 8) { + Button(action: { + withAnimation { + isExpanded.toggle() + } + }) { + HStack { + Text("User Credentials") + .font(.headline) + .fontWeight(.semibold) + .foregroundColor(.primary) + Spacer() + Image(systemName: isExpanded ? "chevron.up" : "chevron.down") + .font(.caption) + .foregroundColor(.secondary) + } + } - // JWT Access Token Details (if applicable) - if tokenFormat.lowercased() == "jwt", let jwtToken = try? JwtAccessToken(jwt: accessToken) { - JwtAccessView(jwtToken: jwtToken) + if isExpanded { + InfoRowView(label: "Username:", value: username) + InfoRowView(label: "Access Token:", value: accessToken, isSensitive: true) + InfoRowView(label: "Token Format:", value: tokenFormat) + InfoRowView(label: "Refresh Token:", value: refreshToken, isSensitive: true) + InfoRowView(label: "Client ID:", value: clientId, isSensitive: true) + InfoRowView(label: "Redirect URI:", value: redirectUri) + InfoRowView(label: "Instance URL:", value: instanceUrl) + InfoRowView(label: "Scopes:", value: credentialsScopes) + InfoRowView(label: "Beacon Child Consumer Key:", value: beaconChildConsumerKey) } } .padding() From 5fae3850072eff75839eda7adb2d808c17754351 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Fri, 24 Oct 2025 17:05:40 -0700 Subject: [PATCH 09/19] Instead of passing a consumer key, apps pass in a block that will be given the login host and will return the desired app config. --- .../Classes/Common/SalesforceSDKManager.h | 25 ++++++++-------- .../Classes/Common/SalesforceSDKManager.m | 30 +++++++------------ .../SFUserAccountManager+Internal.h | 3 +- .../UserAccount/SFUserAccountManager.m | 16 ++++++---- .../Classes/Util/SalesforceSDKCoreDefines.h | 7 +++++ 5 files changed, 41 insertions(+), 40 deletions(-) diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.h b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.h index 04457a1534..95a8e8635c 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.h +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.h @@ -203,6 +203,11 @@ NS_SWIFT_NAME(SalesforceManager) */ @property (nonatomic, copy) SFSDKUserAgentCreationBlock userAgentString NS_SWIFT_NAME(userAgentGenerator); +/** + Block to dynamically select the app config at runtime based on login host. + */ + @property (nonatomic, copy, nullable) SFSDKAppConfigRuntimeSelectorBlock appConfigRuntimeSelectorBlock NS_SWIFT_NAME(bootConfigRuntimeSelector); + /** Use this flag to indicate if the APP will be an identity provider. When enabled this flag allows this application to perform authentication on behalf of another app. */ @property (nonatomic,assign) BOOL isIdentityProvider NS_SWIFT_NAME(isIdentityProvider); @@ -306,6 +311,13 @@ NS_SWIFT_NAME(SalesforceManager) */ - (id )biometricAuthenticationManager; +/** + * Returns app config (aka boot config) at runtime based on login host + * + * @param loginHost The selected login host + */ +- (SFSDKAppConfig*)runtimeSelectedAppConfig:(nullable NSString *)loginHost NS_SWIFT_NAME(runtimeSelectedBootConfig( loginHost:)); + /** * Creates the NativeLoginManager instance. * @@ -334,19 +346,6 @@ NS_SWIFT_NAME(SalesforceManager) nativeLoginViewController:(nonnull UIViewController *)nativeLoginViewController scene:(nullable UIScene *)scene; -/** - * Call this method before initiating a login if the application needs to revert back to the consumer key/callback url statically configured in bootconfig.plist - */ -- (void) revertToBootConfig; - -/** - * Call this method before initiating a login if the application needs to use a consumer key/callback url different from the one statically configured in bootconfig.plist - * @param consumerKey The Connected App consumer key. - * @param callbackUrl The Connected App redirect URI. - */ -- (void) overrideBootConfigWithConsumerKey:(nonnull NSString *)consumerKey - callbackUrl:(nonnull NSString *)callbackUrl NS_SWIFT_NAME(overrideBootConfig(consumerKey:callbackUrl:)); - /** * Returns The NativeLoginManager instance. * diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.m b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.m index 417b90bad2..cbbb696265 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.m +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.m @@ -914,6 +914,16 @@ - (void)biometricAuthenticationFlowDidComplete:(NSNotification *)notification { return [SFScreenLockManagerInternal shared]; } +#pragma mark - Dynamic Boot Config + +- (SFSDKAppConfig*) runtimeSelectedAppConfig:(nullable NSString *)loginHost { + if (self.appConfigRuntimeSelectorBlock) { + return self.appConfigRuntimeSelectorBlock(loginHost); + } else { + return nil; + } +} + #pragma mark - Native Login - (id )useNativeLoginWithConsumerKey:(nonnull NSString *)consumerKey @@ -964,27 +974,7 @@ - (void)biometricAuthenticationFlowDidComplete:(NSNotification *)notification { return nativeLogin; } - -#pragma - Dynamic boot config - -- (void) revertToBootConfig { - _appConfig = nil; // next access will read from default bootconfig - [self setupServiceConfiguration]; -} - -- (void) overrideBootConfigWithConsumerKey:(nonnull NSString *)consumerKey - callbackUrl:(nonnull NSString *)callbackUrl { - - NSDictionary *dict = @{ - @"remoteAccessConsumerKey": consumerKey, - @"oauthRedirectURI": callbackUrl - }; - SFSDKAppConfig *config = [[SFSDKAppConfig alloc] initWithDict:dict]; - _appConfig = config; - [self setupServiceConfiguration]; -} - @end NSString *SFAppTypeGetDescription(SFAppType appType){ diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager+Internal.h b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager+Internal.h index 068e5a639e..2c2164ef3d 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager+Internal.h +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager+Internal.h @@ -201,7 +201,8 @@ Set this block to handle presentation of the Authentication View Controller. - (SFSDKAuthRequest *)defaultAuthRequest; -- (SFSDKAuthRequest *)defaultAuthRequestWithLoginHost:(nullable NSString *)loginHost; +- (SFSDKAuthRequest *)authRequestWithLoginHost:(nullable NSString *)loginHost appConfig:(nullable SFSDKAppConfig*)appConfig; + - (BOOL)loginWithCompletion:(nullable SFUserAccountManagerSuccessCallbackBlock)completionBlock failure:(nullable SFUserAccountManagerFailureCallbackBlock)failureBlock diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.m b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.m index beb8aaceaa..110d59b16b 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.m +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.m @@ -534,7 +534,9 @@ - (BOOL)authenticateWithCompletion:(SFUserAccountManagerSuccessCallbackBlock)com if (self.nativeLoginEnabled && !self.shouldFallbackToWebAuthentication) { request = [self nativeLoginAuthRequest]; } else { - request = [self defaultAuthRequestWithLoginHost:loginHost]; + // NB: Will be nil if application did not provide a appConfigRuntimeSelectorBlock + SFSDKAppConfig* appConfig = [[SalesforceSDKManager sharedManager] runtimeSelectedAppConfig:loginHost]; + request = [self authRequestWithLoginHost:loginHost appConfig:appConfig]; } if (scene) { @@ -552,15 +554,17 @@ - (BOOL)authenticateWithCompletion:(SFUserAccountManagerSuccessCallbackBlock)com codeVerifier:codeVerifier]; } --(SFSDKAuthRequest *)defaultAuthRequestWithLoginHost:(nullable NSString *)loginHost { +-(SFSDKAuthRequest *)authRequestWithLoginHost:(nullable NSString *)loginHost + appConfig:(nullable SFSDKAppConfig *)appConfig +{ SFSDKAuthRequest *request = [[SFSDKAuthRequest alloc] init]; request.loginHost = loginHost != nil ? loginHost : self.loginHost; request.additionalOAuthParameterKeys = self.additionalOAuthParameterKeys; request.loginViewControllerConfig = self.loginViewControllerConfig; request.brandLoginPath = self.brandLoginPath; - request.oauthClientId = self.oauthClientId; - request.oauthCompletionUrl = self.oauthCompletionUrl; - request.scopes = self.scopes; + request.oauthClientId = appConfig != nil ? appConfig.remoteAccessConsumerKey : self.oauthClientId; + request.oauthCompletionUrl = appConfig != nil ? appConfig.oauthRedirectURI : self.oauthCompletionUrl; + request.scopes = appConfig != nil ? appConfig.oauthScopes : self.scopes; request.retryLoginAfterFailure = self.retryLoginAfterFailure; request.useBrowserAuth = self.useBrowserAuth; request.spAppLoginFlowSelectionAction = self.idpLoginFlowSelectionAction; @@ -570,7 +574,7 @@ -(SFSDKAuthRequest *)defaultAuthRequestWithLoginHost:(nullable NSString *)loginH } -(SFSDKAuthRequest *)defaultAuthRequest { - return [self defaultAuthRequestWithLoginHost:nil]; + return [self authRequestWithLoginHost:nil appConfig:nil]; } -(SFSDKAuthRequest *)nativeLoginAuthRequest { diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Util/SalesforceSDKCoreDefines.h b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Util/SalesforceSDKCoreDefines.h index b18450db03..b5162e3085 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Util/SalesforceSDKCoreDefines.h +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Util/SalesforceSDKCoreDefines.h @@ -24,7 +24,9 @@ #import @class SFUserAccount; +@class SFSDKAppConfig; @class UIViewController; +@class SFSDKAppConfig; @protocol SFSDKLoginFlowSelectionView; @protocol SFSDKUserSelectionView; @@ -53,4 +55,9 @@ typedef UIViewController*_Nonnull (^SFIDPLoginFlowS */ typedef UIViewController*_Nonnull (^SFIDPUserSelectionBlock)(void) NS_SWIFT_NAME(IDPUserSelectionBlock); +/** + Block to select an app config at runtime based on the login host. + */ + typedef SFSDKAppConfig* _Nullable (^SFSDKAppConfigRuntimeSelectorBlock)(NSString * _Nonnull loginHost) NS_SWIFT_NAME(BootConfigRuntimeSelector); + NS_ASSUME_NONNULL_END From 3fa16e5db101783dc971bf306de3ff3145b4d392 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Fri, 24 Oct 2025 17:11:22 -0700 Subject: [PATCH 10/19] Using SalesforceManager.shared.bootConfigRuntimeSelector Also showing scopes in config screen and allowing user to enter scopes for dynamic config --- .../ConfigPickerViewController.swift | 35 ++++++++++++++----- .../Views/DefaultConfigView.swift | 17 +++++++++ .../Views/DynamicConfigView.swift | 12 ++++++- .../Views/OAuthConfigurationView.swift | 8 +++++ 4 files changed, 63 insertions(+), 9 deletions(-) diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/ConfigPickerViewController.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/ConfigPickerViewController.swift index dd1a9e4610..f85eceac24 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/ConfigPickerViewController.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/ConfigPickerViewController.swift @@ -32,6 +32,7 @@ struct ConfigPickerView: View { @State private var isLoading = false @State private var dynamicConsumerKey = "" @State private var dynamicCallbackUrl = "" + @State private var dynamicScopes = "" let onConfigurationCompleted: () -> Void @@ -57,6 +58,7 @@ struct ConfigPickerView: View { DynamicConfigView( consumerKey: $dynamicConsumerKey, callbackUrl: $dynamicCallbackUrl, + scopes: $dynamicScopes, isLoading: isLoading, onUseConfig: handleDynamicBootconfig ) @@ -94,8 +96,9 @@ struct ConfigPickerView: View { private func loadDynamicConfigDefaults() { // Load initial values from bootconfig2.plist if let config = BootConfig("/bootconfig2.plist") { - dynamicConsumerKey = config.remoteAccessConsumerKey ?? "" - dynamicCallbackUrl = config.oauthRedirectURI ?? "" + dynamicConsumerKey = config.remoteAccessConsumerKey + dynamicCallbackUrl = config.oauthRedirectURI + dynamicScopes = config.oauthScopes.sorted().joined(separator: " ") } } @@ -104,7 +107,7 @@ struct ConfigPickerView: View { private func handleDefaultConfig() { isLoading = true - SalesforceManager.shared.revertToBootConfig() + SalesforceManager.shared.bootConfigRuntimeSelector = nil // Use default bootconfig - no additional setup needed onConfigurationCompleted() @@ -113,11 +116,27 @@ struct ConfigPickerView: View { private func handleDynamicBootconfig() { isLoading = true - // Use the values from the text fields - SalesforceManager.shared.overrideBootConfig( - consumerKey: dynamicConsumerKey, - callbackUrl: dynamicCallbackUrl - ) + SalesforceManager.shared.bootConfigRuntimeSelector = { _ in + // Create dynamic BootConfig from user-entered values + // Parse scopes from space-separated string + let scopesArray = self.dynamicScopes + .split(separator: " ") + .map { String($0) } + .filter { !$0.isEmpty } + + var configDict: [String: Any] = [ + "remoteAccessConsumerKey": self.dynamicConsumerKey, + "oauthRedirectURI": self.dynamicCallbackUrl, + "shouldAuthenticate": true + ] + + // Only add scopes if not empty + if !scopesArray.isEmpty { + configDict["oauthScopes"] = scopesArray + } + + return BootConfig(configDict) + } // Proceed with login onConfigurationCompleted() diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DefaultConfigView.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DefaultConfigView.swift index 75c1fc76ac..eaa2b6ba48 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DefaultConfigView.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DefaultConfigView.swift @@ -72,6 +72,16 @@ struct DefaultConfigView: View { .frame(maxWidth: .infinity, alignment: .leading) .background(Color(.systemGray6)) .cornerRadius(4) + + Text("Scopes:") + .font(.caption) + .foregroundColor(.secondary) + Text(defaultScopes) + .font(.system(.caption, design: .monospaced)) + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(.systemGray6)) + .cornerRadius(4) } .padding(.horizontal) } @@ -100,5 +110,12 @@ struct DefaultConfigView: View { private var defaultCallbackUrl: String { return SalesforceManager.shared.bootConfig?.oauthRedirectURI ?? "" } + + private var defaultScopes: String { + guard let scopes = SalesforceManager.shared.bootConfig?.oauthScopes else { + return "(none)" + } + return scopes.isEmpty ? "(none)" : scopes.sorted().joined(separator: ", ") + } } diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DynamicConfigView.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DynamicConfigView.swift index d311fc5337..b53da714c7 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DynamicConfigView.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DynamicConfigView.swift @@ -31,6 +31,7 @@ import SalesforceSDKCore struct DynamicConfigView: View { @Binding var consumerKey: String @Binding var callbackUrl: String + @Binding var scopes: String let isLoading: Bool let onUseConfig: () -> Void @State private var isExpanded: Bool = false @@ -72,12 +73,21 @@ struct DynamicConfigView: View { .textFieldStyle(RoundedBorderTextFieldStyle()) .autocapitalization(.none) .disableAutocorrection(true) + + Text("Scopes (space-separated):") + .font(.caption) + .foregroundColor(.secondary) + TextField("e.g. id api refresh_token", text: $scopes) + .font(.system(.caption, design: .monospaced)) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .autocapitalization(.none) + .disableAutocorrection(true) } .padding(.horizontal) } Button(action: onUseConfig) { - Text("Use dynamic bootconfig") + Text("Use dynamic config") .font(.headline) .foregroundColor(.white) .frame(maxWidth: .infinity) diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/OAuthConfigurationView.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/OAuthConfigurationView.swift index a951f87955..cb112b6b95 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/OAuthConfigurationView.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/OAuthConfigurationView.swift @@ -56,6 +56,9 @@ struct OAuthConfigurationView: View { InfoRowView(label: "Configured Callback URL:", value: configuredCallbackUrl) + + InfoRowView(label: "Configured Scopes:", + value: configuredScopes) } } .padding() @@ -81,5 +84,10 @@ struct OAuthConfigurationView: View { private var configuredCallbackUrl: String { return UserAccountManager.shared.oauthCompletionURL ?? "" } + + private var configuredScopes: String { + let scopes = UserAccountManager.shared.scopes + return scopes.isEmpty ? "(none)" : scopes.sorted().joined(separator: ", ") + } } From 25659800f1d8aa462f6182f789e739366d4892a0 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Fri, 24 Oct 2025 17:32:00 -0700 Subject: [PATCH 11/19] New unit tests --- .../SFUserAccountManagerTests.m | 92 +++++++++++++++++++ .../SalesforceSDKManagerTests.m | 72 +++++++++++++++ 2 files changed, 164 insertions(+) diff --git a/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SFUserAccountManagerTests.m b/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SFUserAccountManagerTests.m index 057823ade9..c6344ffb80 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SFUserAccountManagerTests.m +++ b/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SFUserAccountManagerTests.m @@ -664,4 +664,96 @@ - (void)testUserAccountEncoding { XCTAssertEqual(userIn.accessRestrictions, userOut.accessRestrictions, @"accessRestrictions mismatch"); } +#pragma mark - authRequestWithLoginHost Tests + +- (void)testAuthRequestWithLoginHostDefaults { + // Setup default values + NSString *expectedLoginHost = @"https://login.salesforce.com"; + NSString *expectedClientId = @"testClientId"; + NSString *expectedRedirectUri = @"testapp://oauth/done"; + NSSet *expectedScopes = [NSSet setWithObjects:@"api", @"web", @"refresh_token", nil]; + + self.uam.loginHost = expectedLoginHost; + self.uam.oauthClientId = expectedClientId; + self.uam.oauthCompletionUrl = expectedRedirectUri; + self.uam.scopes = expectedScopes; + + // Call with nil loginHost and nil appConfig (should use defaults) + SFSDKAuthRequest *request = [self.uam authRequestWithLoginHost:nil appConfig:nil]; + + XCTAssertNotNil(request, @"Auth request should not be nil"); + XCTAssertEqualObjects(request.loginHost, expectedLoginHost, @"Login host should match default"); + XCTAssertEqualObjects(request.oauthClientId, expectedClientId, @"Client ID should match default"); + XCTAssertEqualObjects(request.oauthCompletionUrl, expectedRedirectUri, @"Redirect URI should match default"); + XCTAssertEqualObjects(request.scopes, expectedScopes, @"Scopes should match default"); +} + +- (void)testAuthRequestWithLoginHostCustomHost { + // Setup default values + NSString *defaultLoginHost = @"https://login.salesforce.com"; + NSString *customLoginHost = @"https://test.salesforce.com"; + NSString *expectedClientId = @"testClientId"; + + self.uam.loginHost = defaultLoginHost; + self.uam.oauthClientId = expectedClientId; + + // Call with custom loginHost + SFSDKAuthRequest *request = [self.uam authRequestWithLoginHost:customLoginHost appConfig:nil]; + + XCTAssertNotNil(request, @"Auth request should not be nil"); + XCTAssertEqualObjects(request.loginHost, customLoginHost, @"Login host should be custom value, not default"); + XCTAssertEqualObjects(request.oauthClientId, expectedClientId, @"Client ID should match default"); +} + +- (void)testAuthRequestWithLoginHostWithAppConfig { + // Setup default values + NSString *defaultLoginHost = @"https://login.salesforce.com"; + NSString *defaultClientId = @"defaultClientId"; + NSString *defaultRedirectUri = @"defaultapp://oauth/done"; + NSSet *defaultScopes = [NSSet setWithObjects:@"api", @"web", nil]; + + self.uam.loginHost = defaultLoginHost; + self.uam.oauthClientId = defaultClientId; + self.uam.oauthCompletionUrl = defaultRedirectUri; + self.uam.scopes = defaultScopes; + + // Create app config with different values + NSString *appConfigClientId = @"appConfigClientId"; + NSString *appConfigRedirectUri = @"appconfig://oauth/done"; + NSSet *appConfigScopes = [NSSet setWithObjects:@"id", @"api", @"refresh_token", nil]; + + NSDictionary *configDict = @{ + @"remoteAccessConsumerKey": appConfigClientId, + @"oauthRedirectURI": appConfigRedirectUri, + @"oauthScopes": [appConfigScopes allObjects], + @"shouldAuthenticate": @YES + }; + SFSDKAppConfig *appConfig = [[SFSDKAppConfig alloc] initWithDict:configDict]; + + // Call with appConfig + SFSDKAuthRequest *request = [self.uam authRequestWithLoginHost:nil appConfig:appConfig]; + + XCTAssertNotNil(request, @"Auth request should not be nil"); + XCTAssertEqualObjects(request.loginHost, defaultLoginHost, @"Login host should match default"); + XCTAssertEqualObjects(request.oauthClientId, appConfigClientId, @"Client ID should come from appConfig"); + XCTAssertEqualObjects(request.oauthCompletionUrl, appConfigRedirectUri, @"Redirect URI should come from appConfig"); + XCTAssertEqualObjects(request.scopes, appConfigScopes, @"Scopes should come from appConfig"); +} + +- (void)testDefaultAuthRequestUsesAuthRequestWithLoginHost { + // Setup default values + NSString *expectedLoginHost = @"https://login.salesforce.com"; + NSString *expectedClientId = @"testClientId"; + + self.uam.loginHost = expectedLoginHost; + self.uam.oauthClientId = expectedClientId; + + // defaultAuthRequest should call authRequestWithLoginHost:nil appConfig:nil + SFSDKAuthRequest *request = [self.uam defaultAuthRequest]; + + XCTAssertNotNil(request, @"Default auth request should not be nil"); + XCTAssertEqualObjects(request.loginHost, expectedLoginHost, @"Login host should match default"); + XCTAssertEqualObjects(request.oauthClientId, expectedClientId, @"Client ID should match default"); +} + @end diff --git a/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SalesforceSDKManagerTests.m b/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SalesforceSDKManagerTests.m index 6cac184e9e..dcb50bfeca 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SalesforceSDKManagerTests.m +++ b/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SalesforceSDKManagerTests.m @@ -485,4 +485,76 @@ - (void)compareAppNames:(NSString *)expectedAppName XCTAssertTrue([userAgent containsString:expectedAppName], @"App names should match"); } +#pragma mark - Runtime Selected App Config Tests + +- (void)testRuntimeSelectedAppConfigReturnsNilWhenBlockNotSet { + // Clear any existing block + [SalesforceSDKManager sharedManager].appConfigRuntimeSelectorBlock = nil; + + // Call with nil loginHost + SFSDKAppConfig *config = [[SalesforceSDKManager sharedManager] runtimeSelectedAppConfig:nil]; + XCTAssertNil(config, @"Should return nil when no selector block is set"); + + // Call with a loginHost + config = [[SalesforceSDKManager sharedManager] runtimeSelectedAppConfig:@"https://test.salesforce.com"]; + XCTAssertNil(config, @"Should return nil when no selector block is set, regardless of loginHost"); +} + +- (void)testRuntimeSelectedAppConfigWithDifferentLoginHosts { + NSString *loginHost1 = @"https://login.salesforce.com"; + NSString *loginHost2 = @"https://test.salesforce.com"; + + NSDictionary *config1Dict = @{ + @"remoteAccessConsumerKey": @"clientId1", + @"oauthRedirectURI": @"app1://oauth/done", + @"shouldAuthenticate": @YES + }; + SFSDKAppConfig *config1 = [[SFSDKAppConfig alloc] initWithDict:config1Dict]; + + NSDictionary *config2Dict = @{ + @"remoteAccessConsumerKey": @"clientId2", + @"oauthRedirectURI": @"app2://oauth/done", + @"shouldAuthenticate": @YES + }; + SFSDKAppConfig *config2 = [[SFSDKAppConfig alloc] initWithDict:config2Dict]; + + // Set the selector block to return different configs based on loginHost + [SalesforceSDKManager sharedManager].appConfigRuntimeSelectorBlock = ^SFSDKAppConfig *(NSString *loginHost) { + if ([loginHost isEqualToString:loginHost1]) { + return config1; + } else if ([loginHost isEqualToString:loginHost2]) { + return config2; + } + return nil; + }; + + // Test first loginHost + SFSDKAppConfig *result1 = [[SalesforceSDKManager sharedManager] runtimeSelectedAppConfig:loginHost1]; + XCTAssertNotNil(result1, @"Should return config for loginHost1"); + XCTAssertEqual(result1, config1, @"Should return config1 for loginHost1"); + XCTAssertEqualObjects(result1.remoteAccessConsumerKey, @"clientId1", @"Should have correct client ID for config1"); + + // Test second loginHost + SFSDKAppConfig *result2 = [[SalesforceSDKManager sharedManager] runtimeSelectedAppConfig:loginHost2]; + XCTAssertNotNil(result2, @"Should return config for loginHost2"); + XCTAssertEqual(result2, config2, @"Should return config2 for loginHost2"); + XCTAssertEqualObjects(result2.remoteAccessConsumerKey, @"clientId2", @"Should have correct client ID for config2"); +} + +- (void)testRuntimeSelectedAppConfigBlockReturnsNil { + __block BOOL blockWasCalled = NO; + + // Set the selector block to return nil + [SalesforceSDKManager sharedManager].appConfigRuntimeSelectorBlock = ^SFSDKAppConfig *(NSString *loginHost) { + blockWasCalled = YES; + return nil; + }; + + // Call the method + SFSDKAppConfig *config = [[SalesforceSDKManager sharedManager] runtimeSelectedAppConfig:@"https://test.salesforce.com"]; + + XCTAssertTrue(blockWasCalled, @"Block should have been called"); + XCTAssertNil(config, @"Should return nil when block returns nil"); +} + @end From 9ec1996df2196ce9005f3087cdda4fcd61860d66 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Fri, 24 Oct 2025 17:51:35 -0700 Subject: [PATCH 12/19] (Incomplete) Tests for AuthFlowTester NB: using "instant login" for authenticated tests --- .../xcschemes/Everything.xcscheme | 2 +- .../xcshareddata/xcschemes/UnitTests.xcscheme | 2 +- .../Classes/Test/SFSDKTestCredentialsData.h | 2 + .../Classes/Test/SFSDKTestCredentialsData.m | 10 + .../AuthFlowTester.xcodeproj/project.pbxproj | 139 ++++++++++- .../xcschemes/AuthFlowTester.xcscheme | 11 + .../Classes/SceneDelegate.swift | 9 +- .../AuthFlowTesterAuthenticatedUITests.swift | 102 ++++++++ ...AuthFlowTesterUnauthenticatedUITests.swift | 152 ++++++++++++ .../AuthFlowTesterUITests/TestHelper.swift | 233 ++++++++++++++++++ 10 files changed, 656 insertions(+), 6 deletions(-) create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterAuthenticatedUITests.swift create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterUnauthenticatedUITests.swift create mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/TestHelper.swift diff --git a/SalesforceMobileSDK.xcworkspace/xcshareddata/xcschemes/Everything.xcscheme b/SalesforceMobileSDK.xcworkspace/xcshareddata/xcschemes/Everything.xcscheme index 431ae10097..83e72c023c 100644 --- a/SalesforceMobileSDK.xcworkspace/xcshareddata/xcschemes/Everything.xcscheme +++ b/SalesforceMobileSDK.xcworkspace/xcshareddata/xcschemes/Everything.xcscheme @@ -266,7 +266,7 @@ buildForAnalyzing = "YES"> diff --git a/SalesforceMobileSDK.xcworkspace/xcshareddata/xcschemes/UnitTests.xcscheme b/SalesforceMobileSDK.xcworkspace/xcshareddata/xcschemes/UnitTests.xcscheme index 0b5c13add0..074c1a6680 100644 --- a/SalesforceMobileSDK.xcworkspace/xcshareddata/xcschemes/UnitTests.xcscheme +++ b/SalesforceMobileSDK.xcworkspace/xcshareddata/xcschemes/UnitTests.xcscheme @@ -266,7 +266,7 @@ buildForAnalyzing = "YES"> diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Test/SFSDKTestCredentialsData.h b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Test/SFSDKTestCredentialsData.h index 87eb81f293..3dbd031653 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Test/SFSDKTestCredentialsData.h +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Test/SFSDKTestCredentialsData.h @@ -39,6 +39,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) NSString *redirectUri; @property (nonatomic, readonly) NSString *loginHost; @property (nonatomic, readonly) NSString *communityUrl; +@property (nonatomic, readonly) NSString *username; +@property (nonatomic, readonly) NSString *displayName; @end diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Test/SFSDKTestCredentialsData.m b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Test/SFSDKTestCredentialsData.m index 9f9bcf19b2..ef3b6c5022 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Test/SFSDKTestCredentialsData.m +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Test/SFSDKTestCredentialsData.m @@ -88,4 +88,14 @@ - (NSString *)communityUrl return _credentialsDict[@"community_url"]; } +- (NSString *)username +{ + return _credentialsDict[@"username"]; +} + +- (NSString *)displayName +{ + return _credentialsDict[@"display_name"]; +} + @end diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj b/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj index 98b06f34e2..a1244dda6d 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 4F1A8CCA2EAB16450037DC89 /* bootconfig.plist in Resources */ = {isa = PBXBuildFile; fileRef = AUTH010 /* bootconfig.plist */; }; + 4F1A8CCB2EAB16490037DC89 /* bootconfig2.plist in Resources */ = {isa = PBXBuildFile; fileRef = AUTH036 /* bootconfig2.plist */; }; + 4F95A89B2EAAD00000000001 /* test_credentials.json in Resources */ = {isa = PBXBuildFile; fileRef = 4F95A89A2EAAD00000000001 /* test_credentials.json */; }; 4F95A89C2EA806E700C98D18 /* SalesforceAnalytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F95A8962EA801DC00C98D18 /* SalesforceAnalytics.framework */; }; 4F95A89D2EA806E700C98D18 /* SalesforceAnalytics.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4F95A8962EA801DC00C98D18 /* SalesforceAnalytics.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4F95A89F2EA806E900C98D18 /* SalesforceSDKCommon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F95A8982EA801DC00C98D18 /* SalesforceSDKCommon.framework */; }; @@ -32,6 +35,16 @@ AUTH053 /* FlowTypesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH054 /* FlowTypesView.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 4FEBAF342EAAC98E00D4880A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AUTH027 /* Project object */; + proxyType = 1; + remoteGlobalIDString = AUTH023; + remoteInfo = AuthFlowTester; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 4F95A89E2EA806E700C98D18 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -50,7 +63,9 @@ 4F95A8962EA801DC00C98D18 /* SalesforceAnalytics.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SalesforceAnalytics.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4F95A8982EA801DC00C98D18 /* SalesforceSDKCommon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SalesforceSDKCommon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4F95A89A2EA801DC00C98D18 /* SalesforceSDKCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SalesforceSDKCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4F95A89A2EAAD00000000001 /* test_credentials.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = test_credentials.json; path = ../../../../shared/test/test_credentials.json; sourceTree = ""; }; 4FEBAF282EA9B91500D4880A /* RevokeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevokeView.swift; sourceTree = ""; }; + 4FEBAF2E2EAAC98E00D4880A /* AuthFlowTesterUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AuthFlowTesterUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; AUTH002 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; AUTH004 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; AUTH006 /* ConfigPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigPickerViewController.swift; sourceTree = ""; }; @@ -72,7 +87,20 @@ AUTH054 /* FlowTypesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowTypesView.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 4FEBAF2F2EAAC98E00D4880A /* AuthFlowTesterUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = AuthFlowTesterUITests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ + 4FEBAF2B2EAAC98E00D4880A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + files = ( + ); + }; AUTH018 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; files = ( @@ -94,6 +122,15 @@ name = Frameworks; sourceTree = ""; }; + 4FEBAF2F2EAAD00000000002 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 4F95A89A2EAAD00000000001 /* test_credentials.json */, + ); + name = "Supporting Files"; + path = AuthFlowTesterUITests; + sourceTree = ""; + }; 4FF0EF9E2EA8568C005E4474 /* Supporting Files */ = { isa = PBXGroup; children = ( @@ -110,6 +147,8 @@ isa = PBXGroup; children = ( AUTH020 /* AuthFlowTester */, + 4FEBAF2F2EAAC98E00D4880A /* AuthFlowTesterUITests */, + 4FEBAF2F2EAAD00000000002 /* Supporting Files */, 4F95A8952EA801DC00C98D18 /* Frameworks */, AUTH021 /* Products */, ); @@ -131,6 +170,7 @@ isa = PBXGroup; children = ( AUTH017 /* AuthFlowTester.app */, + 4FEBAF2E2EAAC98E00D4880A /* AuthFlowTesterUITests.xctest */, ); name = Products; sourceTree = ""; @@ -180,6 +220,27 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 4FEBAF2D2EAAC98E00D4880A /* AuthFlowTesterUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4FEBAF362EAAC98E00D4880A /* Build configuration list for PBXNativeTarget "AuthFlowTesterUITests" */; + buildPhases = ( + 4FEBAF2A2EAAC98E00D4880A /* Sources */, + 4FEBAF2B2EAAC98E00D4880A /* Frameworks */, + 4FEBAF2C2EAAC98E00D4880A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4FEBAF352EAAC98E00D4880A /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 4FEBAF2F2EAAC98E00D4880A /* AuthFlowTesterUITests */, + ); + name = AuthFlowTesterUITests; + productName = AuthFlowTesterUITests; + productReference = 4FEBAF2E2EAAC98E00D4880A /* AuthFlowTesterUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; AUTH023 /* AuthFlowTester */ = { isa = PBXNativeTarget; buildConfigurationList = AUTH024 /* Build configuration list for PBXNativeTarget "AuthFlowTester" */; @@ -203,9 +264,13 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1500; + LastSwiftUpdateCheck = 2600; LastUpgradeCheck = 2600; TargetAttributes = { + 4FEBAF2D2EAAC98E00D4880A = { + CreatedOnToolsVersion = 26.0.1; + TestTargetID = AUTH023; + }; AUTH023 = { CreatedOnToolsVersion = 15.0; }; @@ -225,11 +290,20 @@ projectRoot = ""; targets = ( AUTH023 /* AuthFlowTester */, + 4FEBAF2D2EAAC98E00D4880A /* AuthFlowTesterUITests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 4FEBAF2C2EAAC98E00D4880A /* Resources */ = { + isa = PBXResourcesBuildPhase; + files = ( + 4F95A89B2EAAD00000000001 /* test_credentials.json in Resources */, + 4F1A8CCA2EAB16450037DC89 /* bootconfig.plist in Resources */, + 4F1A8CCB2EAB16490037DC89 /* bootconfig2.plist in Resources */, + ); + }; AUTH026 /* Resources */ = { isa = PBXResourcesBuildPhase; files = ( @@ -242,6 +316,11 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 4FEBAF2A2EAAC98E00D4880A /* Sources */ = { + isa = PBXSourcesBuildPhase; + files = ( + ); + }; AUTH025 /* Sources */ = { isa = PBXSourcesBuildPhase; files = ( @@ -262,7 +341,57 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 4FEBAF352EAAC98E00D4880A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = AUTH023 /* AuthFlowTester */; + targetProxy = 4FEBAF342EAAC98E00D4880A /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ + 4FEBAF372EAAC98E00D4880A /* Debug configuration for PBXNativeTarget "AuthFlowTesterUITests" */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = XD7TD9S6ZU; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.salesforce.salesforce.AuthFlowTesterUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = AuthFlowTester; + }; + name = Debug; + }; + 4FEBAF382EAAC98E00D4880A /* Release configuration for PBXNativeTarget "AuthFlowTesterUITests" */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = XD7TD9S6ZU; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.salesforce.salesforce.AuthFlowTesterUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = AuthFlowTester; + }; + name = Release; + }; AUTH029 /* Debug configuration for PBXProject "AuthFlowTester" */ = { isa = XCBuildConfiguration; buildSettings = { @@ -447,6 +576,14 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 4FEBAF362EAAC98E00D4880A /* Build configuration list for PBXNativeTarget "AuthFlowTesterUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4FEBAF372EAAC98E00D4880A /* Debug configuration for PBXNativeTarget "AuthFlowTesterUITests" */, + 4FEBAF382EAAC98E00D4880A /* Release configuration for PBXNativeTarget "AuthFlowTesterUITests" */, + ); + defaultConfigurationName = Release; + }; AUTH024 /* Build configuration list for PBXNativeTarget "AuthFlowTester" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/xcshareddata/xcschemes/AuthFlowTester.xcscheme b/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/xcshareddata/xcschemes/AuthFlowTester.xcscheme index fdff2a002a..a8d8db2bbe 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/xcshareddata/xcschemes/AuthFlowTester.xcscheme +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/xcshareddata/xcschemes/AuthFlowTester.xcscheme @@ -28,6 +28,17 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + 0, "Expected to find scopes: \(expectedScopes)") + + // Also verify other OAuth fields are present + XCTAssertTrue(app.staticTexts.matching(NSPredicate(format: "label CONTAINS 'Configured Consumer Key'")).count > 0) + XCTAssertTrue(app.staticTexts["Configured Callback URL:"].exists) + } + + // + // TODO write more tests + // +} + + diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterUnauthenticatedUITests.swift b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterUnauthenticatedUITests.swift new file mode 100644 index 0000000000..1a9bb24f7f --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterUnauthenticatedUITests.swift @@ -0,0 +1,152 @@ +/* + Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. + + Redistribution and use of this software in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions + and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior written + permission of salesforce.com, inc. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import XCTest + +/// Tests for the unauthenticated state - Config Picker screen +final class AuthFlowTesterUnauthenticatedUITests: XCTestCase { + + var app: XCUIApplication! + + // Expected values loaded from bootconfig.plist and bootconfig2.plist + var bootconfigConsumerKey: String! + var bootconfigRedirectURI: String! + var bootconfigScopes: String! + var bootconfig2ConsumerKey: String! + var bootconfig2RedirectURI: String! + var bootconfig2Scopes: String! + + override func setUpWithError() throws { + continueAfterFailure = false + app = XCUIApplication() + + // Load expected values from bootconfig files in the test bundle + let bootconfig = try TestHelper.loadBootConfig(named: "bootconfig") + let bootconfig2 = try TestHelper.loadBootConfig(named: "bootconfig2") + bootconfigConsumerKey = bootconfig.consumerKey + bootconfigRedirectURI = bootconfig.redirectURI + bootconfigScopes = bootconfig.scopes + bootconfig2ConsumerKey = bootconfig2.consumerKey + bootconfig2RedirectURI = bootconfig2.redirectURI + bootconfig2Scopes = bootconfig2.scopes + + TestHelper.launchWithoutCredentials(app) + } + + override func tearDownWithError() throws { + app = nil + } + + // MARK: - Screen visible test + func testConfigPickerScreenIsVisible() throws { + // Navigation title + XCTAssertTrue(app.navigationBars["AuthFlowTester"].waitForExistence(timeout: 5)) + // Primary actions + XCTAssertTrue(app.buttons["Use default config"].waitForExistence(timeout: 5)) + XCTAssertTrue(app.buttons["Use dynamic config"].exists) + } + + // MARK: - Default Configuration Test + + func testDefaultConfigSection() throws { + let defaultConfigHeader = app.buttons["Default Configuration"] + XCTAssertTrue(defaultConfigHeader.waitForExistence(timeout: 5)) + + let useDefaultConfigButton = app.buttons["Use default config"] + XCTAssertTrue(useDefaultConfigButton.waitForExistence(timeout: 5)) + XCTAssertTrue(useDefaultConfigButton.isEnabled) + + // Initially collapsed - fields should not be visible + XCTAssertFalse(app.staticTexts["Consumer Key:"].exists) + XCTAssertFalse(app.staticTexts["Callback URL:"].exists) + XCTAssertFalse(app.staticTexts["Scopes:"].exists) + + // Tap to expand + defaultConfigHeader.tap() + + // Fields should now be visible + XCTAssertTrue(app.staticTexts["Consumer Key:"].exists) + XCTAssertTrue(app.staticTexts.matching(NSPredicate(format: "label CONTAINS %@", bootconfigConsumerKey)).count > 0) + XCTAssertTrue(app.staticTexts["Callback URL:"].exists) + let callbackPredicate = NSPredicate(format: "label CONTAINS %@", bootconfigRedirectURI) + XCTAssertTrue(app.staticTexts.containing(callbackPredicate).count > 0) + XCTAssertTrue(app.staticTexts["Scopes:"].exists) + let scopesPredicate = NSPredicate(format: "label CONTAINS %@", bootconfigScopes) + XCTAssertTrue(app.staticTexts.containing(scopesPredicate).count > 0) + + // Tap to collapse + defaultConfigHeader.tap() + + // Fields should be hidden again + XCTAssertFalse(app.staticTexts["Consumer Key:"].exists) + XCTAssertFalse(app.staticTexts["Callback URL:"].exists) + XCTAssertFalse(app.staticTexts["Scopes:"].exists) + } + + + // MARK: - Dynamic Configuration Test + + func testDynamicConfigSection() throws { + let dynamicConfigHeader = app.buttons["Dynamic Configuration"] + XCTAssertTrue(dynamicConfigHeader.waitForExistence(timeout: 5)) + + let useDynamicConfigButton = app.buttons["Use dynamic config"] + XCTAssertTrue(useDynamicConfigButton.waitForExistence(timeout: 5)) + XCTAssertTrue(useDynamicConfigButton.isEnabled) + + // Initially collapsed - fields should not be visible + XCTAssertFalse(app.staticTexts["Consumer Key:"].exists) + XCTAssertFalse(app.staticTexts["Callback URL:"].exists) + XCTAssertFalse(app.staticTexts["Scopes (space-separated):"].exists) + + // Tap to expand + dynamicConfigHeader.tap() + + // Fields should now be visible + XCTAssertTrue(app.staticTexts["Consumer Key:"].exists) + XCTAssertTrue(app.textFields.matching(NSPredicate(format: "value CONTAINS %@", bootconfig2ConsumerKey)).count > 0) + XCTAssertTrue(app.staticTexts["Callback URL:"].exists) + let callbackPredicate = NSPredicate(format: "value CONTAINS %@", bootconfig2RedirectURI) + XCTAssertTrue(app.textFields.containing(callbackPredicate).count > 0) + XCTAssertTrue(app.staticTexts["Scopes (space-separated):"].exists) + let scopesPredicate = NSPredicate(format: "value CONTAINS %@", bootconfig2Scopes) + XCTAssertTrue(app.textFields.containing(scopesPredicate).count > 0) + + // Tap to collapse + dynamicConfigHeader.tap() + + // Fields should be hidden again + XCTAssertFalse(app.staticTexts["Consumer Key:"].exists) + XCTAssertFalse(app.staticTexts["Callback URL:"].exists) + XCTAssertFalse(app.staticTexts["Scopes (space-separated):"].exists) + } + + // MARK: - Flow Types Test + func testFlowTypes() throws { + XCTAssertTrue(app.staticTexts["Authentication Flow Types"].waitForExistence(timeout: 5)) + XCTAssertTrue(app.staticTexts.matching(NSPredicate(format: "label CONTAINS %@", "Use Web Server Flow")).count > 0) + XCTAssertTrue(app.staticTexts.matching(NSPredicate(format: "label CONTAINS %@", "Use Hybrid Flow")).count > 0) + } +} + diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/TestHelper.swift b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/TestHelper.swift new file mode 100644 index 0000000000..15deb5068d --- /dev/null +++ b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/TestHelper.swift @@ -0,0 +1,233 @@ +/* + Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. + + Redistribution and use of this software in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list of conditions + and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior written + permission of salesforce.com, inc. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import XCTest + +/// Helper functions for AuthFlowTester UI Tests +class TestHelper { + + /// Launches the app with test credentials for authenticated tests + /// - Parameter app: The XCUIApplication instance to configure + /// - Throws: If test_credentials.json cannot be loaded + static func launchWithCredentials(_ app: XCUIApplication) throws { + // Load test credentials from bundle + guard let credentialsPath = Bundle(for: TestHelper.self).path(forResource: "test_credentials", ofType: "json") else { + throw TestHelperError.credentialsFileNotFound + } + + guard let credentialsData = try? Data(contentsOf: URL(fileURLWithPath: credentialsPath)) else { + throw TestHelperError.credentialsFileNotReadable + } + + // Minify JSON (remove newlines/whitespace) for passing via launch args + let credentialsString: String + if let jsonObject = try? JSONSerialization.jsonObject(with: credentialsData), + let minifiedData = try? JSONSerialization.data(withJSONObject: jsonObject, options: []), + let minifiedString = String(data: minifiedData, encoding: .utf8) { + credentialsString = minifiedString + } else if let asString = String(data: credentialsData, encoding: .utf8) { + // Fallback: use original string if minification fails + credentialsString = asString.replacingOccurrences(of: "\n", with: "").replacingOccurrences(of: "\r", with: "") + } else { + throw TestHelperError.credentialsNotValidString + } + + // Validate that credentials have been configured (not using default values) + if credentialsString.contains("__INSERT_TOKEN_HERE__") || + credentialsString.contains("__INSERT_REMOTE_ACCESS_CLIENT_KEY_HERE__") || + credentialsString.contains("__INSERT_REMOTE_ACCESS_CALLBACK_URL_HERE__") { + throw TestHelperError.credentialsNotConfigured + } + + // Configure launch arguments + app.launchArguments = [ + "-creds", credentialsString + ] + + app.launch() + } + + /// Launches the app in UI testing mode without credentials (unauthenticated tests) + /// - Parameter app: The XCUIApplication instance to configure + static func launchWithoutCredentials(_ app: XCUIApplication) { + app.launchArguments = ["CONFIG_PICKER"] + app.launch() + } +} + +/// Structure to hold bootconfig values loaded from plist +struct BootConfigData { + let consumerKey: String + let redirectURI: String + let scopes: String +} + +extension TestHelper { + /// Loads a bootconfig plist (e.g. "bootconfig" or "bootconfig2") from the test bundle + /// - Parameter name: The plist name without extension + /// - Returns: Parsed BootConfigData + /// - Throws: TestHelperError if the file cannot be found or parsed + static func loadBootConfig(named name: String) throws -> BootConfigData { + guard let url = Bundle(for: TestHelper.self).url(forResource: name, withExtension: "plist") else { + throw TestHelperError.bootconfigFileNotFound(name) + } + guard let dict = NSDictionary(contentsOf: url) as? [String: Any] else { + throw TestHelperError.bootconfigNotParseable(name) + } + guard let consumerKey = dict["remoteAccessConsumerKey"] as? String, + let redirectURI = dict["oauthRedirectURI"] as? String else { + throw TestHelperError.bootconfigMissingFields(name) + } + + // Parse scopes (optional field) - convert array to space-separated string + let scopes: String + if let scopesArray = dict["oauthScopes"] as? [String] { + scopes = scopesArray.sorted().joined(separator: " ") + } else { + scopes = "(none)" + } + + return BootConfigData(consumerKey: consumerKey, redirectURI: redirectURI, scopes: scopes) + } +} + +/// Structure to hold test credentials loaded from test_credentials.json +/// This mirrors the structure of SFSDKTestCredentialsData from TestSetupUtils +struct TestCredentials { + let username: String + let instanceUrl: String + let clientId: String + let redirectUri: String + let displayName: String + let accessToken: String + let refreshToken: String + let identityUrl: String + let organizationId: String + let userId: String + let photoUrl: String + let loginDomain: String + let scopes: String + + /// Loads test credentials from test_credentials.json in the test bundle + /// This follows the same pattern as TestSetupUtils.populateAuthCredentialsFromConfigFileForClass + /// - Returns: TestCredentials parsed from the JSON file + /// - Throws: TestHelperError if the file cannot be loaded or parsed + static func loadFromBundle() throws -> TestCredentials { + // Load credentials file from bundle - similar to TestSetupUtils line 53 + guard let credentialsPath = Bundle(for: TestHelper.self).path(forResource: "test_credentials", ofType: "json") else { + throw TestHelperError.credentialsFileNotFound + } + + guard let credentialsData = try? Data(contentsOf: URL(fileURLWithPath: credentialsPath)) else { + throw TestHelperError.credentialsFileNotReadable + } + + // Parse JSON into dictionary - similar to TestSetupUtils line 56-57 + guard let jsonDict = try? JSONSerialization.jsonObject(with: credentialsData) as? [String: Any] else { + throw TestHelperError.credentialsNotParseable(NSError(domain: "TestHelper", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid JSON format"])) + } + + // Extract values from dictionary - similar to SFSDKTestCredentialsData property getters + guard let username = jsonDict["username"] as? String, + let instanceUrl = jsonDict["instance_url"] as? String, + let clientId = jsonDict["test_client_id"] as? String, + let redirectUri = jsonDict["test_redirect_uri"] as? String, + let displayName = jsonDict["display_name"] as? String, + let accessToken = jsonDict["access_token"] as? String, + let refreshToken = jsonDict["refresh_token"] as? String, + let identityUrl = jsonDict["identity_url"] as? String, + let organizationId = jsonDict["organization_id"] as? String, + let userId = jsonDict["user_id"] as? String, + let photoUrl = jsonDict["photo_url"] as? String, + let loginDomain = jsonDict["test_login_domain"] as? String else { + throw TestHelperError.credentialsNotParseable(NSError(domain: "TestHelper", code: -2, userInfo: [NSLocalizedDescriptionKey: "Missing required credentials fields"])) + } + + // Parse scopes (optional field) - convert array to space-separated string + let scopes: String + if let scopesArray = jsonDict["test_scopes"] as? [String] { + scopes = scopesArray.sorted().joined(separator: ", ") + } else { + scopes = "(none)" + } + + // Check if credentials have been configured - similar to TestSetupUtils line 135 + guard refreshToken != "__INSERT_TOKEN_HERE__" else { + throw TestHelperError.credentialsNotConfigured + } + + return TestCredentials( + username: username, + instanceUrl: instanceUrl, + clientId: clientId, + redirectUri: redirectUri, + displayName: displayName, + accessToken: accessToken, + refreshToken: refreshToken, + identityUrl: identityUrl, + organizationId: organizationId, + userId: userId, + photoUrl: photoUrl, + loginDomain: loginDomain, + scopes: scopes + ) + } +} + +/// Errors that can occur during test setup +enum TestHelperError: Error, LocalizedError { + case credentialsFileNotFound + case credentialsFileNotReadable + case credentialsNotValidString + case credentialsNotConfigured + case credentialsNotParseable(Error) + case bootconfigFileNotFound(String) + case bootconfigNotParseable(String) + case bootconfigMissingFields(String) + + var errorDescription: String? { + switch self { + case .credentialsFileNotFound: + return "test_credentials.json file not found in test bundle. Make sure it's added to Copy Bundle Resources." + case .credentialsFileNotReadable: + return "test_credentials.json file could not be read." + case .credentialsNotValidString: + return "test_credentials.json contains invalid UTF-8 data." + case .credentialsNotConfigured: + return """ + test_credentials.json has not been configured with real credentials. + Please replace the placeholder values with actual test org credentials. + """ + case .credentialsNotParseable(let error): + return "test_credentials.json could not be parsed: \(error.localizedDescription)" + case .bootconfigFileNotFound(let name): + return "\(name).plist file not found in test bundle. Make sure it's added to Copy Bundle Resources." + case .bootconfigNotParseable(let name): + return "\(name).plist could not be parsed as a property list dictionary." + case .bootconfigMissingFields(let name): + return "\(name).plist is missing required keys: remoteAccessConsumerKey and/or oauthRedirectURI." + } + } +} + From ed39a21e0ee3e2b3fd63f673a39caf24451f3a7f Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Fri, 24 Oct 2025 17:55:55 -0700 Subject: [PATCH 13/19] Building new sample app and running new UI tests in CI --- .github/DangerFiles/TestOrchestrator.rb | 16 +++++++++++++--- .github/workflows/nightly.yaml | 4 ++-- .github/workflows/pr.yaml | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/DangerFiles/TestOrchestrator.rb b/.github/DangerFiles/TestOrchestrator.rb index a3a5ad60d6..c8765148c1 100644 --- a/.github/DangerFiles/TestOrchestrator.rb +++ b/.github/DangerFiles/TestOrchestrator.rb @@ -7,12 +7,22 @@ fail("Please re-submit this PR to the dev branch, we may have already fixed your issue.", sticky: true) if github.branch_for_base != "dev" # List of supported xcode schemes for testing -SCHEMES = ['SalesforceSDKCommon', 'SalesforceAnalytics', 'SalesforceSDKCore', 'SmartStore', 'MobileSync'] +SCHEMES = ['SalesforceSDKCommon', 'SalesforceAnalytics', 'SalesforceSDKCore', 'SmartStore', 'MobileSync', 'AuthFlowTester'] modifed_libs = Set[] + for file in (git.modified_files + git.added_files); - scheme = file.split("libs/").last.split("/").first - if SCHEMES.include?(scheme) + scheme = nil + + # Check if SDK libs are modified + if file.include?("libs/") + scheme = file.split("libs/").last.split("/").first + # Check if sample apps are modified + elsif file.include?("native/SampleApps/") + scheme = file.split("native/SampleApps/").last.split("/").first + end + + if scheme && SCHEMES.include?(scheme) modifed_libs.add(scheme) end end diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml index e89b777b6f..43d9c65ff9 100644 --- a/.github/workflows/nightly.yaml +++ b/.github/workflows/nightly.yaml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - lib: [SalesforceSDKCommon, SalesforceAnalytics, SalesforceSDKCore, SmartStore, MobileSync] + lib: [SalesforceSDKCommon, SalesforceAnalytics, SalesforceSDKCore, SmartStore, MobileSync, AuthFlowTester] ios: [^26, ^18, ^17] include: - ios: ^26 @@ -31,7 +31,7 @@ jobs: strategy: fail-fast: false matrix: - app: [RestAPIExplorer, MobileSyncExplorer] + app: [RestAPIExplorer, MobileSyncExplorer, AuthFlowTester] ios: [^26, ^18, ^17] include: - ios: ^26 diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 584ef4575a..cba913c27f 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -120,7 +120,7 @@ jobs: strategy: fail-fast: false matrix: - app: [RestAPIExplorer, MobileSyncExplorer] + app: [RestAPIExplorer, MobileSyncExplorer, AuthFlowTester] ios: [^26, ^18] include: - ios: ^26 From d22f67fbeb9366c5f0a84dad1822cdabd43c6636 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Fri, 24 Oct 2025 18:52:58 -0700 Subject: [PATCH 14/19] Masking client id in JWT payload details --- .../AuthFlowTester/Views/JwtAccessView.swift | 63 ++++++++++--------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/JwtAccessView.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/JwtAccessView.swift index 4e05932fc4..dfc7710dbe 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/JwtAccessView.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/JwtAccessView.swift @@ -83,12 +83,24 @@ struct JwtHeaderView: View { VStack(alignment: .leading, spacing: 8) { let header = token.header - JwtFieldRow(label: "Algorithm (alg):", value: header.algorithm) - JwtFieldRow(label: "Type (typ):", value: header.type) - JwtFieldRow(label: "Key ID (kid):", value: header.keyId) - JwtFieldRow(label: "Token Type (tty):", value: header.tokenType) - JwtFieldRow(label: "Tenant Key (tnk):", value: header.tenantKey) - JwtFieldRow(label: "Version (ver):", value: header.version) + if let algorithm = header.algorithm { + InfoRowView(label: "Algorithm (alg):", value: algorithm) + } + if let type = header.type { + InfoRowView(label: "Type (typ):", value: type) + } + if let keyId = header.keyId { + InfoRowView(label: "Key ID (kid):", value: keyId) + } + if let tokenType = header.tokenType { + InfoRowView(label: "Token Type (tty):", value: tokenType) + } + if let tenantKey = header.tenantKey { + InfoRowView(label: "Tenant Key (tnk):", value: tenantKey) + } + if let version = header.version { + InfoRowView(label: "Version (ver):", value: version) + } } .padding(8) .background(Color(.systemGray6)) @@ -106,17 +118,25 @@ struct JwtPayloadView: View { let payload = token.payload if let audience = payload.audience { - JwtFieldRow(label: "Audience (aud):", value: audience.joined(separator: ", ")) + InfoRowView(label: "Audience (aud):", value: audience.joined(separator: ", ")) } if let expirationDate = token.expirationDate() { - JwtFieldRow(label: "Expiration Date (exp):", value: formatDate(expirationDate)) + InfoRowView(label: "Expiration Date (exp):", value: formatDate(expirationDate)) } - JwtFieldRow(label: "Issuer (iss):", value: payload.issuer) - JwtFieldRow(label: "Subject (sub):", value: payload.subject) - JwtFieldRow(label: "Scopes (scp):", value: payload.scopes) - JwtFieldRow(label: "Client ID (client_id):", value: payload.clientId) + if let issuer = payload.issuer { + InfoRowView(label: "Issuer (iss):", value: issuer) + } + if let subject = payload.subject { + InfoRowView(label: "Subject (sub):", value: subject) + } + if let scopes = payload.scopes { + InfoRowView(label: "Scopes (scp):", value: scopes) + } + if let clientId = payload.clientId { + InfoRowView(label: "Client ID (client_id):", value: clientId, isSensitive: true) + } } .padding(8) .background(Color(.systemGray6)) @@ -131,23 +151,4 @@ struct JwtPayloadView: View { } } -// MARK: - Helper Views - -struct JwtFieldRow: View { - let label: String - let value: String? - - var body: some View { - if let value = value, !value.isEmpty { - VStack(alignment: .leading, spacing: 2) { - Text(label) - .font(.caption) - .foregroundColor(.secondary) - Text(value) - .font(.system(.caption, design: .monospaced)) - .foregroundColor(.primary) - } - } - } -} From 0fc755ce09fd8d7b7c372108858e0336e9cb5e95 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Mon, 27 Oct 2025 11:36:15 -0700 Subject: [PATCH 15/19] Added explaining comment in SalesforceSDKManager Static configuration can be edited Took out broken new tests (will bring them back if they make sense) --- .../Classes/Common/SalesforceSDKManager.h | 8 ++ .../AuthFlowTester.xcodeproj/project.pbxproj | 12 +- .../ConfigPickerViewController.swift | 62 +++++++-- ...onfigView.swift => BootConfigEditor.swift} | 16 ++- .../Views/DefaultConfigView.swift | 121 ------------------ .../AuthFlowTesterAuthenticatedUITests.swift | 24 ---- ...AuthFlowTesterUnauthenticatedUITests.swift | 112 +++++++++------- 7 files changed, 143 insertions(+), 212 deletions(-) rename native/SampleApps/AuthFlowTester/AuthFlowTester/Views/{DynamicConfigView.swift => BootConfigEditor.swift} (90%) delete mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DefaultConfigView.swift diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.h b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.h index 95a8e8635c..7c6aa9c618 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.h +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.h @@ -205,6 +205,14 @@ NS_SWIFT_NAME(SalesforceManager) /** Block to dynamically select the app config at runtime based on login host. + + NB: SFUserAccountManager stores the consumer key, callback URL, etc. in its shared + instance, backed by shared prefs and initialized from the static boot config. + Previously, the app always used these shared instance values for login. + Now, the app can inject alternate values instead — in that case, the shared + instance and prefs are left untouched (not read or overwritten). + The consumer key and related values used for login are saved in the user + account credentials (as before) and therefore used later for token refresh. */ @property (nonatomic, copy, nullable) SFSDKAppConfigRuntimeSelectorBlock appConfigRuntimeSelectorBlock NS_SWIFT_NAME(bootConfigRuntimeSelector); diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj b/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj index a1244dda6d..8ca0e35c23 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 4F1A8CCA2EAB16450037DC89 /* bootconfig.plist in Resources */ = {isa = PBXBuildFile; fileRef = AUTH010 /* bootconfig.plist */; }; 4F1A8CCB2EAB16490037DC89 /* bootconfig2.plist in Resources */ = {isa = PBXBuildFile; fileRef = AUTH036 /* bootconfig2.plist */; }; + 4F1A8CCD2EAFEA7C0037DC89 /* BootConfigEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F1A8CCC2EAFEA7C0037DC89 /* BootConfigEditor.swift */; }; 4F95A89B2EAAD00000000001 /* test_credentials.json in Resources */ = {isa = PBXBuildFile; fileRef = 4F95A89A2EAAD00000000001 /* test_credentials.json */; }; 4F95A89C2EA806E700C98D18 /* SalesforceAnalytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F95A8962EA801DC00C98D18 /* SalesforceAnalytics.framework */; }; 4F95A89D2EA806E700C98D18 /* SalesforceAnalytics.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4F95A8962EA801DC00C98D18 /* SalesforceAnalytics.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -28,8 +29,6 @@ AUTH037 /* UserCredentialsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH038 /* UserCredentialsView.swift */; }; AUTH040 /* RestApiTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH041 /* RestApiTestView.swift */; }; AUTH042 /* OAuthConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH043 /* OAuthConfigurationView.swift */; }; - AUTH045 /* DefaultConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH046 /* DefaultConfigView.swift */; }; - AUTH047 /* DynamicConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH048 /* DynamicConfigView.swift */; }; AUTH049 /* JwtAccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH050 /* JwtAccessView.swift */; }; AUTH051 /* InfoRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH052 /* InfoRowView.swift */; }; AUTH053 /* FlowTypesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH054 /* FlowTypesView.swift */; }; @@ -60,6 +59,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 4F1A8CCC2EAFEA7C0037DC89 /* BootConfigEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BootConfigEditor.swift; sourceTree = ""; }; 4F95A8962EA801DC00C98D18 /* SalesforceAnalytics.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SalesforceAnalytics.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4F95A8982EA801DC00C98D18 /* SalesforceSDKCommon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SalesforceSDKCommon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4F95A89A2EA801DC00C98D18 /* SalesforceSDKCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SalesforceSDKCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -80,8 +80,6 @@ AUTH038 /* UserCredentialsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCredentialsView.swift; sourceTree = ""; }; AUTH041 /* RestApiTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestApiTestView.swift; sourceTree = ""; }; AUTH043 /* OAuthConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthConfigurationView.swift; sourceTree = ""; }; - AUTH046 /* DefaultConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultConfigView.swift; sourceTree = ""; }; - AUTH048 /* DynamicConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicConfigView.swift; sourceTree = ""; }; AUTH050 /* JwtAccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JwtAccessView.swift; sourceTree = ""; }; AUTH052 /* InfoRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoRowView.swift; sourceTree = ""; }; AUTH054 /* FlowTypesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowTypesView.swift; sourceTree = ""; }; @@ -195,12 +193,11 @@ AUTH039 /* Views */ = { isa = PBXGroup; children = ( + 4F1A8CCC2EAFEA7C0037DC89 /* BootConfigEditor.swift */, 4FEBAF282EA9B91500D4880A /* RevokeView.swift */, AUTH038 /* UserCredentialsView.swift */, AUTH041 /* RestApiTestView.swift */, AUTH043 /* OAuthConfigurationView.swift */, - AUTH046 /* DefaultConfigView.swift */, - AUTH048 /* DynamicConfigView.swift */, AUTH050 /* JwtAccessView.swift */, AUTH052 /* InfoRowView.swift */, AUTH054 /* FlowTypesView.swift */, @@ -331,11 +328,10 @@ AUTH037 /* UserCredentialsView.swift in Sources */, AUTH040 /* RestApiTestView.swift in Sources */, AUTH042 /* OAuthConfigurationView.swift in Sources */, - AUTH045 /* DefaultConfigView.swift in Sources */, - AUTH047 /* DynamicConfigView.swift in Sources */, 4FEBAF292EA9B91500D4880A /* RevokeView.swift in Sources */, AUTH049 /* JwtAccessView.swift in Sources */, AUTH051 /* InfoRowView.swift in Sources */, + 4F1A8CCD2EAFEA7C0037DC89 /* BootConfigEditor.swift in Sources */, AUTH053 /* FlowTypesView.swift in Sources */, ); }; diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/ConfigPickerViewController.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/ConfigPickerViewController.swift index f85eceac24..2bfd69c789 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/ConfigPickerViewController.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/ConfigPickerViewController.swift @@ -30,6 +30,9 @@ import SalesforceSDKCore struct ConfigPickerView: View { @State private var isLoading = false + @State private var staticConsumerKey = "" + @State private var staticCallbackUrl = "" + @State private var staticScopes = "" @State private var dynamicConsumerKey = "" @State private var dynamicCallbackUrl = "" @State private var dynamicScopes = "" @@ -46,16 +49,25 @@ struct ConfigPickerView: View { Divider() - // Default config section - DefaultConfigView( + // Static config section + BootConfigEditor( + title: "Static Configuration", + buttonLabel: "Use static config", + buttonColor: .blue, + consumerKey: $staticConsumerKey, + callbackUrl: $staticCallbackUrl, + scopes: $staticScopes, isLoading: isLoading, - onUseConfig: handleDefaultConfig + onUseConfig: handleStaticConfig ) Divider() // Dynamic config section - DynamicConfigView( + BootConfigEditor( + title: "Dynamic Configuration", + buttonLabel: "Use dynamic config", + buttonColor: .green, consumerKey: $dynamicConsumerKey, callbackUrl: $dynamicCallbackUrl, scopes: $dynamicScopes, @@ -77,7 +89,7 @@ struct ConfigPickerView: View { } .navigationViewStyle(.stack) .onAppear { - loadDynamicConfigDefaults() + loadConfigDefaults() } } @@ -93,8 +105,15 @@ struct ConfigPickerView: View { // MARK: - Helper Methods - private func loadDynamicConfigDefaults() { - // Load initial values from bootconfig2.plist + private func loadConfigDefaults() { + // Load static config from bootconfig.plist (via SalesforceManager) + if let config = SalesforceManager.shared.bootConfig { + staticConsumerKey = config.remoteAccessConsumerKey + staticCallbackUrl = config.oauthRedirectURI + staticScopes = config.oauthScopes.sorted().joined(separator: " ") + } + + // Load dynamic config defaults from bootconfig2.plist if let config = BootConfig("/bootconfig2.plist") { dynamicConsumerKey = config.remoteAccessConsumerKey dynamicCallbackUrl = config.oauthRedirectURI @@ -104,12 +123,37 @@ struct ConfigPickerView: View { // MARK: - Button Actions - private func handleDefaultConfig() { + private func handleStaticConfig() { isLoading = true + // Parse scopes from space-separated string + let scopesArray = staticScopes + .split(separator: " ") + .map { String($0) } + .filter { !$0.isEmpty } + + // Create BootConfig with values from the editor + var configDict: [String: Any] = [ + "remoteAccessConsumerKey": staticConsumerKey, + "oauthRedirectURI": staticCallbackUrl, + "shouldAuthenticate": true + ] + + // Only add scopes if not empty + if !scopesArray.isEmpty { + configDict["oauthScopes"] = scopesArray + } + + // Set as the bootConfig + SalesforceManager.shared.bootConfig = BootConfig(configDict) SalesforceManager.shared.bootConfigRuntimeSelector = nil - // Use default bootconfig - no additional setup needed + // Update UserAccountManager properties + UserAccountManager.shared.oauthClientID = staticConsumerKey + UserAccountManager.shared.oauthCompletionURL = staticCallbackUrl + UserAccountManager.shared.scopes = scopesArray.isEmpty ? [] : Set(scopesArray) + + // Proceed with login onConfigurationCompleted() } diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DynamicConfigView.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/BootConfigEditor.swift similarity index 90% rename from native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DynamicConfigView.swift rename to native/SampleApps/AuthFlowTester/AuthFlowTester/Views/BootConfigEditor.swift index b53da714c7..7badf71362 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DynamicConfigView.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/BootConfigEditor.swift @@ -1,5 +1,5 @@ /* - DynamicConfigView.swift + BootConfigEditor.swift AuthFlowTester Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. @@ -28,7 +28,10 @@ import SwiftUI import SalesforceSDKCore -struct DynamicConfigView: View { +struct BootConfigEditor: View { + let title: String + let buttonLabel: String + let buttonColor: Color @Binding var consumerKey: String @Binding var callbackUrl: String @Binding var scopes: String @@ -44,7 +47,7 @@ struct DynamicConfigView: View { } }) { HStack { - Text("Dynamic Configuration") + Text(title) .font(.headline) .foregroundColor(.primary) Spacer() @@ -64,6 +67,7 @@ struct DynamicConfigView: View { .textFieldStyle(RoundedBorderTextFieldStyle()) .autocapitalization(.none) .disableAutocorrection(true) + .accessibilityIdentifier("consumerKeyTextField") Text("Callback URL:") .font(.caption) @@ -73,6 +77,7 @@ struct DynamicConfigView: View { .textFieldStyle(RoundedBorderTextFieldStyle()) .autocapitalization(.none) .disableAutocorrection(true) + .accessibilityIdentifier("callbackUrlTextField") Text("Scopes (space-separated):") .font(.caption) @@ -82,17 +87,18 @@ struct DynamicConfigView: View { .textFieldStyle(RoundedBorderTextFieldStyle()) .autocapitalization(.none) .disableAutocorrection(true) + .accessibilityIdentifier("scopesTextField") } .padding(.horizontal) } Button(action: onUseConfig) { - Text("Use dynamic config") + Text(buttonLabel) .font(.headline) .foregroundColor(.white) .frame(maxWidth: .infinity) .frame(height: 44) - .background(Color.green) + .background(buttonColor) .cornerRadius(8) } .disabled(isLoading) diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DefaultConfigView.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DefaultConfigView.swift deleted file mode 100644 index eaa2b6ba48..0000000000 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/DefaultConfigView.swift +++ /dev/null @@ -1,121 +0,0 @@ -/* - DefaultConfigView.swift - AuthFlowTester - - Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. - - Redistribution and use of this software in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list of conditions - and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of - conditions and the following disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to - endorse or promote products derived from this software without specific prior written - permission of salesforce.com, inc. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR - IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY - WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import SwiftUI -import SalesforceSDKCore - -struct DefaultConfigView: View { - let isLoading: Bool - let onUseConfig: () -> Void - @State private var isExpanded: Bool = false - - var body: some View { - VStack(alignment: .leading, spacing: 12) { - Button(action: { - withAnimation { - isExpanded.toggle() - } - }) { - HStack { - Text("Default Configuration") - .font(.headline) - .foregroundColor(.primary) - Spacer() - Image(systemName: isExpanded ? "chevron.up" : "chevron.down") - .foregroundColor(.secondary) - } - .padding(.horizontal) - } - - if isExpanded { - VStack(alignment: .leading, spacing: 8) { - Text("Consumer Key:") - .font(.caption) - .foregroundColor(.secondary) - Text(defaultConsumerKey) - .font(.system(.caption, design: .monospaced)) - .padding(8) - .frame(maxWidth: .infinity, alignment: .leading) - .background(Color(.systemGray6)) - .cornerRadius(4) - - Text("Callback URL:") - .font(.caption) - .foregroundColor(.secondary) - Text(defaultCallbackUrl) - .font(.system(.caption, design: .monospaced)) - .padding(8) - .frame(maxWidth: .infinity, alignment: .leading) - .background(Color(.systemGray6)) - .cornerRadius(4) - - Text("Scopes:") - .font(.caption) - .foregroundColor(.secondary) - Text(defaultScopes) - .font(.system(.caption, design: .monospaced)) - .padding(8) - .frame(maxWidth: .infinity, alignment: .leading) - .background(Color(.systemGray6)) - .cornerRadius(4) - } - .padding(.horizontal) - } - - Button(action: onUseConfig) { - Text("Use default config") - .font(.headline) - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .frame(height: 44) - .background(Color.blue) - .cornerRadius(8) - } - .disabled(isLoading) - .padding(.horizontal) - } - .padding(.vertical) - } - - // MARK: - Computed Properties - - private var defaultConsumerKey: String { - return SalesforceManager.shared.bootConfig?.remoteAccessConsumerKey ?? "" - } - - private var defaultCallbackUrl: String { - return SalesforceManager.shared.bootConfig?.oauthRedirectURI ?? "" - } - - private var defaultScopes: String { - guard let scopes = SalesforceManager.shared.bootConfig?.oauthScopes else { - return "(none)" - } - return scopes.isEmpty ? "(none)" : scopes.sorted().joined(separator: ", ") - } -} - diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterAuthenticatedUITests.swift b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterAuthenticatedUITests.swift index 591eaaeddb..0c5c932b5d 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterAuthenticatedUITests.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterAuthenticatedUITests.swift @@ -70,30 +70,6 @@ final class AuthFlowTesterAuthenticatedUITests: XCTestCase { } - func testOAuthConfigurationDisplaysScopes() throws { - // Verify we're on the session detail screen - XCTAssertTrue(app.navigationBars["AuthFlowTester"].waitForExistence(timeout: 10)) - - // Locate and tap the OAuth Configuration header to expand it - let oauthConfigHeader = app.buttons["OAuth Configuration"] - XCTAssertTrue(oauthConfigHeader.waitForExistence(timeout: 5)) - - // Initially collapsed - scopes label should not be visible - XCTAssertFalse(app.staticTexts["Configured Scopes:"].exists) - - // Tap to expand - oauthConfigHeader.tap() - - // Scopes should now be visible - XCTAssertTrue(app.staticTexts["Configured Scopes:"].exists) - let scopesPredicate = NSPredicate(format: "label CONTAINS %@", expectedScopes) - XCTAssertTrue(app.staticTexts.containing(scopesPredicate).count > 0, "Expected to find scopes: \(expectedScopes)") - - // Also verify other OAuth fields are present - XCTAssertTrue(app.staticTexts.matching(NSPredicate(format: "label CONTAINS 'Configured Consumer Key'")).count > 0) - XCTAssertTrue(app.staticTexts["Configured Callback URL:"].exists) - } - // // TODO write more tests // diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterUnauthenticatedUITests.swift b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterUnauthenticatedUITests.swift index 1a9bb24f7f..0b1662e639 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterUnauthenticatedUITests.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterUnauthenticatedUITests.swift @@ -63,45 +63,56 @@ final class AuthFlowTesterUnauthenticatedUITests: XCTestCase { // Navigation title XCTAssertTrue(app.navigationBars["AuthFlowTester"].waitForExistence(timeout: 5)) // Primary actions - XCTAssertTrue(app.buttons["Use default config"].waitForExistence(timeout: 5)) + XCTAssertTrue(app.buttons["Use static config"].waitForExistence(timeout: 5)) XCTAssertTrue(app.buttons["Use dynamic config"].exists) } - // MARK: - Default Configuration Test + // MARK: - Static Configuration Test - func testDefaultConfigSection() throws { - let defaultConfigHeader = app.buttons["Default Configuration"] - XCTAssertTrue(defaultConfigHeader.waitForExistence(timeout: 5)) + func testStaticConfigSection() throws { + let staticConfigHeader = app.buttons["Static Configuration"] + XCTAssertTrue(staticConfigHeader.waitForExistence(timeout: 5), "Static Configuration header should exist") - let useDefaultConfigButton = app.buttons["Use default config"] - XCTAssertTrue(useDefaultConfigButton.waitForExistence(timeout: 5)) - XCTAssertTrue(useDefaultConfigButton.isEnabled) + let useStaticConfigButton = app.buttons["Use static config"] + XCTAssertTrue(useStaticConfigButton.waitForExistence(timeout: 5), "Use static config button should exist") + XCTAssertTrue(useStaticConfigButton.isEnabled, "Use static config button should be enabled") // Initially collapsed - fields should not be visible - XCTAssertFalse(app.staticTexts["Consumer Key:"].exists) - XCTAssertFalse(app.staticTexts["Callback URL:"].exists) - XCTAssertFalse(app.staticTexts["Scopes:"].exists) - + XCTAssertFalse(app.staticTexts["Consumer Key:"].exists, "Consumer Key label should not be visible when collapsed") + // Tap to expand - defaultConfigHeader.tap() - - // Fields should now be visible - XCTAssertTrue(app.staticTexts["Consumer Key:"].exists) - XCTAssertTrue(app.staticTexts.matching(NSPredicate(format: "label CONTAINS %@", bootconfigConsumerKey)).count > 0) - XCTAssertTrue(app.staticTexts["Callback URL:"].exists) - let callbackPredicate = NSPredicate(format: "label CONTAINS %@", bootconfigRedirectURI) - XCTAssertTrue(app.staticTexts.containing(callbackPredicate).count > 0) - XCTAssertTrue(app.staticTexts["Scopes:"].exists) - let scopesPredicate = NSPredicate(format: "label CONTAINS %@", bootconfigScopes) - XCTAssertTrue(app.staticTexts.containing(scopesPredicate).count > 0) + staticConfigHeader.tap() + + // Fields should now be visible - wait for animation + XCTAssertTrue(app.staticTexts["Consumer Key:"].waitForExistence(timeout: 3), "Consumer Key label should appear when expanded") + XCTAssertTrue(app.staticTexts["Callback URL:"].exists, "Callback URL label should exist") + XCTAssertTrue(app.staticTexts["Scopes (space-separated):"].exists, "Scopes label should exist") + + // Check text fields exist and have correct values + let consumerKeyField = app.textFields["consumerKeyTextField"] + XCTAssertTrue(consumerKeyField.waitForExistence(timeout: 3), "Consumer key text field should exist") + XCTAssertEqual(consumerKeyField.value as? String, bootconfigConsumerKey, "Consumer key should match boot config") + + let callbackUrlField = app.textFields["callbackUrlTextField"] + XCTAssertTrue(callbackUrlField.exists, "Callback URL text field should exist") + XCTAssertEqual(callbackUrlField.value as? String, bootconfigRedirectURI, "Callback URL should match boot config") + + let scopesField = app.textFields["scopesTextField"] + XCTAssertTrue(scopesField.exists, "Scopes text field should exist") + // Scopes text field will be empty if no scopes configured + let scopesValue = scopesField.value as? String + if bootconfigScopes == "(none)" { + XCTAssert(scopesValue == "" || scopesValue == nil || scopesValue == "e.g. id api refresh_token", + "Scopes should be empty or placeholder when not configured, got: '\(String(describing: scopesValue))'") + } else { + XCTAssertEqual(scopesValue, bootconfigScopes, "Scopes should match boot config") + } // Tap to collapse - defaultConfigHeader.tap() + staticConfigHeader.tap() // Fields should be hidden again - XCTAssertFalse(app.staticTexts["Consumer Key:"].exists) - XCTAssertFalse(app.staticTexts["Callback URL:"].exists) - XCTAssertFalse(app.staticTexts["Scopes:"].exists) + XCTAssertFalse(app.staticTexts["Consumer Key:"].waitForExistence(timeout: 2), "Consumer Key label should be hidden when collapsed") } @@ -109,37 +120,48 @@ final class AuthFlowTesterUnauthenticatedUITests: XCTestCase { func testDynamicConfigSection() throws { let dynamicConfigHeader = app.buttons["Dynamic Configuration"] - XCTAssertTrue(dynamicConfigHeader.waitForExistence(timeout: 5)) + XCTAssertTrue(dynamicConfigHeader.waitForExistence(timeout: 5), "Dynamic Configuration header should exist") let useDynamicConfigButton = app.buttons["Use dynamic config"] - XCTAssertTrue(useDynamicConfigButton.waitForExistence(timeout: 5)) - XCTAssertTrue(useDynamicConfigButton.isEnabled) + XCTAssertTrue(useDynamicConfigButton.waitForExistence(timeout: 5), "Use dynamic config button should exist") + XCTAssertTrue(useDynamicConfigButton.isEnabled, "Use dynamic config button should be enabled") // Initially collapsed - fields should not be visible - XCTAssertFalse(app.staticTexts["Consumer Key:"].exists) - XCTAssertFalse(app.staticTexts["Callback URL:"].exists) - XCTAssertFalse(app.staticTexts["Scopes (space-separated):"].exists) + XCTAssertFalse(app.staticTexts["Consumer Key:"].exists, "Consumer Key label should not be visible when collapsed") // Tap to expand dynamicConfigHeader.tap() - // Fields should now be visible - XCTAssertTrue(app.staticTexts["Consumer Key:"].exists) - XCTAssertTrue(app.textFields.matching(NSPredicate(format: "value CONTAINS %@", bootconfig2ConsumerKey)).count > 0) - XCTAssertTrue(app.staticTexts["Callback URL:"].exists) - let callbackPredicate = NSPredicate(format: "value CONTAINS %@", bootconfig2RedirectURI) - XCTAssertTrue(app.textFields.containing(callbackPredicate).count > 0) - XCTAssertTrue(app.staticTexts["Scopes (space-separated):"].exists) - let scopesPredicate = NSPredicate(format: "value CONTAINS %@", bootconfig2Scopes) - XCTAssertTrue(app.textFields.containing(scopesPredicate).count > 0) + // Fields should now be visible - wait for animation + XCTAssertTrue(app.staticTexts["Consumer Key:"].waitForExistence(timeout: 3), "Consumer Key label should appear when expanded") + XCTAssertTrue(app.staticTexts["Callback URL:"].exists, "Callback URL label should exist") + XCTAssertTrue(app.staticTexts["Scopes (space-separated):"].exists, "Scopes label should exist") + + // Check text fields exist and have correct values + let consumerKeyField = app.textFields["consumerKeyTextField"] + XCTAssertTrue(consumerKeyField.waitForExistence(timeout: 3), "Consumer key text field should exist") + XCTAssertEqual(consumerKeyField.value as? String, bootconfig2ConsumerKey, "Consumer key should match bootconfig2") + + let callbackUrlField = app.textFields["callbackUrlTextField"] + XCTAssertTrue(callbackUrlField.exists, "Callback URL text field should exist") + XCTAssertEqual(callbackUrlField.value as? String, bootconfig2RedirectURI, "Callback URL should match bootconfig2") + + let scopesField = app.textFields["scopesTextField"] + XCTAssertTrue(scopesField.exists, "Scopes text field should exist") + // Scopes text field will be empty if no scopes configured + let scopesValue = scopesField.value as? String + if bootconfig2Scopes == "(none)" { + XCTAssert(scopesValue == "" || scopesValue == nil || scopesValue == "e.g. id api refresh_token", + "Scopes should be empty or placeholder when not configured, got: '\(String(describing: scopesValue))'") + } else { + XCTAssertEqual(scopesValue, bootconfig2Scopes, "Scopes should match bootconfig2") + } // Tap to collapse dynamicConfigHeader.tap() // Fields should be hidden again - XCTAssertFalse(app.staticTexts["Consumer Key:"].exists) - XCTAssertFalse(app.staticTexts["Callback URL:"].exists) - XCTAssertFalse(app.staticTexts["Scopes (space-separated):"].exists) + XCTAssertFalse(app.staticTexts["Consumer Key:"].waitForExistence(timeout: 2), "Consumer Key label should be hidden when collapsed") } // MARK: - Flow Types Test From 5ff8ac4842466529ee021bf4639629b4616b0958 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Mon, 27 Oct 2025 15:25:20 -0700 Subject: [PATCH 16/19] Runtime app config (aka boot config) selection can now be asynchronous - changed SFSDKAppConfigRuntimeSelectorBlock to take a callback parameter - renamed runtimeSelectedAppConfig to appConfigForLoginHost and it now takes a callback parameter - moved the call to appConfigForLoginHost in SFUserAccountManager to the async part of authenticateWithRequest (leaving the building of the auth request unchanged from before this PR) - updated tests / sample app --- .../Classes/Common/SalesforceSDKManager.h | 8 +- .../Classes/Common/SalesforceSDKManager.m | 11 +- .../UserAccount/SFUserAccountManager.m | 27 ++--- .../Classes/Util/SalesforceSDKCoreDefines.h | 3 +- .../SFUserAccountManagerTests.m | 92 ---------------- .../SalesforceSDKManagerTests.m | 100 +++++++++++++----- .../ConfigPickerViewController.swift | 4 +- 7 files changed, 103 insertions(+), 142 deletions(-) diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.h b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.h index 7c6aa9c618..ff29973c67 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.h +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.h @@ -320,11 +320,15 @@ NS_SWIFT_NAME(SalesforceManager) - (id )biometricAuthenticationManager; /** - * Returns app config (aka boot config) at runtime based on login host + * Asynchronously retrieves the app config (aka boot config) for the specified login host. + * + * If an appConfigRuntimeSelectorBlock is set, it will be invoked to select the appropriate config. + * If the block is not set or returns nil, the default appConfig will be returned. * * @param loginHost The selected login host + * @param callback The callback invoked with the selected app config */ -- (SFSDKAppConfig*)runtimeSelectedAppConfig:(nullable NSString *)loginHost NS_SWIFT_NAME(runtimeSelectedBootConfig( loginHost:)); +- (void)appConfigForLoginHost:(nullable NSString *)loginHost callback:(nonnull void (^)(SFSDKAppConfig * _Nullable))callback NS_SWIFT_NAME(bootConfig(forLoginHost:callback:)); /** * Creates the NativeLoginManager instance. diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.m b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.m index cbbb696265..6857cc6bc0 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.m +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.m @@ -914,13 +914,16 @@ - (void)biometricAuthenticationFlowDidComplete:(NSNotification *)notification { return [SFScreenLockManagerInternal shared]; } -#pragma mark - Dynamic Boot Config +#pragma mark - Runtime App Config (aka Bootconfig) Override -- (SFSDKAppConfig*) runtimeSelectedAppConfig:(nullable NSString *)loginHost { +- (void) appConfigForLoginHost:(nullable NSString *)loginHost callback:(nonnull void (^)(SFSDKAppConfig * _Nullable))callback { if (self.appConfigRuntimeSelectorBlock) { - return self.appConfigRuntimeSelectorBlock(loginHost); + self.appConfigRuntimeSelectorBlock(loginHost, ^(SFSDKAppConfig *config) { + // Fall back to default appConfig if the selector block returns nil + callback(config ?: self.appConfig); + }); } else { - return nil; + callback(self.appConfig); } } diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.m b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.m index 110d59b16b..b85fbe5cbb 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.m +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.m @@ -534,9 +534,7 @@ - (BOOL)authenticateWithCompletion:(SFUserAccountManagerSuccessCallbackBlock)com if (self.nativeLoginEnabled && !self.shouldFallbackToWebAuthentication) { request = [self nativeLoginAuthRequest]; } else { - // NB: Will be nil if application did not provide a appConfigRuntimeSelectorBlock - SFSDKAppConfig* appConfig = [[SalesforceSDKManager sharedManager] runtimeSelectedAppConfig:loginHost]; - request = [self authRequestWithLoginHost:loginHost appConfig:appConfig]; + request = [self defaultAuthRequestWithLoginHost:loginHost]; } if (scene) { @@ -554,17 +552,15 @@ - (BOOL)authenticateWithCompletion:(SFUserAccountManagerSuccessCallbackBlock)com codeVerifier:codeVerifier]; } --(SFSDKAuthRequest *)authRequestWithLoginHost:(nullable NSString *)loginHost - appConfig:(nullable SFSDKAppConfig *)appConfig -{ +-(SFSDKAuthRequest *)defaultAuthRequestWithLoginHost:(nullable NSString *)loginHost { SFSDKAuthRequest *request = [[SFSDKAuthRequest alloc] init]; request.loginHost = loginHost != nil ? loginHost : self.loginHost; request.additionalOAuthParameterKeys = self.additionalOAuthParameterKeys; request.loginViewControllerConfig = self.loginViewControllerConfig; request.brandLoginPath = self.brandLoginPath; - request.oauthClientId = appConfig != nil ? appConfig.remoteAccessConsumerKey : self.oauthClientId; - request.oauthCompletionUrl = appConfig != nil ? appConfig.oauthRedirectURI : self.oauthCompletionUrl; - request.scopes = appConfig != nil ? appConfig.oauthScopes : self.scopes; + request.oauthClientId = self.oauthClientId; + request.oauthCompletionUrl = self.oauthCompletionUrl; + request.scopes = self.scopes; request.retryLoginAfterFailure = self.retryLoginAfterFailure; request.useBrowserAuth = self.useBrowserAuth; request.spAppLoginFlowSelectionAction = self.idpLoginFlowSelectionAction; @@ -574,7 +570,7 @@ -(SFSDKAuthRequest *)authRequestWithLoginHost:(nullable NSString *)loginHost } -(SFSDKAuthRequest *)defaultAuthRequest { - return [self authRequestWithLoginHost:nil appConfig:nil]; + return [self defaultAuthRequestWithLoginHost:nil]; } -(SFSDKAuthRequest *)nativeLoginAuthRequest { @@ -617,9 +613,16 @@ - (BOOL)authenticateWithRequest:(SFSDKAuthRequest *)request dispatch_async(dispatch_get_main_queue(), ^{ [SFSDKWebViewStateManager removeSessionForcefullyWithCompletionHandler:^{ - [authSession.oauthCoordinator authenticateWithCredentials:authSession.credentials]; + // Get app config for the login host. If appConfigRuntimeSelectorBlock is set, + // it will be invoked to select the appropriate config. Otherwise, returns the default appConfig. + [[SalesforceSDKManager sharedManager] appConfigForLoginHost:request.loginHost callback:^(SFSDKAppConfig* appConfig) { + authSession.credentials.clientId = appConfig.remoteAccessConsumerKey; + authSession.credentials.redirectUri = appConfig.oauthRedirectURI; + authSession.credentials.scopes = [appConfig.oauthScopes allObjects]; + [authSession.oauthCoordinator authenticateWithCredentials:authSession.credentials]; + }]; }]; - + }); return self.authSessions[sceneId].isAuthenticating; } diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Util/SalesforceSDKCoreDefines.h b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Util/SalesforceSDKCoreDefines.h index b5162e3085..c9a2500339 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Util/SalesforceSDKCoreDefines.h +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Util/SalesforceSDKCoreDefines.h @@ -57,7 +57,8 @@ typedef UIViewController*_Nonnull (^SFIDPUserSelectionBl /** Block to select an app config at runtime based on the login host. + The block takes a login host and a callback. The callback should be invoked with the selected app config. */ - typedef SFSDKAppConfig* _Nullable (^SFSDKAppConfigRuntimeSelectorBlock)(NSString * _Nonnull loginHost) NS_SWIFT_NAME(BootConfigRuntimeSelector); + typedef void (^SFSDKAppConfigRuntimeSelectorBlock)(NSString * _Nonnull loginHost, void (^_Nonnull callback)(SFSDKAppConfig * _Nullable)) NS_SWIFT_NAME(BootConfigRuntimeSelector); NS_ASSUME_NONNULL_END diff --git a/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SFUserAccountManagerTests.m b/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SFUserAccountManagerTests.m index c6344ffb80..057823ade9 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SFUserAccountManagerTests.m +++ b/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SFUserAccountManagerTests.m @@ -664,96 +664,4 @@ - (void)testUserAccountEncoding { XCTAssertEqual(userIn.accessRestrictions, userOut.accessRestrictions, @"accessRestrictions mismatch"); } -#pragma mark - authRequestWithLoginHost Tests - -- (void)testAuthRequestWithLoginHostDefaults { - // Setup default values - NSString *expectedLoginHost = @"https://login.salesforce.com"; - NSString *expectedClientId = @"testClientId"; - NSString *expectedRedirectUri = @"testapp://oauth/done"; - NSSet *expectedScopes = [NSSet setWithObjects:@"api", @"web", @"refresh_token", nil]; - - self.uam.loginHost = expectedLoginHost; - self.uam.oauthClientId = expectedClientId; - self.uam.oauthCompletionUrl = expectedRedirectUri; - self.uam.scopes = expectedScopes; - - // Call with nil loginHost and nil appConfig (should use defaults) - SFSDKAuthRequest *request = [self.uam authRequestWithLoginHost:nil appConfig:nil]; - - XCTAssertNotNil(request, @"Auth request should not be nil"); - XCTAssertEqualObjects(request.loginHost, expectedLoginHost, @"Login host should match default"); - XCTAssertEqualObjects(request.oauthClientId, expectedClientId, @"Client ID should match default"); - XCTAssertEqualObjects(request.oauthCompletionUrl, expectedRedirectUri, @"Redirect URI should match default"); - XCTAssertEqualObjects(request.scopes, expectedScopes, @"Scopes should match default"); -} - -- (void)testAuthRequestWithLoginHostCustomHost { - // Setup default values - NSString *defaultLoginHost = @"https://login.salesforce.com"; - NSString *customLoginHost = @"https://test.salesforce.com"; - NSString *expectedClientId = @"testClientId"; - - self.uam.loginHost = defaultLoginHost; - self.uam.oauthClientId = expectedClientId; - - // Call with custom loginHost - SFSDKAuthRequest *request = [self.uam authRequestWithLoginHost:customLoginHost appConfig:nil]; - - XCTAssertNotNil(request, @"Auth request should not be nil"); - XCTAssertEqualObjects(request.loginHost, customLoginHost, @"Login host should be custom value, not default"); - XCTAssertEqualObjects(request.oauthClientId, expectedClientId, @"Client ID should match default"); -} - -- (void)testAuthRequestWithLoginHostWithAppConfig { - // Setup default values - NSString *defaultLoginHost = @"https://login.salesforce.com"; - NSString *defaultClientId = @"defaultClientId"; - NSString *defaultRedirectUri = @"defaultapp://oauth/done"; - NSSet *defaultScopes = [NSSet setWithObjects:@"api", @"web", nil]; - - self.uam.loginHost = defaultLoginHost; - self.uam.oauthClientId = defaultClientId; - self.uam.oauthCompletionUrl = defaultRedirectUri; - self.uam.scopes = defaultScopes; - - // Create app config with different values - NSString *appConfigClientId = @"appConfigClientId"; - NSString *appConfigRedirectUri = @"appconfig://oauth/done"; - NSSet *appConfigScopes = [NSSet setWithObjects:@"id", @"api", @"refresh_token", nil]; - - NSDictionary *configDict = @{ - @"remoteAccessConsumerKey": appConfigClientId, - @"oauthRedirectURI": appConfigRedirectUri, - @"oauthScopes": [appConfigScopes allObjects], - @"shouldAuthenticate": @YES - }; - SFSDKAppConfig *appConfig = [[SFSDKAppConfig alloc] initWithDict:configDict]; - - // Call with appConfig - SFSDKAuthRequest *request = [self.uam authRequestWithLoginHost:nil appConfig:appConfig]; - - XCTAssertNotNil(request, @"Auth request should not be nil"); - XCTAssertEqualObjects(request.loginHost, defaultLoginHost, @"Login host should match default"); - XCTAssertEqualObjects(request.oauthClientId, appConfigClientId, @"Client ID should come from appConfig"); - XCTAssertEqualObjects(request.oauthCompletionUrl, appConfigRedirectUri, @"Redirect URI should come from appConfig"); - XCTAssertEqualObjects(request.scopes, appConfigScopes, @"Scopes should come from appConfig"); -} - -- (void)testDefaultAuthRequestUsesAuthRequestWithLoginHost { - // Setup default values - NSString *expectedLoginHost = @"https://login.salesforce.com"; - NSString *expectedClientId = @"testClientId"; - - self.uam.loginHost = expectedLoginHost; - self.uam.oauthClientId = expectedClientId; - - // defaultAuthRequest should call authRequestWithLoginHost:nil appConfig:nil - SFSDKAuthRequest *request = [self.uam defaultAuthRequest]; - - XCTAssertNotNil(request, @"Default auth request should not be nil"); - XCTAssertEqualObjects(request.loginHost, expectedLoginHost, @"Login host should match default"); - XCTAssertEqualObjects(request.oauthClientId, expectedClientId, @"Client ID should match default"); -} - @end diff --git a/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SalesforceSDKManagerTests.m b/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SalesforceSDKManagerTests.m index dcb50bfeca..f592a8c540 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SalesforceSDKManagerTests.m +++ b/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SalesforceSDKManagerTests.m @@ -487,20 +487,40 @@ - (void)compareAppNames:(NSString *)expectedAppName #pragma mark - Runtime Selected App Config Tests -- (void)testRuntimeSelectedAppConfigReturnsNilWhenBlockNotSet { +- (void)verifyAppConfigForLoginHost:(NSString *)loginHost + description:(NSString *)description + assertions:(void (^)(SFSDKAppConfig *config))assertions { + XCTestExpectation *expectation = [self expectationWithDescription:description]; + [[SalesforceSDKManager sharedManager] appConfigForLoginHost:loginHost callback:^(SFSDKAppConfig *config) { + assertions(config); + [expectation fulfill]; + }]; + [self waitForExpectations:@[expectation] timeout:1.0]; +} + +- (void)testAppConfigForLoginHostReturnsDefaultWhenBlockNotSet { // Clear any existing block [SalesforceSDKManager sharedManager].appConfigRuntimeSelectorBlock = nil; - // Call with nil loginHost - SFSDKAppConfig *config = [[SalesforceSDKManager sharedManager] runtimeSelectedAppConfig:nil]; - XCTAssertNil(config, @"Should return nil when no selector block is set"); + // Get the default app config for comparison + SFSDKAppConfig *defaultConfig = [SalesforceSDKManager sharedManager].appConfig; + + // Test with nil loginHost - should return default config + [self verifyAppConfigForLoginHost:nil + description:@"Callback should be called with default config" + assertions:^(SFSDKAppConfig *config) { + XCTAssertEqual(config, defaultConfig, @"Should return default appConfig when no selector block is set"); + }]; - // Call with a loginHost - config = [[SalesforceSDKManager sharedManager] runtimeSelectedAppConfig:@"https://test.salesforce.com"]; - XCTAssertNil(config, @"Should return nil when no selector block is set, regardless of loginHost"); + // Test with a loginHost - should still return default config + [self verifyAppConfigForLoginHost:@"https://test.salesforce.com" + description:@"Callback should be called with default config" + assertions:^(SFSDKAppConfig *config) { + XCTAssertEqual(config, defaultConfig, @"Should return default appConfig when no selector block is set, regardless of loginHost"); + }]; } -- (void)testRuntimeSelectedAppConfigWithDifferentLoginHosts { +- (void)testAppConfigForLoginHostWithDifferentLoginHosts { NSString *loginHost1 = @"https://login.salesforce.com"; NSString *loginHost2 = @"https://test.salesforce.com"; @@ -518,43 +538,65 @@ - (void)testRuntimeSelectedAppConfigWithDifferentLoginHosts { }; SFSDKAppConfig *config2 = [[SFSDKAppConfig alloc] initWithDict:config2Dict]; + // Get the default app config for comparison + SFSDKAppConfig *defaultConfig = [SalesforceSDKManager sharedManager].appConfig; + // Set the selector block to return different configs based on loginHost - [SalesforceSDKManager sharedManager].appConfigRuntimeSelectorBlock = ^SFSDKAppConfig *(NSString *loginHost) { + [SalesforceSDKManager sharedManager].appConfigRuntimeSelectorBlock = ^(NSString *loginHost, void (^callback)(SFSDKAppConfig *)) { if ([loginHost isEqualToString:loginHost1]) { - return config1; + callback(config1); } else if ([loginHost isEqualToString:loginHost2]) { - return config2; + callback(config2); + } else { + callback(nil); } - return nil; }; // Test first loginHost - SFSDKAppConfig *result1 = [[SalesforceSDKManager sharedManager] runtimeSelectedAppConfig:loginHost1]; - XCTAssertNotNil(result1, @"Should return config for loginHost1"); - XCTAssertEqual(result1, config1, @"Should return config1 for loginHost1"); - XCTAssertEqualObjects(result1.remoteAccessConsumerKey, @"clientId1", @"Should have correct client ID for config1"); + [self verifyAppConfigForLoginHost:loginHost1 + description:@"First callback should be called" + assertions:^(SFSDKAppConfig *result1) { + XCTAssertNotNil(result1, @"Should return config for loginHost1"); + XCTAssertEqual(result1, config1, @"Should return config1 for loginHost1"); + XCTAssertEqualObjects(result1.remoteAccessConsumerKey, @"clientId1", @"Should have correct client ID for config1"); + }]; // Test second loginHost - SFSDKAppConfig *result2 = [[SalesforceSDKManager sharedManager] runtimeSelectedAppConfig:loginHost2]; - XCTAssertNotNil(result2, @"Should return config for loginHost2"); - XCTAssertEqual(result2, config2, @"Should return config2 for loginHost2"); - XCTAssertEqualObjects(result2.remoteAccessConsumerKey, @"clientId2", @"Should have correct client ID for config2"); + [self verifyAppConfigForLoginHost:loginHost2 + description:@"Second callback should be called" + assertions:^(SFSDKAppConfig *result2) { + XCTAssertNotNil(result2, @"Should return config for loginHost2"); + XCTAssertEqual(result2, config2, @"Should return config2 for loginHost2"); + XCTAssertEqualObjects(result2.remoteAccessConsumerKey, @"clientId2", @"Should have correct client ID for config2"); + }]; + + // Test with nil loginHost - should return default config + [self verifyAppConfigForLoginHost:nil + description:@"Callback should be called with default config" + assertions:^(SFSDKAppConfig *config) { + XCTAssertEqual(config, defaultConfig, @"Should return default appConfig when nil loginHost is passed"); + }]; } -- (void)testRuntimeSelectedAppConfigBlockReturnsNil { +- (void)testAppConfigForLoginHostReturnsDefaultWhenBlockReturnsNil { __block BOOL blockWasCalled = NO; - // Set the selector block to return nil - [SalesforceSDKManager sharedManager].appConfigRuntimeSelectorBlock = ^SFSDKAppConfig *(NSString *loginHost) { + // Get the default app config for comparison + SFSDKAppConfig *defaultConfig = [SalesforceSDKManager sharedManager].appConfig; + + // Set the selector block to return nil via callback + [SalesforceSDKManager sharedManager].appConfigRuntimeSelectorBlock = ^(NSString *loginHost, void (^callback)(SFSDKAppConfig *)) { blockWasCalled = YES; - return nil; + callback(nil); }; - // Call the method - SFSDKAppConfig *config = [[SalesforceSDKManager sharedManager] runtimeSelectedAppConfig:@"https://test.salesforce.com"]; - - XCTAssertTrue(blockWasCalled, @"Block should have been called"); - XCTAssertNil(config, @"Should return nil when block returns nil"); + // Call the method - should fall back to default config even though block returns nil + [self verifyAppConfigForLoginHost:@"https://test.salesforce.com" + description:@"Callback should be called" + assertions:^(SFSDKAppConfig *config) { + XCTAssertTrue(blockWasCalled, @"Block should have been called"); + XCTAssertEqual(config, defaultConfig, @"Should return default appConfig when block returns nil"); + }]; } @end diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/ConfigPickerViewController.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/ConfigPickerViewController.swift index 2bfd69c789..60239f26e9 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/ConfigPickerViewController.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/ConfigPickerViewController.swift @@ -160,7 +160,7 @@ struct ConfigPickerView: View { private func handleDynamicBootconfig() { isLoading = true - SalesforceManager.shared.bootConfigRuntimeSelector = { _ in + SalesforceManager.shared.bootConfigRuntimeSelector = { _, callback in // Create dynamic BootConfig from user-entered values // Parse scopes from space-separated string let scopesArray = self.dynamicScopes @@ -179,7 +179,7 @@ struct ConfigPickerView: View { configDict["oauthScopes"] = scopesArray } - return BootConfig(configDict) + callback(BootConfig(configDict)) } // Proceed with login From 2cdd53a6e795b54f2fafe28827e8845ace483459 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Tue, 28 Oct 2025 10:45:31 -0700 Subject: [PATCH 17/19] Taking out one of the new tests - for now It's not high value and we will be back in that code when we will add support for refresh token migration --- .../AuthFlowTesterAuthenticatedUITests.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterAuthenticatedUITests.swift b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterAuthenticatedUITests.swift index 0c5c932b5d..001d9ef47d 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterAuthenticatedUITests.swift +++ b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterAuthenticatedUITests.swift @@ -60,15 +60,15 @@ final class AuthFlowTesterAuthenticatedUITests: XCTestCase { // MARK: - Session Detail Screen Tests - func testSessionDetailScreenIsVisible() throws { - // Verify we're on the session detail screen (not config picker) - XCTAssertTrue(app.navigationBars["AuthFlowTester"].waitForExistence(timeout: 10)) - - // Should see authenticated UI elements - XCTAssertTrue(app.buttons["Revoke Access Token"].waitForExistence(timeout: 5)) - XCTAssertTrue(app.buttons["Make REST API Request"].waitForExistence(timeout: 5)) - - } +// func testSessionDetailScreenIsVisible() throws { +// // Verify we're on the session detail screen (not config picker) +// XCTAssertTrue(app.navigationBars["AuthFlowTester"].waitForExistence(timeout: 10)) +// +// // Should see authenticated UI elements +// XCTAssertTrue(app.buttons["Revoke Access Token"].waitForExistence(timeout: 5)) +// XCTAssertTrue(app.buttons["Make REST API Request"].waitForExistence(timeout: 5)) +// +// } // // TODO write more tests From 485997a36c7bca361cba14b64c4d0efed84bde39 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Thu, 30 Oct 2025 10:43:49 -0700 Subject: [PATCH 18/19] Removing keychain group Have the default bootconfig be the "old" one (opaque token) and the dynamic one be the "new" one (jwt based token) --- .../Supporting Files/AuthFlowTester.entitlements | 4 +--- .../AuthFlowTester/Supporting Files/bootconfig.plist | 4 ++-- .../AuthFlowTester/Supporting Files/bootconfig2.plist | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/AuthFlowTester.entitlements b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/AuthFlowTester.entitlements index b9d14fee5b..fbad02378c 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/AuthFlowTester.entitlements +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/AuthFlowTester.entitlements @@ -3,8 +3,6 @@ keychain-access-groups - - $(AppIdentifierPrefix)com.salesforce.mobilesdk.sample.authflowtester - + diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig.plist b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig.plist index 927fefb46a..16e10f7aec 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig.plist +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig.plist @@ -3,9 +3,9 @@ remoteAccessConsumerKey - 3MVG9SemV5D80oBcXZ2EUzbcJw6aYF3RcTY1FgYlgnWAA72zHubsit4NKIA.DNcINzbDjz23yUJP3ucnY99F6 + 3MVG9SemV5D80oBcXZ2EUzbcJwzJCe2n4LaHH_Z2JSpIJqJ1MzFK_XRlHrupqNdeus8.NRonkpx0sAAWKzfK8 oauthRedirectURI - testerjwt://mobilesdk/done + testeropaque://mobilesdk/done shouldAuthenticate diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig2.plist b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig2.plist index 16e10f7aec..927fefb46a 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig2.plist +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig2.plist @@ -3,9 +3,9 @@ remoteAccessConsumerKey - 3MVG9SemV5D80oBcXZ2EUzbcJwzJCe2n4LaHH_Z2JSpIJqJ1MzFK_XRlHrupqNdeus8.NRonkpx0sAAWKzfK8 + 3MVG9SemV5D80oBcXZ2EUzbcJw6aYF3RcTY1FgYlgnWAA72zHubsit4NKIA.DNcINzbDjz23yUJP3ucnY99F6 oauthRedirectURI - testeropaque://mobilesdk/done + testerjwt://mobilesdk/done shouldAuthenticate From 90df65a20a4092f4e5cda15da46a97d1be4339eb Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Mon, 3 Nov 2025 11:49:57 -0800 Subject: [PATCH 19/19] Removing UI tests for the new test app AuthFlowTester We are planning to build robust UI tests that will leverage AuthFlowTester to test various login flows etc --- .github/DangerFiles/TestOrchestrator.rb | 15 +- .github/workflows/nightly.yaml | 2 +- .../AuthFlowTester.xcodeproj/project.pbxproj | 137 ---------- .../AuthFlowTesterAuthenticatedUITests.swift | 78 ------ ...AuthFlowTesterUnauthenticatedUITests.swift | 174 ------------- .../AuthFlowTesterUITests/TestHelper.swift | 233 ------------------ 6 files changed, 4 insertions(+), 635 deletions(-) delete mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterAuthenticatedUITests.swift delete mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterUnauthenticatedUITests.swift delete mode 100644 native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/TestHelper.swift diff --git a/.github/DangerFiles/TestOrchestrator.rb b/.github/DangerFiles/TestOrchestrator.rb index c8765148c1..569539b346 100644 --- a/.github/DangerFiles/TestOrchestrator.rb +++ b/.github/DangerFiles/TestOrchestrator.rb @@ -7,22 +7,13 @@ fail("Please re-submit this PR to the dev branch, we may have already fixed your issue.", sticky: true) if github.branch_for_base != "dev" # List of supported xcode schemes for testing -SCHEMES = ['SalesforceSDKCommon', 'SalesforceAnalytics', 'SalesforceSDKCore', 'SmartStore', 'MobileSync', 'AuthFlowTester'] +SCHEMES = ['SalesforceSDKCommon', 'SalesforceAnalytics', 'SalesforceSDKCore', 'SmartStore', 'MobileSync'] modifed_libs = Set[] for file in (git.modified_files + git.added_files); - scheme = nil - - # Check if SDK libs are modified - if file.include?("libs/") - scheme = file.split("libs/").last.split("/").first - # Check if sample apps are modified - elsif file.include?("native/SampleApps/") - scheme = file.split("native/SampleApps/").last.split("/").first - end - - if scheme && SCHEMES.include?(scheme) + scheme = file.split("libs/").last.split("/").first + if SCHEMES.include?(scheme) modifed_libs.add(scheme) end end diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml index 43d9c65ff9..6342db1455 100644 --- a/.github/workflows/nightly.yaml +++ b/.github/workflows/nightly.yaml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - lib: [SalesforceSDKCommon, SalesforceAnalytics, SalesforceSDKCore, SmartStore, MobileSync, AuthFlowTester] + lib: [SalesforceSDKCommon, SalesforceAnalytics, SalesforceSDKCore, SmartStore, MobileSync] ios: [^26, ^18, ^17] include: - ios: ^26 diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj b/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj index 8ca0e35c23..43856d0a19 100644 --- a/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj +++ b/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj @@ -7,10 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - 4F1A8CCA2EAB16450037DC89 /* bootconfig.plist in Resources */ = {isa = PBXBuildFile; fileRef = AUTH010 /* bootconfig.plist */; }; - 4F1A8CCB2EAB16490037DC89 /* bootconfig2.plist in Resources */ = {isa = PBXBuildFile; fileRef = AUTH036 /* bootconfig2.plist */; }; 4F1A8CCD2EAFEA7C0037DC89 /* BootConfigEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F1A8CCC2EAFEA7C0037DC89 /* BootConfigEditor.swift */; }; - 4F95A89B2EAAD00000000001 /* test_credentials.json in Resources */ = {isa = PBXBuildFile; fileRef = 4F95A89A2EAAD00000000001 /* test_credentials.json */; }; 4F95A89C2EA806E700C98D18 /* SalesforceAnalytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F95A8962EA801DC00C98D18 /* SalesforceAnalytics.framework */; }; 4F95A89D2EA806E700C98D18 /* SalesforceAnalytics.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4F95A8962EA801DC00C98D18 /* SalesforceAnalytics.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4F95A89F2EA806E900C98D18 /* SalesforceSDKCommon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F95A8982EA801DC00C98D18 /* SalesforceSDKCommon.framework */; }; @@ -34,16 +31,6 @@ AUTH053 /* FlowTypesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AUTH054 /* FlowTypesView.swift */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - 4FEBAF342EAAC98E00D4880A /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = AUTH027 /* Project object */; - proxyType = 1; - remoteGlobalIDString = AUTH023; - remoteInfo = AuthFlowTester; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXCopyFilesBuildPhase section */ 4F95A89E2EA806E700C98D18 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -63,9 +50,7 @@ 4F95A8962EA801DC00C98D18 /* SalesforceAnalytics.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SalesforceAnalytics.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4F95A8982EA801DC00C98D18 /* SalesforceSDKCommon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SalesforceSDKCommon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4F95A89A2EA801DC00C98D18 /* SalesforceSDKCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SalesforceSDKCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 4F95A89A2EAAD00000000001 /* test_credentials.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = test_credentials.json; path = ../../../../shared/test/test_credentials.json; sourceTree = ""; }; 4FEBAF282EA9B91500D4880A /* RevokeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevokeView.swift; sourceTree = ""; }; - 4FEBAF2E2EAAC98E00D4880A /* AuthFlowTesterUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AuthFlowTesterUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; AUTH002 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; AUTH004 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; AUTH006 /* ConfigPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigPickerViewController.swift; sourceTree = ""; }; @@ -85,20 +70,7 @@ AUTH054 /* FlowTypesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowTypesView.swift; sourceTree = ""; }; /* End PBXFileReference section */ -/* Begin PBXFileSystemSynchronizedRootGroup section */ - 4FEBAF2F2EAAC98E00D4880A /* AuthFlowTesterUITests */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = AuthFlowTesterUITests; - sourceTree = ""; - }; -/* End PBXFileSystemSynchronizedRootGroup section */ - /* Begin PBXFrameworksBuildPhase section */ - 4FEBAF2B2EAAC98E00D4880A /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - files = ( - ); - }; AUTH018 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; files = ( @@ -120,15 +92,6 @@ name = Frameworks; sourceTree = ""; }; - 4FEBAF2F2EAAD00000000002 /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 4F95A89A2EAAD00000000001 /* test_credentials.json */, - ); - name = "Supporting Files"; - path = AuthFlowTesterUITests; - sourceTree = ""; - }; 4FF0EF9E2EA8568C005E4474 /* Supporting Files */ = { isa = PBXGroup; children = ( @@ -145,8 +108,6 @@ isa = PBXGroup; children = ( AUTH020 /* AuthFlowTester */, - 4FEBAF2F2EAAC98E00D4880A /* AuthFlowTesterUITests */, - 4FEBAF2F2EAAD00000000002 /* Supporting Files */, 4F95A8952EA801DC00C98D18 /* Frameworks */, AUTH021 /* Products */, ); @@ -168,7 +129,6 @@ isa = PBXGroup; children = ( AUTH017 /* AuthFlowTester.app */, - 4FEBAF2E2EAAC98E00D4880A /* AuthFlowTesterUITests.xctest */, ); name = Products; sourceTree = ""; @@ -217,27 +177,6 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 4FEBAF2D2EAAC98E00D4880A /* AuthFlowTesterUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 4FEBAF362EAAC98E00D4880A /* Build configuration list for PBXNativeTarget "AuthFlowTesterUITests" */; - buildPhases = ( - 4FEBAF2A2EAAC98E00D4880A /* Sources */, - 4FEBAF2B2EAAC98E00D4880A /* Frameworks */, - 4FEBAF2C2EAAC98E00D4880A /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 4FEBAF352EAAC98E00D4880A /* PBXTargetDependency */, - ); - fileSystemSynchronizedGroups = ( - 4FEBAF2F2EAAC98E00D4880A /* AuthFlowTesterUITests */, - ); - name = AuthFlowTesterUITests; - productName = AuthFlowTesterUITests; - productReference = 4FEBAF2E2EAAC98E00D4880A /* AuthFlowTesterUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; AUTH023 /* AuthFlowTester */ = { isa = PBXNativeTarget; buildConfigurationList = AUTH024 /* Build configuration list for PBXNativeTarget "AuthFlowTester" */; @@ -264,10 +203,6 @@ LastSwiftUpdateCheck = 2600; LastUpgradeCheck = 2600; TargetAttributes = { - 4FEBAF2D2EAAC98E00D4880A = { - CreatedOnToolsVersion = 26.0.1; - TestTargetID = AUTH023; - }; AUTH023 = { CreatedOnToolsVersion = 15.0; }; @@ -287,20 +222,11 @@ projectRoot = ""; targets = ( AUTH023 /* AuthFlowTester */, - 4FEBAF2D2EAAC98E00D4880A /* AuthFlowTesterUITests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 4FEBAF2C2EAAC98E00D4880A /* Resources */ = { - isa = PBXResourcesBuildPhase; - files = ( - 4F95A89B2EAAD00000000001 /* test_credentials.json in Resources */, - 4F1A8CCA2EAB16450037DC89 /* bootconfig.plist in Resources */, - 4F1A8CCB2EAB16490037DC89 /* bootconfig2.plist in Resources */, - ); - }; AUTH026 /* Resources */ = { isa = PBXResourcesBuildPhase; files = ( @@ -313,11 +239,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 4FEBAF2A2EAAC98E00D4880A /* Sources */ = { - isa = PBXSourcesBuildPhase; - files = ( - ); - }; AUTH025 /* Sources */ = { isa = PBXSourcesBuildPhase; files = ( @@ -337,57 +258,7 @@ }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - 4FEBAF352EAAC98E00D4880A /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = AUTH023 /* AuthFlowTester */; - targetProxy = 4FEBAF342EAAC98E00D4880A /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin XCBuildConfiguration section */ - 4FEBAF372EAAC98E00D4880A /* Debug configuration for PBXNativeTarget "AuthFlowTesterUITests" */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = XD7TD9S6ZU; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.6; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.salesforce.salesforce.AuthFlowTesterUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - STRING_CATALOG_GENERATE_SYMBOLS = NO; - SWIFT_APPROACHABLE_CONCURRENCY = YES; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = AuthFlowTester; - }; - name = Debug; - }; - 4FEBAF382EAAC98E00D4880A /* Release configuration for PBXNativeTarget "AuthFlowTesterUITests" */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = XD7TD9S6ZU; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.6; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.salesforce.salesforce.AuthFlowTesterUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - STRING_CATALOG_GENERATE_SYMBOLS = NO; - SWIFT_APPROACHABLE_CONCURRENCY = YES; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = AuthFlowTester; - }; - name = Release; - }; AUTH029 /* Debug configuration for PBXProject "AuthFlowTester" */ = { isa = XCBuildConfiguration; buildSettings = { @@ -572,14 +443,6 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 4FEBAF362EAAC98E00D4880A /* Build configuration list for PBXNativeTarget "AuthFlowTesterUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 4FEBAF372EAAC98E00D4880A /* Debug configuration for PBXNativeTarget "AuthFlowTesterUITests" */, - 4FEBAF382EAAC98E00D4880A /* Release configuration for PBXNativeTarget "AuthFlowTesterUITests" */, - ); - defaultConfigurationName = Release; - }; AUTH024 /* Build configuration list for PBXNativeTarget "AuthFlowTester" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterAuthenticatedUITests.swift b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterAuthenticatedUITests.swift deleted file mode 100644 index 001d9ef47d..0000000000 --- a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterAuthenticatedUITests.swift +++ /dev/null @@ -1,78 +0,0 @@ -/* - Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. - - Redistribution and use of this software in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list of conditions - and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of - conditions and the following disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to - endorse or promote products derived from this software without specific prior written - permission of salesforce.com, inc. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR - IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY - WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import XCTest - -/// Tests for the authenticated state - Session Detail screen -/// These tests require test_credentials.json to be properly configured -final class AuthFlowTesterAuthenticatedUITests: XCTestCase { - - var app: XCUIApplication! - - // Expected values loaded from test_credentials.json - var expectedUsername: String! - var expectedInstanceUrl: String! - var expectedConsumerKey: String! - var expectedRedirectURI: String! - var expectedDisplayName: String! - var expectedScopes: String! - - override func setUpWithError() throws { - continueAfterFailure = false - app = XCUIApplication() - - // Load test credentials from test_credentials.json - let credentials = try TestCredentials.loadFromBundle() - expectedUsername = credentials.username - expectedInstanceUrl = credentials.instanceUrl - expectedConsumerKey = credentials.clientId - expectedRedirectURI = credentials.redirectUri - expectedDisplayName = credentials.displayName - expectedScopes = credentials.scopes - - try TestHelper.launchWithCredentials(app) - } - - override func tearDownWithError() throws { - app = nil - } - - // MARK: - Session Detail Screen Tests - -// func testSessionDetailScreenIsVisible() throws { -// // Verify we're on the session detail screen (not config picker) -// XCTAssertTrue(app.navigationBars["AuthFlowTester"].waitForExistence(timeout: 10)) -// -// // Should see authenticated UI elements -// XCTAssertTrue(app.buttons["Revoke Access Token"].waitForExistence(timeout: 5)) -// XCTAssertTrue(app.buttons["Make REST API Request"].waitForExistence(timeout: 5)) -// -// } - - // - // TODO write more tests - // -} - - diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterUnauthenticatedUITests.swift b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterUnauthenticatedUITests.swift deleted file mode 100644 index 0b1662e639..0000000000 --- a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/AuthFlowTesterUnauthenticatedUITests.swift +++ /dev/null @@ -1,174 +0,0 @@ -/* - Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. - - Redistribution and use of this software in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list of conditions - and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of - conditions and the following disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to - endorse or promote products derived from this software without specific prior written - permission of salesforce.com, inc. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR - IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY - WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import XCTest - -/// Tests for the unauthenticated state - Config Picker screen -final class AuthFlowTesterUnauthenticatedUITests: XCTestCase { - - var app: XCUIApplication! - - // Expected values loaded from bootconfig.plist and bootconfig2.plist - var bootconfigConsumerKey: String! - var bootconfigRedirectURI: String! - var bootconfigScopes: String! - var bootconfig2ConsumerKey: String! - var bootconfig2RedirectURI: String! - var bootconfig2Scopes: String! - - override func setUpWithError() throws { - continueAfterFailure = false - app = XCUIApplication() - - // Load expected values from bootconfig files in the test bundle - let bootconfig = try TestHelper.loadBootConfig(named: "bootconfig") - let bootconfig2 = try TestHelper.loadBootConfig(named: "bootconfig2") - bootconfigConsumerKey = bootconfig.consumerKey - bootconfigRedirectURI = bootconfig.redirectURI - bootconfigScopes = bootconfig.scopes - bootconfig2ConsumerKey = bootconfig2.consumerKey - bootconfig2RedirectURI = bootconfig2.redirectURI - bootconfig2Scopes = bootconfig2.scopes - - TestHelper.launchWithoutCredentials(app) - } - - override func tearDownWithError() throws { - app = nil - } - - // MARK: - Screen visible test - func testConfigPickerScreenIsVisible() throws { - // Navigation title - XCTAssertTrue(app.navigationBars["AuthFlowTester"].waitForExistence(timeout: 5)) - // Primary actions - XCTAssertTrue(app.buttons["Use static config"].waitForExistence(timeout: 5)) - XCTAssertTrue(app.buttons["Use dynamic config"].exists) - } - - // MARK: - Static Configuration Test - - func testStaticConfigSection() throws { - let staticConfigHeader = app.buttons["Static Configuration"] - XCTAssertTrue(staticConfigHeader.waitForExistence(timeout: 5), "Static Configuration header should exist") - - let useStaticConfigButton = app.buttons["Use static config"] - XCTAssertTrue(useStaticConfigButton.waitForExistence(timeout: 5), "Use static config button should exist") - XCTAssertTrue(useStaticConfigButton.isEnabled, "Use static config button should be enabled") - - // Initially collapsed - fields should not be visible - XCTAssertFalse(app.staticTexts["Consumer Key:"].exists, "Consumer Key label should not be visible when collapsed") - - // Tap to expand - staticConfigHeader.tap() - - // Fields should now be visible - wait for animation - XCTAssertTrue(app.staticTexts["Consumer Key:"].waitForExistence(timeout: 3), "Consumer Key label should appear when expanded") - XCTAssertTrue(app.staticTexts["Callback URL:"].exists, "Callback URL label should exist") - XCTAssertTrue(app.staticTexts["Scopes (space-separated):"].exists, "Scopes label should exist") - - // Check text fields exist and have correct values - let consumerKeyField = app.textFields["consumerKeyTextField"] - XCTAssertTrue(consumerKeyField.waitForExistence(timeout: 3), "Consumer key text field should exist") - XCTAssertEqual(consumerKeyField.value as? String, bootconfigConsumerKey, "Consumer key should match boot config") - - let callbackUrlField = app.textFields["callbackUrlTextField"] - XCTAssertTrue(callbackUrlField.exists, "Callback URL text field should exist") - XCTAssertEqual(callbackUrlField.value as? String, bootconfigRedirectURI, "Callback URL should match boot config") - - let scopesField = app.textFields["scopesTextField"] - XCTAssertTrue(scopesField.exists, "Scopes text field should exist") - // Scopes text field will be empty if no scopes configured - let scopesValue = scopesField.value as? String - if bootconfigScopes == "(none)" { - XCTAssert(scopesValue == "" || scopesValue == nil || scopesValue == "e.g. id api refresh_token", - "Scopes should be empty or placeholder when not configured, got: '\(String(describing: scopesValue))'") - } else { - XCTAssertEqual(scopesValue, bootconfigScopes, "Scopes should match boot config") - } - - // Tap to collapse - staticConfigHeader.tap() - - // Fields should be hidden again - XCTAssertFalse(app.staticTexts["Consumer Key:"].waitForExistence(timeout: 2), "Consumer Key label should be hidden when collapsed") - } - - - // MARK: - Dynamic Configuration Test - - func testDynamicConfigSection() throws { - let dynamicConfigHeader = app.buttons["Dynamic Configuration"] - XCTAssertTrue(dynamicConfigHeader.waitForExistence(timeout: 5), "Dynamic Configuration header should exist") - - let useDynamicConfigButton = app.buttons["Use dynamic config"] - XCTAssertTrue(useDynamicConfigButton.waitForExistence(timeout: 5), "Use dynamic config button should exist") - XCTAssertTrue(useDynamicConfigButton.isEnabled, "Use dynamic config button should be enabled") - - // Initially collapsed - fields should not be visible - XCTAssertFalse(app.staticTexts["Consumer Key:"].exists, "Consumer Key label should not be visible when collapsed") - - // Tap to expand - dynamicConfigHeader.tap() - - // Fields should now be visible - wait for animation - XCTAssertTrue(app.staticTexts["Consumer Key:"].waitForExistence(timeout: 3), "Consumer Key label should appear when expanded") - XCTAssertTrue(app.staticTexts["Callback URL:"].exists, "Callback URL label should exist") - XCTAssertTrue(app.staticTexts["Scopes (space-separated):"].exists, "Scopes label should exist") - - // Check text fields exist and have correct values - let consumerKeyField = app.textFields["consumerKeyTextField"] - XCTAssertTrue(consumerKeyField.waitForExistence(timeout: 3), "Consumer key text field should exist") - XCTAssertEqual(consumerKeyField.value as? String, bootconfig2ConsumerKey, "Consumer key should match bootconfig2") - - let callbackUrlField = app.textFields["callbackUrlTextField"] - XCTAssertTrue(callbackUrlField.exists, "Callback URL text field should exist") - XCTAssertEqual(callbackUrlField.value as? String, bootconfig2RedirectURI, "Callback URL should match bootconfig2") - - let scopesField = app.textFields["scopesTextField"] - XCTAssertTrue(scopesField.exists, "Scopes text field should exist") - // Scopes text field will be empty if no scopes configured - let scopesValue = scopesField.value as? String - if bootconfig2Scopes == "(none)" { - XCTAssert(scopesValue == "" || scopesValue == nil || scopesValue == "e.g. id api refresh_token", - "Scopes should be empty or placeholder when not configured, got: '\(String(describing: scopesValue))'") - } else { - XCTAssertEqual(scopesValue, bootconfig2Scopes, "Scopes should match bootconfig2") - } - - // Tap to collapse - dynamicConfigHeader.tap() - - // Fields should be hidden again - XCTAssertFalse(app.staticTexts["Consumer Key:"].waitForExistence(timeout: 2), "Consumer Key label should be hidden when collapsed") - } - - // MARK: - Flow Types Test - func testFlowTypes() throws { - XCTAssertTrue(app.staticTexts["Authentication Flow Types"].waitForExistence(timeout: 5)) - XCTAssertTrue(app.staticTexts.matching(NSPredicate(format: "label CONTAINS %@", "Use Web Server Flow")).count > 0) - XCTAssertTrue(app.staticTexts.matching(NSPredicate(format: "label CONTAINS %@", "Use Hybrid Flow")).count > 0) - } -} - diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/TestHelper.swift b/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/TestHelper.swift deleted file mode 100644 index 15deb5068d..0000000000 --- a/native/SampleApps/AuthFlowTester/AuthFlowTesterUITests/TestHelper.swift +++ /dev/null @@ -1,233 +0,0 @@ -/* - Copyright (c) 2025-present, salesforce.com, inc. All rights reserved. - - Redistribution and use of this software in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list of conditions - and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of - conditions and the following disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to - endorse or promote products derived from this software without specific prior written - permission of salesforce.com, inc. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR - IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY - WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import XCTest - -/// Helper functions for AuthFlowTester UI Tests -class TestHelper { - - /// Launches the app with test credentials for authenticated tests - /// - Parameter app: The XCUIApplication instance to configure - /// - Throws: If test_credentials.json cannot be loaded - static func launchWithCredentials(_ app: XCUIApplication) throws { - // Load test credentials from bundle - guard let credentialsPath = Bundle(for: TestHelper.self).path(forResource: "test_credentials", ofType: "json") else { - throw TestHelperError.credentialsFileNotFound - } - - guard let credentialsData = try? Data(contentsOf: URL(fileURLWithPath: credentialsPath)) else { - throw TestHelperError.credentialsFileNotReadable - } - - // Minify JSON (remove newlines/whitespace) for passing via launch args - let credentialsString: String - if let jsonObject = try? JSONSerialization.jsonObject(with: credentialsData), - let minifiedData = try? JSONSerialization.data(withJSONObject: jsonObject, options: []), - let minifiedString = String(data: minifiedData, encoding: .utf8) { - credentialsString = minifiedString - } else if let asString = String(data: credentialsData, encoding: .utf8) { - // Fallback: use original string if minification fails - credentialsString = asString.replacingOccurrences(of: "\n", with: "").replacingOccurrences(of: "\r", with: "") - } else { - throw TestHelperError.credentialsNotValidString - } - - // Validate that credentials have been configured (not using default values) - if credentialsString.contains("__INSERT_TOKEN_HERE__") || - credentialsString.contains("__INSERT_REMOTE_ACCESS_CLIENT_KEY_HERE__") || - credentialsString.contains("__INSERT_REMOTE_ACCESS_CALLBACK_URL_HERE__") { - throw TestHelperError.credentialsNotConfigured - } - - // Configure launch arguments - app.launchArguments = [ - "-creds", credentialsString - ] - - app.launch() - } - - /// Launches the app in UI testing mode without credentials (unauthenticated tests) - /// - Parameter app: The XCUIApplication instance to configure - static func launchWithoutCredentials(_ app: XCUIApplication) { - app.launchArguments = ["CONFIG_PICKER"] - app.launch() - } -} - -/// Structure to hold bootconfig values loaded from plist -struct BootConfigData { - let consumerKey: String - let redirectURI: String - let scopes: String -} - -extension TestHelper { - /// Loads a bootconfig plist (e.g. "bootconfig" or "bootconfig2") from the test bundle - /// - Parameter name: The plist name without extension - /// - Returns: Parsed BootConfigData - /// - Throws: TestHelperError if the file cannot be found or parsed - static func loadBootConfig(named name: String) throws -> BootConfigData { - guard let url = Bundle(for: TestHelper.self).url(forResource: name, withExtension: "plist") else { - throw TestHelperError.bootconfigFileNotFound(name) - } - guard let dict = NSDictionary(contentsOf: url) as? [String: Any] else { - throw TestHelperError.bootconfigNotParseable(name) - } - guard let consumerKey = dict["remoteAccessConsumerKey"] as? String, - let redirectURI = dict["oauthRedirectURI"] as? String else { - throw TestHelperError.bootconfigMissingFields(name) - } - - // Parse scopes (optional field) - convert array to space-separated string - let scopes: String - if let scopesArray = dict["oauthScopes"] as? [String] { - scopes = scopesArray.sorted().joined(separator: " ") - } else { - scopes = "(none)" - } - - return BootConfigData(consumerKey: consumerKey, redirectURI: redirectURI, scopes: scopes) - } -} - -/// Structure to hold test credentials loaded from test_credentials.json -/// This mirrors the structure of SFSDKTestCredentialsData from TestSetupUtils -struct TestCredentials { - let username: String - let instanceUrl: String - let clientId: String - let redirectUri: String - let displayName: String - let accessToken: String - let refreshToken: String - let identityUrl: String - let organizationId: String - let userId: String - let photoUrl: String - let loginDomain: String - let scopes: String - - /// Loads test credentials from test_credentials.json in the test bundle - /// This follows the same pattern as TestSetupUtils.populateAuthCredentialsFromConfigFileForClass - /// - Returns: TestCredentials parsed from the JSON file - /// - Throws: TestHelperError if the file cannot be loaded or parsed - static func loadFromBundle() throws -> TestCredentials { - // Load credentials file from bundle - similar to TestSetupUtils line 53 - guard let credentialsPath = Bundle(for: TestHelper.self).path(forResource: "test_credentials", ofType: "json") else { - throw TestHelperError.credentialsFileNotFound - } - - guard let credentialsData = try? Data(contentsOf: URL(fileURLWithPath: credentialsPath)) else { - throw TestHelperError.credentialsFileNotReadable - } - - // Parse JSON into dictionary - similar to TestSetupUtils line 56-57 - guard let jsonDict = try? JSONSerialization.jsonObject(with: credentialsData) as? [String: Any] else { - throw TestHelperError.credentialsNotParseable(NSError(domain: "TestHelper", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid JSON format"])) - } - - // Extract values from dictionary - similar to SFSDKTestCredentialsData property getters - guard let username = jsonDict["username"] as? String, - let instanceUrl = jsonDict["instance_url"] as? String, - let clientId = jsonDict["test_client_id"] as? String, - let redirectUri = jsonDict["test_redirect_uri"] as? String, - let displayName = jsonDict["display_name"] as? String, - let accessToken = jsonDict["access_token"] as? String, - let refreshToken = jsonDict["refresh_token"] as? String, - let identityUrl = jsonDict["identity_url"] as? String, - let organizationId = jsonDict["organization_id"] as? String, - let userId = jsonDict["user_id"] as? String, - let photoUrl = jsonDict["photo_url"] as? String, - let loginDomain = jsonDict["test_login_domain"] as? String else { - throw TestHelperError.credentialsNotParseable(NSError(domain: "TestHelper", code: -2, userInfo: [NSLocalizedDescriptionKey: "Missing required credentials fields"])) - } - - // Parse scopes (optional field) - convert array to space-separated string - let scopes: String - if let scopesArray = jsonDict["test_scopes"] as? [String] { - scopes = scopesArray.sorted().joined(separator: ", ") - } else { - scopes = "(none)" - } - - // Check if credentials have been configured - similar to TestSetupUtils line 135 - guard refreshToken != "__INSERT_TOKEN_HERE__" else { - throw TestHelperError.credentialsNotConfigured - } - - return TestCredentials( - username: username, - instanceUrl: instanceUrl, - clientId: clientId, - redirectUri: redirectUri, - displayName: displayName, - accessToken: accessToken, - refreshToken: refreshToken, - identityUrl: identityUrl, - organizationId: organizationId, - userId: userId, - photoUrl: photoUrl, - loginDomain: loginDomain, - scopes: scopes - ) - } -} - -/// Errors that can occur during test setup -enum TestHelperError: Error, LocalizedError { - case credentialsFileNotFound - case credentialsFileNotReadable - case credentialsNotValidString - case credentialsNotConfigured - case credentialsNotParseable(Error) - case bootconfigFileNotFound(String) - case bootconfigNotParseable(String) - case bootconfigMissingFields(String) - - var errorDescription: String? { - switch self { - case .credentialsFileNotFound: - return "test_credentials.json file not found in test bundle. Make sure it's added to Copy Bundle Resources." - case .credentialsFileNotReadable: - return "test_credentials.json file could not be read." - case .credentialsNotValidString: - return "test_credentials.json contains invalid UTF-8 data." - case .credentialsNotConfigured: - return """ - test_credentials.json has not been configured with real credentials. - Please replace the placeholder values with actual test org credentials. - """ - case .credentialsNotParseable(let error): - return "test_credentials.json could not be parsed: \(error.localizedDescription)" - case .bootconfigFileNotFound(let name): - return "\(name).plist file not found in test bundle. Make sure it's added to Copy Bundle Resources." - case .bootconfigNotParseable(let name): - return "\(name).plist could not be parsed as a property list dictionary." - case .bootconfigMissingFields(let name): - return "\(name).plist is missing required keys: remoteAccessConsumerKey and/or oauthRedirectURI." - } - } -} -