Skip to Content

Enable Multi-User Mode for iOS Application

Learn how to configure the assistant generated application to enable the multi-user mode (one device, multiple users with secure access).
You will learn
sandeep-tdsSandeep T D SJanuary 24, 2023
Created by
sandeep-tds
January 24, 2023
Contributors
sandeep-tds
  • How to create a native iOS application that supports multiple users on the same device
  • Step 1

    An air carrier organisation uses an iOS application built using SAP BTP SDK for iOS to keep a track of an aircraft’s vital information (Fuel Level, Tyre Pressure, etc.) before each flight. The application must support offline use-case to comply with the network regulations at the airport. Since the airline has flights departing round the clock, it deploys ground staff in three 8-hour shifts. To maximise efficiency, the organisation wants ground staff to share mobile devices.

    The ground staff members want a solution that is reliable even in the absence of network. They also aren’t keen on logging out and logging in every time a shift ends, as they believe this could lead to erroneous data.

    In this tutorial, you will learn how to enhance your SAP BTP SDK for iOS Assistant generated application to create an offline enabled application that supports multiple users.

    Log in to complete tutorial
  • Step 2
    1. In your mobile services account, click Mobile Applications → Native/Hybrid → .

      Mobile Service Cockpit App Config
    2. Under Assigned Features, click Mobile Settings Exchange.

      Mobile Settings Exchange

      Assigned features can be found under the Info tab.

    3. Under Shared Devices section, enable the Allow Upload of Pending Changes from Previous User (Enable Multiple User Mode): checkbox.

      Mobile Settings Exchange

      You must enable this checkbox if you are building an offline capable multi-user application.

    In which tab can you find the Assigned Features section:

    Log in to complete tutorial
  • Step 3

    In the given scenario any pending changes done by a user should be uploaded before another user signs in. Thus, you must configure trust to enable upload of pending changes from previous users of mobile applications.

    1. Open your mobile services account.

      Mobile Service Cockpit Home
    2. In the Side Navigation Bar, click Settings → Security.

      Mobile Service Security
    3. Click Metadata Download.

      Mobile Service Metadata Download Button

      NoAuth authentication type is not supported. Even if multi-user mode is turned on, an application using the NoAuth authentication type will revert to single user mode.

    4. Go to your sub account on SAP BTP.

      BTP Cockpit Home
    5. In the Side Navigation Bar, click Security → Trust Configuration.

      BTP Trust Configurations
    6. Click New Trust Configuration.

      New Trust Configurations Button
    7. Click Upload, and select the XML file downloaded in the earlier step.

      XML Upload Button
    8. Enter a name, and click Save.

      Rename & Save Step
    Log in to complete tutorial
  • Step 4
    1. In Xcode, Open AppParameters.plist.

      Ensure that you have completed the prerequisites before starting this step.

    2. Add a new parameter by providing the following key/value.

      Key Type Value
      User Mode String Multiple
      AppParameters.plist Xcode View
    Log in to complete tutorial
  • Step 5
    1. Open AppDelegate.swift.

    2. Add a public variable to track user change status.

      swift
      Copy
      public var userDidChange = false
      
    3. Replace the applicationWillEnterForeground function with the given code to trigger multi-user flow.

      Swift
      Copy
      func applicationWillEnterForeground(_: UIApplication) {
          // Triggers to show the multi-user passcode screen
          OnboardingSessionManager.shared.unlock() { error in
              guard let error = error else {
                  if self.userDidChange {
                      self.afterOnboard()
                      self.userDidChange = false
                  }
                  return
              }
      
              self.onboardingErrorHandler?.handleUnlockingError(error)
          }
      }
      
    4. Replace the initializeOnboarding function with the following code to configure OnboardingSessionManager which includes MultiUserOnboardingIDManager.

      Swift
      Copy
      func initializeOnboarding() {
          let presentationDelegate = ApplicationUIManager(window: self.window!)
          self.onboardingErrorHandler = OnboardingErrorHandler()
          self.sessionManager = OnboardingSessionManager(presentationDelegate: presentationDelegate, flowProvider: self.flowProvider, onboardingIDManager: MultiUserOnboardingIDManager(), delegate: self.onboardingErrorHandler)
          presentationDelegate.showSplashScreenForOnboarding { _ in }
      
          self.onboardUser()
      }
      
    Log in to complete tutorial
  • Step 6
    1. Open OnboardingFlowProvider.

    2. Add a new extension for FUIPasscodeControllerDelegate by pasting the following code block for multi-user callbacks:

      Swift
      Copy
      extension OnboardingFlowProvider: FUIPasscodeControllerDelegate {
          public func shouldTryPasscode(_ passcode: String, forInputMode inputMode: FUIPasscodeInputMode, fromController passcodeController: FUIPasscodeController) throws {
              print("Called shouldTryPasscode")
          }
      
          public func shouldResetPasscode(fromController passcodeController: FUIPasscodeController) {
              print("Called shouldResetPasscode")
          }
      
      
          public func addNewUser(_ passcodeController: FUIPasscodeController) {
              print("Called addNewUser")
              AppDelegate.shared.userDidChange = true
          }
      
          public func switchUser(_ newUserId: String, passcodeController: FUIPasscodeController) {
              print("Called switchUser")
              AppDelegate.shared.userDidChange = true
          }
      }
      
    Log in to complete tutorial
  • Step 7
    1. In OnboardingFlowProvider, add a new function called configuredStoreManagerStep.

      Swift
      Copy
      private func configuredStoreManagerStep() -> StoreManagerStep {
          let st = StoreManagerStep()
          st.userPasscodeControllerDelegate = self
          return st
      }
      
    2. Replace the calls for StoreManagerStep() with configuredStoreManagerStep() in onboardingStepsand restoringSteps to onboard a new user or restore the session of a previously logged in user.

      Swift
      Copy
      public var onboardingSteps: [OnboardingStep] {
              return [
                  self.configuredWelcomeScreenStep(),
                  CompositeStep(steps: SAPcpmsDefaultSteps.configuration),
                  OAuth2AuthenticationStep(),
                  CompositeStep(steps: SAPcpmsDefaultSteps.settingsDownload),
                  CompositeStep(steps: SAPcpmsDefaultSteps.applyDuringOnboard),
                  self.configuredUserConsentStep(),
                  self.configuredDataCollectionConsentStep(),
                  configuredStoreManagerStep(),
                  ODataOnboardingStep(),
              ]
          }
      
          public var restoringSteps: [OnboardingStep] {
              return [
                  configuredStoreManagerStep(),
                  self.configuredWelcomeScreenStep(),
                  CompositeStep(steps: SAPcpmsDefaultSteps.configuration),
                  OAuth2AuthenticationStep(),
                  CompositeStep(steps: SAPcpmsDefaultSteps.settingsDownload),
                  CompositeStep(steps: SAPcpmsDefaultSteps.applyDuringRestore),
                  self.configuredDataCollectionConsentStep(),
                  ODataOnboardingStep(),
              ]
          }
      

      Replace onboardingSteps & restoringSteps with the given code.

    Log in to complete tutorial
  • Step 8
    1. Open ODataControlling.Swift.

    2. Import SAPOfflineOdata

      Swift
      Copy
          import SAPOfflineOData
      
    3. Add a new protocol for configureOData function:

      Swift
      Copy
      func configureOData(sapURLSession: SAPURLSession, serviceRoot: URL, onboardingID: UUID, offlineParameters: OfflineODataParameters) throws
      
    4. Open ODataOnboardingStep.Swift.

    5. Import SAPOfflineOdata

      Swift
      Copy
          import SAPOfflineOData
      
    6. Add offline store name’s key value.

      Swift
      Copy
          let offlineStoreNameKey: String = "SAP.OfflineOData.MultiUser"
      
    7. Add a new function offlineStoreID that generates a UUID if the offline store name is nil.

      Swift
      Copy
        private func offlineStoreID() -> UUID {
           var offlineStoreName: String? = UserDefaults.standard.value(forKey: self.offlineStoreNameKey) as? String
           if offlineStoreName == nil {
               offlineStoreName = UUID().uuidString
               UserDefaults.standard.set(offlineStoreName, forKey: self.offlineStoreNameKey)
           }
           let offlineStoreNameID: UUID = UUID(uuidString: offlineStoreName!)!
           return offlineStoreNameID
       }
      
    8. Replace the reset function with the following code to pass the offlineStoreID().

      Swift
      Copy
      public func reset(context: OnboardingContext, completionHandler: @escaping () -> Void) {
          defer { completionHandler() }
          do {
              try ESPMContainerOfflineODataController.removeStore(for: offlineStoreID())
          } catch {
              self.logger.error("Remove Offline Store failed", error: error)
          }
      }
      
    9. Add a new function getOfflineODataParameters to determine the user who is logging in.

      Swift
      Copy
        private func getOfflineODataParameters(using context: OnboardingContext, completionnHandler: @escaping (OfflineODataParameters) -> Void) {
            var currentUser: String? = nil
            var forceUploadOnUserSwitch: Bool = false
            var storeEncryptionKey: String? = nil
      
            if let onboardedUser = UserManager().get(forKey: context.onboardingID), let userId = onboardedUser.infoString {
                currentUser = userId
                storeEncryptionKey = try? context.credentialStore.get(String.self, for: EncryptionConfigLoader.encryptionKeyID)
                if let enabled = (context.info[.sapcpmsSharedDeviceSettings] as? SAPcpmsSharedDeviceSettings)?.allowUploadPendingChangesFromPreviousUser {
                    forceUploadOnUserSwitch = enabled
                }
                let offlineParameters = OfflineODataParameters()
                offlineParameters.currentUser = currentUser
                offlineParameters.forceUploadOnUserSwitch = forceUploadOnUserSwitch
                offlineParameters.storeEncryptionKey = storeEncryptionKey
                completionnHandler(offlineParameters)
            } else {
                fatalError("Failed to fetch user information!")
            }
        }
      
    10. Replace the configureOData function with the following code to determine the user mode, and configure parameters accordingly.

      Swift
      Copy
      private func configureOData(using context: OnboardingContext, completionHandler: @escaping (OnboardingResult) -> Void) {
        let semaphore: DispatchSemaphore =  DispatchSemaphore(value: 0)
      
        var offlineParameters: OfflineODataParameters = OfflineODataParameters()
        if UserManager.userMode == .multiUser {
            self.getOfflineODataParameters(using: context) { parameters in
                offlineParameters = parameters
                semaphore.signal()
            }
        }
        semaphore.wait()
        let banner = topBanner()
        let group = DispatchGroup()
        var odataControllers = [String: ODataControlling]()
        let destinations = FileConfigurationProvider("AppParameters").provideConfiguration().configuration["Destinations"] as! NSDictionary
      
        let eSPMContainerOfflineODataDelegateSample = OfflineODataDelegateSample(for: "ESPMContainer", with: banner)
        odataControllers[ODataContainerType.eSPMContainer.description] = ESPMContainerOfflineODataController(delegate: eSPMContainerOfflineODataDelegateSample)
      
        for (odataServiceName, odataController) in odataControllers {
            group.enter()
            let destinationId = destinations[odataServiceName] as! String
            // Adjust this path so it can be called after authentication and returns an HTTP 200 code. This is used to validate the authentication was successful.
            let configurationURL = URL(string: (context.info[.sapcpmsSettingsParameters] as! SAPcpmsSettingsParameters).backendURL.appendingPathComponent(destinationId).absoluteString)!
      
            do {
                try odataController.configureOData(sapURLSession: context.sapURLSession, serviceRoot: configurationURL, onboardingID: offlineStoreID(), offlineParameters: offlineParameters)
                let connectivityStatus = ConnectivityUtils.isConnected()
                self.logger.info("Network connectivity status: \(connectivityStatus)")
                odataController.openOfflineStore(synchronize: connectivityStatus) { error in
                    if let error = error {
                        completionHandler(.failed(error))
                        return
                    }
                    self.controllers[odataServiceName] = odataController
                    group.leave()
                }
            } catch {
                completionHandler(.failed(error))
            }
        }
        group.notify(queue: .main) {
            completionHandler(.success(context))
        }
      }
      
    Log in to complete tutorial
  • Step 9
    1. Open ESPMContainerOfflineODataController.Swift

    2. Update the error cases to include syncFailed

      Swift
      Copy
        public enum Error: Swift.Error {
            case cannotCreateOfflinePath
            case storeClosed
            case syncFailed
        }
      
    3. Replace the configureOData function with the following code to accept OfflineODataParameters.

      Swift
      Copy
      public func configureOData(sapURLSession: SAPURLSession, serviceRoot: URL, onboardingID: UUID, offlineParameters: OfflineODataParameters = OfflineODataParameters()) throws {
          offlineParameters.enableRepeatableRequests = true
      
          // Configure the path of the Offline Store
          let offlinePath = try ESPMContainerOfflineODataController.offlineStorePath(for: onboardingID)
          try FileManager.default.createDirectory(at: offlinePath, withIntermediateDirectories: true)
          offlineParameters.storePath = offlinePath
      
          // Setup an instance of delegate. See sample code below for definition of OfflineODataDelegateSample class.
          let offlineODataProvider = try! OfflineODataProvider(serviceRoot: serviceRoot, parameters: offlineParameters, sapURLSession: sapURLSession, delegate: delegate)
          try configureDefiningQueries(on: offlineODataProvider)
          self.dataService = ESPMContainer(provider: offlineODataProvider)
      }
      
    4. Replace the openOfflineStore function with the following code to catch the sync error.

      Swift
      Copy
        public func openOfflineStore(synchronize: Bool, completionHandler: @escaping (Swift.Error?) -> Void) {
          if !self.isOfflineStoreOpened {
              // The OfflineODataProvider needs to be opened before performing any operations.
              self.dataService.open { error in
                  if let error = error {
                      self.logger.error("Could not open offline store.", error: error)
                      if (error.code == -10425 || error.code == -10426) {
                          completionHandler(Error.syncFailed)
                      } else {
                          completionHandler(error)
                      }
                      return
                  }
                  self.isOfflineStoreOpened = true
                  self.logger.info("Offline store opened.")
                  if synchronize {
                      // You might want to consider doing the synchronization based on an explicit user interaction instead of automatically synchronizing during startup
                      self.synchronize(completionHandler: completionHandler)
                  } else {
                      completionHandler(nil)
                  }
              }
          } else if synchronize {
              // You might want to consider doing the synchronization based on an explicit user interaction instead of automatically synchronizing during startup
              self.synchronize(completionHandler: completionHandler)
          } else {
              completionHandler(nil)
          }
        }
      
    Log in to complete tutorial
  • Step 10
    1. Open OnboardingErrorHandler.swift.

    2. Replace the onboardingController function with the following code to handle application specific error handling.

      swift
      Copy

      public func onboardingController(_ controller: OnboardingControlling, didFail flow: OnboardingFlow, with error: Error, completionHandler: @escaping (OnboardingErrorDisposition) -> Void) { switch flow.flowType { case .onboard, .resetPasscode: self.onboardFailed(with: error, completionHandler: completionHandler) case .restore: self.restoreFailed(with: error, controller: controller, context: flow.context, completionHandler: completionHandler) default: completionHandler(.retry) } }
    3. Replace onboardFailed function with the given code to handle duplicate user in case of add user, user mismatch in case of reset passcode and authenticationManager related errors.

      swift
      Copy
      private func onboardFailed(with error: Error, completionHandler: @escaping (OnboardingErrorDisposition) -> Void) {
          switch error {
          case WelcomeScreenError.demoModeRequested:
              completionHandler(.stop(error))
              return
          default:
              showAlertWith(error: error)
          }
      
          func showAlertWith(error: Error) {
              let alertController = UIAlertController(
                  title: LocalizedStrings.Onboarding.failedToLogonTitle,
                  message: error.localizedDescription,
                  preferredStyle: .alert
              )
              switch error {
      
              case SAPcpmsAuthenticationManagerError.userSwitch(from: let fromId, to: let toId):
      
                  if let _ = UserManager().get(forKey: UUID(uuidString: toId)!) {
      
                      alertController.addAction(UIAlertAction(title: "Restore", style: .default) { _ in
                          self.switchToDuplicateUserWith(onboardingID: toId, completionHandler: completionHandler)
                      })
      
                  } else {
                      alertController.addAction(UIAlertAction(title: "Onboard", style: .default) { _ in
                          self.switchToDuplicateUserWith(onboardingID: nil, completionHandler: completionHandler)
                      })
                  }
              case UserManagerError.userAlreadyExists(with: let onboardingID):
                  alertController.addAction(UIAlertAction(title: "Restore", style: .default) { _ in
                      self.switchToDuplicateUserWith(onboardingID: onboardingID, completionHandler: completionHandler)
                  })
              case UserManagerError.userMismatch(with: let id):
                  if let idNotNil = id {
                      alertController.addAction(UIAlertAction(title: "Restore", style: .default) { _ in
                          self.switchToDuplicateUserWith(onboardingID: idNotNil, completionHandler: completionHandler)
                      })
      
                  } else {
                      alertController.addAction(UIAlertAction(title: "Onboard", style: .default) { _ in
                          self.switchToDuplicateUserWith(onboardingID: nil, completionHandler: completionHandler)
                      })
      
                  }
              default:
                  alertController.addAction(UIAlertAction(title: LocalizedStrings.Onboarding.retryTitle, style: .default) { _ in
                      completionHandler(.retry)
                  })
              }
      
              DispatchQueue.main.async {
                  guard let topViewController = ModalUIViewControllerPresenter.topPresentedViewController() else {
                      fatalError("Invalid UI state")
                  }
                  topViewController.present(alertController, animated: true)
              }
          }
      }
      
    4. Replace restoreFailed function with the given code to handle multi-user errors during restore.

      swift
      Copy
      private func restoreFailed(with error: Error, controller: OnboardingControlling, onboardingID: UUID?, completionHandler: @escaping (OnboardingErrorDisposition) -> Void) {
          let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .alert)
      
          switch error {
          case StoreManagerError.cancelPasscodeEntry, StoreManagerError.skipPasscodeSetup, StoreManagerError.resetPasscode:
              resetOnboarding(onboardingID, controller: controller, completionHandler: completionHandler)
              return
          case StoreManagerError.passcodeRetryLimitReached:
              alertController.title = LocalizedStrings.Onboarding.passcodeRetryLimitReachedTitle
              alertController.message = LocalizedStrings.Onboarding.passcodeRetryLimitReachedMessage
          case SAPcpmsAuthenticationManagerError.userSwitch(from: let fromId, to: let toId):
      
              if let _ = UserManager().get(forKey: UUID(uuidString: toId)!) {
      
                  alertController.addAction(UIAlertAction(title: "Restore", style: .default) { _ in
                      self.switchToDuplicateUserWith(onboardingID: toId, completionHandler: completionHandler)
                  })
      
              } else {
                  alertController.addAction(UIAlertAction(title: "Onboard", style: .default) { _ in
                      self.switchToDuplicateUserWith(onboardingID: nil, completionHandler: completionHandler)
                  })
              }
          case ApplicationVersioningError.inactive:
              alertController.title = LocalizedStrings.Onboarding.failedToLogonTitle
              alertController.message = error.localizedDescription
              alertController.addAction(UIAlertAction(title: LocalizedStrings.Onboarding.retryTitle, style: .default) { _ in
                  completionHandler(.retry)
              })
          default:
              alertController.title = LocalizedStrings.Onboarding.failedToLogonTitle
              alertController.message = error.localizedDescription
              alertController.addAction(UIAlertAction(title: LocalizedStrings.Onboarding.retryTitle, style: .default) { _ in
                  completionHandler(.retry)
              })
          }
      
          alertController.addAction(UIAlertAction(title: LocalizedStrings.Onboarding.resetTitle, style: .destructive) { _ in
              self.resetOnboarding(onboardingID, controller: controller, completionHandler: completionHandler)
          })
      
          DispatchQueue.main.async {
              guard let topViewController = ModalUIViewControllerPresenter.topPresentedViewController() else {
                  fatalError("Invalid UI state")
              }
              topViewController.present(alertController, animated: true)
          }
      }
      
    5. Add a new function switchToDuplicateUserWith to set transientUser.

      swift
      Copy
      private func switchToDuplicateUserWith(onboardingID: String?, completionHandler: @escaping (OnboardingErrorDisposition) -> Void) {
          if let id = onboardingID {
              let user = UserManager().get(forKey: UUID(uuidString: id)!)
              user?.onboardingStatus = .restoreInProgress
              UserManager.transientUser = user
          } else {
              UserManager.transientUser = nil
          }
          completionHandler(.retry)
      }
      
    Log in to complete tutorial
  • Step 11
    1. In the menu bar, click Product → Build.

      Xcode Build

      Any errors you see will be cleared after building the project.

    2. Select a suitable simulator/device and run your project.

      Xcode Run
    Log in to complete tutorial
  • Step 12
    1. Click Start.

      iOS App Start
    2. Select Default Identity Provider.

      iOS App Identity Provider Selection
    3. Enter username and password, and click Log On.

      iOS App Log in
    4. Click Allow Data.

      iOS App Allow Data
    5. Click Allow Usage.

      iOS App Allow Usage
    6. Choose a passcode, and click Next.

      iOS App Choose Passcode

      Biometric authentication is not supported. The biometric screen will not be shown in the onboarding or unlock processes.

      No passcode policy is not supported. A default passcode policy will be used if the server has disabled it.

    7. Enter the passcode again, and click Done.

      iOS App Confirm Passcode
    8. Terminate the app or Send the app to background.

      iOS App Background
    9. In the sign in screen, click Switch or Add User.

      iOS App Sign in screen
    10. Click

      Add Users Icon
      to add a new user.

      Ensure all users being onboarded have access to the BTP account. You can configure this in the users section under security on your SAP BTP sub-account.

      Create User

    11. Follow the onboarding flow for the second user.

      iOS App Second user Sign in flow
    12. Terminate the app or Send the app to background.

      iOS App Background
    13. In the sign in screen, click Switch or Add User.

      iOS App Sign in screen
    14. Select a different user.

      iOS App User select
    15. Enter the selected user’s passcode.

      iOS App User passcode

    Which of the following security feature is permitted for multi-user support:

    Log in to complete tutorial
  • Step 13
    1. Sign into User A’s account.

      iOS App User passcode
    2. Turn off your network.

      If you are running a simulator, turn off your parent system’s WiFi.

    3. Update an entry.

      iOS App field update
    4. Turn on your network.

    5. Sign in using User B’s account.

      iOS App field update
    6. Verify the change done by user A.

      iOS App verify update

    In this step you’ve seen how the changes done by User A are not lost even when they were done in the absence of network.

    Congratulations on completing the tutorial!

    Log in to complete tutorial
Back to top