diff --git a/iOSNativeSwiftTemplate/Podfile b/iOSNativeSwiftTemplate/Podfile index 5e7b5019..8b1e4d46 100644 --- a/iOSNativeSwiftTemplate/Podfile +++ b/iOSNativeSwiftTemplate/Podfile @@ -1,4 +1,4 @@ -require_relative './mobile_sdk/SalesforceMobileSDK-iOS/mobilesdk_pods' +require_relative '../../SalesforceMobileSDK-iOS/mobilesdk_pods' platform :ios, '16.0' @@ -14,4 +14,4 @@ post_install do |installer| signposts_post_install(installer) mobile_sdk_post_install(installer) -end \ No newline at end of file +end diff --git a/iOSNativeSwiftTemplate/iOSNativeSwiftTemplate.xcodeproj/project.pbxproj b/iOSNativeSwiftTemplate/iOSNativeSwiftTemplate.xcodeproj/project.pbxproj index 653a9efb..2e95b00e 100644 --- a/iOSNativeSwiftTemplate/iOSNativeSwiftTemplate.xcodeproj/project.pbxproj +++ b/iOSNativeSwiftTemplate/iOSNativeSwiftTemplate.xcodeproj/project.pbxproj @@ -18,13 +18,17 @@ 4FD6C47A1B754AF1002F9F90 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD6C4791B754AF1002F9F90 /* AppDelegate.swift */; }; 4FD6C4A01B755242002F9F90 /* InitialViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD6C49D1B755242002F9F90 /* InitialViewController.swift */; }; 69B48C322AD4E7AD0026AEC6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 69B48C312AD4E7AD0026AEC6 /* PrivacyInfo.xcprivacy */; }; - B7168FBC1FACC6F900A48DB5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B7168FBB1FACC6F900A48DB5 /* LaunchScreen.storyboard */; }; + 8A7D16C2D3B653B9C7BC8413 /* Pods_iOSNativeSwiftTemplate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9E4923BCE151D652A2E314ED /* Pods_iOSNativeSwiftTemplate.framework */; }; B73B520D234FA65F001B069B /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B73B520C234FA65F001B069B /* SceneDelegate.swift */; }; B77BD8A321125B510036B284 /* bootconfig.plist in Resources */ = {isa = PBXBuildFile; fileRef = B77BD8A221125B510036B284 /* bootconfig.plist */; }; - CEA8CA041F071B2300448B51 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CEA8CA031F071B2300448B51 /* Images.xcassets */; }; + D35055542C879DD500DCDC31 /* LoginTypeSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D35055532C879DD500DCDC31 /* LoginTypeSelectionViewController.swift */; }; + D3848AE42C82351300A78C0A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D3848AE22C82351300A78C0A /* Images.xcassets */; }; + D3848AE52C82351400A78C0A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D3848AE32C82351300A78C0A /* LaunchScreen.storyboard */; }; + D3848AE92C82392600A78C0A /* QrCodeScanController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3848AE82C82392600A78C0A /* QrCodeScanController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 42BEE51B49F5BAD7CCCD23AB /* Pods-iOSNativeSwiftTemplate.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOSNativeSwiftTemplate.debug.xcconfig"; path = "Target Support Files/Pods-iOSNativeSwiftTemplate/Pods-iOSNativeSwiftTemplate.debug.xcconfig"; sourceTree = ""; }; 4F13E936237DD5D9005BCAE9 /* ContactDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactDetailsView.swift; sourceTree = ""; }; 4F13E937237DD5D9005BCAE9 /* ContactsForAccountModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsForAccountModel.swift; sourceTree = ""; }; 4F13E938237DD5D9005BCAE9 /* ContactsForAccountListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsForAccountListView.swift; sourceTree = ""; }; @@ -39,10 +43,14 @@ 4FD6C49D1B755242002F9F90 /* InitialViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InitialViewController.swift; sourceTree = ""; }; 4FF12F8A1DCA91F5004D9EF9 /* iOSNativeSwiftTemplate.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = iOSNativeSwiftTemplate.entitlements; sourceTree = ""; }; 69B48C312AD4E7AD0026AEC6 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; - B7168FBB1FACC6F900A48DB5 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = "../mobile_sdk/SalesforceMobileSDK-iOS/shared/resources/LaunchScreen.storyboard"; sourceTree = ""; }; + 83C576D5B3530B7210E42288 /* Pods-iOSNativeSwiftTemplate.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOSNativeSwiftTemplate.release.xcconfig"; path = "Target Support Files/Pods-iOSNativeSwiftTemplate/Pods-iOSNativeSwiftTemplate.release.xcconfig"; sourceTree = ""; }; + 9E4923BCE151D652A2E314ED /* Pods_iOSNativeSwiftTemplate.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iOSNativeSwiftTemplate.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B73B520C234FA65F001B069B /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; B77BD8A221125B510036B284 /* bootconfig.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = bootconfig.plist; sourceTree = ""; }; - CEA8CA031F071B2300448B51 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = "mobile_sdk/SalesforceMobileSDK-iOS/shared/resources/Images.xcassets"; sourceTree = SOURCE_ROOT; }; + D35055532C879DD500DCDC31 /* LoginTypeSelectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginTypeSelectionViewController.swift; sourceTree = ""; }; + D3848AE22C82351300A78C0A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = "../../../SalesforceMobileSDK-iOS/shared/resources/Images.xcassets"; sourceTree = ""; }; + D3848AE32C82351300A78C0A /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = "../../../SalesforceMobileSDK-iOS/shared/resources/LaunchScreen.storyboard"; sourceTree = ""; }; + D3848AE82C82392600A78C0A /* QrCodeScanController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QrCodeScanController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -50,19 +58,37 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 8A7D16C2D3B653B9C7BC8413 /* Pods_iOSNativeSwiftTemplate.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 231DD8297D85560CF8533961 /* Pods */ = { + isa = PBXGroup; + children = ( + 42BEE51B49F5BAD7CCCD23AB /* Pods-iOSNativeSwiftTemplate.debug.xcconfig */, + 83C576D5B3530B7210E42288 /* Pods-iOSNativeSwiftTemplate.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 38A3D00284A22BBDCDF04290 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 9E4923BCE151D652A2E314ED /* Pods_iOSNativeSwiftTemplate.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 4F13E944237DD626005BCAE9 /* SwiftUI */ = { isa = PBXGroup; children = ( - B73B520C234FA65F001B069B /* SceneDelegate.swift */, 4F13E939237DD5D9005BCAE9 /* AccountsListView.swift */, - 4F13E938237DD5D9005BCAE9 /* ContactsForAccountListView.swift */, 4F13E936237DD5D9005BCAE9 /* ContactDetailsView.swift */, + 4F13E938237DD5D9005BCAE9 /* ContactsForAccountListView.swift */, + B73B520C234FA65F001B069B /* SceneDelegate.swift */, ); name = SwiftUI; sourceTree = ""; @@ -71,8 +97,8 @@ isa = PBXGroup; children = ( 4F13E946237DD72E005BCAE9 /* AccountsListModel.swift */, - 4F13E937237DD5D9005BCAE9 /* ContactsForAccountModel.swift */, 4F13E93A237DD5D9005BCAE9 /* ContactDetailModel.swift */, + 4F13E937237DD5D9005BCAE9 /* ContactsForAccountModel.swift */, ); name = Models; sourceTree = ""; @@ -80,7 +106,9 @@ 4FD6C46B1B754AF1002F9F90 = { isa = PBXGroup; children = ( + 38A3D00284A22BBDCDF04290 /* Frameworks */, 4FD6C4761B754AF1002F9F90 /* iOSNativeSwiftTemplate */, + 231DD8297D85560CF8533961 /* Pods */, 4FD6C4751B754AF1002F9F90 /* Products */, ); sourceTree = ""; @@ -96,9 +124,9 @@ 4FD6C4761B754AF1002F9F90 /* iOSNativeSwiftTemplate */ = { isa = PBXGroup; children = ( - 4FF12F8A1DCA91F5004D9EF9 /* iOSNativeSwiftTemplate.entitlements */, 4FD6C4A11B755265002F9F90 /* Classes */, 4FD6C4771B754AF1002F9F90 /* Supporting Files */, + 4FF12F8A1DCA91F5004D9EF9 /* iOSNativeSwiftTemplate.entitlements */, ); path = iOSNativeSwiftTemplate; sourceTree = ""; @@ -106,13 +134,13 @@ 4FD6C4771B754AF1002F9F90 /* Supporting Files */ = { isa = PBXGroup; children = ( + B77BD8A221125B510036B284 /* bootconfig.plist */, + D3848AE22C82351300A78C0A /* Images.xcassets */, + 4FD6C4781B754AF1002F9F90 /* Info.plist */, + D3848AE32C82351300A78C0A /* LaunchScreen.storyboard */, 69B48C312AD4E7AD0026AEC6 /* PrivacyInfo.xcprivacy */, 4F13E941237DD5EE005BCAE9 /* userstore.json */, 4F13E940237DD5EE005BCAE9 /* usersyncs.json */, - B77BD8A221125B510036B284 /* bootconfig.plist */, - B7168FBB1FACC6F900A48DB5 /* LaunchScreen.storyboard */, - CEA8CA031F071B2300448B51 /* Images.xcassets */, - 4FD6C4781B754AF1002F9F90 /* Info.plist */, ); name = "Supporting Files"; sourceTree = ""; @@ -120,10 +148,12 @@ 4FD6C4A11B755265002F9F90 /* Classes */ = { isa = PBXGroup; children = ( - 4F13E944237DD626005BCAE9 /* SwiftUI */, 4F13E945237DD62D005BCAE9 /* Models */, - 4FD6C49D1B755242002F9F90 /* InitialViewController.swift */, + 4F13E944237DD626005BCAE9 /* SwiftUI */, 4FD6C4791B754AF1002F9F90 /* AppDelegate.swift */, + 4FD6C49D1B755242002F9F90 /* InitialViewController.swift */, + D35055532C879DD500DCDC31 /* LoginTypeSelectionViewController.swift */, + D3848AE82C82392600A78C0A /* QrCodeScanController.swift */, ); name = Classes; sourceTree = ""; @@ -135,9 +165,11 @@ isa = PBXNativeTarget; buildConfigurationList = 4FD6C4931B754AF1002F9F90 /* Build configuration list for PBXNativeTarget "iOSNativeSwiftTemplate" */; buildPhases = ( + 469B1AB89DC48EAD107F5D36 /* [CP] Check Pods Manifest.lock */, 4FD6C4701B754AF1002F9F90 /* Sources */, 4FD6C4711B754AF1002F9F90 /* Frameworks */, 4FD6C4721B754AF1002F9F90 /* Resources */, + D39806A3A492C535683B691F /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -193,25 +225,69 @@ buildActionMask = 2147483647; files = ( 69B48C322AD4E7AD0026AEC6 /* PrivacyInfo.xcprivacy in Resources */, + D3848AE42C82351300A78C0A /* Images.xcassets in Resources */, 4F13E943237DD5EE005BCAE9 /* userstore.json in Resources */, + D3848AE52C82351400A78C0A /* LaunchScreen.storyboard in Resources */, B77BD8A321125B510036B284 /* bootconfig.plist in Resources */, 4F13E942237DD5EE005BCAE9 /* usersyncs.json in Resources */, - CEA8CA041F071B2300448B51 /* Images.xcassets in Resources */, - B7168FBC1FACC6F900A48DB5 /* LaunchScreen.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 469B1AB89DC48EAD107F5D36 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-iOSNativeSwiftTemplate-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + D39806A3A492C535683B691F /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-iOSNativeSwiftTemplate/Pods-iOSNativeSwiftTemplate-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-iOSNativeSwiftTemplate/Pods-iOSNativeSwiftTemplate-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iOSNativeSwiftTemplate/Pods-iOSNativeSwiftTemplate-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 4FD6C4701B754AF1002F9F90 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( B73B520D234FA65F001B069B /* SceneDelegate.swift in Sources */, + D3848AE92C82392600A78C0A /* QrCodeScanController.swift in Sources */, 4FD6C47A1B754AF1002F9F90 /* AppDelegate.swift in Sources */, 4F13E93B237DD5D9005BCAE9 /* ContactDetailsView.swift in Sources */, 4F13E947237DD72E005BCAE9 /* AccountsListModel.swift in Sources */, + D35055542C879DD500DCDC31 /* LoginTypeSelectionViewController.swift in Sources */, 4F13E93C237DD5D9005BCAE9 /* ContactsForAccountModel.swift in Sources */, 4F13E93F237DD5D9005BCAE9 /* ContactDetailModel.swift in Sources */, 4F13E93D237DD5D9005BCAE9 /* ContactsForAccountListView.swift in Sources */, @@ -333,9 +409,11 @@ }; 4FD6C4941B754AF1002F9F90 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 42BEE51B49F5BAD7CCCD23AB /* Pods-iOSNativeSwiftTemplate.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = iOSNativeSwiftTemplate/iOSNativeSwiftTemplate.entitlements; + DEVELOPMENT_TEAM = XD7TD9S6ZU; INFOPLIST_FILE = iOSNativeSwiftTemplate/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -343,6 +421,7 @@ ); OTHER_SWIFT_FLAGS = "-DSIGNPOST_ENABLED"; PRODUCT_BUNDLE_IDENTIFIER = "com.salesforce.${PRODUCT_NAME:rfc1034identifier}"; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = com.salesforce.iOSNativeTemplate; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "iOSNativeSwiftTemplate/Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -351,15 +430,18 @@ }; 4FD6C4951B754AF1002F9F90 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 83C576D5B3530B7210E42288 /* Pods-iOSNativeSwiftTemplate.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = iOSNativeSwiftTemplate/iOSNativeSwiftTemplate.entitlements; + DEVELOPMENT_TEAM = XD7TD9S6ZU; INFOPLIST_FILE = iOSNativeSwiftTemplate/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = "com.salesforce.${PRODUCT_NAME:rfc1034identifier}"; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = com.salesforce.iOSNativeTemplate; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "iOSNativeSwiftTemplate/Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/iOSNativeSwiftTemplate/iOSNativeSwiftTemplate/AppDelegate.swift b/iOSNativeSwiftTemplate/iOSNativeSwiftTemplate/AppDelegate.swift index 313cf288..fd6c7182 100644 --- a/iOSNativeSwiftTemplate/iOSNativeSwiftTemplate/AppDelegate.swift +++ b/iOSNativeSwiftTemplate/iOSNativeSwiftTemplate/AppDelegate.swift @@ -34,6 +34,9 @@ class AppDelegate : UIResponder, UIApplicationDelegate { override init() { super.init() MobileSyncSDKManager.initializeSDK() + UserAccountManager.shared.loginViewControllerConfig.loginViewControllerCreationBlock = { + return LoginTypeSelectionViewController() + } } // MARK: UISceneSession Lifecycle diff --git a/iOSNativeSwiftTemplate/iOSNativeSwiftTemplate/Info.plist b/iOSNativeSwiftTemplate/iOSNativeSwiftTemplate/Info.plist index 900c1543..a170cf28 100644 --- a/iOSNativeSwiftTemplate/iOSNativeSwiftTemplate/Info.plist +++ b/iOSNativeSwiftTemplate/iOSNativeSwiftTemplate/Info.plist @@ -2,23 +2,6 @@ - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - - - - CFBundleDevelopmentRegion en CFBundleDisplayName @@ -47,6 +30,23 @@ SFDCOAuthLoginHost login.salesforce.com + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities @@ -66,5 +66,7 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSCameraUsageDescription + This app requires camera usage permissions to capture QR codes for log in. diff --git a/iOSNativeSwiftTemplate/iOSNativeSwiftTemplate/LoginTypeSelectionViewController.swift b/iOSNativeSwiftTemplate/iOSNativeSwiftTemplate/LoginTypeSelectionViewController.swift new file mode 100644 index 00000000..9e6c5efb --- /dev/null +++ b/iOSNativeSwiftTemplate/iOSNativeSwiftTemplate/LoginTypeSelectionViewController.swift @@ -0,0 +1,80 @@ +// +// LoginTypeSelectionViewController.swift +// iOSNativeSwiftTemplate +// +// Created by Eric Johnson on 9/3/24. +// Copyright © 2024 iOSNativeSwiftTemplateOrganizationName. All rights reserved. +// + +import Foundation +import SalesforceSDKCore +import UIKit + +/** + * Adds QR code log in to the Salesforce Mobile SDK login view. + */ +class LoginTypeSelectionViewController: SalesforceLoginViewController { + + /** A button for QR code log in */ + let loginWithQrCodeButton = UIButton() + + override func loadView() { + super.loadView() + + // Load the Log In With QR Code button. + loginWithQrCodeButton.addTarget( + self, + action: #selector(loginWithQrCodeButtonTapped), + for: .touchUpInside) + loginWithQrCodeButton.backgroundColor = UIColor.purple + loginWithQrCodeButton.setTitle("Log In With QR Code", for: .normal) + + view.addSubview(loginWithQrCodeButton) + } + + override func viewWillLayoutSubviews() { + // Intentionally blank to negate legacy super view layout. + } + + override func updateViewConstraints() { + super.updateViewConstraints() + + // Replace legacy super view layout with a comparable constraint layout including the Log In With QR Code button. + self.view.translatesAutoresizingMaskIntoConstraints = true + + if let oauthView = oauthView { + oauthView.translatesAutoresizingMaskIntoConstraints = false + oauthView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + oauthView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + oauthView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + + if let biometricButton = self.biometricButton { + biometricButton.translatesAutoresizingMaskIntoConstraints = false + biometricButton.topAnchor.constraint(equalTo: oauthView.bottomAnchor, constant: 22.0).isActive = true + loginWithQrCodeButton.topAnchor.constraint(equalTo: biometricButton.bottomAnchor, constant: 22.0).isActive = true + } else { + loginWithQrCodeButton.topAnchor.constraint(equalTo: oauthView.bottomAnchor, constant: 22.0).isActive = true + } + + loginWithQrCodeButton.translatesAutoresizingMaskIntoConstraints = false + loginWithQrCodeButton.widthAnchor.constraint(equalToConstant: 200.0).isActive = true + loginWithQrCodeButton.heightAnchor.constraint(equalToConstant: 44.0).isActive = true + loginWithQrCodeButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true + loginWithQrCodeButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -22.0).isActive = true + } + } + + @objc + func loginWithQrCodeButtonTapped(_: Any?) { + + // Present a QR code scan controller to capture the log in QR code. + let qrCodeScanController = QrCodeScanController() + qrCodeScanController.onQrCodeCaptured = { qrCodePayloadString in + + // Login using the QR code payload. + print("🤘🏻 \(qrCodePayloadString)") + let _ = self.loginFromQrCode(loginQrCodeContent: qrCodePayloadString) + } + present(qrCodeScanController, animated: true) + } +} diff --git a/iOSNativeSwiftTemplate/iOSNativeSwiftTemplate/QrCodeScanController.swift b/iOSNativeSwiftTemplate/iOSNativeSwiftTemplate/QrCodeScanController.swift new file mode 100644 index 00000000..f931de22 --- /dev/null +++ b/iOSNativeSwiftTemplate/iOSNativeSwiftTemplate/QrCodeScanController.swift @@ -0,0 +1,163 @@ +// +// QrCodeScanController.swift +// iOSNativeSwiftTemplate +// +// Created by Eric Johnson on 8/30/24. +// Copyright © 2024 iOSNativeSwiftTemplateOrganizationName. All rights reserved. +// + +import AVFoundation +import Foundation +import SwiftUI + +/** + * A view enabling QR code capture. + */ +class QrCodeScanController: UIViewController { + + // MARK: - View Controller Implementation + + override func viewDidLoad() { + super.viewDidLoad() + + Task { + await setupCaptureView() + } + } + + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + + // Layout the video capture preview view. + avVideoPreviewLayer?.frame = view.bounds + } + + // MARK: - QR Code Scan Controller Implementation + + /** A callback when the QR code is captured */ + var onQrCodeCaptured: ((String) -> ())? = nil + + // MARK: - QR Code Scan Controller A/V Implementation + + /** The A/V video capture preview layer used as the QR code viewfinder */ + private var avVideoPreviewLayer: AVCaptureVideoPreviewLayer? = nil + + /** + * Determines if the user has authorized camera access for this app, requested authorization from the + * user if needed. + * + * See https://developer.apple.com/documentation/avfoundation/capture_setup/requesting_authorization_to_capture_and_save_media + */ + private var isAuthorized: Bool { + get async { + // Fetch video capture authorization status. + let status = AVCaptureDevice.authorizationStatus(for: .video) + + // Determine if the user previously authorized video capture. + var isAuthorized = status == .authorized + + // If the system hasn't determined the user's authorization status, explicitly prompt them for approval. + if status == .notDetermined { + isAuthorized = await AVCaptureDevice.requestAccess(for: .video) + } + + return isAuthorized + } + } + + /** + * Sets up for image capture via the device camera and A/V foundation. + */ + private func setupCaptureView() async { + + // Review video capture authorization status. + guard await isAuthorized else { return } + + // Acquire the default video capture device. + guard let device = AVCaptureDevice.default(for: .video) else { return } + + do { + // Fetch the video capture input. + let input = try AVCaptureDeviceInput(device: device) + + let session = AVCaptureSession() + session.addInput(input) + + // Create the video capture preview view. + let avVideoPreviewLayer = AVCaptureVideoPreviewLayer(session: session) + avVideoPreviewLayer.videoGravity = .resizeAspectFill + self.avVideoPreviewLayer = avVideoPreviewLayer + + // Create the video capture metadata output used to extract the QR code. + let output = AVCaptureMetadataOutput() + + // Create the A/V session. + session.addOutput(output) + + // Add the video capture preview view to the root view. + view.layer.addSublayer(avVideoPreviewLayer) + + // Start the A/V session. + DispatchQueue.global(qos: .background).async { + + output.setMetadataObjectsDelegate(self, queue: .main) + let x = output.availableMetadataObjectTypes + output.metadataObjectTypes = [.qr] + + session.startRunning() + } + } catch { + + // Handle A/V errors. + displayAlert() + } + } + + // MARK: - Alerts + + private func displayAlert() { + let alert = UIAlertController(title: Constants.alertTitle, + message: Constants.alertMessage, + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: Constants.alertButtonTitle, + style: .default)) + present(alert, animated: true) + } + + // MARK: - Constants + + private enum Constants { + static let alertTitle = "Camera Session Error" + static let alertMessage = "Cannot scan QR codes as a camera session could not be started." + static let alertButtonTitle = "OK" + } +} + +// MARK: - A/V Capture Metadata Output Objects Delegate Implementation + +let lock = NSLock() + +extension QrCodeScanController: AVCaptureMetadataOutputObjectsDelegate { + + func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { + + guard lock.try() else { return } + + + // Guard for QR metadata objects. + guard let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject, + metadataObject.type == .qr, + let qrCodePayloadString = metadataObject.stringValue, + qrCodePayloadString.starts(with: "mobileapp://") else { return } + + // Deliver the first QR code when it is captured. + DispatchQueue.global().asyncAfter(deadline: .now() + 5) { + lock.unlock() + self.onQrCodeCaptured?(qrCodePayloadString) + self.onQrCodeCaptured = nil + } + + // Automatically dismiss. + dismiss(animated: true) + } +} diff --git a/iOSNativeSwiftTemplate/iOSNativeSwiftTemplate/bootconfig.plist b/iOSNativeSwiftTemplate/iOSNativeSwiftTemplate/bootconfig.plist index f5d6f120..c069a65c 100644 --- a/iOSNativeSwiftTemplate/iOSNativeSwiftTemplate/bootconfig.plist +++ b/iOSNativeSwiftTemplate/iOSNativeSwiftTemplate/bootconfig.plist @@ -3,9 +3,9 @@ remoteAccessConsumerKey - 3MVG98dostKihXN53TYStBIiS8FC2a3tE3XhGId0hQ37iQjF0xe4fxMSb2mFaWZn9e3GiLs1q67TNlyRji.Xw + 3MVG9.AgwtoIvERSd8i8lePrqfnKG_MM7P9KAJ4g53iaPA4EN8zUt3__o.8YA_hCeRn_kGR.Xe9I9_pnsFuAW oauthRedirectURI - testsfdc:///mobilesdk/detect/oauth/done + mobilesdk://android/pn/tester oauthScopes web