├── .gitignore ├── LICENSE.md ├── README.md ├── Settings.bundle ├── Root.plist └── en.lproj │ └── Root.strings ├── chatr.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ └── chatr.xcscheme ├── chatr ├── AboutUsPage.swift ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── DM (2).jpg │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x-1.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x-1.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x-1.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ ├── ChatPage.swift │ ├── Contents.json │ ├── backarrow.imageset │ │ ├── Contents.json │ │ └── backarrow.png │ ├── information.imageset │ │ ├── Contents.json │ │ └── Picture1.png │ ├── logo.imageset │ │ ├── Contents.json │ │ ├── iTunesArtwork@2x.png │ │ ├── iTunesArtwork@3x.png │ │ └── logo.png │ ├── menubar.imageset │ │ ├── Contents.json │ │ └── DM-3.jpg │ ├── print.imageset │ │ ├── Contents.json │ │ └── print - graphic.png │ ├── profile.imageset │ │ ├── Contents.json │ │ └── person - graphic.png │ ├── save.imageset │ │ ├── Contents.json │ │ └── save - graphic.png │ ├── settings.imageset │ │ ├── Contents.json │ │ └── settings-graphic.png │ └── sidebar.imageset │ │ ├── Contents.json │ │ └── DM-3.jpg ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── ChatPage.swift ├── ChatTableViewCell.swift ├── EnrollmentDelegate.swift ├── Info.plist ├── KeychainManager.swift ├── LoginPage.swift ├── PolicyDelegate.swift ├── SettingsPage.swift ├── SideBarTableCell.swift ├── UIUtils.swift └── chatr.entitlements └── logo.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | xcuserdata/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Microsoft Intune 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chatr - An Intune MAM iOS SDK Example (Swift) 2 | This application is a demonstration of the [Microsoft Intune SDK for iOS](https://github.com/msintuneappsdk/ms-intune-app-sdk-ios). A developer guide to the SDK is available [here](https://learn.microsoft.com/mem/intune/developer/app-sdk-ios). This project implements some commonly used features so developers integrating their apps with the SDK have an example to follow. 3 | 4 | Chatr offers a simple messaging interface allowing users to send messages, print, and save conversations to their local device. It uses the [Microsoft Authentication Library](https://github.com/AzureAD/microsoft-authentication-library-for-objc) to authenticate users. 5 | 6 | ## Steps to run the app 7 | In order to deploy this sample you will need an Intune subscription. Free trials are sufficient for this demo. 8 | 9 | ### Step 1: Setting up Intune 10 | You will need at least one user assigned to a user group. You can see how to create new users [here](https://learn.microsoft.com/mem/intune/fundamentals/quickstart-create-user) and user groups [here](https://learn.microsoft.com/mem/intune/fundamentals/quickstart-create-group). Be sure to assign Intune licenses to your users [here](https://learn.microsoft.com/mem/intune/fundamentals/quickstart-create-user#assign-a-license-to-an-individual-user). 11 | 12 | ### Step 2: Create and Deploy App Protection Policy (APP) 13 | To enable MAM without device enrollment (MAM-WE), we must create a new App Protection Policy with Intune. Instructions for creating and deploying a new APP can be found [here](https://learn.microsoft.com/mem/intune/apps/quickstart-create-assign-app-policy). 14 | 1. To create an APP targeting the Chatr sample app, click on **Create policy** in the "App protection policies" pane. 15 | 1. In the "Create policy" pane specify the protection policy name, description, and platform. Click on **Select required apps**. 16 | 1. At the top of the pane click **More apps** and scroll to the bottom of the pane. 17 | 1. Enter the Bundle ID of your app and click **Add**. The Chatr bundle ID can be found by selecting the project file in the Xcode project explorer, selecting the chatr target, and selecting the "General" tab. This app's bundle ID is `Intune.chatr`. 18 | 1. Hit **Select** at the bottom of the "Apps" pane. 19 | 1. Click the **Settings** button in the "Create policy" pane and set the policy settings you would like to apply to a user group for your app. 20 | 1. Once you have selected the settings click **OK** at the bottom of the Settings pane and then click **Create** at the bottom of the "Create policy" pane. Your app should now appear in the "App protection policies" pane. 21 | 22 | ### Step 3: Create and Deploy App Configuration Policy 23 | Instructions for creating and deploying a new App Configuration Policy can be found [here](https://learn.microsoft.com/mem/intune/apps/app-configuration-policies-use-ios). 24 | 1. To create an App Configuration Policy targeting the Chatr sample app, click on **Add** in the "App configuration policies" pane. 25 | 1. In the "Add configuration policy" pane specify the configuration policy name and description. Under "Device enrollment type" select **Managed apps**. Click on **Select the required app**. 26 | 1. At the top of the pane click **More apps** and scroll to the bottom of the pane. 27 | 1. Enter the Bundle ID of your app and click **OK**. The Chatr bundle ID can be found by selecting the project file in the Xcode project explorer, selecting the chatr target, and selecting the "General" tab. This app's Package ID is `Intune.chatr`. 28 | 1. Click the **Configuration settings** button in the "Add configuration policy" pane and set the key-value pair configuration you would like to apply to a user group for your app. For intance, to change the messaging group name on the Chat Page of the Chatr sample app to "Intune", you can create a configuration where the key is "GroupName" and the value is "Intune". 29 | 1. Once you have added the key-value pair configuration click **OK** at the bottom of the "Configuration" pane and then click **Add** at the bottom of the "Add configuration policy" pane. Your app should now appear in the "App configuration policies" pane. 30 | 31 | ### Step 4: Launch the App & Sign-In 32 | Chatr should now be properly configured with Intune. When prompted to sign in, use one of the users in the group used in Step 2 or Step 3. 33 | ## Relevant Files 34 | - `Chatr/LoginPage.swift` contains logic for authenticating and enrolling the user with Intune. 35 | - `Chatr/EnrollmentDelegate.swift` contains logic which responds to an Intune enrollment or unenrollment attempt. 36 | - `Chatr/PolicyDelegate.swift` contains logic for removing data for a specific user when a selective wipe command is received from the Intune MAM service and responding to when the Intune SDK needs to restart the application. 37 | - `Chatr/KeychainManager.swift` contains logic for adding, updating, and removing user data in the keychain. 38 | - `Chatr/ChatPage.swift` 39 | - Registers the application to receive notifications when an IT administrator updates app configuration or protection policies. 40 | - Contains all of the main functionality of the app. 41 | - `Chatr/SettingsPage.swift` contains option to display the Intune Diagnostics Console, which end users can use to help IT administrators and Microsoft support diagnose issues. 42 | -------------------------------------------------------------------------------- /Settings.bundle/Root.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | StringsTable 6 | Root 7 | PreferenceSpecifiers 8 | 9 | 10 | Type 11 | PSToggleSwitchSpecifier 12 | Title 13 | Display Intune Diagnostic Console 14 | Key 15 | PSToggleSwitchSpecifier 16 | DefaultValue 17 | 18 | 19 | 20 | Type 21 | PSToggleSwitchSpecifier 22 | Title 23 | Intune Diagnostic Enabled 24 | Key 25 | IntuneDiagnosticEnabled 26 | DefaultValue 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Settings.bundle/en.lproj/Root.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/Settings.bundle/en.lproj/Root.strings -------------------------------------------------------------------------------- /chatr.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 68AF9BE321E8E19300127E37 /* SettingsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68AF9BE021E8E19200127E37 /* SettingsPage.swift */; }; 11 | 68BD62D222039F2F00FC3F16 /* PolicyDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68BD62D122039F2F00FC3F16 /* PolicyDelegate.swift */; }; 12 | 68BD62D422039F4700FC3F16 /* EnrollmentDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68BD62D322039F4700FC3F16 /* EnrollmentDelegate.swift */; }; 13 | 68EE322C21F141C600C47FA1 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68EE322B21F141C600C47FA1 /* KeychainManager.swift */; }; 14 | 68FECA9C2204CA1700540C42 /* UIUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68FECA9B2204CA1700540C42 /* UIUtils.swift */; }; 15 | AC008E442D7A4C8900CBC247 /* MSAL in Frameworks */ = {isa = PBXBuildFile; productRef = AC008E432D7A4C8900CBC247 /* MSAL */; }; 16 | AC124D5D246B30FC00409590 /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AC124D5C246B30FC00409590 /* CoreServices.framework */; }; 17 | AC7BDDA62D7A745F00C46F2D /* IntuneMAMSwift in Frameworks */ = {isa = PBXBuildFile; productRef = AC7BDDA52D7A745F00C46F2D /* IntuneMAMSwift */; }; 18 | AC7BDDA82D7A745F00C46F2D /* IntuneMAMTelemetry in Frameworks */ = {isa = PBXBuildFile; productRef = AC7BDDA72D7A745F00C46F2D /* IntuneMAMTelemetry */; }; 19 | C01A67E020DAB03F00F80AFB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01A67DF20DAB03F00F80AFB /* AppDelegate.swift */; }; 20 | C01A67E220DAB03F00F80AFB /* LoginPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01A67E120DAB03F00F80AFB /* LoginPage.swift */; }; 21 | C01A67E520DAB03F00F80AFB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C01A67E320DAB03F00F80AFB /* Main.storyboard */; }; 22 | C01A67E720DAB03F00F80AFB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C01A67E620DAB03F00F80AFB /* Assets.xcassets */; }; 23 | C01A67EA20DAB03F00F80AFB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C01A67E820DAB03F00F80AFB /* LaunchScreen.storyboard */; }; 24 | C01A680E20DAE2BB00F80AFB /* logo.jpg in Resources */ = {isa = PBXBuildFile; fileRef = C01A680D20DAE2BB00F80AFB /* logo.jpg */; }; 25 | C01A681320E166CA00F80AFB /* SideBarTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01A681220E166CA00F80AFB /* SideBarTableCell.swift */; }; 26 | C027B3B120E687DF00392A38 /* ChatPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C027B3B020E687DF00392A38 /* ChatPage.swift */; }; 27 | C0571FB420EAAB57008716A8 /* AboutUsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0571FB320EAAB57008716A8 /* AboutUsPage.swift */; }; 28 | C0E7DA0320EE766600EA7494 /* ChatTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E7DA0220EE766600EA7494 /* ChatTableViewCell.swift */; }; 29 | C0E7DA0A20FE897200EA7494 /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0E7DA0920FE897200EA7494 /* MessageUI.framework */; }; 30 | C0E7DA0C20FE897900EA7494 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0E7DA0B20FE897900EA7494 /* Security.framework */; }; 31 | C0E7DA1020FE898D00EA7494 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0E7DA0F20FE898D00EA7494 /* SystemConfiguration.framework */; }; 32 | C0E7DA1220FE899900EA7494 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = C0E7DA1120FE899900EA7494 /* libsqlite3.tbd */; }; 33 | C0E7DA1420FE89A300EA7494 /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = C0E7DA1320FE89A300EA7494 /* libc++.tbd */; }; 34 | C0E7DA1620FE89AD00EA7494 /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0E7DA1520FE89AD00EA7494 /* ImageIO.framework */; }; 35 | C0E7DA1820FE89B500EA7494 /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0E7DA1720FE89B500EA7494 /* LocalAuthentication.framework */; }; 36 | C0E7DA1A20FE89C800EA7494 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0E7DA1920FE89C800EA7494 /* AudioToolbox.framework */; }; 37 | C0E7DA1C20FE89D400EA7494 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0E7DA1B20FE89D400EA7494 /* QuartzCore.framework */; }; 38 | C0E7DA1E20FE89DB00EA7494 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0E7DA1D20FE89DB00EA7494 /* WebKit.framework */; }; 39 | /* End PBXBuildFile section */ 40 | 41 | /* Begin PBXFileReference section */ 42 | 68AF9BE021E8E19200127E37 /* SettingsPage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsPage.swift; sourceTree = ""; }; 43 | 68BD62D122039F2F00FC3F16 /* PolicyDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PolicyDelegate.swift; sourceTree = ""; }; 44 | 68BD62D322039F4700FC3F16 /* EnrollmentDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnrollmentDelegate.swift; sourceTree = ""; }; 45 | 68EE322B21F141C600C47FA1 /* KeychainManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainManager.swift; sourceTree = ""; }; 46 | 68FECA9B2204CA1700540C42 /* UIUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIUtils.swift; sourceTree = ""; }; 47 | AC124D5C246B30FC00409590 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; }; 48 | C01A67DC20DAB03F00F80AFB /* chatr.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = chatr.app; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | C01A67DF20DAB03F00F80AFB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 50 | C01A67E120DAB03F00F80AFB /* LoginPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginPage.swift; sourceTree = ""; }; 51 | C01A67E420DAB03F00F80AFB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 52 | C01A67E620DAB03F00F80AFB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 53 | C01A67E920DAB03F00F80AFB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 54 | C01A67EB20DAB03F00F80AFB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 55 | C01A680D20DAE2BB00F80AFB /* logo.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = logo.jpg; path = ../logo.jpg; sourceTree = ""; }; 56 | C01A681220E166CA00F80AFB /* SideBarTableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideBarTableCell.swift; sourceTree = ""; }; 57 | C027B3B020E687DF00392A38 /* ChatPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPage.swift; sourceTree = ""; }; 58 | C0571FB320EAAB57008716A8 /* AboutUsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutUsPage.swift; sourceTree = ""; }; 59 | C0E7DA0220EE766600EA7494 /* ChatTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTableViewCell.swift; sourceTree = ""; }; 60 | C0E7DA0920FE897200EA7494 /* MessageUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageUI.framework; path = System/Library/Frameworks/MessageUI.framework; sourceTree = SDKROOT; }; 61 | C0E7DA0B20FE897900EA7494 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; 62 | C0E7DA0F20FE898D00EA7494 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; 63 | C0E7DA1120FE899900EA7494 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; 64 | C0E7DA1320FE89A300EA7494 /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; }; 65 | C0E7DA1520FE89AD00EA7494 /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = System/Library/Frameworks/ImageIO.framework; sourceTree = SDKROOT; }; 66 | C0E7DA1720FE89B500EA7494 /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = System/Library/Frameworks/LocalAuthentication.framework; sourceTree = SDKROOT; }; 67 | C0E7DA1920FE89C800EA7494 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; 68 | C0E7DA1B20FE89D400EA7494 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 69 | C0E7DA1D20FE89DB00EA7494 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; 70 | C0E7DA1F20FE8CAC00EA7494 /* chatr.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = chatr.entitlements; sourceTree = ""; }; 71 | /* End PBXFileReference section */ 72 | 73 | /* Begin PBXFrameworksBuildPhase section */ 74 | C01A67D920DAB03F00F80AFB /* Frameworks */ = { 75 | isa = PBXFrameworksBuildPhase; 76 | buildActionMask = 2147483647; 77 | files = ( 78 | C0E7DA1E20FE89DB00EA7494 /* WebKit.framework in Frameworks */, 79 | AC7BDDA82D7A745F00C46F2D /* IntuneMAMTelemetry in Frameworks */, 80 | AC7BDDA62D7A745F00C46F2D /* IntuneMAMSwift in Frameworks */, 81 | C0E7DA1C20FE89D400EA7494 /* QuartzCore.framework in Frameworks */, 82 | C0E7DA1A20FE89C800EA7494 /* AudioToolbox.framework in Frameworks */, 83 | C0E7DA1820FE89B500EA7494 /* LocalAuthentication.framework in Frameworks */, 84 | AC008E442D7A4C8900CBC247 /* MSAL in Frameworks */, 85 | AC124D5D246B30FC00409590 /* CoreServices.framework in Frameworks */, 86 | C0E7DA1620FE89AD00EA7494 /* ImageIO.framework in Frameworks */, 87 | C0E7DA1420FE89A300EA7494 /* libc++.tbd in Frameworks */, 88 | C0E7DA1220FE899900EA7494 /* libsqlite3.tbd in Frameworks */, 89 | C0E7DA1020FE898D00EA7494 /* SystemConfiguration.framework in Frameworks */, 90 | C0E7DA0C20FE897900EA7494 /* Security.framework in Frameworks */, 91 | C0E7DA0A20FE897200EA7494 /* MessageUI.framework in Frameworks */, 92 | ); 93 | runOnlyForDeploymentPostprocessing = 0; 94 | }; 95 | /* End PBXFrameworksBuildPhase section */ 96 | 97 | /* Begin PBXGroup section */ 98 | C01A67D320DAB03F00F80AFB = { 99 | isa = PBXGroup; 100 | children = ( 101 | C01A67DE20DAB03F00F80AFB /* chatr */, 102 | C0E7DA0820FE897100EA7494 /* Frameworks */, 103 | C01A67DD20DAB03F00F80AFB /* Products */, 104 | ); 105 | sourceTree = ""; 106 | }; 107 | C01A67DD20DAB03F00F80AFB /* Products */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | C01A67DC20DAB03F00F80AFB /* chatr.app */, 111 | ); 112 | name = Products; 113 | sourceTree = ""; 114 | }; 115 | C01A67DE20DAB03F00F80AFB /* chatr */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | C0571FB320EAAB57008716A8 /* AboutUsPage.swift */, 119 | C01A67DF20DAB03F00F80AFB /* AppDelegate.swift */, 120 | C01A67E620DAB03F00F80AFB /* Assets.xcassets */, 121 | C027B3B020E687DF00392A38 /* ChatPage.swift */, 122 | C0E7DA1F20FE8CAC00EA7494 /* chatr.entitlements */, 123 | C0E7DA0220EE766600EA7494 /* ChatTableViewCell.swift */, 124 | 68BD62D322039F4700FC3F16 /* EnrollmentDelegate.swift */, 125 | C01A67EB20DAB03F00F80AFB /* Info.plist */, 126 | 68EE322B21F141C600C47FA1 /* KeychainManager.swift */, 127 | C01A67E820DAB03F00F80AFB /* LaunchScreen.storyboard */, 128 | C01A67E120DAB03F00F80AFB /* LoginPage.swift */, 129 | C01A680D20DAE2BB00F80AFB /* logo.jpg */, 130 | C01A67E320DAB03F00F80AFB /* Main.storyboard */, 131 | 68BD62D122039F2F00FC3F16 /* PolicyDelegate.swift */, 132 | 68AF9BE021E8E19200127E37 /* SettingsPage.swift */, 133 | C01A681220E166CA00F80AFB /* SideBarTableCell.swift */, 134 | 68FECA9B2204CA1700540C42 /* UIUtils.swift */, 135 | ); 136 | path = chatr; 137 | sourceTree = ""; 138 | }; 139 | C0E7DA0820FE897100EA7494 /* Frameworks */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | C0E7DA1920FE89C800EA7494 /* AudioToolbox.framework */, 143 | AC124D5C246B30FC00409590 /* CoreServices.framework */, 144 | C0E7DA1520FE89AD00EA7494 /* ImageIO.framework */, 145 | C0E7DA1320FE89A300EA7494 /* libc++.tbd */, 146 | C0E7DA1120FE899900EA7494 /* libsqlite3.tbd */, 147 | C0E7DA1720FE89B500EA7494 /* LocalAuthentication.framework */, 148 | C0E7DA0920FE897200EA7494 /* MessageUI.framework */, 149 | C0E7DA1B20FE89D400EA7494 /* QuartzCore.framework */, 150 | C0E7DA0B20FE897900EA7494 /* Security.framework */, 151 | C0E7DA0F20FE898D00EA7494 /* SystemConfiguration.framework */, 152 | C0E7DA1D20FE89DB00EA7494 /* WebKit.framework */, 153 | ); 154 | name = Frameworks; 155 | sourceTree = ""; 156 | }; 157 | /* End PBXGroup section */ 158 | 159 | /* Begin PBXNativeTarget section */ 160 | C01A67DB20DAB03F00F80AFB /* chatr */ = { 161 | isa = PBXNativeTarget; 162 | buildConfigurationList = C01A680420DAB03F00F80AFB /* Build configuration list for PBXNativeTarget "chatr" */; 163 | buildPhases = ( 164 | C01A67D820DAB03F00F80AFB /* Sources */, 165 | C01A67D920DAB03F00F80AFB /* Frameworks */, 166 | C01A67DA20DAB03F00F80AFB /* Resources */, 167 | ); 168 | buildRules = ( 169 | ); 170 | dependencies = ( 171 | ); 172 | name = chatr; 173 | productName = chatr; 174 | productReference = C01A67DC20DAB03F00F80AFB /* chatr.app */; 175 | productType = "com.apple.product-type.application"; 176 | }; 177 | /* End PBXNativeTarget section */ 178 | 179 | /* Begin PBXProject section */ 180 | C01A67D420DAB03F00F80AFB /* Project object */ = { 181 | isa = PBXProject; 182 | attributes = { 183 | BuildIndependentTargetsInParallel = YES; 184 | LastSwiftUpdateCheck = 0920; 185 | LastUpgradeCheck = 1610; 186 | ORGANIZATIONNAME = "Microsoft Intune"; 187 | TargetAttributes = { 188 | C01A67DB20DAB03F00F80AFB = { 189 | CreatedOnToolsVersion = 9.2; 190 | LastSwiftMigration = 1020; 191 | ProvisioningStyle = Automatic; 192 | SystemCapabilities = { 193 | com.apple.Keychain = { 194 | enabled = 1; 195 | }; 196 | }; 197 | }; 198 | }; 199 | }; 200 | buildConfigurationList = C01A67D720DAB03F00F80AFB /* Build configuration list for PBXProject "chatr" */; 201 | compatibilityVersion = "Xcode 8.0"; 202 | developmentRegion = en; 203 | hasScannedForEncodings = 0; 204 | knownRegions = ( 205 | en, 206 | Base, 207 | ); 208 | mainGroup = C01A67D320DAB03F00F80AFB; 209 | packageReferences = ( 210 | AC008E422D7A4C8900CBC247 /* XCRemoteSwiftPackageReference "microsoft-authentication-library-for-objc" */, 211 | AC7BDDA42D7A745F00C46F2D /* XCRemoteSwiftPackageReference "ms-intune-app-sdk-ios" */, 212 | ); 213 | productRefGroup = C01A67DD20DAB03F00F80AFB /* Products */; 214 | projectDirPath = ""; 215 | projectRoot = ""; 216 | targets = ( 217 | C01A67DB20DAB03F00F80AFB /* chatr */, 218 | ); 219 | }; 220 | /* End PBXProject section */ 221 | 222 | /* Begin PBXResourcesBuildPhase section */ 223 | C01A67DA20DAB03F00F80AFB /* Resources */ = { 224 | isa = PBXResourcesBuildPhase; 225 | buildActionMask = 2147483647; 226 | files = ( 227 | C01A680E20DAE2BB00F80AFB /* logo.jpg in Resources */, 228 | C01A67EA20DAB03F00F80AFB /* LaunchScreen.storyboard in Resources */, 229 | C01A67E720DAB03F00F80AFB /* Assets.xcassets in Resources */, 230 | C01A67E520DAB03F00F80AFB /* Main.storyboard in Resources */, 231 | ); 232 | runOnlyForDeploymentPostprocessing = 0; 233 | }; 234 | /* End PBXResourcesBuildPhase section */ 235 | 236 | /* Begin PBXSourcesBuildPhase section */ 237 | C01A67D820DAB03F00F80AFB /* Sources */ = { 238 | isa = PBXSourcesBuildPhase; 239 | buildActionMask = 2147483647; 240 | files = ( 241 | 68BD62D222039F2F00FC3F16 /* PolicyDelegate.swift in Sources */, 242 | 68AF9BE321E8E19300127E37 /* SettingsPage.swift in Sources */, 243 | C0571FB420EAAB57008716A8 /* AboutUsPage.swift in Sources */, 244 | C0E7DA0320EE766600EA7494 /* ChatTableViewCell.swift in Sources */, 245 | 68EE322C21F141C600C47FA1 /* KeychainManager.swift in Sources */, 246 | 68FECA9C2204CA1700540C42 /* UIUtils.swift in Sources */, 247 | 68BD62D422039F4700FC3F16 /* EnrollmentDelegate.swift in Sources */, 248 | C01A67E220DAB03F00F80AFB /* LoginPage.swift in Sources */, 249 | C01A67E020DAB03F00F80AFB /* AppDelegate.swift in Sources */, 250 | C01A681320E166CA00F80AFB /* SideBarTableCell.swift in Sources */, 251 | C027B3B120E687DF00392A38 /* ChatPage.swift in Sources */, 252 | ); 253 | runOnlyForDeploymentPostprocessing = 0; 254 | }; 255 | /* End PBXSourcesBuildPhase section */ 256 | 257 | /* Begin PBXVariantGroup section */ 258 | C01A67E320DAB03F00F80AFB /* Main.storyboard */ = { 259 | isa = PBXVariantGroup; 260 | children = ( 261 | C01A67E420DAB03F00F80AFB /* Base */, 262 | ); 263 | name = Main.storyboard; 264 | sourceTree = ""; 265 | }; 266 | C01A67E820DAB03F00F80AFB /* LaunchScreen.storyboard */ = { 267 | isa = PBXVariantGroup; 268 | children = ( 269 | C01A67E920DAB03F00F80AFB /* Base */, 270 | ); 271 | name = LaunchScreen.storyboard; 272 | sourceTree = ""; 273 | }; 274 | /* End PBXVariantGroup section */ 275 | 276 | /* Begin XCBuildConfiguration section */ 277 | C01A680220DAB03F00F80AFB /* Debug */ = { 278 | isa = XCBuildConfiguration; 279 | buildSettings = { 280 | ALWAYS_SEARCH_USER_PATHS = NO; 281 | CLANG_ANALYZER_NONNULL = YES; 282 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 283 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 284 | CLANG_CXX_LIBRARY = "libc++"; 285 | CLANG_ENABLE_MODULES = YES; 286 | CLANG_ENABLE_OBJC_ARC = YES; 287 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 288 | CLANG_WARN_BOOL_CONVERSION = YES; 289 | CLANG_WARN_COMMA = YES; 290 | CLANG_WARN_CONSTANT_CONVERSION = YES; 291 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 292 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 293 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 294 | CLANG_WARN_EMPTY_BODY = YES; 295 | CLANG_WARN_ENUM_CONVERSION = YES; 296 | CLANG_WARN_INFINITE_RECURSION = YES; 297 | CLANG_WARN_INT_CONVERSION = YES; 298 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 299 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 300 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 301 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 302 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 303 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 304 | CLANG_WARN_STRICT_PROTOTYPES = YES; 305 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 306 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 307 | CLANG_WARN_UNREACHABLE_CODE = YES; 308 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 309 | CODE_SIGN_IDENTITY = "iPhone Developer"; 310 | COPY_PHASE_STRIP = NO; 311 | DEBUG_INFORMATION_FORMAT = dwarf; 312 | ENABLE_BITCODE = NO; 313 | ENABLE_STRICT_OBJC_MSGSEND = YES; 314 | ENABLE_TESTABILITY = YES; 315 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 316 | GCC_C_LANGUAGE_STANDARD = gnu11; 317 | GCC_DYNAMIC_NO_PIC = NO; 318 | GCC_NO_COMMON_BLOCKS = YES; 319 | GCC_OPTIMIZATION_LEVEL = 0; 320 | GCC_PREPROCESSOR_DEFINITIONS = ( 321 | "DEBUG=1", 322 | "$(inherited)", 323 | ); 324 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 325 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 326 | GCC_WARN_UNDECLARED_SELECTOR = YES; 327 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 328 | GCC_WARN_UNUSED_FUNCTION = YES; 329 | GCC_WARN_UNUSED_VARIABLE = YES; 330 | IPHONEOS_DEPLOYMENT_TARGET = 12.2; 331 | MTL_ENABLE_DEBUG_INFO = YES; 332 | ONLY_ACTIVE_ARCH = YES; 333 | SDKROOT = iphoneos; 334 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 335 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 336 | SWIFT_VERSION = 5.0; 337 | TARGETED_DEVICE_FAMILY = 1; 338 | }; 339 | name = Debug; 340 | }; 341 | C01A680320DAB03F00F80AFB /* Release */ = { 342 | isa = XCBuildConfiguration; 343 | buildSettings = { 344 | ALWAYS_SEARCH_USER_PATHS = NO; 345 | CLANG_ANALYZER_NONNULL = YES; 346 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 347 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 348 | CLANG_CXX_LIBRARY = "libc++"; 349 | CLANG_ENABLE_MODULES = YES; 350 | CLANG_ENABLE_OBJC_ARC = YES; 351 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 352 | CLANG_WARN_BOOL_CONVERSION = YES; 353 | CLANG_WARN_COMMA = YES; 354 | CLANG_WARN_CONSTANT_CONVERSION = YES; 355 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 356 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 357 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 358 | CLANG_WARN_EMPTY_BODY = YES; 359 | CLANG_WARN_ENUM_CONVERSION = YES; 360 | CLANG_WARN_INFINITE_RECURSION = YES; 361 | CLANG_WARN_INT_CONVERSION = YES; 362 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 363 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 364 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 365 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 366 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 367 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 368 | CLANG_WARN_STRICT_PROTOTYPES = YES; 369 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 370 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 371 | CLANG_WARN_UNREACHABLE_CODE = YES; 372 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 373 | CODE_SIGN_IDENTITY = "iPhone Developer"; 374 | COPY_PHASE_STRIP = NO; 375 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 376 | ENABLE_BITCODE = NO; 377 | ENABLE_NS_ASSERTIONS = NO; 378 | ENABLE_STRICT_OBJC_MSGSEND = YES; 379 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 380 | GCC_C_LANGUAGE_STANDARD = gnu11; 381 | GCC_NO_COMMON_BLOCKS = YES; 382 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 383 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 384 | GCC_WARN_UNDECLARED_SELECTOR = YES; 385 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 386 | GCC_WARN_UNUSED_FUNCTION = YES; 387 | GCC_WARN_UNUSED_VARIABLE = YES; 388 | IPHONEOS_DEPLOYMENT_TARGET = 12.2; 389 | MTL_ENABLE_DEBUG_INFO = NO; 390 | SDKROOT = iphoneos; 391 | SWIFT_COMPILATION_MODE = wholemodule; 392 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 393 | SWIFT_VERSION = 5.0; 394 | TARGETED_DEVICE_FAMILY = 1; 395 | VALIDATE_PRODUCT = YES; 396 | }; 397 | name = Release; 398 | }; 399 | C01A680520DAB03F00F80AFB /* Debug */ = { 400 | isa = XCBuildConfiguration; 401 | buildSettings = { 402 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 403 | CLANG_ENABLE_MODULES = YES; 404 | CODE_SIGN_ENTITLEMENTS = chatr/chatr.entitlements; 405 | CODE_SIGN_STYLE = Automatic; 406 | FRAMEWORK_SEARCH_PATHS = ( 407 | "$(inherited)", 408 | "$(PROJECT_DIR)", 409 | "$(SRCROOT)", 410 | ); 411 | INFOPLIST_FILE = chatr/Info.plist; 412 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 413 | LD_RUNPATH_SEARCH_PATHS = ( 414 | "$(inherited)", 415 | "@executable_path/Frameworks", 416 | ); 417 | ONLY_ACTIVE_ARCH = NO; 418 | PRODUCT_BUNDLE_IDENTIFIER = Intune.chatr; 419 | PRODUCT_NAME = "$(TARGET_NAME)"; 420 | SWIFT_OBJC_BRIDGING_HEADER = ""; 421 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 422 | SWIFT_VERSION = 5.0; 423 | TARGETED_DEVICE_FAMILY = "1,2"; 424 | }; 425 | name = Debug; 426 | }; 427 | C01A680620DAB03F00F80AFB /* Release */ = { 428 | isa = XCBuildConfiguration; 429 | buildSettings = { 430 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 431 | CLANG_ENABLE_MODULES = YES; 432 | CODE_SIGN_ENTITLEMENTS = chatr/chatr.entitlements; 433 | CODE_SIGN_STYLE = Automatic; 434 | FRAMEWORK_SEARCH_PATHS = ( 435 | "$(inherited)", 436 | "$(PROJECT_DIR)", 437 | "$(SRCROOT)", 438 | ); 439 | INFOPLIST_FILE = chatr/Info.plist; 440 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 441 | LD_RUNPATH_SEARCH_PATHS = ( 442 | "$(inherited)", 443 | "@executable_path/Frameworks", 444 | ); 445 | ONLY_ACTIVE_ARCH = NO; 446 | PRODUCT_BUNDLE_IDENTIFIER = Intune.chatr; 447 | PRODUCT_NAME = "$(TARGET_NAME)"; 448 | SWIFT_OBJC_BRIDGING_HEADER = ""; 449 | SWIFT_VERSION = 5.0; 450 | TARGETED_DEVICE_FAMILY = "1,2"; 451 | }; 452 | name = Release; 453 | }; 454 | /* End XCBuildConfiguration section */ 455 | 456 | /* Begin XCConfigurationList section */ 457 | C01A67D720DAB03F00F80AFB /* Build configuration list for PBXProject "chatr" */ = { 458 | isa = XCConfigurationList; 459 | buildConfigurations = ( 460 | C01A680220DAB03F00F80AFB /* Debug */, 461 | C01A680320DAB03F00F80AFB /* Release */, 462 | ); 463 | defaultConfigurationIsVisible = 0; 464 | defaultConfigurationName = Release; 465 | }; 466 | C01A680420DAB03F00F80AFB /* Build configuration list for PBXNativeTarget "chatr" */ = { 467 | isa = XCConfigurationList; 468 | buildConfigurations = ( 469 | C01A680520DAB03F00F80AFB /* Debug */, 470 | C01A680620DAB03F00F80AFB /* Release */, 471 | ); 472 | defaultConfigurationIsVisible = 0; 473 | defaultConfigurationName = Release; 474 | }; 475 | /* End XCConfigurationList section */ 476 | 477 | /* Begin XCRemoteSwiftPackageReference section */ 478 | AC008E422D7A4C8900CBC247 /* XCRemoteSwiftPackageReference "microsoft-authentication-library-for-objc" */ = { 479 | isa = XCRemoteSwiftPackageReference; 480 | repositoryURL = "https://github.com/AzureAD/microsoft-authentication-library-for-objc.git"; 481 | requirement = { 482 | kind = upToNextMajorVersion; 483 | minimumVersion = 1.7.0; 484 | }; 485 | }; 486 | AC7BDDA42D7A745F00C46F2D /* XCRemoteSwiftPackageReference "ms-intune-app-sdk-ios" */ = { 487 | isa = XCRemoteSwiftPackageReference; 488 | repositoryURL = "https://github.com/microsoftconnect/ms-intune-app-sdk-ios.git"; 489 | requirement = { 490 | kind = upToNextMajorVersion; 491 | minimumVersion = 20.4.0; 492 | }; 493 | }; 494 | /* End XCRemoteSwiftPackageReference section */ 495 | 496 | /* Begin XCSwiftPackageProductDependency section */ 497 | AC008E432D7A4C8900CBC247 /* MSAL */ = { 498 | isa = XCSwiftPackageProductDependency; 499 | package = AC008E422D7A4C8900CBC247 /* XCRemoteSwiftPackageReference "microsoft-authentication-library-for-objc" */; 500 | productName = MSAL; 501 | }; 502 | AC7BDDA52D7A745F00C46F2D /* IntuneMAMSwift */ = { 503 | isa = XCSwiftPackageProductDependency; 504 | package = AC7BDDA42D7A745F00C46F2D /* XCRemoteSwiftPackageReference "ms-intune-app-sdk-ios" */; 505 | productName = IntuneMAMSwift; 506 | }; 507 | AC7BDDA72D7A745F00C46F2D /* IntuneMAMTelemetry */ = { 508 | isa = XCSwiftPackageProductDependency; 509 | package = AC7BDDA42D7A745F00C46F2D /* XCRemoteSwiftPackageReference "ms-intune-app-sdk-ios" */; 510 | productName = IntuneMAMTelemetry; 511 | }; 512 | /* End XCSwiftPackageProductDependency section */ 513 | }; 514 | rootObject = C01A67D420DAB03F00F80AFB /* Project object */; 515 | } 516 | -------------------------------------------------------------------------------- /chatr.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /chatr.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /chatr.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "041cf42cc35dd3b9b3847f9285240426c190666d2897e1e018ed53f1701920a8", 3 | "pins" : [ 4 | { 5 | "identity" : "microsoft-authentication-library-for-objc", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/AzureAD/microsoft-authentication-library-for-objc.git", 8 | "state" : { 9 | "revision" : "be848ee7fa9516cec47ae6de47cf1087d51bc774", 10 | "version" : "1.9.0" 11 | } 12 | }, 13 | { 14 | "identity" : "ms-intune-app-sdk-ios", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/microsoftconnect/ms-intune-app-sdk-ios.git", 17 | "state" : { 18 | "revision" : "8be6896791fd45228a6906f9b8608418b96ab24f", 19 | "version" : "20.5.0" 20 | } 21 | } 22 | ], 23 | "version" : 3 24 | } 25 | -------------------------------------------------------------------------------- /chatr.xcodeproj/xcshareddata/xcschemes/chatr.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /chatr/AboutUsPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | import IntuneMAMSwift 6 | 7 | class AboutUsPage: UIViewController, UITextViewDelegate, IntuneMAMPolicyDelegate{ 8 | 9 | // variables used throughout this class 10 | @IBOutlet weak var aboutUsText: UITextView! 11 | @IBOutlet weak var backButton: UIButton! 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | // set up the link and text that will be displayed on the About us page 17 | let developerGuide: [NSAttributedString.Key : Any] = [.link: NSURL(string: "https://docs.microsoft.com/intune/app-sdk-ios")!, .foregroundColor: UIColor.blue] 18 | let text = NSMutableAttributedString(string: "Chatr was built to demonstrate the integration of line-of-business apps with Microsoft Intune's iOS MAM SDK. Chatr is a messaging application that allows users to save and print their conversation transcript. \n \nMore information about the SDK is available here. \n \nCurrent SDK Version: \(IntuneMAMVersionInfo.sdkVersion())") 19 | text.setAttributes(developerGuide, range: NSMakeRange(256, 4)) 20 | 21 | // assign the link and text to the page 22 | self.aboutUsText.delegate = self 23 | self.aboutUsText.attributedText = text 24 | self.aboutUsText.isUserInteractionEnabled = true 25 | self.aboutUsText.isEditable = false 26 | self.aboutUsText.layer.cornerRadius = 10 27 | self.backButton.setImage(#imageLiteral(resourceName: "backarrow.png"), for: UIControl.State.normal) 28 | self.backButton.imageView!.contentMode = .scaleAspectFit 29 | self.backButton.addTarget(self, action: #selector (self.dismiss), for: .touchUpInside) 30 | } 31 | 32 | //Button action triggered when back button is pressed on the "About Us" page. 33 | //Dismisses the view. 34 | @IBAction func dismiss(_ sender: UIButton) { 35 | self.dismiss(animated: true, completion: nil) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /chatr/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | import IntuneMAMSwift 6 | 7 | @UIApplicationMain 8 | class AppDelegate: UIResponder, UIApplicationDelegate { 9 | 10 | let enrollmentDelegate = EnrollmentDelegateClass.init() 11 | let policyDelegate = PolicyDelegateClass.init() 12 | var window: UIWindow? 13 | 14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 15 | // Override point for customization after application launch. 16 | 17 | let storyboard: UIStoryboard = UIStoryboard(name:"Main", bundle: Bundle.main) 18 | 19 | //check for enrolled account 20 | let currentUser = IntuneMAMEnrollmentManager.instance().enrolledAccountId() 21 | if nil != currentUser && !currentUser!.isEmpty { 22 | //if an account is enrolled, skip over login page to main page 23 | //Do this by setting the main chat page to the rootViewController 24 | let mainPage = storyboard.instantiateViewController(withIdentifier: "ChatPage") 25 | self.window?.rootViewController = mainPage 26 | } else{ 27 | //if not logged in, set the login page to the rootViewController 28 | let loginPage = storyboard.instantiateViewController(withIdentifier: "LoginPage") 29 | self.window?.rootViewController = loginPage 30 | } 31 | 32 | return true 33 | } 34 | 35 | func applicationWillResignActive(_ application: UIApplication) { 36 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 37 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 38 | } 39 | 40 | func applicationDidEnterBackground(_ application: UIApplication) { 41 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 42 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 43 | } 44 | 45 | func applicationWillEnterForeground(_ application: UIApplication) { 46 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 47 | } 48 | 49 | func applicationDidBecomeActive(_ application: UIApplication) { 50 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 51 | } 52 | 53 | func applicationWillTerminate(_ application: UIApplication) { 54 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 55 | } 56 | 57 | func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 58 | 59 | //Set the delegate of the IntuneMAMPolicyManager to an instance of the PolicyDelegateClass 60 | IntuneMAMPolicyManager.instance().delegate = self.policyDelegate 61 | 62 | //Set the delegate of the IntuneMAMEnrollmentManager to an instance of the EnrollmentDelegateClass 63 | IntuneMAMEnrollmentManager.instance().delegate = self.enrollmentDelegate 64 | 65 | return true 66 | } 67 | 68 | } 69 | 70 | -------------------------------------------------------------------------------- /chatr/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-40x40@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-60x60@2x.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@3x.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "20x20", 53 | "idiom" : "ipad", 54 | "filename" : "Icon-App-20x20@1x.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@2x-1.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-29x29@1x.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@2x-1.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-40x40@1x.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@2x-1.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-76x76@1x.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@2x.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-83.5x83.5@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "1024x1024", 107 | "idiom" : "ios-marketing", 108 | "filename" : "DM (2).jpg", 109 | "scale" : "1x" 110 | } 111 | ], 112 | "info" : { 113 | "version" : 1, 114 | "author" : "xcode" 115 | } 116 | } -------------------------------------------------------------------------------- /chatr/Assets.xcassets/AppIcon.appiconset/DM (2).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/DM (2).jpg -------------------------------------------------------------------------------- /chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/ChatPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // Code for sidebar implementation adopted from: https://youtu.be/GOSIz7JbZMA by Yogesh Patel 5 | // Code for alert message adopted from: https://www.simplifiedios.net/ios-show-alert-using-uialertcontroller/ by Belal Khan 6 | // Code for print adopted from: https://stackoverflow.com/questions/32403634/airprint-contents-of-a-uiview answer by Jody Heavener 7 | // Other inspiration: https://stackoverflow.com/questions/31870206/how-to-insert-new-cell-into-uitableview-in-swift responses by EI Captain v2.0 and Dharmesh Kheni 8 | // 9 | // 10 | 11 | import UIKit 12 | 13 | // global variable used for saving conversations 14 | var conversation:[(sender:String, message:NSAttributedString)] = [] 15 | let savedConvo = UserDefaults.init() 16 | 17 | let appDelegate = UIApplication.shared.delegate as! AppDelegate 18 | 19 | class ChatPage: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate { 20 | 21 | // variable used to keep track of whether the send button is already displayed 22 | var alreadyDisplayedSendButton = false 23 | 24 | // variables used for creating the sidebar 25 | @IBOutlet weak var sideBarTable: UITableView! 26 | var isMenu:Bool = false // variable that indicates if the menu is being displayed 27 | let sideBarFeatures = ["Save","Print", "About us","Settings", "Log out"] // the options on the sidebar 28 | let sideBarImg = [#imageLiteral(resourceName: "save"),#imageLiteral(resourceName: "print"),#imageLiteral(resourceName: "information"),#imageLiteral(resourceName: "settings"),#imageLiteral(resourceName: "profile")] // images for the sidebar options 29 | 30 | // variables used for chat 31 | @IBOutlet weak var typedChatView: UITextView! 32 | @IBOutlet weak var chatTable: UITableView! 33 | 34 | // variables used for printing 35 | @IBOutlet var wholePageView: UIView! 36 | 37 | // variable to display group name on top of the chat page, default set to 'Chatr' 38 | // Onpage load: updated to be user's group name based on targeted app config 39 | @IBOutlet weak var groupName: UITextField! 40 | 41 | // variable to move textfield for keyboard actions 42 | @IBOutlet weak var keyboardHeightLayoutConstraint: NSLayoutConstraint! 43 | 44 | //variable to reference the position and dimensions of the top bar 45 | @IBOutlet weak var topBarView: UIView! 46 | 47 | //variable to store initial save by policy permissions 48 | var isSaveAllowed = Bool() 49 | 50 | //override the ChatPage View Controller initializer 51 | required init? (coder aDecoder: NSCoder) { 52 | 53 | super.init(coder: aDecoder) 54 | 55 | //register for the IntuneMAMAppConfigDidChange notification 56 | NotificationCenter.default.addObserver(self, 57 | selector: #selector(onIntuneMAMAppConfigDidChange), 58 | name: NSNotification.Name.IntuneMAMAppConfigDidChange, 59 | object: IntuneMAMAppConfigManager.instance()) 60 | 61 | //register for the IntuneMAMPolicyDidChange notification 62 | NotificationCenter.default.addObserver(self, 63 | selector: #selector(onIntuneMAMPolicyDidChange), 64 | name: NSNotification.Name.IntuneMAMPolicyDidChange, 65 | object: IntuneMAMPolicyManager.instance()) 66 | } 67 | 68 | @objc func onIntuneMAMAppConfigDidChange() { 69 | //query the app config and update the user name on the top of the chat page 70 | self.groupName.text = ObjCUtils.getUserGroupName() 71 | } 72 | 73 | @objc func onIntuneMAMPolicyDidChange() { 74 | if ObjCUtils.isSaveToLocalDriveAllowed() { 75 | saveAllowedByPolicy() 76 | } 77 | else { 78 | saveNotAllowedByPolicy() 79 | } 80 | } 81 | 82 | /*! 83 | Button action triggered when send button is pressed on chat page 84 | 85 | Empties out the text field, creates a new view with the message and triggers a response. 86 | */ 87 | @IBAction func sendChat(_ sender: UIButton) { 88 | 89 | let align = NSMutableParagraphStyle() 90 | align.alignment = .right 91 | 92 | let fromMessage = NSMutableAttributedString(string: typedChatView.text!, attributes: [.paragraphStyle: align]) 93 | typedChatView.text = "" 94 | conversation.append((sender: "from", message: fromMessage)) 95 | 96 | // update the message board to include the update 97 | self.chatTable.beginUpdates() 98 | self.chatTable.insertRows(at: [IndexPath.init(row: conversation.count-1, section: 0)], with: .automatic) 99 | self.chatTable.endUpdates() 100 | 101 | // send the reply 102 | replyChat() 103 | } 104 | 105 | /*! 106 | Creates a response message with links to the documentation and adds it to the chat view. 107 | Called each time a message is sent. 108 | */ 109 | func replyChat() { 110 | 111 | //create reply message 112 | let developerGuide: [NSAttributedStringKey : Any] = [.link: NSURL(string: "https://docs.microsoft.com/en-us/intune/app-sdk-ios")!, .foregroundColor: UIColor.blue] 113 | let faqPage: [NSAttributedStringKey : Any] = [.link : NSURL(string: "https://docs.microsoft.com/en-us/intune/app-sdk-ios#faqs")!, .foregroundColor: UIColor.blue] 114 | 115 | let align = NSMutableParagraphStyle() 116 | align.alignment = .left 117 | 118 | let replyMessage = NSMutableAttributedString(string: "Please refer to our documentation and read our faq page for any outstanding concerns. \nThank you.", attributes: [.paragraphStyle: align]) 119 | replyMessage.setAttributes(developerGuide, range: NSMakeRange(20, 13)) 120 | replyMessage.setAttributes(faqPage, range: NSMakeRange(47, 3)) 121 | 122 | // add it to the conversation list 123 | conversation.append((sender: "to", message: replyMessage)) 124 | 125 | //update the message board to include the reply 126 | self.chatTable.beginUpdates() 127 | self.chatTable.insertRows(at: [IndexPath.init(row: conversation.count-1, section: 0)], with: .automatic) 128 | self.chatTable.endUpdates() 129 | } 130 | 131 | override func viewDidLoad() { 132 | super.viewDidLoad() 133 | 134 | //prevent the display of empty cells at the bottom of the sidebar menu by adding a zero height table footer view 135 | sideBarTable.tableFooterView = UIView(frame: CGRect(x:0, y:0, width: 0, height: 0)) 136 | sideBarTable.tableFooterView?.isHidden = true 137 | sideBarTable.backgroundColor = UIColor.clear 138 | 139 | //ensures self-sizing sidebar table view cells 140 | //the sidebar table view will use Auto Layout constraints and the cell's contents to determine each cell's height 141 | sideBarTable.estimatedRowHeight = 40 142 | sideBarTable.rowHeight = UITableViewAutomaticDimension 143 | 144 | // when the view is loaded, hide the sidebar table 145 | sideBarTable.isHidden = true 146 | isMenu = false 147 | 148 | // round the corners of the chat view 149 | typedChatView.layer.cornerRadius = 10 150 | 151 | // change user's group name on top of the chat page to the initial group name 152 | self.groupName.text = ObjCUtils.getUserGroupName() 153 | 154 | NotificationCenter.default.addObserver(self, 155 | selector: #selector(self.keyboardNotification(notification:)), 156 | name: NSNotification.Name.UIKeyboardWillChangeFrame, 157 | object: nil) 158 | 159 | } 160 | 161 | //programmatically create send button after Auto Layout lays out the main view and subviews 162 | override func viewDidLayoutSubviews() { 163 | //ensures a new send button is not added every time a message is sent in the chat 164 | if !alreadyDisplayedSendButton { 165 | 166 | let sendButton = UIButton(frame: CGRect(x: typedChatView.frame.origin.x + typedChatView.frame.width + 4, y: typedChatView.frame.origin.y - 4, width: 57, height: 39)) 167 | sendButton.backgroundColor = .clear 168 | sendButton.setTitle("SEND", for: .normal) 169 | sendButton.addTarget(self, action: #selector (sendChat), for: .touchUpInside) 170 | 171 | self.view.addSubview(sendButton) 172 | 173 | alreadyDisplayedSendButton = true 174 | } 175 | } 176 | 177 | deinit { 178 | NotificationCenter.default.removeObserver(self) 179 | } 180 | 181 | @objc func keyboardNotification(notification: NSNotification) { 182 | if let userInfo = notification.userInfo { 183 | let endFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue 184 | let endFrameY = endFrame?.origin.y ?? 0 185 | let duration:TimeInterval = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0 186 | let animationCurveRawNSN = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber 187 | let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIViewAnimationOptions.curveEaseInOut.rawValue 188 | let animationCurve:UIViewAnimationOptions = UIViewAnimationOptions(rawValue: animationCurveRaw) 189 | if endFrameY >= UIScreen.main.bounds.size.height { 190 | self.keyboardHeightLayoutConstraint?.constant = 10.0 191 | } else { 192 | self.keyboardHeightLayoutConstraint?.constant = (endFrame?.size.height)! + 10.0 193 | } 194 | UIView.animate(withDuration: duration, 195 | delay: TimeInterval(0), 196 | options: animationCurve, 197 | animations: { self.view.layoutIfNeeded() }, 198 | completion: nil) 199 | } 200 | } 201 | 202 | //hide the keyboard when the user taps the return key 203 | func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool { 204 | if (text == "\n") { 205 | textView.resignFirstResponder() 206 | } 207 | return true 208 | } 209 | 210 | 211 | override func didReceiveMemoryWarning() { 212 | super.didReceiveMemoryWarning() 213 | // Dispose of any resources that can be recreated. 214 | } 215 | 216 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 217 | if tableView == sideBarTable { // this is the sideBar table 218 | return sideBarFeatures.count 219 | } else { // this is the conversations table 220 | return conversation.count 221 | } 222 | } 223 | 224 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 225 | if tableView == sideBarTable { 226 | let cell:SideBarTableCell = tableView.dequeueReusableCell(withIdentifier: "sideCell") as! SideBarTableCell 227 | cell.cellImg.image = sideBarImg[indexPath.row] 228 | cell.cellLbl.text = sideBarFeatures[indexPath.row] 229 | return cell 230 | } else { 231 | let sendMessageCell:chatTableViewCell = chatTable.dequeueReusableCell(withIdentifier: "chatCell") as! chatTableViewCell 232 | 233 | sendMessageCell.messageView.attributedText = conversation[indexPath.row].message 234 | 235 | return sendMessageCell 236 | } 237 | } 238 | 239 | /* 240 | Button action triggered when the the Chatr logo button on the top left is clicked 241 | 242 | Reveals/hides the sideBar menu when the button is pressed depending on the state 243 | - If the menu was hidden when the button is pressed, then it will reveal the menu and vise versa. 244 | */ 245 | @IBAction func sideBarMenu(_ sender: Any) { 246 | sideBarTable.isHidden = false 247 | 248 | if !isMenu { 249 | // reveal sideBar menu 250 | isMenu = true 251 | sideBarTable.frame = CGRect(x: 0, y: topBarView.frame.height + topBarView.frame.origin.y, width: 0, height: 341) 252 | UIView.setAnimationDuration(0.15) 253 | UIView.setAnimationDelegate(self) 254 | UIView.beginAnimations("sideBarAnimation", context: nil) 255 | sideBarTable.frame = CGRect(x: 0, y: topBarView.frame.height + topBarView.frame.origin.y, width: 176, height: 341) 256 | UIView.commitAnimations() 257 | } 258 | else { 259 | hideSideBarMenu() 260 | } 261 | } 262 | 263 | /*! 264 | Hides the sidebar menu 265 | */ 266 | func hideSideBarMenu() { 267 | // hide sideBar menu 268 | sideBarTable.isHidden = true 269 | isMenu = false 270 | sideBarTable.frame = CGRect(x: 0, y: topBarView.frame.height + topBarView.frame.origin.y, width: 176, height: 341) 271 | UIView.setAnimationDuration(0.15) 272 | UIView.setAnimationDelegate(self) 273 | UIView.beginAnimations("sideBarAnimation", context: nil) 274 | sideBarTable.frame = CGRect(x: 0, y: topBarView.frame.height + topBarView.frame.origin.y, width: 0, height: 341) 275 | UIView.commitAnimations() 276 | } 277 | 278 | //Enumeration that defines options within the side bar menu 279 | enum SideBarOptions: Int{ 280 | case save = 0 281 | case print 282 | case aboutUs 283 | case settings 284 | case logout 285 | } 286 | 287 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 288 | if tableView == sideBarTable { 289 | let sideBarOption = SideBarOptions(rawValue: indexPath.row) 290 | // Complete an action based on the item pressed on the sidebar 291 | switch sideBarOption { 292 | case .save?: 293 | // Check if save is allowed by policy 294 | if ObjCUtils.isSaveToLocalDriveAllowed() { 295 | saveAllowedByPolicy() 296 | } 297 | else { 298 | saveNotAllowedByPolicy() 299 | } 300 | case .print?: 301 | //Print the conversation 302 | printConvo() 303 | case .aboutUs?: 304 | //Display the about us page 305 | let aboutUs:AboutUsPage = self.storyboard?.instantiateViewController(withIdentifier: "aboutPage") as! AboutUsPage 306 | present(aboutUs, animated:true, completion: nil) 307 | case .settings?: 308 | //Display the settings page 309 | let settings:SettingsPage = self.storyboard?.instantiateViewController(withIdentifier: "settingsPage") as! SettingsPage 310 | present(settings, animated:true, completion: nil) 311 | case .logout?: 312 | //Log out user 313 | ObjCUtils.logout() 314 | default: break 315 | } 316 | } 317 | } 318 | 319 | func saveAllowedByPolicy() { 320 | savedConvo.set(conversation, forKey: "savedConversation ") 321 | //Alert the user that saving is enabled 322 | let alert = UIAlertController(title: "Conversation Saved", 323 | message: "Your conversation has been successfully saved to your device.", 324 | preferredStyle: .alert) 325 | let closeAlert = UIAlertAction(title: "Ok", 326 | style: .default, 327 | handler: nil) 328 | alert.addAction(closeAlert) 329 | present(alert, animated: true, completion: nil) 330 | } 331 | 332 | func saveNotAllowedByPolicy() { 333 | // Alert the user that saving is disabled 334 | let alert = UIAlertController(title: "Save Disabled", 335 | message: "Saving conversations to local storage has been disabled by your IT admin.", 336 | preferredStyle: .alert) 337 | let closeAlert = UIAlertAction(title: "Ok", 338 | style: .default, 339 | handler: nil) 340 | alert.addAction(closeAlert) 341 | present(alert, animated: true, completion: nil) 342 | } 343 | 344 | /*! 345 | Presents the user with a print preview of the chat page. 346 | Called by the side bar table 347 | */ 348 | func printConvo() { 349 | hideSideBarMenu() // hide the side bar before you move on 350 | 351 | let printInfo = UIPrintInfo(dictionary:nil) 352 | printInfo.outputType = UIPrintInfoOutputType.general 353 | printInfo.jobName = "Print Chat Page" 354 | 355 | let printController = UIPrintInteractionController.shared 356 | printController.printInfo = printInfo 357 | printController.printingItem = wholePageView.toImage() 358 | printController.present(animated: true, completionHandler: nil) 359 | } 360 | } 361 | 362 | extension UIView { 363 | // converts a UIView to an image 364 | // used in the print() function for ChatPage 365 | func toImage() -> UIImage { 366 | // code logic modified from answer by Jody Heavener @ https://stackoverflow.com/questions/32403634/airprint-contents-of-a-uiview 367 | UIGraphicsBeginImageContextWithOptions(bounds.size, false, UIScreen.main.scale) 368 | 369 | drawHierarchy(in: self.bounds, afterScreenUpdates: true) 370 | 371 | let image = UIGraphicsGetImageFromCurrentImageContext() 372 | UIGraphicsEndImageContext() 373 | return image! 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /chatr/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /chatr/Assets.xcassets/backarrow.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "backarrow.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /chatr/Assets.xcassets/backarrow.imageset/backarrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/backarrow.imageset/backarrow.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/information.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Picture1.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /chatr/Assets.xcassets/information.imageset/Picture1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/information.imageset/Picture1.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "logo.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "iTunesArtwork@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "iTunesArtwork@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /chatr/Assets.xcassets/logo.imageset/iTunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/logo.imageset/iTunesArtwork@2x.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/logo.imageset/iTunesArtwork@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/logo.imageset/iTunesArtwork@3x.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/logo.imageset/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/logo.imageset/logo.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/menubar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "DM-3.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /chatr/Assets.xcassets/menubar.imageset/DM-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/menubar.imageset/DM-3.jpg -------------------------------------------------------------------------------- /chatr/Assets.xcassets/print.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "print - graphic.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /chatr/Assets.xcassets/print.imageset/print - graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/print.imageset/print - graphic.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/profile.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "person - graphic.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /chatr/Assets.xcassets/profile.imageset/person - graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/profile.imageset/person - graphic.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/save.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "save - graphic.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /chatr/Assets.xcassets/save.imageset/save - graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/save.imageset/save - graphic.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/settings.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "settings-graphic.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /chatr/Assets.xcassets/settings.imageset/settings-graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/settings.imageset/settings-graphic.png -------------------------------------------------------------------------------- /chatr/Assets.xcassets/sidebar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "resizing" : { 5 | "mode" : "9-part", 6 | "center" : { 7 | "mode" : "tile", 8 | "width" : 1, 9 | "height" : 1 10 | }, 11 | "cap-insets" : { 12 | "bottom" : 240, 13 | "top" : 257, 14 | "right" : 357, 15 | "left" : 142 16 | } 17 | }, 18 | "idiom" : "universal", 19 | "filename" : "DM-3.jpg", 20 | "scale" : "1x" 21 | }, 22 | { 23 | "idiom" : "universal", 24 | "scale" : "2x" 25 | }, 26 | { 27 | "idiom" : "universal", 28 | "scale" : "3x" 29 | } 30 | ], 31 | "info" : { 32 | "version" : 1, 33 | "author" : "xcode" 34 | } 35 | } -------------------------------------------------------------------------------- /chatr/Assets.xcassets/sidebar.imageset/DM-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/sidebar.imageset/DM-3.jpg -------------------------------------------------------------------------------- /chatr/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /chatr/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 31 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 99 | 100 | 101 | 102 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | -------------------------------------------------------------------------------- /chatr/ChatPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | import IntuneMAMSwift 6 | 7 | // global variable used for saving conversations 8 | var conversation:[(sender:String, message:NSAttributedString)] = [] 9 | 10 | class ChatPage: UIViewController, UITableViewDelegate, UITableViewDataSource { 11 | 12 | // variable used to keep track of whether the send button is already displayed 13 | var alreadyDisplayedSendButton = false 14 | 15 | //variable for the currently enrolled user 16 | var currentUser: String! 17 | 18 | // variables used for creating the sidebar 19 | @IBOutlet weak var sideBarTable: UITableView! 20 | var isMenu:Bool = false // variable that indicates if the menu is being displayed 21 | let sideBarFeatures = ["Save","Print", "About us","Settings", "Log out"] // the options on the sidebar 22 | let sideBarImg = [#imageLiteral(resourceName: "save"),#imageLiteral(resourceName: "print"),#imageLiteral(resourceName: "information"),#imageLiteral(resourceName: "settings"),#imageLiteral(resourceName: "profile")] // images for the sidebar options 23 | var menuWidthConstraint: NSLayoutConstraint! // Contraint to animate the menu 24 | 25 | // variables used for chat 26 | @IBOutlet weak var typedChatView: UITextView! 27 | @IBOutlet weak var chatTable: UITableView! 28 | 29 | // variables used for printing 30 | @IBOutlet var wholePageView: UIView! 31 | 32 | // variable to display group name on top of the chat page, default set to 'Chatr' 33 | // Onpage load: updated to be user's group name based on targeted app config 34 | @IBOutlet weak var groupName: UITextField! 35 | 36 | // variable to move textfield for keyboard actions 37 | @IBOutlet weak var keyboardHeightLayoutConstraint: NSLayoutConstraint! 38 | 39 | //variable to reference the position and dimensions of the top bar 40 | @IBOutlet weak var topBarView: UIView! 41 | 42 | //variable to store initial save by policy permissions 43 | var isSaveAllowed = Bool() 44 | 45 | //override the ChatPage View Controller initializer 46 | required init? (coder aDecoder: NSCoder) { 47 | 48 | super.init(coder: aDecoder) 49 | 50 | //register for the IntuneMAMAppConfigDidChange notification 51 | NotificationCenter.default.addObserver(self, 52 | selector: #selector(onIntuneMAMAppConfigDidChange), 53 | name: NSNotification.Name.IntuneMAMAppConfigDidChange, 54 | object: IntuneMAMAppConfigManager.instance()) 55 | 56 | //register for the IntuneMAMPolicyDidChange notification 57 | NotificationCenter.default.addObserver(self, 58 | selector: #selector(onIntuneMAMPolicyDidChange), 59 | name: NSNotification.Name.IntuneMAMPolicyDidChange, 60 | object: IntuneMAMPolicyManager.instance()) 61 | 62 | //Get the current user 63 | self.currentUser = IntuneMAMEnrollmentManager.instance().enrolledAccountId()! 64 | 65 | //query the app policy and update the initial save-as policy permissions 66 | self.isSaveAllowed = self.getSaveStatus() 67 | } 68 | 69 | @objc func onIntuneMAMAppConfigDidChange() { 70 | //query the app config and update the user name on the top of the chat page 71 | self.groupName.text = self.getUserGroupName() 72 | } 73 | 74 | @objc func onIntuneMAMPolicyDidChange() { 75 | self.isSaveAllowed = self.getSaveStatus() 76 | } 77 | 78 | override func viewDidLoad() { 79 | super.viewDidLoad() 80 | 81 | //prevent the display of empty cells at the bottom of the sidebar menu by adding a zero height table footer view 82 | self.sideBarTable.tableFooterView = UIView(frame: CGRect(x:0, y:0, width: 0, height: 0)) 83 | self.sideBarTable.tableFooterView?.isHidden = true 84 | self.sideBarTable.backgroundColor = UIColor.clear 85 | 86 | //ensures self-sizing sidebar table view cells 87 | //the sidebar table view will use Auto Layout constraints and the cell's contents to determine each cell's height 88 | self.sideBarTable.estimatedRowHeight = 40 89 | self.sideBarTable.rowHeight = UITableView.automaticDimension 90 | 91 | // when the view is loaded, hide the sidebar table 92 | self.sideBarTable.isHidden = true 93 | self.isMenu = false 94 | 95 | // round the corners of the chat view 96 | self.typedChatView.layer.cornerRadius = 10 97 | 98 | // change user's group name on top of the chat page, one of the app config settings 99 | self.groupName.text = self.getUserGroupName() 100 | 101 | NotificationCenter.default.addObserver(self, 102 | selector: #selector(self.keyboardNotification(notification:)), 103 | name: UIResponder.keyboardWillChangeFrameNotification, 104 | object: nil) 105 | //Check the keychain for chat messages and drafted messages to load into the view 106 | if let messageArray: [String] = KeychainManager.getSentMessages(forUser: self.currentUser){ 107 | //If messages are present, populate the screen with them 108 | self.populateChatScreen(messageArray: messageArray) 109 | } 110 | let draftMessage: String? = KeychainManager.getDraftedMessage(forUser: self.currentUser) 111 | if nil != draftMessage{ 112 | //If a draft message is present, add it to the message entry bar 113 | self.typedChatView.text = draftMessage! 114 | } 115 | 116 | //Add an observer to save any drafted message when the app terminates 117 | NotificationCenter.default.addObserver(self, selector: #selector(self.saveDraftedMessage), name: UIApplication.willResignActiveNotification, object: nil) 118 | } 119 | 120 | //programmatically create send button after Auto Layout lays out the main view and subviews 121 | override func viewDidLayoutSubviews() { 122 | //ensures a new send button is not added every time a message is sent in the chat 123 | if !self.alreadyDisplayedSendButton { 124 | 125 | let sendButton = UIButton(frame: CGRect(x: self.typedChatView.frame.origin.x + self.typedChatView.frame.width + 4, y: self.typedChatView.frame.origin.y - 4, width: 57, height: 39)) 126 | sendButton.backgroundColor = .clear 127 | sendButton.setTitle("SEND", for: .normal) 128 | sendButton.addTarget(self, action: #selector (self.sendChat), for: .touchUpInside) 129 | 130 | self.view.addSubview(sendButton) 131 | sendButton.translatesAutoresizingMaskIntoConstraints = false 132 | sendButton.bottomAnchor.constraint(equalTo: self.typedChatView.bottomAnchor).isActive = true 133 | sendButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -8).isActive = true 134 | 135 | self.alreadyDisplayedSendButton = true 136 | } 137 | } 138 | 139 | /*! 140 | Button action triggered when send button is pressed on chat page 141 | 142 | Empties out the text field, creates a new view with the message and triggers a response. 143 | 144 | This function also stores the message in the keychain using the KeychainManager 145 | */ 146 | @IBAction func sendChat(_ sender: UIButton) { 147 | //First format the string appropriately 148 | let align = NSMutableParagraphStyle() 149 | align.alignment = .right 150 | let fromMessage = NSMutableAttributedString(string: self.typedChatView.text!, attributes: [.paragraphStyle: align]) 151 | 152 | //Only take action if there is text in the message 153 | if 0 != fromMessage.length { 154 | //Reset the entry field 155 | self.typedChatView.text = "" 156 | //When any message is sent, delete any draft message in the keychain 157 | _ = KeychainManager.deleteDraftMessage(forUser: self.currentUser) 158 | self.displayChatMessage(message: fromMessage) 159 | 160 | //Add the message to the stored messages in the keychain 161 | KeychainManager.storeSentMessage(sentMessage: fromMessage.string, forUser: self.currentUser) 162 | //Scroll to the bottom of this message 163 | self.scrollToBottom(animated: true) 164 | } 165 | } 166 | 167 | /* 168 | Helper function that takes a formatted message, displays it on the chat page table, and calls replyChat to add a response message. 169 | */ 170 | func displayChatMessage(message:NSAttributedString) { 171 | //Add the message to the data source for the chatTable 172 | conversation.append((sender: "from", message: message)) 173 | 174 | //Update the message board to include the new message 175 | self.chatTable.beginUpdates() 176 | self.chatTable.insertRows(at: [IndexPath.init(row: conversation.count-1, section: 0)], with: .automatic) 177 | self.chatTable.endUpdates() 178 | 179 | // send the reply 180 | self.replyChat() 181 | } 182 | 183 | /*! 184 | Creates a response message with links to the documentation and adds it to the chat view. 185 | Called each time a message is sent. 186 | */ 187 | func replyChat() { 188 | 189 | //create reply message 190 | let developerGuide: [NSAttributedString.Key : Any] = [.link: NSURL(string: "https://docs.microsoft.com/intune/app-sdk-ios")!, .foregroundColor: UIColor.blue] 191 | let faqPage: [NSAttributedString.Key : Any] = [.link : NSURL(string: "https://docs.microsoft.com/intune/app-sdk-ios#faqs")!, .foregroundColor: UIColor.blue] 192 | 193 | let align = NSMutableParagraphStyle() 194 | align.alignment = .left 195 | 196 | let replyMessage = NSMutableAttributedString(string: "Please refer to our documentation and read our faq page for any outstanding concerns. \nThank you.", attributes: [.paragraphStyle: align]) 197 | replyMessage.setAttributes(developerGuide, range: NSMakeRange(20, 13)) 198 | replyMessage.setAttributes(faqPage, range: NSMakeRange(47, 3)) 199 | 200 | // add it to the conversation list 201 | conversation.append((sender: "to", message: replyMessage)) 202 | 203 | //update the message board to include the reply 204 | self.chatTable.beginUpdates() 205 | self.chatTable.insertRows(at: [IndexPath.init(row: conversation.count-1, section: 0)], with: .automatic) 206 | self.chatTable.endUpdates() 207 | } 208 | 209 | /* 210 | Function used to clear the chat page messages from the screen 211 | */ 212 | func clearChatPage(){ 213 | //First clears the array that is a data source for the chat table 214 | conversation.removeAll() 215 | //Then realoads the chat table with the empty data source 216 | self.chatTable.reloadData() 217 | } 218 | 219 | /* 220 | Function used to display a group of messages on the chat page. 221 | @param messageArray: the array of messages to display 222 | */ 223 | public func populateChatScreen(messageArray: [String]){ 224 | for message in messageArray{ 225 | //For every string from the message array, format it and display it 226 | let align = NSMutableParagraphStyle() 227 | align.alignment = .right 228 | let fromMessage = NSMutableAttributedString.init(string: message, attributes: [.paragraphStyle: align]) 229 | 230 | self.displayChatMessage(message: fromMessage) 231 | } 232 | } 233 | 234 | /* 235 | Function used to save a drafted message in the message entry field. 236 | If a draft message is present, then it will be saved to the keychain using the KeychainManager class. 237 | */ 238 | @objc public func saveDraftedMessage(){ 239 | //Save any draft message to the keychain using the KeychainManager class 240 | KeychainManager.storeDraftMessage(draftMessage: typedChatView.text!, forUser: self.currentUser) 241 | } 242 | 243 | //Scrolls to the bottom of the table view 244 | func scrollToBottom(animated: Bool) { 245 | if self.chatTable.numberOfRows(inSection: 0) > 0 { 246 | let index = IndexPath(row: self.chatTable.numberOfRows(inSection: 0)-1, section: 0) 247 | self.chatTable.scrollToRow(at: index, at: .bottom, animated: animated) 248 | } 249 | } 250 | 251 | func getUserGroupName() -> String{ 252 | // Get the GroupName value for the user - key value pairing set in the portal 253 | let groupNameKey = "GroupName" 254 | let data = IntuneMAMAppConfigManager.instance().appConfig(forAccountId:self.currentUser) 255 | 256 | // If there are no conflicts for that key, find the value associated with the key 257 | if !data.hasConflict(groupNameKey){ 258 | if let groupName = data.stringValue(forKey: groupNameKey, queryType: IntuneMAMStringQueryType.any){ 259 | return groupName 260 | } 261 | } else { 262 | // Resolve the conflict by taking the max value 263 | return data.stringValue(forKey: groupNameKey, queryType: IntuneMAMStringQueryType.max)! 264 | } 265 | return "Chatr" // Default, if no GroupName value is set 266 | } 267 | 268 | deinit { 269 | NotificationCenter.default.removeObserver(self) 270 | } 271 | 272 | @objc func keyboardNotification(notification: NSNotification) { 273 | if let userInfo = notification.userInfo { 274 | let endFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue 275 | let endFrameY = endFrame?.origin.y ?? 0 276 | let duration:TimeInterval = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0 277 | let animationCurveRawNSN = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber 278 | let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIView.AnimationOptions.curveEaseInOut.rawValue 279 | let animationCurve:UIView.AnimationOptions = UIView.AnimationOptions(rawValue: animationCurveRaw) 280 | if endFrameY >= UIScreen.main.bounds.size.height { 281 | self.keyboardHeightLayoutConstraint?.constant = 10.0 282 | } else { 283 | self.keyboardHeightLayoutConstraint?.constant = (endFrame?.size.height)! + 10.0 284 | } 285 | UIView.animate(withDuration: duration, 286 | delay: TimeInterval(0), 287 | options: animationCurve, 288 | animations: { self.view.layoutIfNeeded() }, 289 | completion: nil) 290 | 291 | if (endFrame?.size.height)! > 0.0 { 292 | //Scroll to the bottom of the message table if the keyboard is appearing 293 | self.scrollToBottom(animated: true) 294 | } 295 | } 296 | } 297 | 298 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 299 | if tableView == self.sideBarTable { // this is the sideBar table 300 | return self.sideBarFeatures.count 301 | } else { // this is the conversations table 302 | return conversation.count 303 | } 304 | } 305 | 306 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 307 | if tableView == self.sideBarTable { 308 | let cell:SideBarTableCell = tableView.dequeueReusableCell(withIdentifier: "sideCell") as! SideBarTableCell 309 | cell.cellImg.image = sideBarImg[indexPath.row] 310 | cell.cellLbl.text = sideBarFeatures[indexPath.row] 311 | return cell 312 | } else { 313 | let sendMessageCell = self.chatTable.dequeueReusableCell(withIdentifier: "chatCell") as! ChatTableViewCell 314 | 315 | sendMessageCell.messageView.attributedText = conversation[indexPath.row].message 316 | 317 | return sendMessageCell 318 | } 319 | } 320 | 321 | /* 322 | Button action triggered when the the Chatr logo button on the top left is clicked 323 | 324 | Reveals/hides the sideBar menu when the button is pressed depending on the state 325 | - If the menu was hidden when the button is pressed, then it will reveal the menu and vise versa. 326 | */ 327 | @IBAction func sideBarMenu(_ sender: Any) { 328 | self.sideBarTable.isHidden = false 329 | 330 | if !isMenu { 331 | // reveal sideBar menu 332 | self.isMenu = true 333 | self.sideBarTable.frame = CGRect(x: 0, y: topBarView.frame.height + topBarView.frame.origin.y, width: 0, height: 341) 334 | self.menuWidthConstraint = self.sideBarTable.widthAnchor.constraint(equalToConstant: 176) 335 | self.sideBarTable.addConstraint(self.menuWidthConstraint) 336 | 337 | UIView.animate(withDuration: 0.15) { 338 | self.sideBarTable.frame = CGRect(x: 0, y: self.topBarView.frame.height + self.topBarView.frame.origin.y, width: 176, height: 341); 339 | } 340 | } 341 | else { 342 | self.hideSideBarMenu() 343 | } 344 | } 345 | 346 | /*! 347 | Hides the sidebar menu 348 | */ 349 | func hideSideBarMenu() { 350 | self.isMenu = false 351 | self.sideBarTable.frame = CGRect(x: 0, y: topBarView.frame.height + topBarView.frame.origin.y, width: 176, height: 341) 352 | self.sideBarTable.removeConstraint(self.menuWidthConstraint) 353 | 354 | UIView.animate(withDuration: 0.15, animations: { 355 | self.sideBarTable.frame = CGRect(x: 0, y: self.topBarView.frame.height + self.topBarView.frame.origin.y, width: 0, height: 341); 356 | }) { (_ : Bool) in 357 | self.sideBarTable.isHidden = true 358 | } 359 | } 360 | 361 | //Enumeration that defines options within the side bar menu 362 | enum SideBarOptions: Int{ 363 | case save = 0 364 | case print 365 | case aboutUs 366 | case settings 367 | case logout 368 | } 369 | 370 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 371 | if tableView == self.sideBarTable { 372 | self.hideSideBarMenu() 373 | let sideBarOption = SideBarOptions(rawValue: indexPath.row) 374 | // Complete an action based on the item pressed on the sidebar 375 | switch sideBarOption { 376 | case .save?: 377 | // Check if save is allowed by policy 378 | if self.isSaveAllowed { 379 | //Save the conversation and present success alert to user 380 | self.saveConversation(fileName: "savedConversation", conversation: conversation) 381 | 382 | let alert = UIAlertController(title: "Conversation Saved", 383 | message: "Your conversation has been successfully saved to your device.", 384 | preferredStyle: .alert) 385 | let closeAlert = UIAlertAction(title: "OK", 386 | style: .default, 387 | handler: nil) 388 | alert.addAction(closeAlert) 389 | present(alert, animated: true, completion: nil) 390 | } 391 | else { 392 | // Alert the user that saving is disabled 393 | let alert = UIAlertController(title: "Save Disabled", 394 | message: "Saving conversations to local storage has been disabled by your IT admin.", 395 | preferredStyle: .alert) 396 | let closeAlert = UIAlertAction(title: "OK", 397 | style: .default, 398 | handler: nil) 399 | alert.addAction(closeAlert) 400 | present(alert, animated: true, completion: nil) 401 | } 402 | case .print?: 403 | //Check if printing is currently available 404 | //NOTE: While the Intune SDK can prevent printing, this is not the only reason that printing could be unavailable 405 | if UIPrintInteractionController.isPrintingAvailable{ 406 | //If printing is available, print the conversation 407 | self.printConvo() 408 | } else { 409 | // Alert the user that saving is unavailable 410 | let alert = UIAlertController(title: "Printing Unavailable", 411 | message: "Printing conversations is currently unavailable on this device.", 412 | preferredStyle: .alert) 413 | let closeAlert = UIAlertAction(title: "OK", 414 | style: .default, 415 | handler: nil) 416 | alert.addAction(closeAlert) 417 | present(alert, animated: true, completion: nil) 418 | } 419 | 420 | case .aboutUs?: 421 | //Display the about us page 422 | let aboutUs:AboutUsPage = self.storyboard?.instantiateViewController(withIdentifier: "aboutPage") as! AboutUsPage 423 | present(aboutUs, animated:true, completion: nil) 424 | case .settings?: 425 | //Display the settings page 426 | let settings:SettingsPage = self.storyboard?.instantiateViewController(withIdentifier: "settingsPage") as! SettingsPage 427 | present(settings, animated:true, completion: nil) 428 | case .logout?: 429 | //To log out user, deregister the user from the SDK and initate a selective wipe of the app 430 | //In the EnrollmentDelegate, the unenrollRequestWithStatus block is executed, and includes logic to wipe tokens on unenrollment 431 | IntuneMAMEnrollmentManager.instance().deRegisterAndUnenrollAccountId(self.currentUser, withWipe: true) 432 | case .none: 433 | return; 434 | } 435 | } 436 | } 437 | 438 | /*! 439 | Presents the user with a print preview of the chat page. 440 | Called by the side bar table 441 | */ 442 | func printConvo() { 443 | //Provide basic information about print job 444 | let printInfo = UIPrintInfo(dictionary:nil) 445 | printInfo.outputType = UIPrintInfo.OutputType.general 446 | printInfo.jobName = "Print Chat Page" 447 | 448 | //Initialize a controller to handle the print 449 | let printController = UIPrintInteractionController.shared 450 | printController.printInfo = printInfo 451 | //Convert the current view to the image that will be printed 452 | printController.printingItem = self.wholePageView.toImage() 453 | //Present the print UI to the user 454 | printController.present(animated: true, completionHandler: nil) 455 | } 456 | 457 | func getSaveStatus() -> Bool { 458 | let policy = IntuneMAMPolicyManager.instance().policy(forAccountId: self.currentUser) 459 | if (nil == policy || (policy?.isSaveToAllowed(for: IntuneMAMSaveLocation.localDrive, withAccountId: self.currentUser))!){ 460 | return true 461 | } else { 462 | return false 463 | } 464 | } 465 | 466 | override func viewWillDisappear(_ animated: Bool) { 467 | super.viewWillDisappear(animated) 468 | 469 | //Save the draft message if there is one present 470 | self.saveDraftedMessage() 471 | } 472 | 473 | //Saves the conversation as a text file in the document directory of the application. 474 | func saveConversation(fileName: String, conversation: [(sender: String, message: NSAttributedString)]) { 475 | let newFormat = self.reformatConversation(conversation: conversation) 476 | self.writeFile(fileContent: newFormat, fileName: fileName) 477 | } 478 | 479 | //convert the array of (sender, message) tuples to a single string that represents the entire conversation 480 | func reformatConversation(conversation: [(sender: String, message: NSAttributedString)]) -> String { 481 | var newFormat = "" 482 | for element in conversation { 483 | newFormat.append(element.sender) 484 | newFormat.append(": ") 485 | newFormat.append((element.message).string) 486 | newFormat.append("\n") 487 | } 488 | return newFormat 489 | } 490 | 491 | //write the conversation text to the file in the document directory 492 | func writeFile(fileContent: String, fileName: String) { 493 | do { 494 | let url: URL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) 495 | try fileContent.write(to: url.appendingPathComponent(fileName).appendingPathExtension("txt"), atomically: true, encoding: .utf8) 496 | }catch let error { 497 | print("Error saving file: " + error.localizedDescription) 498 | } 499 | } 500 | } 501 | 502 | extension UIView { 503 | // converts a UIView to an image 504 | // used in the print() function for ChatPage 505 | func toImage() -> UIImage { 506 | // code logic modified from answer by Jody Heavener @ https://stackoverflow.com/questions/32403634/airprint-contents-of-a-uiview 507 | UIGraphicsBeginImageContextWithOptions(bounds.size, false, UIScreen.main.scale) 508 | 509 | drawHierarchy(in: self.bounds, afterScreenUpdates: true) 510 | 511 | let image = UIGraphicsGetImageFromCurrentImageContext() 512 | UIGraphicsEndImageContext() 513 | return image! 514 | } 515 | } 516 | -------------------------------------------------------------------------------- /chatr/ChatTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | class ChatTableViewCell: UITableViewCell { 8 | 9 | @IBOutlet weak var messageView: UITextView! 10 | 11 | } 12 | -------------------------------------------------------------------------------- /chatr/EnrollmentDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | import IntuneMAMSwift 6 | 7 | /* 8 | This enrollment delegate class can be initialized and set as the enrollment delegate of the IntuneMAMEnrollmentManager 9 | Doing this will trigger the enrollmentRequestWithStatus method whenever an enrollment is attempted. 10 | It can also be used to trigger unenrollRequestWithStatus whenever unenrollment is attempted. 11 | This allows for the app to check if an enrollment was successful 12 | 13 | NOTE: A number of other methods are avaliable in the IntuneMAMEnrollmentDelegate. See documentation or header file for more info. 14 | */ 15 | class EnrollmentDelegateClass: NSObject, IntuneMAMEnrollmentDelegate { 16 | 17 | var presentingViewController: UIViewController? 18 | 19 | override init() { 20 | super.init() 21 | self.presentingViewController = nil 22 | } 23 | 24 | /* 25 | To be able to change the view, the class should be initialzed with the curent view controller. Then this view controller can move to the desired view based on the enrollment success 26 | 27 | @param viewController - the view controller this class should use when triggered 28 | */ 29 | init(viewController : UIViewController){ 30 | super.init() 31 | self.presentingViewController = viewController 32 | } 33 | 34 | /* 35 | This is a method of the delegate that is triggered when an instance of this class is set as the delegate of the IntuneMAMEnrollmentManager and an enrollment is attempted. 36 | The status parameter is a member of the IntuneMAMEnrollmentStatus class. This object can be used to check for the status of an attempted enrollment 37 | If successful, logic for enrollment is initiated 38 | */ 39 | func enrollmentRequest(with status: IntuneMAMEnrollmentStatus) { 40 | if status.didSucceed{ 41 | //If enrollment was successful, change from the current view (which should have been initialized with the class) to the desired page on the app (in this case ChatPage) 42 | print("Login successful") 43 | 44 | let storyboard = UIStoryboard.init(name: "Main", bundle: nil) 45 | let chatPage = storyboard.instantiateViewController(withIdentifier: "ChatPage") 46 | if nil != self.presentingViewController { 47 | self.presentingViewController!.present(chatPage, animated: false, completion: nil) 48 | } else { 49 | print("Warning: EnrollmentDelegate initialized without a view controller before attempting enrollment.") 50 | UIUtils.getCurrentViewController().present(chatPage, animated: false, completion: nil) 51 | } 52 | 53 | } else if IntuneMAMEnrollmentStatusCode.loginCanceled != status.statusCode { 54 | //In the case of a failure, log failure error status and code 55 | print("Enrollment result for identity \(status.identity) with status code \(status.statusCode)") 56 | print("Debug message: \(String(describing: status.errorString))") 57 | 58 | //Present the user with an alert asking them to sign in again. 59 | let alert = UIAlertController(title: "Error Authenticating", message: "There was an error while logging you into your account. Please check your log in credentials and try again.", preferredStyle: .alert) 60 | let closeAlert = UIAlertAction.init(title: "OK", style: .default, handler: nil) 61 | alert.addAction(closeAlert) 62 | if nil != self.presentingViewController { 63 | self.presentingViewController!.present(alert, animated: true, completion: nil) 64 | } else { 65 | print("Warning: EnrollmentDelegate initialized without a view controller before attempting enrollment.") 66 | UIUtils.getCurrentViewController().present(alert, animated: true, completion: nil) 67 | } 68 | } 69 | } 70 | 71 | /* 72 | This is a method of the delegate that is triggered when an instance of this class is set as the delegate of the IntuneMAMEnrollmentManager and an unenrollment is attempted. 73 | The status parameter is a member of the IntuneMAMEnrollmentStatus class. This object can be used to check for the status of an attempted unenrollment. 74 | Logic for logout/token clearing is initiated here. 75 | */ 76 | func unenrollRequest(with status: IntuneMAMEnrollmentStatus) { 77 | //Go back to login page from current view controller 78 | let presentingViewController = UIUtils.getCurrentViewController() 79 | let storyboard = UIStoryboard.init(name: "Main", bundle: nil) 80 | let loginPage = storyboard.instantiateViewController(withIdentifier: "LoginPage") 81 | 82 | presentingViewController.present(loginPage, animated: true, completion: nil) 83 | 84 | if status.didSucceed != true { 85 | //In the case unenrollment failed, log error 86 | print("Unenrollment result for identity \(status.identity) with status code \(status.statusCode)") 87 | print("Debug message: \(String(describing: status.errorString))") 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /chatr/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleURLTypes 20 | 21 | 22 | CFBundleTypeRole 23 | Viewer 24 | CFBundleURLIconFile 25 | logo 26 | CFBundleURLName 27 | chatr 28 | CFBundleURLSchemes 29 | 30 | chatr 31 | chatr-intunemam 32 | msauth.com.microsoft.intunemam 33 | 34 | 35 | 36 | CFBundleVersion 37 | 1 38 | LSApplicationQueriesSchemes 39 | 40 | mvisionmobile 41 | microsoft-edge-http 42 | microsoft-edge-http-intunemam 43 | scmx 44 | lookoutwork-ase 45 | microsoft-edge-https 46 | microsoft-edge-https-intunemam 47 | lacoonsecurity 48 | zips 49 | skycure 50 | smsec 51 | smart-ns 52 | betteractiveshield 53 | companyportal 54 | ms-outlook 55 | ms-outlook-intunemam 56 | wandera 57 | https-intunemam 58 | http-intunemam 59 | 60 | LSRequiresIPhoneOS 61 | 62 | UILaunchStoryboardName 63 | LaunchScreen 64 | UIMainStoryboardFile 65 | Main 66 | UIRequiredDeviceCapabilities 67 | 68 | armv7 69 | 70 | UISupportedInterfaceOrientations 71 | 72 | UIInterfaceOrientationPortrait 73 | 74 | UISupportedInterfaceOrientations~ipad 75 | 76 | UIInterfaceOrientationPortrait 77 | UIInterfaceOrientationPortraitUpsideDown 78 | UIInterfaceOrientationLandscapeLeft 79 | UIInterfaceOrientationLandscapeRight 80 | 81 | UIUserInterfaceStyle 82 | Light 83 | 84 | 85 | -------------------------------------------------------------------------------- /chatr/KeychainManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | /* 8 | Class with methods used to manage adding, updating, and removing user data to the keychain. 9 | The keychain is used in this app to securely store user messages and drafted messages. 10 | The data in the keychain persists across app instances unless deleted from the keychain. 11 | */ 12 | @objc public class KeychainManager : NSObject{ 13 | 14 | //Keys used in keychain to distinguish draft messages from sent messages 15 | static let draftMessageKey: String = "draftMessage" 16 | static let sentMessageKey: String = "sentMessages" 17 | 18 | /* 19 | This function updates a user's current item in the keychain with new data 20 | @param user: the String representing the upn of the user to update the keychain for 21 | @param data: the data to be stored in the keychain, can be of any type 22 | @param key: a string representing the key for the data 23 | */ 24 | private class func updateItem(forUser: String, data: Any, key:String){ 25 | //Convert given data to archived data that can be stored in the keychain 26 | var archivedData:Data 27 | do { 28 | archivedData = try NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding:true) 29 | } 30 | catch { 31 | print("Unexpected error when updating data in the keychain: Failed to create archived data.") 32 | return 33 | } 34 | 35 | //First define a query that will locate the user's keychain item 36 | let query = [kSecClass: kSecClassGenericPassword, 37 | kSecAttrAccount: forUser, 38 | kSecAttrService: key, 39 | ] as CFDictionary 40 | 41 | //Next define the attributes to be updated (the archived data) 42 | let attributes = [kSecValueData: archivedData] as CFDictionary 43 | 44 | //Update the item in the keychain 45 | let status = SecItemUpdate(query, attributes) 46 | if status != errSecSuccess { 47 | //In the case of an unexpected error, log the error information 48 | print("Unexpected error when updating data in the keychain.") 49 | print("Error code: \(Int(status))") 50 | } 51 | } 52 | 53 | /* 54 | This function is used to retrieve the current sent messages in the keychain for a given user 55 | If any are found, the function returns an array of the sent messages. 56 | If none are found, or there is an error in searching for the item, the function returns nil. 57 | @param forUser: the String representing the upn of the user to search the keychain for 58 | */ 59 | @objc public class func getSentMessages(forUser: String) -> [String]?{ 60 | //A query specific to the user is defined 61 | //Within the query, kSecReturnData is set to true so that the search will return the message data 62 | let query = [kSecClass: kSecClassGenericPassword, 63 | kSecAttrAccount: forUser, 64 | kSecAttrService: self.sentMessageKey, 65 | kSecReturnData: true, 66 | kSecReturnAttributes: true, 67 | ] as CFDictionary 68 | 69 | var item: CFTypeRef? 70 | //Call the search to find a matching item in the keychain 71 | let status = SecItemCopyMatching(query, &item) 72 | //If no item is found, return nil 73 | guard status != errSecItemNotFound else { return nil } 74 | 75 | if status == errSecSuccess { 76 | //If an item is successfully returned, retrieve the data from the item 77 | guard let queryReturn = item as? [String : Any] 78 | else { 79 | //If the data is not in the appropriate format, log this and return nil 80 | print("Unexpected data returned from keychain") 81 | return nil 82 | } 83 | //Convert the data back to an array and return it 84 | let encodedData = queryReturn[kSecValueData as String] as? Data 85 | do { 86 | let messageArray = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSArray.self, from: encodedData!) as? [String] 87 | return messageArray 88 | } 89 | catch { 90 | print("Unexpected error when searching for data in the keychain: Failed to unarchive sent messages.") 91 | return nil 92 | } 93 | } else { 94 | //Some other unexpected error occurred 95 | print("Unexpected error when searching for data in the keychain.") 96 | print("Error code: \(Int(status))") 97 | return nil 98 | } 99 | } 100 | 101 | /* 102 | This function is used to retrieve the current drafted message in the keychain for a given user 103 | If one is found, it is returned as an String. 104 | If no drafted message is found, or there is an error in searching for the item, the function returns nil. 105 | @param forUser: the String representing the upn of the user to search the keychain for 106 | */ 107 | @objc public class func getDraftedMessage(forUser: String) -> String?{ 108 | //A query specific to the user is defined 109 | //Within the query, kSecReturnData is set to true so that the search will return the message data 110 | let query = [kSecClass: kSecClassGenericPassword, 111 | kSecAttrAccount: forUser, 112 | kSecAttrService: self.draftMessageKey, 113 | kSecReturnData: true, 114 | kSecReturnAttributes: true, 115 | ] as CFDictionary 116 | 117 | var item: CFTypeRef? 118 | //Call the search to find a matching item in the keychain 119 | let status = SecItemCopyMatching(query, &item) 120 | //If no item is found, return nil 121 | guard status != errSecItemNotFound else { return nil } 122 | 123 | if status == errSecSuccess { 124 | //If an item is successfully returned, retrieve the data from the item 125 | guard let queryReturn = item as? [String : Any] 126 | else { 127 | //If the data is not in the appropriate format, log this and return nil 128 | print("Unexpected data returned from keychain") 129 | return nil 130 | } 131 | //Convert the message back to a string and return it 132 | let encodedData = queryReturn[kSecValueData as String] as? Data 133 | do { 134 | let draftMessage = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSString.self, from: encodedData!) as String? 135 | return draftMessage 136 | } 137 | catch { 138 | print("Unexpected error when searching for data in the keychain: Failed to unarchive draft message.") 139 | return nil 140 | } 141 | } else { 142 | //Some other unexpected error occurred 143 | print("Unexpected error when searching for data in the keychain.") 144 | print("Error code: \(Int(status))") 145 | return nil 146 | } 147 | } 148 | 149 | /* 150 | This function is used to store a sent message in the keychain. It first searches to see if any sent messages are present for the given user already. 151 | If the user already has an item in the keychain for sent messages, update this item. Otherwise, create a new keychain item. 152 | @param forUser: the String representing the upn of the user to add a sent message for 153 | @param message: the String containing the sent message to be added to the keychain 154 | */ 155 | @objc public class func storeSentMessage(sentMessage newMessage:String, forUser:String){ 156 | if var currentMessages : [String] = KeychainManager.getSentMessages(forUser: forUser) { 157 | //When dealing with sent messages, add the new message to the existing messages if any are present 158 | currentMessages.append(newMessage) 159 | KeychainManager.updateItem(forUser: forUser, data: currentMessages, key: self.sentMessageKey) 160 | 161 | } else { 162 | //Otherwise define a query to add an item for the user containing the sent message within an array (so that future messages can be added) 163 | let messageArray = [newMessage] 164 | var messageData:Data 165 | do { 166 | messageData = try NSKeyedArchiver.archivedData(withRootObject: messageArray, requiringSecureCoding:true) 167 | } 168 | catch { 169 | print("Unexpected error when adding data to the keychain: Failed to archive sent messages.") 170 | return 171 | } 172 | 173 | let query = [kSecClass: kSecClassGenericPassword, 174 | kSecAttrAccount: forUser, 175 | kSecAttrService: self.sentMessageKey, 176 | kSecValueData: messageData, 177 | ] as CFDictionary 178 | 179 | //Add the new item to the query 180 | let status = SecItemAdd(query, nil) 181 | if status != errSecSuccess { 182 | //Some other unexpected error occured 183 | print("Error occurred when adding data to the keychain.") 184 | print("Error code: \(Int(status))") 185 | } 186 | } 187 | } 188 | 189 | /* 190 | This function is used to store a draft message in the keychain. It first searches to see if any draft messages are present for the given user already. 191 | If the user already has an draft message in the keychain, replace this draft message with the new one. Otherwise, create a new keychain item. 192 | @param forUser: the String representing the upn of the user to add a draft message for 193 | @param message: the String containing the draft message to be added to the keychain 194 | */ 195 | @objc public class func storeDraftMessage(draftMessage newMessage:String, forUser:String){ 196 | if nil != KeychainManager.getDraftedMessage(forUser: forUser) { 197 | //When dealing with draft messages, do not add the new draft message to the existing one, but store only the newest value as only one draft message is stored at a time 198 | KeychainManager.updateItem(forUser: forUser, data: newMessage, key:self.draftMessageKey) 199 | 200 | } else { 201 | //Otherwise define a query to add an item for the user containing the draft message 202 | var messageData:Data 203 | 204 | do { 205 | messageData = try NSKeyedArchiver.archivedData(withRootObject: newMessage, requiringSecureCoding:true) 206 | } 207 | catch { 208 | print("Unexpected error when adding data to the keychain: Failed to create archive draft message.") 209 | return 210 | } 211 | 212 | let query = [kSecClass: kSecClassGenericPassword, 213 | kSecAttrAccount: forUser, 214 | kSecAttrService: self.draftMessageKey, 215 | kSecValueData: messageData, 216 | ] as CFDictionary 217 | 218 | //Add the new item to the query 219 | let status = SecItemAdd(query, nil) 220 | if status != errSecSuccess { 221 | //Some other unexpected error occured 222 | print("Error occurred when adding data to the keychain.") 223 | print("Error code: \(Int(status))") 224 | } 225 | } 226 | } 227 | 228 | /* 229 | Function used to delete the sent messages data for a given user from the keychain. 230 | Returns true if the deletion was successful, and false if the deletion failed. 231 | @param forUser: the String representing the upn of the user to remove keychain items for 232 | */ 233 | @objc public class func deleteSentMessages(forUser:String) -> Bool{ 234 | 235 | //Define a query that will locate the sent messages item in the keychain 236 | let query = [kSecClass: kSecClassGenericPassword, 237 | kSecAttrAccount: forUser, 238 | kSecAttrService: self.sentMessageKey, 239 | ] as CFDictionary 240 | 241 | //Delete the item the query finds 242 | let status = SecItemDelete(query) 243 | //Even if the item was not found, this is considered successful as the item may never have been there 244 | if status != errSecSuccess && status != errSecItemNotFound { 245 | print("Error occurred when removing sent messages from the keychain.") 246 | print("Error code: \(Int(status))") 247 | return false 248 | } 249 | return true 250 | } 251 | 252 | /* 253 | Function used to delete the draft message data for a given user from the keychain. 254 | Returns true if the deletion was successful, and false if the deletion failed. 255 | @param forUser: the String representing the upn of the user to remove keychain items for 256 | */ 257 | @objc public class func deleteDraftMessage(forUser:String) -> Bool{ 258 | 259 | //Define a query that will locate the draft message item in the keychain 260 | let query = [kSecClass: kSecClassGenericPassword, 261 | kSecAttrAccount: forUser, 262 | kSecAttrService: self.draftMessageKey, 263 | ] as CFDictionary 264 | 265 | //Delete the item the query finds 266 | let status = SecItemDelete(query) 267 | //Even if the item was not found, this is considered successful as the item may never have been there 268 | if status != errSecSuccess && status != errSecItemNotFound { 269 | print("Error occurred when removing the draft message from the keychain.") 270 | print("Error code: \(Int(status))") 271 | return false 272 | } 273 | return true 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /chatr/LoginPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | import IntuneMAMSwift 6 | 7 | class LoginPage: UIViewController { 8 | /* 9 | Button action triggered when user presses the log in button 10 | 11 | Sends the user through the Intune Authentication flow for logging in. 12 | Triggers the "homePage" segue if login is successful; raises an alert if there is an error. 13 | */ 14 | @IBAction func logInBtn(_ sender: Any) { 15 | //Set the delegate of the IntuneMAMEnrollmentManager as an instance of the EnrollmentDelegateClass to check the status of attempted enrollments. Also initialize this class with the current view controller 16 | //This is done on launch, but here it is done again to give the delegate the current view controller 17 | let enrollmentDelegate = EnrollmentDelegateClass.init(viewController: self) 18 | IntuneMAMEnrollmentManager.instance().delegate = enrollmentDelegate 19 | 20 | //Login the user through the Intune sign in flow. EnrollmentDelegateClass will handle the outcome of this. 21 | IntuneMAMEnrollmentManager.instance().loginAndEnrollAccount(nil) 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /chatr/PolicyDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | import IntuneMAMSwift 6 | 7 | /* 8 | This policy delegate class can be initialized and set as the delegate of the IntuneMAMPolicyManager 9 | (This is done in the AppDelegate.swift file at app initialization) 10 | Doing this will trigger the wipeDataForAccount and restartApplication methods whenever the Intune SDK needs to do either of these things 11 | 12 | NOTE: A number of other methods are avaliable in the IntuneMAMPolicyDelegate. See documentation or header file for more info. 13 | Methods like identitySwitchRequired and addIdentity are generally used in multi-user apps, unlike this single user app. 14 | See IntuneMAMPolicyDelegate documentation or header file for more information 15 | */ 16 | 17 | class PolicyDelegateClass: NSObject, IntuneMAMPolicyDelegate { 18 | 19 | /* 20 | wipeDataForAccount is called by the Intune SDK when the app needs to wipe all the data for a specified user 21 | With chatr, the only user data stored are the sent chat messages and drafted chat messages. 22 | If this is wiped successfully, return true, otherwise return false 23 | 24 | @param upn is the upn of the user whoes data is to be wiped (for example "user@example.com") 25 | */ 26 | func wipeData(forAccountId: String) -> Bool { 27 | //variable to track if the data wipe was successful 28 | var wipeSuccess = true 29 | 30 | //remove all files in each directory 31 | let fileManager: FileManager = FileManager.default 32 | let paths: [String] = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) 33 | for fileDirectory in paths { 34 | do { 35 | let fileArray = try fileManager.contentsOfDirectory(atPath: fileDirectory) 36 | for fileName in fileArray { 37 | let fileDirectoryURL = URL(fileURLWithPath: fileDirectory) 38 | let fullPathURL = fileDirectoryURL.appendingPathComponent(fileName) 39 | try fileManager.removeItem(atPath: fullPathURL.path) 40 | } 41 | } catch { 42 | print("Could not successfully remove files from directory. Error: \(error)") 43 | wipeSuccess = false 44 | } 45 | } 46 | 47 | //Use the deleteSentMessagesForUser and deleteSentMessagesForUser functions in the KeychainManager class to look into the keychain to wipe any message data stored for a given upn 48 | if !(KeychainManager.deleteDraftMessage(forUser: forAccountId) && KeychainManager.deleteSentMessages(forUser: forAccountId)){ 49 | //If either function call returns false, this indicates the app failed clear the user's messages from the keychain 50 | print("Data wipe from keychain failed") 51 | wipeSuccess = false 52 | } 53 | 54 | return wipeSuccess 55 | } 56 | 57 | /* 58 | In the case that the app needs to perform tasks like save user data before the Intune SDK restarts the app, those tasks can be done here 59 | With Chatr, drafted messages need to be saved if a restart is forced. 60 | 61 | If the app will handle restarting on its own, return true. 62 | If the app wants the Intune SDK to handle the restart, @return false. 63 | See IntuneMAMPolicyDelegate documentation or header file for more information 64 | */ 65 | func restartApplication() -> Bool { 66 | //If the current view is the chat page and there is a message currently being drafted, then save it to the keychain to repopulate it after the restart. 67 | let currentViewController = UIUtils.getCurrentViewController() 68 | if currentViewController is ChatPage{ 69 | //Call saveDraftedMessage on the ChatPage to save the drafted message to the keychain if there is one present. 70 | let page = currentViewController as! ChatPage 71 | page.saveDraftedMessage() 72 | } 73 | return false 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /chatr/SettingsPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | import IntuneMAMSwift 6 | 7 | class SettingsPage: UIViewController { 8 | @IBOutlet weak var displayConsoleButton: UIButton! 9 | @IBOutlet weak var backButton: UIButton! 10 | 11 | override func viewDidLoad() { 12 | self.backButton.setImage(#imageLiteral(resourceName: "backarrow.png"), for: UIControl.State.normal) 13 | self.backButton.imageView!.contentMode = .scaleAspectFit 14 | self.backButton.addTarget(self, action: #selector (self.dismiss), for: .touchUpInside) 15 | } 16 | 17 | //Listener for diagnostic console button 18 | @IBAction func displayConsoleTapped(_ sender: Any) { 19 | //when button is tapped, display console 20 | IntuneMAMDiagnosticConsole.display() 21 | } 22 | 23 | //Button action triggered when back button is pressed on the settings page 24 | //Dismisses the view. 25 | @IBAction func dismiss(_ sender: UIButton) { 26 | self.dismiss(animated: true, completion: nil) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /chatr/SideBarTableCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | class SideBarTableCell: UITableViewCell { 8 | 9 | @IBOutlet weak var cellLbl: UILabel! 10 | @IBOutlet weak var cellImg: UIImageView! 11 | } 12 | -------------------------------------------------------------------------------- /chatr/UIUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | class UIUtils{ 8 | 9 | class func getCurrentViewController() -> UIViewController{ 10 | var topController = UIApplication.shared.keyWindow?.rootViewController 11 | if (nil != topController) { 12 | var presentedViewController = topController!.presentedViewController 13 | //Loop until there are no more view controllers to go to 14 | while (nil != presentedViewController){ 15 | topController = presentedViewController 16 | presentedViewController = topController!.presentedViewController 17 | } 18 | } 19 | //Return the final view controller 20 | return topController! 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /chatr/chatr.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | keychain-access-groups 6 | 7 | $(AppIdentifierPrefix)Intune.chatr 8 | $(AppIdentifierPrefix)com.microsoft.intune.mam 9 | $(AppIdentifierPrefix)com.microsoft.adalcache 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/logo.jpg --------------------------------------------------------------------------------