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;