diff --git a/.github/DangerFiles/TestOrchestrator.rb b/.github/DangerFiles/TestOrchestrator.rb
index a3a5ad60d6..569539b346 100644
--- a/.github/DangerFiles/TestOrchestrator.rb
+++ b/.github/DangerFiles/TestOrchestrator.rb
@@ -10,6 +10,7 @@
SCHEMES = ['SalesforceSDKCommon', 'SalesforceAnalytics', 'SalesforceSDKCore', 'SmartStore', 'MobileSync']
modifed_libs = Set[]
+
for file in (git.modified_files + git.added_files);
scheme = file.split("libs/").last.split("/").first
if SCHEMES.include?(scheme)
diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml
index e89b777b6f..6342db1455 100644
--- a/.github/workflows/nightly.yaml
+++ b/.github/workflows/nightly.yaml
@@ -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
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 @@
+
+
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/Common/SalesforceSDKManager.h b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.h
index 5a9bfa2a47..ff29973c67 100644
--- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.h
+++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.h
@@ -203,6 +203,19 @@ 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.
+
+ 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);
+
/** 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 +319,17 @@ NS_SWIFT_NAME(SalesforceManager)
*/
- (id )biometricAuthenticationManager;
+/**
+ * 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
+ */
+- (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 b855bd9e50..6857cc6bc0 100644
--- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.m
+++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Common/SalesforceSDKManager.m
@@ -914,6 +914,19 @@ - (void)biometricAuthenticationFlowDidComplete:(NSNotification *)notification {
return [SFScreenLockManagerInternal shared];
}
+#pragma mark - Runtime App Config (aka Bootconfig) Override
+
+- (void) appConfigForLoginHost:(nullable NSString *)loginHost callback:(nonnull void (^)(SFSDKAppConfig * _Nullable))callback {
+ if (self.appConfigRuntimeSelectorBlock) {
+ self.appConfigRuntimeSelectorBlock(loginHost, ^(SFSDKAppConfig *config) {
+ // Fall back to default appConfig if the selector block returns nil
+ callback(config ?: self.appConfig);
+ });
+ } else {
+ callback(self.appConfig);
+ }
+}
+
#pragma mark - Native Login
- (id )useNativeLoginWithConsumerKey:(nonnull NSString *)consumerKey
@@ -964,7 +977,7 @@ - (void)biometricAuthenticationFlowDidComplete:(NSNotification *)notification {
return nativeLogin;
}
-
+
@end
NSString *SFAppTypeGetDescription(SFAppType appType){
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/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/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..b85fbe5cbb 100644
--- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.m
+++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.m
@@ -613,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 b18450db03..c9a2500339 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,10 @@ 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.
+ The block takes a login host and a callback. The callback should be invoked with the selected app config.
+ */
+ 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/SalesforceSDKManagerTests.m b/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SalesforceSDKManagerTests.m
index 6cac184e9e..f592a8c540 100644
--- a/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SalesforceSDKManagerTests.m
+++ b/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SalesforceSDKManagerTests.m
@@ -485,4 +485,118 @@ - (void)compareAppNames:(NSString *)expectedAppName
XCTAssertTrue([userAgent containsString:expectedAppName], @"App names should match");
}
+#pragma mark - Runtime Selected App Config Tests
+
+- (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;
+
+ // 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");
+ }];
+
+ // 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)testAppConfigForLoginHostWithDifferentLoginHosts {
+ 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];
+
+ // 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 = ^(NSString *loginHost, void (^callback)(SFSDKAppConfig *)) {
+ if ([loginHost isEqualToString:loginHost1]) {
+ callback(config1);
+ } else if ([loginHost isEqualToString:loginHost2]) {
+ callback(config2);
+ } else {
+ callback(nil);
+ }
+ };
+
+ // Test first loginHost
+ [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
+ [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)testAppConfigForLoginHostReturnsDefaultWhenBlockReturnsNil {
+ __block BOOL blockWasCalled = NO;
+
+ // 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;
+ callback(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.xcodeproj/project.pbxproj b/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj
new file mode 100644
index 0000000000..43856d0a19
--- /dev/null
+++ b/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/project.pbxproj
@@ -0,0 +1,465 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 90;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 4F1A8CCD2EAFEA7C0037DC89 /* BootConfigEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F1A8CCC2EAFEA7C0037DC89 /* BootConfigEditor.swift */; };
+ 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, ); }; };
+ 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 */; };
+ 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 */; };
+ 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 */; };
+/* 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 */
+ 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; };
+ 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 = ""; };
+ 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 = ""; };
+/* 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 = "";
+ };
+ 4FF0EF9E2EA8568C005E4474 /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ AUTH010 /* bootconfig.plist */,
+ AUTH036 /* bootconfig2.plist */,
+ AUTH012 /* Info.plist */,
+ AUTH014 /* PrivacyInfo.xcprivacy */,
+ AUTH016 /* AuthFlowTester.entitlements */,
+ );
+ path = "Supporting Files";
+ sourceTree = "";
+ };
+ AUTH019 = {
+ isa = PBXGroup;
+ children = (
+ AUTH020 /* AuthFlowTester */,
+ 4F95A8952EA801DC00C98D18 /* Frameworks */,
+ AUTH021 /* Products */,
+ );
+ sourceTree = "";
+ };
+ AUTH020 /* AuthFlowTester */ = {
+ isa = PBXGroup;
+ children = (
+ AUTH022 /* Classes */,
+ AUTH044 /* ViewControllers */,
+ AUTH039 /* Views */,
+ AUTH035 /* Resources */,
+ 4FF0EF9E2EA8568C005E4474 /* Supporting Files */,
+ );
+ path = AuthFlowTester;
+ sourceTree = "";
+ };
+ AUTH021 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ AUTH017 /* AuthFlowTester.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ AUTH022 /* Classes */ = {
+ isa = PBXGroup;
+ children = (
+ AUTH002 /* AppDelegate.swift */,
+ AUTH004 /* SceneDelegate.swift */,
+ );
+ path = Classes;
+ sourceTree = "";
+ };
+ AUTH035 /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ AUTH034 /* Images.xcassets */,
+ );
+ name = Resources;
+ sourceTree = "";
+ };
+ AUTH039 /* Views */ = {
+ isa = PBXGroup;
+ children = (
+ 4F1A8CCC2EAFEA7C0037DC89 /* BootConfigEditor.swift */,
+ 4FEBAF282EA9B91500D4880A /* RevokeView.swift */,
+ AUTH038 /* UserCredentialsView.swift */,
+ AUTH041 /* RestApiTestView.swift */,
+ AUTH043 /* OAuthConfigurationView.swift */,
+ AUTH050 /* JwtAccessView.swift */,
+ AUTH052 /* InfoRowView.swift */,
+ AUTH054 /* FlowTypesView.swift */,
+ );
+ path = Views;
+ sourceTree = "";
+ };
+ AUTH044 /* ViewControllers */ = {
+ isa = PBXGroup;
+ children = (
+ AUTH006 /* ConfigPickerViewController.swift */,
+ AUTH008 /* SessionDetailViewController.swift */,
+ );
+ path = ViewControllers;
+ 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 = 2600;
+ 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 */,
+ AUTH011 /* bootconfig2.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 /* ConfigPickerViewController.swift in Sources */,
+ AUTH007 /* SessionDetailViewController.swift in Sources */,
+ AUTH037 /* UserCredentialsView.swift in Sources */,
+ AUTH040 /* RestApiTestView.swift in Sources */,
+ AUTH042 /* OAuthConfigurationView.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 */,
+ );
+ };
+/* 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/Supporting Files/AuthFlowTester.entitlements";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = "";
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = NO;
+ INFOPLIST_FILE = "AuthFlowTester/Supporting Files/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/Supporting Files/AuthFlowTester.entitlements";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = "";
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = NO;
+ INFOPLIST_FILE = "AuthFlowTester/Supporting Files/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..a8d8db2bbe
--- /dev/null
+++ b/native/SampleApps/AuthFlowTester/AuthFlowTester.xcodeproj/xcshareddata/xcschemes/AuthFlowTester.xcscheme
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Classes/AppDelegate.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Classes/AppDelegate.swift
new file mode 100644
index 0000000000..5fba38f5a6
--- /dev/null
+++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Classes/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/Classes/SceneDelegate.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Classes/SceneDelegate.swift
new file mode 100644
index 0000000000..6db5aa4bd4
--- /dev/null
+++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Classes/SceneDelegate.swift
@@ -0,0 +1,130 @@
+//
+// 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
+ }
+
+ // Check ProcessInfo arguments for CONFIG_PICKER flag
+ let shouldShowConfigPicker = ProcessInfo.processInfo.arguments.contains("CONFIG_PICKER")
+
+ // Check if user is already logged in
+ if UserAccountManager.shared.currentUserAccount != nil && !shouldShowConfigPicker {
+ // User is already logged in and not in config picker mode, go directly to session detail
+ self.setupRootViewController()
+ } else {
+ // User is not logged in or config picker mode is enabled, show config picker
+ self.window?.rootViewController = UIHostingController(rootView: ConfigPickerView(onConfigurationCompleted: onConfigurationCompleted))
+ }
+ self.window?.makeKeyAndVisible()
+ }
+
+ func setupRootViewController() {
+ let rootVC = SessionDetailViewController()
+ 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/Supporting Files/AuthFlowTester.entitlements b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/AuthFlowTester.entitlements
new file mode 100644
index 0000000000..fbad02378c
--- /dev/null
+++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/AuthFlowTester.entitlements
@@ -0,0 +1,8 @@
+
+
+
+
+ keychain-access-groups
+
+
+
diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/Info.plist b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/Info.plist
new file mode 100644
index 0000000000..c208cf45da
--- /dev/null
+++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/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/Supporting Files/PrivacyInfo.xcprivacy b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/PrivacyInfo.xcprivacy
new file mode 100644
index 0000000000..cfbe279c7b
--- /dev/null
+++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/PrivacyInfo.xcprivacy
@@ -0,0 +1,8 @@
+
+
+
+
+ NSPrivacyTracking
+
+
+
diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig.plist b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig.plist
new file mode 100644
index 0000000000..16e10f7aec
--- /dev/null
+++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig.plist
@@ -0,0 +1,12 @@
+
+
+
+
+ remoteAccessConsumerKey
+ 3MVG9SemV5D80oBcXZ2EUzbcJwzJCe2n4LaHH_Z2JSpIJqJ1MzFK_XRlHrupqNdeus8.NRonkpx0sAAWKzfK8
+ oauthRedirectURI
+ testeropaque://mobilesdk/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..927fefb46a
--- /dev/null
+++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Supporting Files/bootconfig2.plist
@@ -0,0 +1,12 @@
+
+
+
+
+ remoteAccessConsumerKey
+ 3MVG9SemV5D80oBcXZ2EUzbcJw6aYF3RcTY1FgYlgnWAA72zHubsit4NKIA.DNcINzbDjz23yUJP3ucnY99F6
+ oauthRedirectURI
+ testerjwt://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..60239f26e9
--- /dev/null
+++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/ConfigPickerViewController.swift
@@ -0,0 +1,202 @@
+/*
+ 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 staticConsumerKey = ""
+ @State private var staticCallbackUrl = ""
+ @State private var staticScopes = ""
+ @State private var dynamicConsumerKey = ""
+ @State private var dynamicCallbackUrl = ""
+ @State private var dynamicScopes = ""
+
+ let onConfigurationCompleted: () -> Void
+
+ var body: some View {
+ NavigationView {
+ ScrollView {
+ VStack(spacing: 30) {
+ // Flow types section
+ FlowTypesView()
+ .padding(.top, 20)
+
+ Divider()
+
+ // Static config section
+ BootConfigEditor(
+ title: "Static Configuration",
+ buttonLabel: "Use static config",
+ buttonColor: .blue,
+ consumerKey: $staticConsumerKey,
+ callbackUrl: $staticCallbackUrl,
+ scopes: $staticScopes,
+ isLoading: isLoading,
+ onUseConfig: handleStaticConfig
+ )
+
+ Divider()
+
+ // Dynamic config section
+ BootConfigEditor(
+ title: "Dynamic Configuration",
+ buttonLabel: "Use dynamic config",
+ buttonColor: .green,
+ consumerKey: $dynamicConsumerKey,
+ callbackUrl: $dynamicCallbackUrl,
+ scopes: $dynamicScopes,
+ isLoading: isLoading,
+ onUseConfig: handleDynamicBootconfig
+ )
+
+ // Loading indicator
+ if isLoading {
+ ProgressView("Authenticating...")
+ .padding()
+ }
+ }
+ .padding(.bottom, 40)
+ }
+ .background(Color(.systemBackground))
+ .navigationTitle(appName)
+ .navigationBarTitleDisplayMode(.large)
+ }
+ .navigationViewStyle(.stack)
+ .onAppear {
+ loadConfigDefaults()
+ }
+ }
+
+ // 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 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
+ dynamicScopes = config.oauthScopes.sorted().joined(separator: " ")
+ }
+ }
+
+ // MARK: - Button Actions
+
+ 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
+
+ // Update UserAccountManager properties
+ UserAccountManager.shared.oauthClientID = staticConsumerKey
+ UserAccountManager.shared.oauthCompletionURL = staticCallbackUrl
+ UserAccountManager.shared.scopes = scopesArray.isEmpty ? [] : Set(scopesArray)
+
+ // Proceed with login
+ onConfigurationCompleted()
+ }
+
+ private func handleDynamicBootconfig() {
+ isLoading = true
+
+ SalesforceManager.shared.bootConfigRuntimeSelector = { _, callback 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
+ }
+
+ callback(BootConfig(configDict))
+ }
+
+ // 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..ca50b06792
--- /dev/null
+++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/ViewControllers/SessionDetailViewController.swift
@@ -0,0 +1,173 @@
+/*
+ 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
+ @State private var isUserCredentialsExpanded = false
+ @State private var isJwtDetailsExpanded = false
+ @State private var isOAuthConfigExpanded = false
+
+ var onChangeConsumerKey: () -> Void
+ var onSwitchUser: () -> Void
+ var onLogout: () -> Void
+
+ var body: some View {
+ ScrollView {
+ VStack(alignment: .leading, spacing: 20) {
+ // 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(isExpanded: $isOAuthConfigExpanded)
+ .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/BootConfigEditor.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/BootConfigEditor.swift
new file mode 100644
index 0000000000..7badf71362
--- /dev/null
+++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/BootConfigEditor.swift
@@ -0,0 +1,110 @@
+/*
+ BootConfigEditor.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 BootConfigEditor: View {
+ let title: String
+ let buttonLabel: String
+ let buttonColor: Color
+ @Binding var consumerKey: String
+ @Binding var callbackUrl: String
+ @Binding var scopes: 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(title)
+ .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)
+ .accessibilityIdentifier("consumerKeyTextField")
+
+ Text("Callback URL:")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ TextField("Callback URL", text: $callbackUrl)
+ .font(.system(.caption, design: .monospaced))
+ .textFieldStyle(RoundedBorderTextFieldStyle())
+ .autocapitalization(.none)
+ .disableAutocorrection(true)
+ .accessibilityIdentifier("callbackUrlTextField")
+
+ 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)
+ .accessibilityIdentifier("scopesTextField")
+ }
+ .padding(.horizontal)
+ }
+
+ Button(action: onUseConfig) {
+ Text(buttonLabel)
+ .font(.headline)
+ .foregroundColor(.white)
+ .frame(maxWidth: .infinity)
+ .frame(height: 44)
+ .background(buttonColor)
+ .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..6c3057ce62
--- /dev/null
+++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/InfoRowView.swift
@@ -0,0 +1,83 @@
+/*
+ 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(maskedValue)
+ .font(.system(.caption, design: .monospaced))
+ Spacer()
+ Button(action: { isRevealed.toggle() }) {
+ Image(systemName: "eye")
+ .foregroundColor(.blue)
+ }
+ }
+ } else {
+ HStack {
+ Text(value.isEmpty ? "(empty)" : value)
+ .font(.system(.caption, design: .monospaced))
+ .foregroundColor(value.isEmpty ? .secondary : .primary)
+ Spacer()
+ if isSensitive {
+ Button(action: { isRevealed.toggle() }) {
+ Image(systemName: "eye.slash")
+ .foregroundColor(.blue)
+ }
+ }
+ }
+ }
+ }
+ .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
new file mode 100644
index 0000000000..dfc7710dbe
--- /dev/null
+++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/JwtAccessView.swift
@@ -0,0 +1,154 @@
+/*
+ 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
+ @Binding var isExpanded: Bool
+
+ var body: some View {
+ 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)
+ }
+ }
+
+ 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)
+ }
+}
+
+// MARK: - JWT Header View
+
+struct JwtHeaderView: View {
+ let token: JwtAccessToken
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 8) {
+ let header = token.header
+
+ 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))
+ .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 {
+ InfoRowView(label: "Audience (aud):", value: audience.joined(separator: ", "))
+ }
+
+ if let expirationDate = token.expirationDate() {
+ InfoRowView(label: "Expiration Date (exp):", value: formatDate(expirationDate))
+ }
+
+ 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))
+ .cornerRadius(4)
+ }
+
+ private func formatDate(_ date: Date) -> String {
+ let formatter = DateFormatter()
+ formatter.dateStyle = .medium
+ formatter.timeStyle = .medium
+ return formatter.string(from: date)
+ }
+}
+
+
diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/OAuthConfigurationView.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/OAuthConfigurationView.swift
new file mode 100644
index 0000000000..cb112b6b95
--- /dev/null
+++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/OAuthConfigurationView.swift
@@ -0,0 +1,93 @@
+/*
+ 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 {
+ @Binding var isExpanded: Bool
+
+ var body: some View {
+ 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)
+ }
+ }
+
+ if isExpanded {
+ InfoRowView(label: configuredConsumerKeyLabel,
+ value: configuredConsumerKey, isSensitive: true)
+
+ InfoRowView(label: "Configured Callback URL:",
+ value: configuredCallbackUrl)
+
+ InfoRowView(label: "Configured Scopes:",
+ value: configuredScopes)
+ }
+ }
+ .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 ?? ""
+ }
+
+ private var configuredScopes: String {
+ let scopes = UserAccountManager.shared.scopes
+ return scopes.isEmpty ? "(none)" : scopes.sorted().joined(separator: ", ")
+ }
+}
+
diff --git a/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/RestApiTestView.swift b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/RestApiTestView.swift
new file mode 100644
index 0000000000..d550c669df
--- /dev/null
+++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/RestApiTestView.swift
@@ -0,0 +1,162 @@
+/*
+ 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) {
+ 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/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
new file mode 100644
index 0000000000..2f5937c4b3
--- /dev/null
+++ b/native/SampleApps/AuthFlowTester/AuthFlowTester/Views/UserCredentialsView.swift
@@ -0,0 +1,112 @@
+/*
+ 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 {
+ @Binding var isExpanded: Bool
+
+ var body: some View {
+ 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)
+ }
+ }
+
+ 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()
+ .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 ?? ""
+ }
+
+}
+
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;