├── .circleci └── config.yml ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg ├── husky.sh └── pre-commit ├── .npmignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── android ├── build.gradle └── src │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── mattermost │ │ └── emm │ │ ├── Constants.kt │ │ ├── EmmModuleImpl.kt │ │ └── EmmPackage.kt │ ├── newarch │ └── java │ │ └── com │ │ └── EmmModule.kt │ └── oldarch │ └── java │ └── com │ └── EmmModule.kt ├── babel.config.js ├── example ├── Gemfile ├── Gemfile.lock ├── android │ ├── app │ │ ├── build.gradle │ │ ├── debug.keystore │ │ ├── proguard-rules.pro │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── mattermostreactnativeemm │ │ │ │ ├── MainActivity.kt │ │ │ │ └── MainApplication.kt │ │ │ └── res │ │ │ ├── drawable │ │ │ └── rn_edit_text_material.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── values │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ │ └── xml │ │ │ └── app_restrictions.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle ├── app.json ├── babel.config.js ├── index.tsx ├── ios │ ├── .xcode.env │ ├── File.swift │ ├── Podfile │ ├── Podfile.lock │ ├── ReactNativeEmmExample-Bridging-Header.h │ ├── ReactNativeEmmExample.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── ReactNativeEmmExample.xcscheme │ ├── ReactNativeEmmExample.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── ReactNativeEmmExample │ │ ├── AppDelegate.h │ │ ├── AppDelegate.mm │ │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── Info.plist │ │ ├── LaunchScreen.storyboard │ │ ├── PrivacyInfo.xcprivacy │ │ └── main.m ├── metro.config.js ├── package-lock.json ├── package.json └── src │ ├── App.tsx │ ├── Authentication.tsx │ ├── Button.tsx │ ├── ManagedConfig.tsx │ └── Others.tsx ├── ios ├── Emm.h ├── Emm.mm ├── Emm.swift ├── EmmAuthenticate.swift ├── EmmEvents.swift ├── EmmManagedConfig.swift ├── Extensions │ ├── UINavigationController+Swizzle.swift │ ├── UIView+Shield.swift │ ├── UIViewController+Shield.swift │ ├── UIViewController+Swizzle.swift │ └── UIWindow+Swizzle.swift └── ScreenCaptureManager.swift ├── package-lock.json ├── package.json ├── react-native-emm.podspec ├── src ├── NativeEmm.ts ├── context.tsx ├── emm-native.ts ├── emm.ts ├── index.ts └── types │ ├── authenticate.ts │ └── managed.ts ├── tsconfig.build.json └── tsconfig.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | default: 5 | docker: 6 | - image: circleci/node:14 7 | working_directory: ~/project 8 | 9 | commands: 10 | attach_project: 11 | steps: 12 | - attach_workspace: 13 | at: ~/project 14 | 15 | jobs: 16 | install-dependencies: 17 | executor: default 18 | steps: 19 | - checkout 20 | - attach_project 21 | - restore_cache: 22 | keys: 23 | - dependencies-{{ checksum "package.json" }} 24 | - run: 25 | name: Install dependencies 26 | command: | 27 | npm install 28 | - save_cache: 29 | key: dependencies-{{ checksum "package.json" }} 30 | paths: node_modules 31 | - persist_to_workspace: 32 | root: . 33 | paths: . 34 | 35 | lint: 36 | executor: default 37 | steps: 38 | - attach_project 39 | - run: 40 | name: Lint files 41 | command: | 42 | npm run lint 43 | 44 | typescript: 45 | executor: default 46 | steps: 47 | - attach_project 48 | - run: 49 | name: Typecheck files 50 | command: | 51 | npm run typescript 52 | 53 | unit-tests: 54 | executor: default 55 | steps: 56 | - attach_project 57 | - run: 58 | name: Run unit tests 59 | command: | 60 | npm test --coverage 61 | - store_artifacts: 62 | path: coverage 63 | destination: coverage 64 | 65 | build-package: 66 | executor: default 67 | steps: 68 | - attach_project 69 | - run: 70 | name: Build package 71 | command: | 72 | npm run prepare 73 | 74 | release: 75 | executor: default 76 | steps: 77 | - attach_project 78 | - run: 79 | name: Release to GitHub 80 | command: npm run release -- --ci 81 | 82 | publish: 83 | executor: default 84 | steps: 85 | - attach_project 86 | - run: 87 | name: Authenticate with registry 88 | command: | 89 | echo "//registry.npmjs.org/:authToken=$NPM_TOKEN" > ~/project/.npmrc 90 | echo "scope=$NPM_SCOPE" 91 | - run: 92 | name: Publish package 93 | run: npm publish --access public 94 | 95 | workflows: 96 | build: 97 | jobs: 98 | - install-dependencies 99 | - lint: 100 | requires: 101 | - install-dependencies 102 | - typescript: 103 | requires: 104 | - install-dependencies 105 | - unit-tests: 106 | requires: 107 | - install-dependencies 108 | - build-package: 109 | requires: 110 | - install-dependencies 111 | - lint 112 | - typescript 113 | - unit-tests 114 | - release: 115 | context: mattermost-rn-libraries 116 | requires: 117 | - build-package 118 | fliters: 119 | branches: 120 | only: /^release-\d+$/ 121 | - publish: 122 | context: mattermost-rn-libraries 123 | requires: 124 | - build-package 125 | filters: 126 | tags: 127 | only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/ 128 | branches: 129 | ignore: /.*/ 130 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | 17 | # Windows files 18 | [*.bat] 19 | end_of_line = crlf 20 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Windows files should use crlf line endings 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | *.bat text eol=crlf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | 32 | # Android/IJ 33 | # 34 | .idea 35 | .gradle 36 | local.properties 37 | android.iml 38 | *.hprof 39 | .cxx/ 40 | *.keystore 41 | !debug.keystore 42 | 43 | # Cocoapods 44 | # 45 | example/ios/Pods 46 | example/ios/.xcode.env.local 47 | 48 | # node.js 49 | # 50 | node_modules/ 51 | npm-debug.log 52 | yarn-debug.log 53 | yarn-error.log 54 | 55 | # Expo 56 | .expo/* 57 | 58 | # generated by bob 59 | lib/ 60 | 61 | # Temporary files created by Metro to check the health of the file watcher 62 | .metro-health-check* 63 | 64 | .settings/ 65 | .project 66 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no-install commitlint --edit 2 | -------------------------------------------------------------------------------- /.husky/husky.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ -z "$husky_skip_init" ]; then 3 | debug () { 4 | [ "$HUSKY_DEBUG" = "1" ] && echo "husky (debug) - $1" 5 | } 6 | 7 | readonly hook_name="$(basename "$0")" 8 | debug "starting $hook_name..." 9 | 10 | if [ "$HUSKY" = "0" ]; then 11 | debug "HUSKY env variable is set to 0, skipping hook" 12 | exit 0 13 | fi 14 | 15 | if [ -f ~/.huskyrc ]; then 16 | debug "sourcing ~/.huskyrc" 17 | . ~/.huskyrc 18 | fi 19 | 20 | export readonly husky_skip_init=1 21 | sh -e "$0" "$@" 22 | exitCode="$?" 23 | 24 | if [ $exitCode != 0 ]; then 25 | echo "husky - $hook_name hook exited with code $exitCode (error)" 26 | exit $exitCode 27 | fi 28 | 29 | exit 0 30 | fi 31 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint && npm run typescript 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example/ 2 | lib/typescript/example 3 | **/__tests__, 4 | **/__fixtures__, 5 | **/__mocks__ 6 | .vscode 7 | **/.idea 8 | **/.gradle 9 | android/build/ 10 | ios/Build 11 | .circleci 12 | .husky -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Code Contribution Guidelines 3 | 4 | Thank you for your interest in contributing! Please see the [Mattermost Contribution Guide](https://developers.mattermost.com/contribute/getting-started/) which describes the process for making code contributions across Mattermost projects and [join our "Native Mobile Apps" community channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) to ask questions from community members and the Mattermost core team. 5 | 6 | When you submit a pull request, it goes through a [code review process outlined here](https://developers.mattermost.com/contribute/getting-started/code-review/). 7 | 8 | 9 | ## Development workflow 10 | 11 | 12 | 13 | To get started with the project, run `npm run bootstrap` in the root directory to install the required dependencies for each package: 14 | 15 | 16 | 17 | ```sh 18 | 19 | npm run bootstrap 20 | 21 | ``` 22 | 23 | 24 | 25 | While developing, you can run the [example app](/example/) to test your changes. 26 | 27 | 28 | 29 | To start the packager: 30 | 31 | 32 | 33 | ```sh 34 | 35 | npm run example start 36 | 37 | ``` 38 | 39 | 40 | 41 | To run the example app on Android: 42 | 43 | 44 | 45 | ```sh 46 | 47 | npm run example run android 48 | 49 | ``` 50 | 51 | 52 | 53 | To run the example app on iOS: 54 | 55 | 56 | 57 | ```sh 58 | 59 | npm run example run ios 60 | 61 | ``` 62 | 63 | 64 | 65 | Make sure your code passes TypeScript and ESLint. Run the following to verify: 66 | 67 | 68 | 69 | ```sh 70 | 71 | npm run typescript 72 | 73 | npm run lint 74 | 75 | ``` 76 | 77 | 78 | 79 | To fix formatting errors, run the following: 80 | 81 | 82 | 83 | ```sh 84 | 85 | npm run lint -- --fix 86 | 87 | ``` 88 | 89 | 90 | 91 | Remember to add tests for your change if possible. Run the unit tests by: 92 | 93 | 94 | 95 | ```sh 96 | 97 | npm test 98 | 99 | ``` 100 | 101 | 102 | 103 | To edit the Objective-C files, open `example/ios/ReactNativeEmmExample.xcworkspace` in XCode and find the source files at `Pods > Development Pods > react-native-emm`. 104 | 105 | 106 | 107 | To edit the Kotlin files, open `example/android` in Android studio and find the source files at `mattermostreactnativeemm` under `Android`. 108 | 109 | 110 | 111 | ### Commit message convention 112 | 113 | 114 | 115 | We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: 116 | 117 | 118 | 119 | - `fix`: bug fixes, e.g. fix crash due to deprecated method. 120 | 121 | - `feat`: new features, e.g. add new method to the module. 122 | 123 | - `refactor`: code refactor, e.g. migrate from class components to hooks. 124 | 125 | - `docs`: changes into documentation, e.g. add usage example for the module.. 126 | 127 | - `test`: adding or updating tests, eg add integration tests using detox. 128 | 129 | - `chore`: tooling changes, e.g. change CI config. 130 | 131 | 132 | 133 | Our pre-commit hooks verify that your commit message matches this format when committing. 134 | 135 | 136 | 137 | ### Linting and tests 138 | 139 | 140 | 141 | [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) 142 | 143 | 144 | 145 | We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing. 146 | 147 | 148 | 149 | Our pre-commit hooks verify that the linter and tests pass when committing. 150 | 151 | 152 | 153 | ### Scripts 154 | 155 | 156 | 157 | The `package.json` file contains various scripts for common tasks: 158 | 159 | 160 | 161 | - `npm run bootstrap`: setup project by installing all dependencies and pods. 162 | 163 | - `npm run typescript`: type-check files with TypeScript. 164 | 165 | - `npm run lint`: lint files with ESLint. 166 | 167 | - `npm test`: run unit tests with Jest. 168 | 169 | - `npm run example start`: start the Metro server for the example app. 170 | 171 | - `npm run example android`: run the example app on Android. 172 | 173 | - `npm run example ios`: run the example app on iOS. 174 | 175 | 176 | 177 | ### Sending a pull request 178 | 179 | 180 | 181 | > **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github). 182 | 183 | 184 | 185 | When you're sending a pull request: 186 | 187 | 188 | 189 | - Prefer small pull requests focused on one change. 190 | 191 | - Verify that linters and tests are passing. 192 | 193 | - Review the documentation to make sure it looks good. 194 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mattermost 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 | ## @mattermost/react-native-emm 2 | 3 | A React Native Module for EMM managed configurations 4 | 5 | ## Table of Contents 6 | 7 | * [Installation](#installation) 8 | * [iOS](#ios-installation) 9 | * [Android](#android-installation) 10 | * [Usage](#usage) 11 | 12 | ## Installation 13 | 14 | Using npm: 15 | 16 | ```shell 17 | npm install --save-exact @mattermost/react-native-emm 18 | ``` 19 | 20 | or using yarn: 21 | 22 | ```shell 23 | yarn add -E @mattermost/react-native-emm 24 | ``` 25 | 26 | Then follow the instructions for your platform to link @mattermost/react-native-emm into your project: 27 | 28 | ### iOS installation 29 |
30 | iOS details 31 | 32 | #### Standard Method 33 | 34 | **React Native 0.60 and above** 35 | 36 | Run `npx pod-install`. Linking is not required in React Native 0.60 and above. 37 | 38 | **React Native 0.59 and below** 39 | 40 | Run `react-native link @mattermost/react-native-emm` to link the react-native-emm library. 41 | 42 | #### Using CocoaPods (required to enable caching) 43 | 44 | Setup your Podfile like it is described in the [react-native documentation](https://facebook.github.io/react-native/docs/integration-with-existing-apps#configuring-cocoapods-dependencies). 45 | 46 | ```diff 47 | pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec' 48 | + `pod 'react-native-emm', :path => '../node_modules/@mattermost/react-native-emm/react-native-emm.podspec'` 49 | end 50 | ``` 51 | 52 |
53 | 54 | ### Android installation 55 |
56 | Android details 57 | 58 | #### ** This library is only compatible with Android M (API level 23) or above" 59 | 60 | **React Native 0.60 and above** 61 | Linking is not required in React Native 0.60 and above. 62 | 63 | **React Native 0.59 and below** 64 | Run `react-native link @mattermost/react-native-emm` to link the react-native-emm library. 65 | 66 | Or if you have trouble, make the following additions to the given files manually: 67 | 68 | #### **android/settings.gradle** 69 | 70 | ```gradle 71 | include ':mattermost.Emm' 72 | project(':mattermost.Emm').projectDir = new File(rootProject.projectDir, '../node_modules/@mattermost/react-native-emm/android') 73 | ``` 74 | 75 | #### **android/app/build.gradle** 76 | 77 | ```diff 78 | dependencies { 79 | ... 80 | + implementation project(':mattermost.Emm') 81 | } 82 | ``` 83 | 84 | #### **android/gradle.properties** 85 | 86 | ```gradle.properties 87 | android.useAndroidX=true 88 | ``` 89 | 90 | #### **MainApplication.java** 91 | 92 | On top, where imports are: 93 | 94 | ```java 95 | import com.mattermost.Emm.EmmPackage; 96 | ``` 97 | 98 | Add the `EmmPackage` class to your list of exported packages. 99 | 100 | ```diff 101 | @Override 102 | protected List getPackages() { 103 | @SuppressWarnings("UnnecessaryLocalVariable") 104 | List packages = new PackageList(this).getPackages(); 105 | // Packages that cannot be autolinked yet can be added manually here, for ReactNativeEmmExample: 106 | // packages.add(new MyReactNativePackage()); 107 | + packages.add(new EmmPackage()); 108 | return packages; 109 | } 110 | ``` 111 | **Configure your Android app to handle managed configurations** 112 | 113 | Perform this steps manually as they are not handled by `Autolinking`. 114 | 115 | #### **android/src/main/AndroidManifest.xml** 116 | 117 | Enable `APP_RESTRICTIONS` in your Android manifest file 118 | 119 | ```diff 120 | 127 | + 128 | 132 | ``` 133 | 134 | #### **android/src/main/res/xml/app_restrictions.xml** 135 | 136 | In this file you'll need to add **all** available managed configuration for the app ([see example](/example/android/app/src/main/res/xml/app_restrictions.xml)). For more information check out [Android's guide: Set up managed configurations]([https://developer.android.com/work/managed-configurations](https://developer.android.com/work/managed-configurations)) 137 | 138 | ```xml 139 | 140 | 141 | 147 | 148 | ``` 149 | **Note:** In a production app, `android:title` and `android:description` should be drawn from a localized resource file. 150 |
151 | 152 | ## Usage 153 | 154 | ```javascript 155 | // Load the module 156 | import Emm from '@mattermost/react-native-emm'; 157 | ``` 158 | 159 | ### Events 160 | * [addListener](#addlistener) 161 | 162 | ### Methods 163 | * [authenticate](#authenticate) 164 | * [deviceSecureWith](#devicesecurewith) 165 | * [enableBlurScreen](#enableblurscreen) 166 | * [exitApp](#exitapp) 167 | * [getManagedConfig](#getmanagedconfig) 168 | * [isDeviceSecured](#isdevicesecured) 169 | * [openSecuritySettings](#opensecuritysettings) 170 | * [setAppGroupId](#setappgroupid) 171 | 172 | ### Types 173 | * [AuthenticateConfig](/src/types/authenticate.ts) 174 | * [AuthenticationMethods](/src/types/authenticate.ts) 175 | * [ManagedConfigCallBack](/src/types/events.ts) 176 | 177 | #### addListener 178 | `addListener(callback: ManagedConfigCallBack): EmitterSubscription;` 179 | 180 | Event used to listen for Managed Configuration changes while the app is running. 181 | 182 | Example: 183 | ```js 184 | useEffect(() => { 185 | const listener = Emm.addListener((config: AuthenticateConfig) => { 186 | setManaged(config); 187 | }); 188 | 189 | return () => { 190 | listener.remove(); 191 | }; 192 | }); 193 | ``` 194 | 195 | **Note**: Don't forget to remove the listener when no longer needed to avoid memory leaks. 196 | 197 | #### authenticate 198 | `authenticate(opts: AuthenticateConfig): Promise` 199 | 200 | Request the user to authenticate using one of the device built-in authentication methods. You should call this after verifying that the [device is secure](#isdevicesecured) 201 | 202 | Example: 203 | ```js 204 | const opts: AuthenticateConfig = { 205 | reason: 'Some Reason', 206 | description: 'Some Description', 207 | fallback: true, 208 | supressEnterPassword: true, 209 | }; 210 | const authenticated = await Emm.authenticate(opts); 211 | ``` 212 | 213 | Platforms: All 214 | 215 | #### deviceSecureWith 216 | `deviceSecureWith(): Promise` 217 | 218 | Get available device authentication methods. 219 | 220 | Example: 221 | ```js 222 | const optionsAvailable: AuthenticationMethods = await Emm.deviceSecureWith() 223 | ``` 224 | 225 | Platforms: All 226 | 227 | #### enableBlurScreen 228 | `enableBlurScreen(enabled: boolean): void` 229 | 230 | iOS: Blurs the application screen in the App Switcher view 231 | Android: Blanks the application screen in the Task Manager 232 | 233 | Example: 234 | ``` 235 | Emm.enableBlurScreen(true); 236 | ``` 237 | 238 | Platforms: All 239 | 240 | #### exitApp 241 | `exitApp(): void` 242 | 243 | Forces the app to exit. 244 | 245 | Example: 246 | ``` 247 | Emm.exitApp(); 248 | ``` 249 | Platforms: All 250 | 251 | #### getManagedConfig 252 | `getManagedConfig(): Promise>` 253 | 254 | Retrieves the Managed Configuration set by the Enterprise Mobility Management provider. 255 | 256 | Notes: 257 | Android uses the Restriction Manager to set the managed configuration settings and values while iOS uses NSUserDefaults under the key `com.apple.configuration.managed` 258 | 259 | Example: 260 | ``` 261 | const manged: Record = Emm.getManagedConfig(); // Managed configuration object containing keys and values 262 | ``` 263 | 264 | Platforms: all 265 | 266 | ##### isDeviceSecured 267 | `isDeviceSecured(): Promise` 268 | 269 | Determines if the device has at least one authentication method enabled. 270 | 271 | Example: 272 | ``` 273 | const secured = await Emm.isDeviceSecured(); 274 | ``` 275 | Platforms: All 276 | 277 | ##### openSecuritySettings 278 | `openSecuritySettings(): void` 279 | 280 | If the device is not secured, you can use this function to take the user to the Device Security Settings to set up an authentication method. 281 | 282 | Example: 283 | ``` 284 | Emm.openSecuritySettings(); 285 | ``` 286 | 287 | **Note**: This function will close the running application. 288 | 289 | Platforms: Android 290 | 291 | ##### setAppGroupId 292 | `setAppGroupId(identifier: string): void` 293 | 294 | At times you may built an iOS extension application (ex: Share Extension / Notification Extension), if you need access to the Managed Configuration you should set this value to your [App Group Identifier]([https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_application-groups](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_application-groups)). This will create a copy of the managed configuration found in NSUserDefaults under the key `com.apple.configuration.managed` to a shared NSUserDefaults with your `App Group identifier` under the same key. 295 | 296 | Example: 297 | ``` 298 | Emm.setAppGroupId('group.com.example.myapp); 299 | ``` 300 | Platforms: iOS 301 | 302 | 303 | ## TODOS 304 | 305 | - [ ] Android: Use BiometricPrompt when available 306 | --- 307 | 308 | **MIT Licensed** 309 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.safeExtGet = {prop, fallback -> 3 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback 4 | } 5 | repositories { 6 | google() 7 | gradlePluginPortal() 8 | } 9 | def kotlin_version = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : "1.9.22" 10 | dependencies { 11 | classpath("com.android.tools.build:gradle:7.3.1") 12 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") 13 | } 14 | } 15 | 16 | def isNewArchitectureEnabled() { 17 | return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true" 18 | } 19 | 20 | apply plugin: 'com.android.library' 21 | apply plugin: 'org.jetbrains.kotlin.android' 22 | if (isNewArchitectureEnabled()) { 23 | apply plugin: 'com.facebook.react' 24 | } 25 | 26 | android { 27 | compileSdkVersion safeExtGet('compileSdkVersion', 34) 28 | namespace "com.mattermost.emm" 29 | 30 | defaultConfig { 31 | minSdkVersion safeExtGet('minSdkVersion', 24) 32 | targetSdkVersion safeExtGet('targetSdkVersion', 34) 33 | buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()) 34 | } 35 | 36 | sourceSets { 37 | main { 38 | if (isNewArchitectureEnabled()) { 39 | java.srcDirs += ['src/newarch'] 40 | } else { 41 | java.srcDirs += ['src/oldarch'] 42 | } 43 | } 44 | } 45 | } 46 | 47 | repositories { 48 | maven { 49 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 50 | url "$projectDir/../node_modules/react-native/android" 51 | } 52 | mavenCentral() 53 | google() 54 | } 55 | 56 | dependencies { 57 | implementation 'com.facebook.react:react-native' 58 | implementation "androidx.biometric:biometric:1.1.0" 59 | implementation 'com.github.Dimezis:BlurView:version-2.0.3' 60 | } 61 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/src/main/java/com/mattermost/emm/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.mattermost.emm 2 | 3 | const val E_ACTIVITY_DOES_NOT_EXIST: String = "E_ACTIVITY_DOES_NOT_EXIST" 4 | const val E_ONE_REQ_AT_A_TIME: String = "E_ONE_REQ_AT_A_TIME" 5 | 6 | const val FAILED: String = "AuthenticationFailed" 7 | const val REQUEST: Int = 18864 8 | const val CANCELLED: String = "UserCancel" 9 | const val ERROR: String = "AuthenticationError" 10 | -------------------------------------------------------------------------------- /android/src/main/java/com/mattermost/emm/EmmModuleImpl.kt: -------------------------------------------------------------------------------- 1 | package com.mattermost.emm 2 | 3 | import android.app.Activity 4 | import android.app.KeyguardManager 5 | import android.content.BroadcastReceiver 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.content.IntentFilter 9 | import android.content.RestrictionsManager 10 | import android.os.Build 11 | import android.os.Bundle 12 | import android.os.CancellationSignal 13 | import android.provider.Settings 14 | import android.util.Log 15 | import android.view.ViewGroup 16 | import android.view.WindowManager 17 | import androidx.biometric.BiometricManager 18 | import androidx.biometric.BiometricPrompt 19 | import androidx.collection.ArraySet 20 | import androidx.core.content.ContextCompat 21 | import androidx.fragment.app.FragmentActivity 22 | import com.facebook.react.bridge.Arguments 23 | import com.facebook.react.bridge.Promise 24 | import com.facebook.react.bridge.ReactApplicationContext 25 | import com.facebook.react.bridge.ReadableMap 26 | import com.facebook.react.bridge.WritableMap 27 | import eightbitlab.com.blurview.BlurView 28 | import eightbitlab.com.blurview.RenderEffectBlur 29 | import eightbitlab.com.blurview.RenderScriptBlur 30 | import java.util.concurrent.Executor 31 | import kotlin.system.exitProcess 32 | 33 | class EmmModuleImpl(reactApplicationContext: ReactApplicationContext) { 34 | private var blurEnabled: Boolean = false 35 | private var blurView: BlurView? = null 36 | 37 | 38 | private var managedConfig: Bundle? = null 39 | 40 | private val restrictionsFilter = IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED) 41 | private val keyguardManager: KeyguardManager = reactApplicationContext.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager 42 | private val context = reactApplicationContext 43 | 44 | companion object { 45 | const val NAME = "Emm" 46 | } 47 | 48 | fun handleReceiveBroadcast(intent: Intent) { 49 | val managed: Bundle = loadManagedConfig(true) 50 | 51 | Log.i("ReactNative", "Managed Configuration Changed") 52 | sendConfigChanged(managed) 53 | } 54 | 55 | fun handleActivityResult (authPromise: Promise?, activity: Activity?, requestCode: Int, resultCode: Int, data: Intent?) { 56 | if (requestCode != REQUEST || authPromise == null) { 57 | return 58 | } 59 | 60 | if (resultCode == Activity.RESULT_CANCELED) { 61 | authPromise.reject(CANCELLED, "User canceled") 62 | } else if (resultCode == Activity.RESULT_OK) { 63 | authPromise.resolve(true) 64 | } 65 | } 66 | 67 | fun handleHostResume(activity: Activity?, restrictionsReceiver: BroadcastReceiver) { 68 | val managed = loadManagedConfig(false) 69 | activity?.registerReceiver(restrictionsReceiver, restrictionsFilter) 70 | if (!equalBundles(managed, this.managedConfig)) { 71 | sendConfigChanged(managed) 72 | } 73 | handleBlurScreen(activity) 74 | } 75 | 76 | fun loadManagedConfig(global: Boolean): Bundle { 77 | synchronized(this) { 78 | val restrictionsManager: RestrictionsManager = context.getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager 79 | val managed = restrictionsManager.applicationRestrictions 80 | 81 | if (global) { 82 | this.managedConfig = managed 83 | } 84 | 85 | return managed 86 | } 87 | } 88 | 89 | fun authenticate(activity: Activity?, map: ReadableMap, promise: Promise?) { 90 | if (activity == null) { 91 | promise?.reject(ERROR, "Activity is null") 92 | return 93 | } 94 | 95 | if (activity !is FragmentActivity) { 96 | promise?.reject(ERROR, "Activity is not a FragmentActivity") 97 | return 98 | } 99 | 100 | val reason = map.getString("reason") ?: "Authenticate" 101 | val description = map.getString("description") ?: "Please authenticate to continue" 102 | val blurOnAuthenticate = map.getBoolean("blurOnAuthenticate") ?: false 103 | val maxRetries = 3 104 | var failedAttempts = 0 105 | 106 | val cancellationSignal = CancellationSignal() 107 | cancellationSignal.setOnCancelListener { 108 | Log.i("ReactNative", "Biometric prompt cancelled") 109 | promise?.reject(CANCELLED, "Biometric prompt cancelled") 110 | } 111 | 112 | try { 113 | val biometricManager = BiometricManager.from(activity.applicationContext) 114 | val canAuthenticate = biometricManager.canAuthenticate( 115 | BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL 116 | ) 117 | 118 | if (canAuthenticate != BiometricManager.BIOMETRIC_SUCCESS) { 119 | promise?.reject(ERROR, "Biometric authentication not available") 120 | return 121 | } 122 | 123 | val executor: Executor = ContextCompat.getMainExecutor(activity.applicationContext) 124 | 125 | if (blurOnAuthenticate) { 126 | applyBlurEffect(8.0) 127 | } 128 | val biometricPrompt = BiometricPrompt(activity, executor, object : BiometricPrompt.AuthenticationCallback() { 129 | override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { 130 | super.onAuthenticationError(errorCode, errString) 131 | when (errorCode) { 132 | BiometricPrompt.ERROR_LOCKOUT -> { 133 | Log.e("ReactNative", "Biometric authentication temporarily locked out") 134 | promise?.reject(ERROR, "Too many failed attempts. Please try again later.") 135 | } 136 | BiometricPrompt.ERROR_LOCKOUT_PERMANENT -> { 137 | Log.e("ReactNative", "Biometric authentication permanently locked") 138 | promise?.reject(ERROR, "Too many failed attempts. Use device credentials.") 139 | } 140 | else -> { 141 | promise?.reject(ERROR, "Error code [$errorCode]: $errString") 142 | } 143 | } 144 | } 145 | 146 | override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { 147 | super.onAuthenticationSucceeded(result) 148 | if (blurOnAuthenticate) { 149 | removeBlurEffect() 150 | } 151 | promise?.resolve(true) 152 | } 153 | 154 | override fun onAuthenticationFailed() { 155 | super.onAuthenticationFailed() 156 | failedAttempts++ 157 | if (failedAttempts >= maxRetries) { 158 | Log.w("ReactNative", "Max retries reached. Stopping authentication.") 159 | promise?.reject(FAILED, "Max biometric attempts reached. Please try again later.") 160 | cancellationSignal.cancel() // Cancel authentication prompt 161 | return 162 | } 163 | Log.w("ReactNative", "Biometric authentication failed ($failedAttempts/$maxRetries), retry allowed") 164 | } 165 | }) 166 | 167 | val promptInfo = BiometricPrompt.PromptInfo.Builder() 168 | .setTitle(reason) 169 | .setDescription(description) 170 | .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL) 171 | .build() 172 | 173 | activity.runOnUiThread { 174 | biometricPrompt.authenticate(promptInfo) 175 | } 176 | 177 | } catch (e: Exception) { 178 | promise?.reject("FAILED", e.localizedMessage ?: "Unknown error") 179 | throw e 180 | } 181 | } 182 | 183 | fun deviceSecureWith(promise: Promise?) { 184 | val map = Arguments.createMap() 185 | val biometricManager = BiometricManager.from(context) 186 | val canAuthenticate = biometricManager.canAuthenticate( 187 | BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL 188 | ) 189 | val hasBiometrics = canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS 190 | 191 | map.putBoolean("passcode", keyguardManager.isDeviceSecure) 192 | map.putBoolean("face", hasBiometrics) 193 | map.putBoolean("fingerprint", hasBiometrics) 194 | promise?.resolve(map) 195 | } 196 | 197 | fun setBlurScreen(activity: Activity?, enabled: Boolean) { 198 | this.blurEnabled = enabled 199 | handleBlurScreen(activity) 200 | } 201 | 202 | fun exitApp(activity: Activity?) { 203 | activity?.finish() 204 | exitProcess(0) 205 | } 206 | 207 | fun getManagedConfig(): WritableMap { 208 | return try { 209 | val managed = loadManagedConfig(false) 210 | Arguments.fromBundle(managed) 211 | } catch (e: Exception) { 212 | Arguments.createMap() 213 | } 214 | } 215 | 216 | fun openSecuritySettings(activity: Activity?) { 217 | val intent = Intent(Settings.ACTION_SECURITY_SETTINGS) 218 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK 219 | context.startActivity(intent) 220 | exitApp(activity) 221 | } 222 | 223 | private fun handleBlurScreen(activity: Activity?) { 224 | activity?.runOnUiThread { 225 | if (this.blurEnabled) { 226 | activity.window?.setFlags( 227 | WindowManager.LayoutParams.FLAG_SECURE, 228 | WindowManager.LayoutParams.FLAG_SECURE 229 | ) 230 | } else { 231 | activity.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) 232 | } 233 | } 234 | } 235 | 236 | private fun sendConfigChanged(config: Bundle?) { 237 | if (context.hasActiveReactInstance()) { 238 | var result = Arguments.createMap() 239 | if (config != null) { 240 | result = Arguments.fromBundle(config) 241 | } 242 | context.emitDeviceEvent("managedConfigChanged", result) 243 | } 244 | } 245 | 246 | private fun equalBundles(one: Bundle?, two: Bundle?): Boolean { 247 | if (one == null && two == null) { 248 | return true 249 | } 250 | 251 | if (one == null || two == null) return false 252 | if (one.size() != two.size()) return false 253 | 254 | var valueOne: Any? 255 | var valueTwo: Any? 256 | val setOne: MutableSet = ArraySet() 257 | 258 | setOne.addAll(one.keySet()) 259 | setOne.addAll(two.keySet()) 260 | 261 | for (key in setOne) { 262 | if (!one.containsKey(key) || !two.containsKey(key)) return false 263 | 264 | valueOne = one[key] 265 | valueTwo = two[key] 266 | if (valueOne is Bundle && valueTwo is Bundle && 267 | !equalBundles(valueOne as Bundle?, valueTwo as Bundle?)) { 268 | return false 269 | } else if (valueOne == null) { 270 | if (valueTwo != null) return false 271 | } else if (valueOne != valueTwo) return false 272 | } 273 | return true 274 | } 275 | 276 | fun applyBlurEffect(radius: Double) { 277 | val activity: Activity? = context.currentActivity 278 | activity?.runOnUiThread { 279 | val decorView = activity.window.decorView as ViewGroup 280 | val rootView = activity.findViewById(android.R.id.content) 281 | 282 | if (blurView == null) { 283 | val blurAlgorithm = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 284 | RenderEffectBlur() 285 | } else { 286 | RenderScriptBlur(context) 287 | } 288 | blurView = BlurView(activity).apply { 289 | layoutParams = ViewGroup.LayoutParams( 290 | ViewGroup.LayoutParams.MATCH_PARENT, 291 | ViewGroup.LayoutParams.MATCH_PARENT 292 | ) 293 | 294 | setupWith(rootView, blurAlgorithm) 295 | .setFrameClearDrawable(decorView.background) 296 | .setBlurRadius(radius.toFloat()) 297 | .setBlurAutoUpdate(true) 298 | } 299 | 300 | rootView.addView(blurView) 301 | } 302 | } 303 | } 304 | 305 | fun removeBlurEffect() { 306 | val activity: Activity? = context.currentActivity 307 | activity?.runOnUiThread { 308 | val rootView = activity.findViewById(android.R.id.content) 309 | blurView?.let { 310 | rootView.removeView(it) 311 | blurView = null 312 | } 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /android/src/main/java/com/mattermost/emm/EmmPackage.kt: -------------------------------------------------------------------------------- 1 | package com.mattermost.emm 2 | 3 | 4 | import com.facebook.react.TurboReactPackage 5 | import com.facebook.react.bridge.NativeModule 6 | import com.facebook.react.bridge.ReactApplicationContext 7 | import com.facebook.react.module.model.ReactModuleInfo 8 | import com.facebook.react.module.model.ReactModuleInfoProvider 9 | 10 | class EmmPackage : TurboReactPackage() { 11 | override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = 12 | if (name == EmmModuleImpl.NAME) { 13 | EmmModule(reactContext) 14 | } else { 15 | null 16 | } 17 | 18 | override fun getReactModuleInfoProvider() = ReactModuleInfoProvider { 19 | mapOf( 20 | EmmModuleImpl.NAME to ReactModuleInfo( 21 | EmmModuleImpl.NAME, 22 | EmmModuleImpl.NAME, 23 | false, 24 | false, 25 | true, 26 | false, 27 | BuildConfig.IS_NEW_ARCHITECTURE_ENABLED 28 | ) 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /android/src/newarch/java/com/EmmModule.kt: -------------------------------------------------------------------------------- 1 | package com.mattermost.emm 2 | 3 | import android.app.Activity 4 | import android.content.BroadcastReceiver 5 | import android.content.Context 6 | import android.content.Intent 7 | import com.facebook.react.bridge.BaseActivityEventListener 8 | import com.facebook.react.bridge.LifecycleEventListener 9 | import com.facebook.react.bridge.Promise 10 | import com.facebook.react.bridge.ReactApplicationContext 11 | import com.facebook.react.bridge.ReadableMap 12 | import com.facebook.react.bridge.WritableMap 13 | 14 | class EmmModule(reactContext: ReactApplicationContext) : NativeEmmSpec(reactContext), LifecycleEventListener { 15 | private var implementation: EmmModuleImpl = EmmModuleImpl(reactContext) 16 | private var authPromise: Promise? = null 17 | 18 | private val restrictionsReceiver: BroadcastReceiver = object : BroadcastReceiver() { 19 | override fun onReceive(ctx: Context, intent: Intent) { 20 | implementation.handleReceiveBroadcast(intent) 21 | } 22 | } 23 | 24 | private val mActivityEventListener = object : BaseActivityEventListener() { 25 | override fun onActivityResult(activity: Activity?, requestCode: Int, resultCode: Int, data: Intent?) { 26 | implementation.handleActivityResult(authPromise, activity, requestCode, resultCode, data) 27 | authPromise = null 28 | } 29 | } 30 | 31 | init { 32 | reactContext.addLifecycleEventListener(this) 33 | reactContext.addActivityEventListener(mActivityEventListener) 34 | implementation.loadManagedConfig(true) 35 | } 36 | 37 | override fun getName(): String = EmmModuleImpl.NAME 38 | 39 | override fun onHostResume() { 40 | implementation.handleHostResume(currentActivity, restrictionsReceiver) 41 | } 42 | 43 | override fun onHostPause() { 44 | try { 45 | currentActivity?.unregisterReceiver(restrictionsReceiver) 46 | } catch (e: IllegalArgumentException) { 47 | // Just ignore this cause the receiver wasn't registered for this activity 48 | } 49 | } 50 | 51 | override fun onHostDestroy() {} 52 | 53 | override fun authenticate(options: ReadableMap?, promise: Promise?) { 54 | if (authPromise != null) { 55 | promise?.reject(E_ONE_REQ_AT_A_TIME, "One auth request at a time") 56 | return 57 | } 58 | 59 | if (currentActivity == null) { 60 | promise?.reject(E_ACTIVITY_DOES_NOT_EXIST, "Activity does not exist") 61 | return 62 | } 63 | 64 | if (options == null) { 65 | promise?.reject(ERROR, "No options provided") 66 | return 67 | } 68 | 69 | authPromise = promise 70 | implementation.authenticate(currentActivity, options, promise) 71 | authPromise = null 72 | } 73 | 74 | override fun deviceSecureWith(promise: Promise?) { 75 | implementation.deviceSecureWith(promise) 76 | } 77 | 78 | override fun setBlurScreen(enabled: Boolean) { 79 | implementation.setBlurScreen(currentActivity, enabled) 80 | } 81 | 82 | override fun exitApp() { 83 | implementation.exitApp(currentActivity) 84 | } 85 | 86 | override fun getManagedConfig(): WritableMap = implementation.getManagedConfig() 87 | 88 | override fun openSecuritySettings() { 89 | implementation.openSecuritySettings(currentActivity) 90 | } 91 | 92 | override fun setAppGroupId(identifier: String?) { 93 | TODO("Not yet implemented") 94 | } 95 | 96 | override fun addListener(eventType: String?) { 97 | // Keep: Required for RN built in Event Emitter Calls 98 | } 99 | 100 | override fun removeListeners(count: Double) { 101 | // Keep: Required for RN built in Event Emitter Calls 102 | } 103 | 104 | override fun applyBlurEffect(radius: Double) { 105 | implementation.applyBlurEffect(radius) 106 | } 107 | 108 | override fun removeBlurEffect() { 109 | implementation.removeBlurEffect() 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /android/src/oldarch/java/com/EmmModule.kt: -------------------------------------------------------------------------------- 1 | package com.mattermost.emm 2 | 3 | import android.app.Activity 4 | import android.content.BroadcastReceiver 5 | import android.content.Context 6 | import android.content.Intent 7 | import com.facebook.react.bridge.BaseActivityEventListener 8 | import com.facebook.react.bridge.LifecycleEventListener 9 | import com.facebook.react.bridge.Promise 10 | import com.facebook.react.bridge.ReactApplicationContext 11 | import com.facebook.react.bridge.ReactContextBaseJavaModule 12 | import com.facebook.react.bridge.ReactMethod 13 | import com.facebook.react.bridge.ReadableMap 14 | import com.facebook.react.bridge.WritableMap 15 | 16 | class EmmModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext), LifecycleEventListener { 17 | private var implementation: EmmModuleImpl = EmmModuleImpl(reactContext) 18 | private var authPromise: Promise? = null 19 | 20 | private val restrictionsReceiver: BroadcastReceiver = object : BroadcastReceiver() { 21 | override fun onReceive(ctx: Context, intent: Intent) { 22 | implementation.handleReceiveBroadcast(intent) 23 | } 24 | } 25 | 26 | private val mActivityEventListener = object : BaseActivityEventListener() { 27 | override fun onActivityResult(activity: Activity?, requestCode: Int, resultCode: Int, data: Intent?) { 28 | implementation.handleActivityResult(authPromise, activity, requestCode, resultCode, data) 29 | authPromise = null 30 | } 31 | } 32 | 33 | init { 34 | reactContext.addLifecycleEventListener(this) 35 | reactContext.addActivityEventListener(mActivityEventListener) 36 | implementation.loadManagedConfig(true) 37 | } 38 | 39 | override fun getName(): String = EmmModuleImpl.NAME 40 | 41 | override fun onHostResume() { 42 | implementation.handleHostResume(currentActivity, restrictionsReceiver) 43 | } 44 | 45 | override fun onHostPause() { 46 | try { 47 | currentActivity?.unregisterReceiver(restrictionsReceiver) 48 | } catch (e: IllegalArgumentException) { 49 | // Just ignore this cause the receiver wasn't registered for this activity 50 | } 51 | } 52 | 53 | override fun onHostDestroy() {} 54 | 55 | @ReactMethod 56 | fun authenticate(map: ReadableMap, promise: Promise) { 57 | if (authPromise != null) { 58 | promise.reject(E_ONE_REQ_AT_A_TIME, "One auth request at a time") 59 | return 60 | } 61 | 62 | if (currentActivity == null) { 63 | promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Activity does not exist") 64 | return 65 | } 66 | 67 | authPromise = promise 68 | implementation.authenticate(currentActivity, map, promise) 69 | authPromise = null 70 | } 71 | 72 | @ReactMethod 73 | fun deviceSecureWith(promise: Promise) { 74 | implementation.deviceSecureWith(promise) 75 | } 76 | 77 | @ReactMethod 78 | fun setBlurScreen(enabled: Boolean) { 79 | implementation.setBlurScreen(currentActivity, enabled) 80 | } 81 | 82 | @ReactMethod 83 | fun exitApp() { 84 | implementation.exitApp(currentActivity) 85 | } 86 | 87 | @ReactMethod(isBlockingSynchronousMethod = true) 88 | fun getManagedConfig(): WritableMap = implementation.getManagedConfig() 89 | 90 | @ReactMethod 91 | fun openSecuritySettings() { 92 | implementation.openSecuritySettings(currentActivity) 93 | } 94 | 95 | @ReactMethod 96 | fun setAppGroupId(identifier: String) { 97 | // Not implemented 98 | } 99 | 100 | @ReactMethod 101 | fun addListener(eventName: String) { 102 | // Keep: Required for RN built in Event Emitter Calls 103 | } 104 | 105 | @ReactMethod 106 | fun removeListeners(count: Int) { 107 | // Keep: Required for RN built in Event Emitter Calls 108 | } 109 | 110 | @ReactMethod 111 | fun applyBlurEffect(radius: Double) { 112 | implementation.applyBlurEffect(radius) 113 | } 114 | 115 | @ReactMethod 116 | fun removeBlurEffect() { 117 | implementation.removeBlurEffect() 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:@react-native/babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 4 | ruby ">= 3.2.0" 5 | 6 | # Cocoapods 1.15 introduced a bug which break the build. We will remove the upper 7 | # bound in the template on Cocoapods with next React Native release. 8 | gem 'cocoapods', '1.16.1' -------------------------------------------------------------------------------- /example/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.6) 5 | rexml 6 | activesupport (7.0.8) 7 | concurrent-ruby (~> 1.0, >= 1.0.2) 8 | i18n (>= 1.6, < 2) 9 | minitest (>= 5.1) 10 | tzinfo (~> 2.0) 11 | addressable (2.8.5) 12 | public_suffix (>= 2.0.2, < 6.0) 13 | algoliasearch (1.27.5) 14 | httpclient (~> 2.8, >= 2.8.3) 15 | json (>= 1.5.1) 16 | atomos (0.1.3) 17 | claide (1.1.0) 18 | cocoapods (1.14.3) 19 | addressable (~> 2.8) 20 | claide (>= 1.0.2, < 2.0) 21 | cocoapods-core (= 1.14.3) 22 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 23 | cocoapods-downloader (>= 2.1, < 3.0) 24 | cocoapods-plugins (>= 1.0.0, < 2.0) 25 | cocoapods-search (>= 1.0.0, < 2.0) 26 | cocoapods-trunk (>= 1.6.0, < 2.0) 27 | cocoapods-try (>= 1.1.0, < 2.0) 28 | colored2 (~> 3.1) 29 | escape (~> 0.0.4) 30 | fourflusher (>= 2.3.0, < 3.0) 31 | gh_inspector (~> 1.0) 32 | molinillo (~> 0.8.0) 33 | nap (~> 1.0) 34 | ruby-macho (>= 2.3.0, < 3.0) 35 | xcodeproj (>= 1.23.0, < 2.0) 36 | cocoapods-core (1.14.3) 37 | activesupport (>= 5.0, < 8) 38 | addressable (~> 2.8) 39 | algoliasearch (~> 1.0) 40 | concurrent-ruby (~> 1.1) 41 | fuzzy_match (~> 2.0.4) 42 | nap (~> 1.0) 43 | netrc (~> 0.11) 44 | public_suffix (~> 4.0) 45 | typhoeus (~> 1.0) 46 | cocoapods-deintegrate (1.0.5) 47 | cocoapods-downloader (2.1) 48 | cocoapods-plugins (1.0.0) 49 | nap 50 | cocoapods-search (1.0.1) 51 | cocoapods-trunk (1.6.0) 52 | nap (>= 0.8, < 2.0) 53 | netrc (~> 0.11) 54 | cocoapods-try (1.2.0) 55 | colored2 (3.1.2) 56 | concurrent-ruby (1.2.2) 57 | escape (0.0.4) 58 | ethon (0.16.0) 59 | ffi (>= 1.15.0) 60 | ffi (1.16.3) 61 | fourflusher (2.3.1) 62 | fuzzy_match (2.0.4) 63 | gh_inspector (1.1.3) 64 | httpclient (2.8.3) 65 | i18n (1.14.1) 66 | concurrent-ruby (~> 1.0) 67 | json (2.6.3) 68 | minitest (5.20.0) 69 | molinillo (0.8.0) 70 | nanaimo (0.3.0) 71 | nap (1.1.0) 72 | netrc (0.11.0) 73 | public_suffix (4.0.7) 74 | rexml (3.2.6) 75 | ruby-macho (2.5.1) 76 | typhoeus (1.4.1) 77 | ethon (>= 0.9.0) 78 | tzinfo (2.0.6) 79 | concurrent-ruby (~> 1.0) 80 | xcodeproj (1.23.0) 81 | CFPropertyList (>= 2.3.3, < 4.0) 82 | atomos (~> 0.1.3) 83 | claide (>= 1.0.2, < 2.0) 84 | colored2 (~> 3.1) 85 | nanaimo (~> 0.3.0) 86 | rexml (~> 3.2.4) 87 | 88 | PLATFORMS 89 | ruby 90 | 91 | DEPENDENCIES 92 | activesupport (>= 6.1.7.5, < 7.1.0) 93 | cocoapods (>= 1.13, < 1.15) 94 | 95 | RUBY VERSION 96 | ruby 2.7.8p225 97 | 98 | BUNDLED WITH 99 | 2.3.26 100 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | apply plugin: "org.jetbrains.kotlin.android" 3 | apply plugin: "com.facebook.react" 4 | 5 | /** 6 | * This is the configuration block to customize your React Native Android app. 7 | * By default you don't need to apply any configuration, just uncomment the lines you need. 8 | */ 9 | react { 10 | /* Folders */ 11 | // The root of your project, i.e. where "package.json" lives. Default is '../..' 12 | // root = file("../../") 13 | // The folder where the react-native NPM package is. Default is ../../node_modules/react-native 14 | // reactNativeDir = file("../../node_modules/react-native") 15 | // The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen 16 | // codegenDir = file("../../node_modules/@react-native/codegen") 17 | // The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js 18 | // cliFile = file("../../node_modules/react-native/cli.js") 19 | 20 | /* Variants */ 21 | // The list of variants to that are debuggable. For those we're going to 22 | // skip the bundling of the JS bundle and the assets. By default is just 'debug'. 23 | // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. 24 | // debuggableVariants = ["liteDebug", "prodDebug"] 25 | 26 | /* Bundling */ 27 | // A list containing the node command and its flags. Default is just 'node'. 28 | // nodeExecutableAndArgs = ["node"] 29 | // 30 | // The command to run when bundling. By default is 'bundle' 31 | // bundleCommand = "ram-bundle" 32 | // 33 | // The path to the CLI configuration file. Default is empty. 34 | // bundleConfig = file(../rn-cli.config.js) 35 | // 36 | // The name of the generated asset file containing your JS bundle 37 | // bundleAssetName = "MyApplication.android.bundle" 38 | // 39 | // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' 40 | entryFile = file("../../index.tsx") 41 | // 42 | // A list of extra flags to pass to the 'bundle' commands. 43 | // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle 44 | // extraPackagerArgs = [] 45 | 46 | /* Hermes Commands */ 47 | // The hermes compiler command to run. By default it is 'hermesc' 48 | // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc" 49 | // 50 | // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" 51 | // hermesFlags = ["-O", "-output-source-map"] 52 | 53 | /* Autolinking */ 54 | autolinkLibrariesWithApp() 55 | } 56 | 57 | /** 58 | * Set this to true to Run Proguard on Release builds to minify the Java bytecode. 59 | */ 60 | def enableProguardInReleaseBuilds = false 61 | 62 | /** 63 | * The preferred build flavor of JavaScriptCore (JSC) 64 | * 65 | * For example, to use the international variant, you can use: 66 | * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` 67 | * 68 | * The international variant includes ICU i18n library and necessary data 69 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that 70 | * give correct results when using with locales other than en-US. Note that 71 | * this variant is about 6MiB larger per architecture than default. 72 | */ 73 | def jscFlavor = 'org.webkit:android-jsc:+' 74 | 75 | android { 76 | ndkVersion rootProject.ext.ndkVersion 77 | 78 | buildToolsVersion rootProject.ext.buildToolsVersion 79 | compileSdkVersion rootProject.ext.compileSdkVersion 80 | 81 | namespace "com.example.mattermostreactnativeemm" 82 | defaultConfig { 83 | applicationId "com.example.mattermostreactnativeemm" 84 | minSdkVersion rootProject.ext.minSdkVersion 85 | targetSdkVersion rootProject.ext.targetSdkVersion 86 | versionCode 1 87 | versionName "1.0" 88 | } 89 | 90 | signingConfigs { 91 | debug { 92 | storeFile file('debug.keystore') 93 | storePassword 'android' 94 | keyAlias 'androiddebugkey' 95 | keyPassword 'android' 96 | } 97 | } 98 | buildTypes { 99 | debug { 100 | signingConfig signingConfigs.debug 101 | } 102 | release { 103 | // Caution! In production, you need to generate your own keystore file. 104 | // see https://reactnative.dev/docs/signed-apk-android. 105 | signingConfig signingConfigs.debug 106 | minifyEnabled enableProguardInReleaseBuilds 107 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 108 | } 109 | } 110 | } 111 | 112 | dependencies { 113 | // The version of react-native is set by the React Native Gradle Plugin 114 | implementation("com.facebook.react:react-android") 115 | 116 | if (hermesEnabled.toBoolean()) { 117 | implementation("com.facebook.react:hermes-android") 118 | } else { 119 | implementation jscFlavor 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /example/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattermost/react-native-emm/f6e57ff80789e36a3bc534f3aed65bbe9b9a4989/example/android/app/debug.keystore -------------------------------------------------------------------------------- /example/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 14 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/example/mattermostreactnativeemm/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.mattermostreactnativeemm 2 | 3 | import com.facebook.react.ReactActivity 4 | import com.facebook.react.ReactActivityDelegate 5 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled 6 | import com.facebook.react.defaults.DefaultReactActivityDelegate 7 | 8 | class MainActivity : ReactActivity() { 9 | 10 | /** 11 | * Returns the name of the main component registered from JavaScript. This is used to schedule 12 | * rendering of the component. 13 | */ 14 | override fun getMainComponentName(): String = "ReactNativeEmmExample" 15 | 16 | /** 17 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] 18 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] 19 | */ 20 | override fun createReactActivityDelegate(): ReactActivityDelegate = 21 | DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) 22 | } 23 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/example/mattermostreactnativeemm/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package com.example.mattermostreactnativeemm 2 | 3 | import android.app.Application 4 | import com.facebook.react.PackageList 5 | import com.facebook.react.ReactApplication 6 | import com.facebook.react.ReactHost 7 | import com.facebook.react.ReactNativeHost 8 | import com.facebook.react.ReactPackage 9 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load 10 | import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost 11 | import com.facebook.react.defaults.DefaultReactNativeHost 12 | import com.facebook.react.soloader.OpenSourceMergedSoMapping 13 | import com.facebook.soloader.SoLoader 14 | 15 | class MainApplication : Application(), ReactApplication { 16 | 17 | override val reactNativeHost: ReactNativeHost = 18 | object : DefaultReactNativeHost(this) { 19 | override fun getPackages(): List = 20 | PackageList(this).packages.apply { 21 | // Packages that cannot be autolinked yet can be added manually here, for example: 22 | // add(MyReactNativePackage()) 23 | } 24 | 25 | override fun getJSMainModuleName(): String = "index" 26 | 27 | override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG 28 | 29 | override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED 30 | override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED 31 | } 32 | 33 | override val reactHost: ReactHost 34 | get() = getDefaultReactHost(applicationContext, reactNativeHost) 35 | 36 | override fun onCreate() { 37 | super.onCreate() 38 | SoLoader.init(this, OpenSourceMergedSoMapping) 39 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 40 | // If you opted-in for the New Architecture, we load the native entry point for this app. 41 | load() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 21 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattermost/react-native-emm/f6e57ff80789e36a3bc534f3aed65bbe9b9a4989/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattermost/react-native-emm/f6e57ff80789e36a3bc534f3aed65bbe9b9a4989/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattermost/react-native-emm/f6e57ff80789e36a3bc534f3aed65bbe9b9a4989/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattermost/react-native-emm/f6e57ff80789e36a3bc534f3aed65bbe9b9a4989/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattermost/react-native-emm/f6e57ff80789e36a3bc534f3aed65bbe9b9a4989/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattermost/react-native-emm/f6e57ff80789e36a3bc534f3aed65bbe9b9a4989/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattermost/react-native-emm/f6e57ff80789e36a3bc534f3aed65bbe9b9a4989/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattermost/react-native-emm/f6e57ff80789e36a3bc534f3aed65bbe9b9a4989/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattermost/react-native-emm/f6e57ff80789e36a3bc534f3aed65bbe9b9a4989/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattermost/react-native-emm/f6e57ff80789e36a3bc534f3aed65bbe9b9a4989/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ReactNativeEmm Example 3 | in-App Pincode 4 | Require users to authenticate as the owner of the phone before using the app. Prompts for fingerprint or passcode when the app first opens and when the app has been in the background for more than 5 minutes. 5 | Blur Application Screen 6 | Blur the app when it’s set to background to protect any confidential on-screen information, it also prevents taking screenshots of the app. 7 | Jailbreak / Root Detection 8 | Disable app launch on Jailbroken or rooted devices. 9 | Copy&Paste Protection 10 | Disable the ability to copy from or paste into any text inputs in the app. 11 | Mattermost Server URL 12 | Set a default Mattermost server URL. 13 | Allow Other Servers 14 | Allow the user to change the above server URL. 15 | Default Username 16 | Set the username or email address to use to authenticate against the Mattermost Server. 17 | EMM Vendor or Company Name 18 | Name of the EMM vendor or company deploying the app. Used in help text when prompting for passcodes so users are aware why the app is being protected. 19 | 20 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/xml/app_restrictions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 16 | 22 | 28 | 34 | 40 | 46 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "35.0.0" 6 | minSdkVersion = 24 7 | compileSdkVersion = 35 8 | targetSdkVersion = 35 9 | ndkVersion = "26.1.10909125" 10 | kotlinVersion = "1.9.24" 11 | } 12 | repositories { 13 | google() 14 | mavenCentral() 15 | } 16 | dependencies { 17 | classpath("com.android.tools.build:gradle") 18 | classpath("com.facebook.react:react-native-gradle-plugin") 19 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") 20 | } 21 | } 22 | 23 | apply plugin: "com.facebook.react.rootproject" -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | 25 | # Use this property to specify which architecture you want to build. 26 | # You can also override it from the CLI using 27 | # ./gradlew -PreactNativeArchitectures=x86_64 28 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 29 | 30 | # Use this property to enable support to the new architecture. 31 | # This will allow you to use TurboModules and the Fabric render in 32 | # your application. You should enable this flag either if you want 33 | # to write custom TurboModules/Fabric components OR use libraries that 34 | # are providing them. 35 | newArchEnabled=true 36 | 37 | # Use this property to enable or disable the Hermes JS engine. 38 | # If set to false, you will be using JSC instead. 39 | hermesEnabled=true -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattermost/react-native-emm/f6e57ff80789e36a3bc534f3aed65bbe9b9a4989/example/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /example/android/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /example/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") } 2 | plugins { id("com.facebook.react.settings") } 3 | extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() } 4 | rootProject.name = 'ReactNativeEmmExample' 5 | include ':app' 6 | includeBuild('../node_modules/@react-native/gradle-plugin') 7 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ReactNativeEmmExample", 3 | "displayName": "ReactNativeEmm Example" 4 | } 5 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pak = require('../package.json'); 3 | 4 | module.exports = { 5 | presets: ['module:@react-native/babel-preset'], 6 | plugins: [ 7 | [ 8 | 'module-resolver', 9 | { 10 | alias: { 11 | [pak.name]: path.join(__dirname, '..', pak.source), 12 | }, 13 | }, 14 | ], 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import { AppRegistry } from 'react-native'; 2 | import App from './src/App'; 3 | import { name as appName } from './app.json'; 4 | 5 | AppRegistry.registerComponent(appName, () => App); 6 | -------------------------------------------------------------------------------- /example/ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | # NODE_BINARY variable contains the PATH to the node executable. 6 | # 7 | # Customize the NODE_BINARY variable here. 8 | # For example, to use nvm with brew, add the following line 9 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 10 | export NODE_BINARY=$(command -v node) 11 | 12 | export ENTRY_FILE="index.tsx" -------------------------------------------------------------------------------- /example/ios/File.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // ReactNativeEmmExample 4 | // 5 | 6 | import Foundation 7 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Resolve react_native_pods.rb with node to allow for hoisting 2 | require Pod::Executable.execute_command('node', ['-p', 3 | 'require.resolve( 4 | "react-native/scripts/react_native_pods.rb", 5 | {paths: [process.argv[1]]}, 6 | )', __dir__]).strip 7 | 8 | platform :ios, min_ios_version_supported 9 | prepare_react_native_project! 10 | 11 | linkage = ENV['USE_FRAMEWORKS'] 12 | if linkage != nil 13 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green 14 | use_frameworks! :linkage => linkage.to_sym 15 | end 16 | 17 | target 'ReactNativeEmmExample' do 18 | config = use_native_modules! 19 | 20 | use_react_native!( 21 | :path => config[:reactNativePath], 22 | 23 | # An absolute path to your application root. 24 | :app_path => "#{Pod::Config.instance.installation_root}/.." 25 | ) 26 | 27 | post_install do |installer| 28 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 29 | react_native_post_install( 30 | installer, 31 | config[:reactNativePath], 32 | :mac_catalyst_enabled => false 33 | ) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /example/ios/ReactNativeEmmExample-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | -------------------------------------------------------------------------------- /example/ios/ReactNativeEmmExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0D1336C0461A88D01186E375 /* libPods-ReactNativeEmmExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BCEA90A70F4BEAD7E9FA28B2 /* libPods-ReactNativeEmmExample.a */; }; 11 | 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; 12 | 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13 | 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 14 | 20F357B024636CDF00C146DC /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20F357AF24636CDF00C146DC /* File.swift */; }; 15 | 7F013AE524F0221800F369A3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7F013AE424F0221800F369A3 /* LaunchScreen.storyboard */; }; 16 | A3A097AC789861A4D3F09235 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 7034B1557B1E5AF675BF6665 /* PrivacyInfo.xcprivacy */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | 13B07F961A680F5B00A75B9A /* ReactNativeEmmExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReactNativeEmmExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = ReactNativeEmmExample/AppDelegate.h; sourceTree = ""; }; 22 | 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = ReactNativeEmmExample/AppDelegate.mm; sourceTree = ""; }; 23 | 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = ReactNativeEmmExample/Images.xcassets; sourceTree = ""; }; 24 | 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ReactNativeEmmExample/Info.plist; sourceTree = ""; }; 25 | 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = ReactNativeEmmExample/main.m; sourceTree = ""; }; 26 | 20F357AD24636CDE00C146DC /* ReactNativeEmmExample-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactNativeEmmExample-Bridging-Header.h"; sourceTree = ""; }; 27 | 20F357AE24636CDF00C146DC /* ReactNativeEmmExample-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ReactNativeEmmExample-Bridging-Header.h"; sourceTree = ""; }; 28 | 20F357AF24636CDF00C146DC /* File.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = ""; }; 29 | 4D7192F03A36A017E887435B /* Pods-ReactNativeEmmExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeEmmExample.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeEmmExample/Pods-ReactNativeEmmExample.release.xcconfig"; sourceTree = ""; }; 30 | 7034B1557B1E5AF675BF6665 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = ReactNativeEmmExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; 31 | 7F013AE424F0221800F369A3 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = ReactNativeEmmExample/LaunchScreen.storyboard; sourceTree = ""; }; 32 | 871719007ECC5EAD276C345C /* Pods-ReactNativeEmmExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeEmmExample.debug.xcconfig"; path = "Target Support Files/Pods-ReactNativeEmmExample/Pods-ReactNativeEmmExample.debug.xcconfig"; sourceTree = ""; }; 33 | BCEA90A70F4BEAD7E9FA28B2 /* libPods-ReactNativeEmmExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReactNativeEmmExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; 35 | /* End PBXFileReference section */ 36 | 37 | /* Begin PBXFrameworksBuildPhase section */ 38 | 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { 39 | isa = PBXFrameworksBuildPhase; 40 | buildActionMask = 2147483647; 41 | files = ( 42 | 0D1336C0461A88D01186E375 /* libPods-ReactNativeEmmExample.a in Frameworks */, 43 | ); 44 | runOnlyForDeploymentPostprocessing = 0; 45 | }; 46 | /* End PBXFrameworksBuildPhase section */ 47 | 48 | /* Begin PBXGroup section */ 49 | 13B07FAE1A68108700A75B9A /* ReactNativeEmmExample */ = { 50 | isa = PBXGroup; 51 | children = ( 52 | 7F013AE424F0221800F369A3 /* LaunchScreen.storyboard */, 53 | 13B07FAF1A68108700A75B9A /* AppDelegate.h */, 54 | 13B07FB01A68108700A75B9A /* AppDelegate.mm */, 55 | 13B07FB51A68108700A75B9A /* Images.xcassets */, 56 | 13B07FB61A68108700A75B9A /* Info.plist */, 57 | 13B07FB71A68108700A75B9A /* main.m */, 58 | 7034B1557B1E5AF675BF6665 /* PrivacyInfo.xcprivacy */, 59 | ); 60 | name = ReactNativeEmmExample; 61 | sourceTree = ""; 62 | }; 63 | 1CFFDEF7170271C97B8B7E5A /* Pods */ = { 64 | isa = PBXGroup; 65 | children = ( 66 | 871719007ECC5EAD276C345C /* Pods-ReactNativeEmmExample.debug.xcconfig */, 67 | 4D7192F03A36A017E887435B /* Pods-ReactNativeEmmExample.release.xcconfig */, 68 | ); 69 | path = Pods; 70 | sourceTree = ""; 71 | }; 72 | 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | ED297162215061F000B7C4FE /* JavaScriptCore.framework */, 76 | BCEA90A70F4BEAD7E9FA28B2 /* libPods-ReactNativeEmmExample.a */, 77 | ); 78 | name = Frameworks; 79 | sourceTree = ""; 80 | }; 81 | 832341AE1AAA6A7D00B99B32 /* Libraries */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | ); 85 | name = Libraries; 86 | sourceTree = ""; 87 | }; 88 | 83CBB9F61A601CBA00E9B192 = { 89 | isa = PBXGroup; 90 | children = ( 91 | 20F357AF24636CDF00C146DC /* File.swift */, 92 | 20F357AE24636CDF00C146DC /* ReactNativeEmmExample-Bridging-Header.h */, 93 | 13B07FAE1A68108700A75B9A /* ReactNativeEmmExample */, 94 | 832341AE1AAA6A7D00B99B32 /* Libraries */, 95 | 83CBBA001A601CBA00E9B192 /* Products */, 96 | 2D16E6871FA4F8E400B85C8A /* Frameworks */, 97 | 1CFFDEF7170271C97B8B7E5A /* Pods */, 98 | 20F357AD24636CDE00C146DC /* ReactNativeEmmExample-Bridging-Header.h */, 99 | ); 100 | indentWidth = 2; 101 | sourceTree = ""; 102 | tabWidth = 2; 103 | usesTabs = 0; 104 | }; 105 | 83CBBA001A601CBA00E9B192 /* Products */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 13B07F961A680F5B00A75B9A /* ReactNativeEmmExample.app */, 109 | ); 110 | name = Products; 111 | sourceTree = ""; 112 | }; 113 | /* End PBXGroup section */ 114 | 115 | /* Begin PBXNativeTarget section */ 116 | 13B07F861A680F5B00A75B9A /* ReactNativeEmmExample */ = { 117 | isa = PBXNativeTarget; 118 | buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "ReactNativeEmmExample" */; 119 | buildPhases = ( 120 | CCCC07BCAFDEF1FCADC0D0C9 /* [CP] Check Pods Manifest.lock */, 121 | 13B07F871A680F5B00A75B9A /* Sources */, 122 | 13B07F8C1A680F5B00A75B9A /* Frameworks */, 123 | 13B07F8E1A680F5B00A75B9A /* Resources */, 124 | 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 125 | 46DCFEE06274DE150C44951F /* [CP] Copy Pods Resources */, 126 | 91BB27379B4B94C65D976421 /* [CP] Embed Pods Frameworks */, 127 | ); 128 | buildRules = ( 129 | ); 130 | dependencies = ( 131 | ); 132 | name = ReactNativeEmmExample; 133 | productName = ReactNativeEmmExample; 134 | productReference = 13B07F961A680F5B00A75B9A /* ReactNativeEmmExample.app */; 135 | productType = "com.apple.product-type.application"; 136 | }; 137 | /* End PBXNativeTarget section */ 138 | 139 | /* Begin PBXProject section */ 140 | 83CBB9F71A601CBA00E9B192 /* Project object */ = { 141 | isa = PBXProject; 142 | attributes = { 143 | LastUpgradeCheck = 0940; 144 | ORGANIZATIONNAME = Facebook; 145 | TargetAttributes = { 146 | 13B07F861A680F5B00A75B9A = { 147 | LastSwiftMigration = 1110; 148 | }; 149 | }; 150 | }; 151 | buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "ReactNativeEmmExample" */; 152 | compatibilityVersion = "Xcode 3.2"; 153 | developmentRegion = English; 154 | hasScannedForEncodings = 0; 155 | knownRegions = ( 156 | English, 157 | en, 158 | Base, 159 | ); 160 | mainGroup = 83CBB9F61A601CBA00E9B192; 161 | productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; 162 | projectDirPath = ""; 163 | projectRoot = ""; 164 | targets = ( 165 | 13B07F861A680F5B00A75B9A /* ReactNativeEmmExample */, 166 | ); 167 | }; 168 | /* End PBXProject section */ 169 | 170 | /* Begin PBXResourcesBuildPhase section */ 171 | 13B07F8E1A680F5B00A75B9A /* Resources */ = { 172 | isa = PBXResourcesBuildPhase; 173 | buildActionMask = 2147483647; 174 | files = ( 175 | 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 176 | 7F013AE524F0221800F369A3 /* LaunchScreen.storyboard in Resources */, 177 | A3A097AC789861A4D3F09235 /* PrivacyInfo.xcprivacy in Resources */, 178 | ); 179 | runOnlyForDeploymentPostprocessing = 0; 180 | }; 181 | /* End PBXResourcesBuildPhase section */ 182 | 183 | /* Begin PBXShellScriptBuildPhase section */ 184 | 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { 185 | isa = PBXShellScriptBuildPhase; 186 | buildActionMask = 2147483647; 187 | files = ( 188 | ); 189 | inputPaths = ( 190 | "$(SRCROOT)/.xcode.env", 191 | "$(SRCROOT)/.xcode.env.local", 192 | ); 193 | name = "Bundle React Native code and images"; 194 | outputPaths = ( 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | shellPath = /bin/sh; 198 | shellScript = "set -e\n\nWITH_ENVIRONMENT=\"../node_modules/react-native/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"../node_modules/react-native/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; 199 | }; 200 | 46DCFEE06274DE150C44951F /* [CP] Copy Pods Resources */ = { 201 | isa = PBXShellScriptBuildPhase; 202 | buildActionMask = 2147483647; 203 | files = ( 204 | ); 205 | inputPaths = ( 206 | "${PODS_ROOT}/Target Support Files/Pods-ReactNativeEmmExample/Pods-ReactNativeEmmExample-resources.sh", 207 | "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", 208 | "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", 209 | "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", 210 | "${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle", 211 | "${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle", 212 | ); 213 | name = "[CP] Copy Pods Resources"; 214 | outputPaths = ( 215 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", 216 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", 217 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", 218 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle", 219 | "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle", 220 | ); 221 | runOnlyForDeploymentPostprocessing = 0; 222 | shellPath = /bin/sh; 223 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeEmmExample/Pods-ReactNativeEmmExample-resources.sh\"\n"; 224 | showEnvVarsInLog = 0; 225 | }; 226 | 91BB27379B4B94C65D976421 /* [CP] Embed Pods Frameworks */ = { 227 | isa = PBXShellScriptBuildPhase; 228 | buildActionMask = 2147483647; 229 | files = ( 230 | ); 231 | inputPaths = ( 232 | "${PODS_ROOT}/Target Support Files/Pods-ReactNativeEmmExample/Pods-ReactNativeEmmExample-frameworks.sh", 233 | "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", 234 | ); 235 | name = "[CP] Embed Pods Frameworks"; 236 | outputPaths = ( 237 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", 238 | ); 239 | runOnlyForDeploymentPostprocessing = 0; 240 | shellPath = /bin/sh; 241 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeEmmExample/Pods-ReactNativeEmmExample-frameworks.sh\"\n"; 242 | showEnvVarsInLog = 0; 243 | }; 244 | CCCC07BCAFDEF1FCADC0D0C9 /* [CP] Check Pods Manifest.lock */ = { 245 | isa = PBXShellScriptBuildPhase; 246 | buildActionMask = 2147483647; 247 | files = ( 248 | ); 249 | inputFileListPaths = ( 250 | ); 251 | inputPaths = ( 252 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 253 | "${PODS_ROOT}/Manifest.lock", 254 | ); 255 | name = "[CP] Check Pods Manifest.lock"; 256 | outputFileListPaths = ( 257 | ); 258 | outputPaths = ( 259 | "$(DERIVED_FILE_DIR)/Pods-ReactNativeEmmExample-checkManifestLockResult.txt", 260 | ); 261 | runOnlyForDeploymentPostprocessing = 0; 262 | shellPath = /bin/sh; 263 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 264 | showEnvVarsInLog = 0; 265 | }; 266 | /* End PBXShellScriptBuildPhase section */ 267 | 268 | /* Begin PBXSourcesBuildPhase section */ 269 | 13B07F871A680F5B00A75B9A /* Sources */ = { 270 | isa = PBXSourcesBuildPhase; 271 | buildActionMask = 2147483647; 272 | files = ( 273 | 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */, 274 | 20F357B024636CDF00C146DC /* File.swift in Sources */, 275 | 13B07FC11A68108700A75B9A /* main.m in Sources */, 276 | ); 277 | runOnlyForDeploymentPostprocessing = 0; 278 | }; 279 | /* End PBXSourcesBuildPhase section */ 280 | 281 | /* Begin XCBuildConfiguration section */ 282 | 13B07F941A680F5B00A75B9A /* Debug */ = { 283 | isa = XCBuildConfiguration; 284 | baseConfigurationReference = 871719007ECC5EAD276C345C /* Pods-ReactNativeEmmExample.debug.xcconfig */; 285 | buildSettings = { 286 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 287 | CLANG_ENABLE_MODULES = YES; 288 | CURRENT_PROJECT_VERSION = 1; 289 | DEAD_CODE_STRIPPING = YES; 290 | INFOPLIST_FILE = ReactNativeEmmExample/Info.plist; 291 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 292 | OTHER_CFLAGS = ( 293 | "$(inherited)", 294 | "-DFB_SONARKIT_ENABLED=1", 295 | ); 296 | OTHER_LDFLAGS = ( 297 | "$(inherited)", 298 | "-ObjC", 299 | "-lc++", 300 | ); 301 | PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.ReactNativeEmmExample.$(PRODUCT_NAME:rfc1034identifier)"; 302 | PRODUCT_NAME = ReactNativeEmmExample; 303 | SWIFT_OBJC_BRIDGING_HEADER = "ReactNativeEmmExample-Bridging-Header.h"; 304 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 305 | SWIFT_VERSION = 5.0; 306 | VERSIONING_SYSTEM = "apple-generic"; 307 | }; 308 | name = Debug; 309 | }; 310 | 13B07F951A680F5B00A75B9A /* Release */ = { 311 | isa = XCBuildConfiguration; 312 | baseConfigurationReference = 4D7192F03A36A017E887435B /* Pods-ReactNativeEmmExample.release.xcconfig */; 313 | buildSettings = { 314 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 315 | CLANG_ENABLE_MODULES = YES; 316 | CURRENT_PROJECT_VERSION = 1; 317 | INFOPLIST_FILE = ReactNativeEmmExample/Info.plist; 318 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 319 | OTHER_CFLAGS = ( 320 | "$(inherited)", 321 | "-DFB_SONARKIT_ENABLED=1", 322 | ); 323 | OTHER_LDFLAGS = ( 324 | "$(inherited)", 325 | "-ObjC", 326 | "-lc++", 327 | ); 328 | PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.ReactNativeEmmExample.$(PRODUCT_NAME:rfc1034identifier)"; 329 | PRODUCT_NAME = ReactNativeEmmExample; 330 | SWIFT_OBJC_BRIDGING_HEADER = "ReactNativeEmmExample-Bridging-Header.h"; 331 | SWIFT_VERSION = 5.0; 332 | VERSIONING_SYSTEM = "apple-generic"; 333 | }; 334 | name = Release; 335 | }; 336 | 83CBBA201A601CBA00E9B192 /* Debug */ = { 337 | isa = XCBuildConfiguration; 338 | buildSettings = { 339 | ALWAYS_SEARCH_USER_PATHS = NO; 340 | CC = ""; 341 | CLANG_CXX_LANGUAGE_STANDARD = "c++20"; 342 | CLANG_CXX_LIBRARY = "libc++"; 343 | CLANG_ENABLE_MODULES = YES; 344 | CLANG_ENABLE_OBJC_ARC = YES; 345 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 346 | CLANG_WARN_BOOL_CONVERSION = YES; 347 | CLANG_WARN_COMMA = YES; 348 | CLANG_WARN_CONSTANT_CONVERSION = YES; 349 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 350 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 351 | CLANG_WARN_EMPTY_BODY = YES; 352 | CLANG_WARN_ENUM_CONVERSION = YES; 353 | CLANG_WARN_INFINITE_RECURSION = YES; 354 | CLANG_WARN_INT_CONVERSION = YES; 355 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 356 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 357 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 358 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 359 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 360 | CLANG_WARN_STRICT_PROTOTYPES = YES; 361 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 362 | CLANG_WARN_UNREACHABLE_CODE = YES; 363 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 364 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 365 | COPY_PHASE_STRIP = NO; 366 | CXX = ""; 367 | ENABLE_STRICT_OBJC_MSGSEND = YES; 368 | ENABLE_TESTABILITY = YES; 369 | "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; 370 | GCC_C_LANGUAGE_STANDARD = gnu99; 371 | GCC_DYNAMIC_NO_PIC = NO; 372 | GCC_NO_COMMON_BLOCKS = YES; 373 | GCC_OPTIMIZATION_LEVEL = 0; 374 | GCC_PREPROCESSOR_DEFINITIONS = ( 375 | "DEBUG=1", 376 | "$(inherited)", 377 | _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION, 378 | ); 379 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 380 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 381 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 382 | GCC_WARN_UNDECLARED_SELECTOR = YES; 383 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 384 | GCC_WARN_UNUSED_FUNCTION = YES; 385 | GCC_WARN_UNUSED_VARIABLE = YES; 386 | IPHONEOS_DEPLOYMENT_TARGET = 13.4; 387 | LD = ""; 388 | LDPLUSPLUS = ""; 389 | MTL_ENABLE_DEBUG_INFO = YES; 390 | ONLY_ACTIVE_ARCH = YES; 391 | OTHER_CFLAGS = "$(inherited)"; 392 | OTHER_CPLUSPLUSFLAGS = "$(inherited)"; 393 | OTHER_LDFLAGS = "$(inherited)"; 394 | REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; 395 | SDKROOT = iphoneos; 396 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; 397 | USE_HERMES = true; 398 | }; 399 | name = Debug; 400 | }; 401 | 83CBBA211A601CBA00E9B192 /* Release */ = { 402 | isa = XCBuildConfiguration; 403 | buildSettings = { 404 | ALWAYS_SEARCH_USER_PATHS = NO; 405 | CC = ""; 406 | CLANG_CXX_LANGUAGE_STANDARD = "c++20"; 407 | CLANG_CXX_LIBRARY = "libc++"; 408 | CLANG_ENABLE_MODULES = YES; 409 | CLANG_ENABLE_OBJC_ARC = YES; 410 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 411 | CLANG_WARN_BOOL_CONVERSION = YES; 412 | CLANG_WARN_COMMA = YES; 413 | CLANG_WARN_CONSTANT_CONVERSION = YES; 414 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 415 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 416 | CLANG_WARN_EMPTY_BODY = YES; 417 | CLANG_WARN_ENUM_CONVERSION = YES; 418 | CLANG_WARN_INFINITE_RECURSION = YES; 419 | CLANG_WARN_INT_CONVERSION = YES; 420 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 421 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 422 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 423 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 424 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 425 | CLANG_WARN_STRICT_PROTOTYPES = YES; 426 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 427 | CLANG_WARN_UNREACHABLE_CODE = YES; 428 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 429 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 430 | COPY_PHASE_STRIP = YES; 431 | CXX = ""; 432 | ENABLE_NS_ASSERTIONS = NO; 433 | ENABLE_STRICT_OBJC_MSGSEND = YES; 434 | "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; 435 | GCC_C_LANGUAGE_STANDARD = gnu99; 436 | GCC_NO_COMMON_BLOCKS = YES; 437 | GCC_PREPROCESSOR_DEFINITIONS = ( 438 | "$(inherited)", 439 | _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION, 440 | ); 441 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 442 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 443 | GCC_WARN_UNDECLARED_SELECTOR = YES; 444 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 445 | GCC_WARN_UNUSED_FUNCTION = YES; 446 | GCC_WARN_UNUSED_VARIABLE = YES; 447 | IPHONEOS_DEPLOYMENT_TARGET = 13.4; 448 | LD = ""; 449 | LDPLUSPLUS = ""; 450 | MTL_ENABLE_DEBUG_INFO = NO; 451 | OTHER_CFLAGS = "$(inherited)"; 452 | OTHER_CPLUSPLUSFLAGS = "$(inherited)"; 453 | OTHER_LDFLAGS = "$(inherited)"; 454 | REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; 455 | SDKROOT = iphoneos; 456 | USE_HERMES = true; 457 | VALIDATE_PRODUCT = YES; 458 | }; 459 | name = Release; 460 | }; 461 | /* End XCBuildConfiguration section */ 462 | 463 | /* Begin XCConfigurationList section */ 464 | 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "ReactNativeEmmExample" */ = { 465 | isa = XCConfigurationList; 466 | buildConfigurations = ( 467 | 13B07F941A680F5B00A75B9A /* Debug */, 468 | 13B07F951A680F5B00A75B9A /* Release */, 469 | ); 470 | defaultConfigurationIsVisible = 0; 471 | defaultConfigurationName = Release; 472 | }; 473 | 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "ReactNativeEmmExample" */ = { 474 | isa = XCConfigurationList; 475 | buildConfigurations = ( 476 | 83CBBA201A601CBA00E9B192 /* Debug */, 477 | 83CBBA211A601CBA00E9B192 /* Release */, 478 | ); 479 | defaultConfigurationIsVisible = 0; 480 | defaultConfigurationName = Release; 481 | }; 482 | /* End XCConfigurationList section */ 483 | }; 484 | rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; 485 | } 486 | -------------------------------------------------------------------------------- /example/ios/ReactNativeEmmExample.xcodeproj/xcshareddata/xcschemes/ReactNativeEmmExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 64 | 66 | 72 | 73 | 74 | 75 | 81 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /example/ios/ReactNativeEmmExample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/ReactNativeEmmExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/ReactNativeEmmExample/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : RCTAppDelegate 5 | 6 | @property (nonatomic, strong) UIWindow *window; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /example/ios/ReactNativeEmmExample/AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | 5 | @implementation AppDelegate 6 | 7 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 8 | { 9 | self.moduleName = @"ReactNativeEmmExample"; 10 | 11 | self.initialProps = @{}; 12 | return [super application:application didFinishLaunchingWithOptions:launchOptions];; 13 | } 14 | 15 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 16 | { 17 | return [self bundleURL]; 18 | } 19 | 20 | - (NSURL *)bundleURL 21 | { 22 | #if DEBUG 23 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; 24 | #else 25 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 26 | #endif 27 | } 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /example/ios/ReactNativeEmmExample/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /example/ios/ReactNativeEmmExample/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/ios/ReactNativeEmmExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ReactNativeEmm Example 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | NSAllowsLocalNetworking 32 | 33 | 34 | NSFaceIDUsageDescription 35 | Testing Face ID 36 | NSLocationWhenInUseUsageDescription 37 | 38 | UILaunchStoryboardName 39 | LaunchScreen 40 | UIRequiredDeviceCapabilities 41 | 42 | armv64 43 | 44 | UISupportedInterfaceOrientations 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | UIViewControllerBasedStatusBarAppearance 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /example/ios/ReactNativeEmmExample/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /example/ios/ReactNativeEmmExample/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryFileTimestamp 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | C617.1 13 | 14 | 15 | 16 | NSPrivacyAccessedAPIType 17 | NSPrivacyAccessedAPICategoryUserDefaults 18 | NSPrivacyAccessedAPITypeReasons 19 | 20 | CA92.1 21 | 22 | 23 | 24 | NSPrivacyAccessedAPIType 25 | NSPrivacyAccessedAPICategorySystemBootTime 26 | NSPrivacyAccessedAPITypeReasons 27 | 28 | 35F9.1 29 | 30 | 31 | 32 | NSPrivacyCollectedDataTypes 33 | 34 | NSPrivacyTracking 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/ReactNativeEmmExample/main.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char *argv[]) 13 | { 14 | @autoreleasepool { 15 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 | // See LICENSE.txt for license information. 3 | 4 | /** 5 | * Metro configuration 6 | * https://facebook.github.io/metro/docs/configuration 7 | * 8 | * @type {import('metro-config').MetroConfig} 9 | */ 10 | 11 | const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); 12 | const path = require('path'); 13 | const exclusionList = require('metro-config/src/defaults/exclusionList'); 14 | const escape = require('escape-string-regexp'); 15 | const pak = require('../package.json'); 16 | 17 | const root = path.resolve(__dirname, '..'); 18 | 19 | const modules = Object.keys({ 20 | ...pak.peerDependencies, 21 | }); 22 | 23 | const config = { 24 | projectRoot: __dirname, 25 | watchFolders: [root], 26 | 27 | // We need to make sure that only one version is loaded for peerDependencies 28 | // So we blacklist them at the root, and alias them to the versions in example's node_modules 29 | resolver: { 30 | blacklistRE: exclusionList( 31 | modules.map( 32 | (m) => 33 | new RegExp( 34 | `^${escape(path.join(root, 'node_modules', m))}\\/.*$` 35 | ) 36 | ) 37 | ), 38 | 39 | extraNodeModules: modules.reduce((acc, name) => { 40 | acc[name] = path.join(__dirname, 'node_modules', name); 41 | return acc; 42 | }, {}), 43 | }, 44 | 45 | transformer: { 46 | getTransformOptions: async () => ({ 47 | transform: { 48 | experimentalImportSupport: false, 49 | inlineRequires: true, 50 | }, 51 | }), 52 | }, 53 | }; 54 | 55 | module.exports = mergeConfig(getDefaultConfig(root), config); 56 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mattermost/react-native-emm-example", 3 | "description": "Example app for @mattermost/react-native-emm", 4 | "version": "0.0.1", 5 | "private": true, 6 | "scripts": { 7 | "android": "react-native run-android", 8 | "ios": "react-native run-ios", 9 | "start": "react-native start" 10 | }, 11 | "dependencies": { 12 | "@mattermost/react-native-emm": "../", 13 | "react": "18.3.1", 14 | "react-native": "0.76.5" 15 | }, 16 | "devDependencies": { 17 | "@babel/core": "7.26.0", 18 | "@babel/runtime": "7.26.0", 19 | "@react-native-community/cli": "15.1.3", 20 | "@react-native-community/cli-platform-android": "15.1.3", 21 | "@react-native-community/cli-platform-ios": "15.1.3", 22 | "@react-native/babel-preset": "0.76.5", 23 | "@types/react": "18.3.1", 24 | "babel-plugin-module-resolver": "5.0.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SafeAreaView, ScrollView, StyleSheet, View } from 'react-native'; 3 | import { Provider } from '@mattermost/react-native-emm'; 4 | 5 | import Authentication from './Authentication'; 6 | import ManagedConfig from './ManagedConfig'; 7 | import Others from './Others'; 8 | 9 | const styles = StyleSheet.create({ 10 | container: { 11 | flex: 1, 12 | alignItems: 'center', 13 | }, 14 | divider: { 15 | width: '100%', 16 | height: 1, 17 | backgroundColor: 'lightgray', 18 | marginVertical: 10, 19 | }, 20 | }); 21 | 22 | const Divider = () => { 23 | return ; 24 | }; 25 | 26 | export default function App() { 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /example/src/Authentication.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState } from 'react'; 2 | import { Alert, StyleSheet, Text, useColorScheme, View } from 'react-native'; 3 | import { Colors } from 'react-native/Libraries/NewAppScreen'; 4 | 5 | import Emm from '@mattermost/react-native-emm'; 6 | import type { AuthenticateConfig } from '@mattermost/react-native-emm'; 7 | 8 | import Button from './Button'; 9 | 10 | type AuthItemProps = { 11 | label: string; 12 | value: any; 13 | }; 14 | 15 | const styles = StyleSheet.create({ 16 | container: { 17 | paddingHorizontal: 12, 18 | width: '100%', 19 | }, 20 | item: { 21 | flexDirection: 'row', 22 | marginBottom: 2, 23 | }, 24 | label: { 25 | fontSize: 16, 26 | fontWeight: 'bold', 27 | marginRight: 5, 28 | }, 29 | section: { 30 | fontSize: 24, 31 | fontWeight: 'bold', 32 | marginBottom: 5, 33 | }, 34 | value: { 35 | fontSize: 16, 36 | }, 37 | }); 38 | 39 | const AuthItem = ({ label, value }: AuthItemProps) => { 40 | const isDarkMode = useColorScheme() === 'dark'; 41 | const color = isDarkMode ? Colors.white : Colors.black; 42 | return ( 43 | 44 | {`${label}:`} 45 | {value.toString()} 46 | 47 | ); 48 | }; 49 | 50 | const Authentication = () => { 51 | const [methods, setMethods] = useState>({}); 52 | const [auth, setAuth] = useState(undefined); 53 | const isDarkMode = useColorScheme() === 'dark'; 54 | const color = isDarkMode ? Colors.white : Colors.black; 55 | 56 | const authenticate = async () => { 57 | const secured = await Emm.isDeviceSecured(); 58 | 59 | if (secured) { 60 | const opts: AuthenticateConfig = { 61 | reason: 'Some Reason', 62 | description: 'Test description', 63 | fallback: true, 64 | supressEnterPassword: false, 65 | }; 66 | const authenticated = await Emm.authenticate(opts); 67 | setAuth(authenticated); 68 | } else { 69 | Alert.alert( 70 | 'Authentication Error', 71 | 'There are no authentication methods availble in this device' 72 | ); 73 | } 74 | }; 75 | 76 | React.useEffect(() => { 77 | Emm.deviceSecureWith().then(setMethods); 78 | }, []); 79 | 80 | const items = useMemo(() => { 81 | return Object.keys(methods).map((key) => { 82 | return ; 83 | }); 84 | }, [methods]); 85 | 86 | return ( 87 | 88 | 89 | {'Device Authentication Methods'} 90 | 91 | {items} 92 | 95 | 96 | ); 97 | }; 98 | 99 | export default Authentication; 100 | -------------------------------------------------------------------------------- /example/src/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { type FunctionComponent } from 'react'; 2 | import { 3 | Pressable, 4 | type PressableProps, 5 | type PressableStateCallbackType, 6 | type StyleProp, 7 | StyleSheet, 8 | type ViewStyle, 9 | } from 'react-native'; 10 | 11 | const ripple = { 12 | color: 'gray', 13 | borderless: false, 14 | }; 15 | 16 | const styles = StyleSheet.create({ 17 | button: { 18 | borderRadius: 8, 19 | padding: 6, 20 | height: 40, 21 | flexShrink: 1, 22 | borderColor: 'lightgray', 23 | borderWidth: 1, 24 | alignItems: 'center', 25 | justifyContent: 'center', 26 | }, 27 | success: { 28 | backgroundColor: 'lightgreen', 29 | }, 30 | failed: { 31 | backgroundColor: 'red', 32 | }, 33 | }); 34 | 35 | interface ButtonProps extends PressableProps { 36 | style?: ViewStyle; 37 | success?: boolean | undefined; 38 | } 39 | 40 | const Button: FunctionComponent = (props) => { 41 | let successStyle: ViewStyle; 42 | if (typeof props.success === 'boolean') { 43 | successStyle = props.success ? styles.success : styles.failed; 44 | } 45 | 46 | const pressedStyle = ({ 47 | pressed, 48 | }: PressableStateCallbackType): StyleProp => [ 49 | { 50 | backgroundColor: pressed ? 'rgba(0, 0, 0, 0.2)' : undefined, 51 | }, 52 | styles.button, 53 | successStyle, 54 | props.style, 55 | ]; 56 | 57 | return ( 58 | 63 | {props.children} 64 | 65 | ); 66 | }; 67 | 68 | export default Button; 69 | -------------------------------------------------------------------------------- /example/src/ManagedConfig.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { StyleSheet, Text, useColorScheme, View } from 'react-native'; 3 | import { Colors } from 'react-native/Libraries/NewAppScreen'; 4 | 5 | import { useManagedConfig } from '@mattermost/react-native-emm'; 6 | 7 | type ItemProps = { 8 | label: string; 9 | value: any; 10 | }; 11 | 12 | const styles = StyleSheet.create({ 13 | container: { 14 | paddingHorizontal: 12, 15 | width: '100%', 16 | }, 17 | item: { 18 | flexDirection: 'row', 19 | marginBottom: 2, 20 | }, 21 | label: { 22 | fontSize: 16, 23 | fontWeight: 'bold', 24 | marginRight: 5, 25 | }, 26 | section: { 27 | fontSize: 24, 28 | fontWeight: 'bold', 29 | marginBottom: 5, 30 | }, 31 | value: { 32 | fontSize: 16, 33 | }, 34 | }); 35 | 36 | const ConfigItem = ({ label, value }: ItemProps) => { 37 | const isDarkMode = useColorScheme() === 'dark'; 38 | const color = isDarkMode ? Colors.white : Colors.black; 39 | return ( 40 | 41 | {label} 42 | {value.toString()} 43 | 44 | ); 45 | }; 46 | 47 | const ManagedConfig = () => { 48 | const managed = useManagedConfig>(); 49 | const isDarkMode = useColorScheme() === 'dark'; 50 | const color = isDarkMode ? Colors.white : Colors.black; 51 | 52 | const items = useMemo(() => { 53 | const keys = Object.keys(managed); 54 | 55 | if (keys.length) { 56 | return keys.map((key) => { 57 | return ; 58 | }); 59 | } 60 | 61 | return ; 62 | }, [managed]); 63 | 64 | return ( 65 | 66 | 67 | {'EMM Managed Configuration'} 68 | 69 | {items} 70 | 71 | ); 72 | }; 73 | 74 | export default ManagedConfig; 75 | -------------------------------------------------------------------------------- /example/src/Others.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Platform, StyleSheet, Text, useColorScheme, View } from 'react-native'; 3 | import { Colors } from 'react-native/Libraries/NewAppScreen'; 4 | 5 | import Emm from '@mattermost/react-native-emm'; 6 | 7 | import Button from './Button'; 8 | 9 | const styles = StyleSheet.create({ 10 | container: { 11 | paddingHorizontal: 12, 12 | width: '100%', 13 | }, 14 | item: { 15 | flexDirection: 'row', 16 | marginBottom: 2, 17 | }, 18 | label: { 19 | fontSize: 16, 20 | fontWeight: 'bold', 21 | marginRight: 5, 22 | }, 23 | section: { 24 | fontSize: 24, 25 | fontWeight: 'bold', 26 | marginBottom: 5, 27 | }, 28 | separator: { 29 | marginVertical: 2.5, 30 | }, 31 | value: { 32 | fontSize: 16, 33 | }, 34 | }); 35 | 36 | const BlurAppScreen = () => { 37 | const [enabled, setEnabled] = useState(undefined); 38 | const isDarkMode = useColorScheme() === 'dark'; 39 | const color = isDarkMode ? Colors.white : Colors.black; 40 | 41 | const toggle = () => { 42 | Emm.enableBlurScreen(!enabled); 43 | setEnabled(!enabled); 44 | }; 45 | 46 | return ( 47 | 50 | ); 51 | }; 52 | 53 | const ExitApp = () => { 54 | const isDarkMode = useColorScheme() === 'dark'; 55 | const color = isDarkMode ? Colors.white : Colors.black; 56 | const exitApp = () => { 57 | Emm.exitApp(); 58 | }; 59 | 60 | return ( 61 | <> 62 | 63 | 66 | 67 | ); 68 | }; 69 | 70 | const SecuritySettings = () => { 71 | const isDarkMode = useColorScheme() === 'dark'; 72 | const color = isDarkMode ? Colors.white : Colors.black; 73 | 74 | const settings = () => { 75 | Emm.openSecuritySettings(); 76 | }; 77 | 78 | if (Platform.OS !== 'android') { 79 | return null; 80 | } 81 | 82 | return ( 83 | <> 84 | 85 | 88 | 89 | ); 90 | }; 91 | 92 | const Authentication = () => { 93 | const isDarkMode = useColorScheme() === 'dark'; 94 | const color = isDarkMode ? Colors.white : Colors.black; 95 | return ( 96 | 97 | {'Other Options'} 98 | 99 | 100 | 101 | 102 | ); 103 | }; 104 | 105 | export default Authentication; 106 | -------------------------------------------------------------------------------- /ios/Emm.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #if RCT_NEW_ARCH_ENABLED 5 | 6 | #import 7 | @interface Emm: RCTEventEmitter 8 | 9 | #else 10 | 11 | #import 12 | @interface Emm : RCTEventEmitter 13 | 14 | #endif 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /ios/Emm.mm: -------------------------------------------------------------------------------- 1 | #import "Emm.h" 2 | 3 | #ifdef RCT_NEW_ARCH_ENABLED 4 | 5 | #import "EmmSpec.h" 6 | #endif 7 | 8 | #if __has_include("react_native_emm-Swift.h") 9 | #import 10 | #else 11 | #import 12 | #endif 13 | 14 | #include 15 | 16 | @interface Emm () 17 | @end 18 | 19 | @implementation Emm { 20 | EmmWrapper *wrapper; 21 | bool hasListeners; 22 | } 23 | 24 | -(instancetype)init { 25 | self = [super init]; 26 | if (self) { 27 | wrapper = [EmmWrapper new]; 28 | wrapper.delegate = self; 29 | [wrapper captureEvents]; 30 | } 31 | return self; 32 | } 33 | 34 | -(void)startObserving { 35 | hasListeners = YES; 36 | } 37 | 38 | // Will be called when this module's last listener is removed, or on dealloc. 39 | -(void)stopObserving { 40 | hasListeners = NO; 41 | } 42 | 43 | RCT_EXPORT_MODULE(Emm) 44 | 45 | RCT_REMAP_METHOD(authenticate, options:(NSDictionary *)options 46 | withResolver:(RCTPromiseResolveBlock)resolve 47 | withRejecter:(RCTPromiseRejectBlock)reject) { 48 | [wrapper authenticateWithOptions:options resolve:resolve reject:reject]; 49 | } 50 | 51 | RCT_REMAP_METHOD(deviceSecureWith, withResolver:(RCTPromiseResolveBlock)resolve 52 | withRejecter:(RCTPromiseRejectBlock)reject) { 53 | [self deviceSecureWith:resolve reject:reject]; 54 | } 55 | 56 | RCT_REMAP_BLOCKING_SYNCHRONOUS_METHOD(getManagedConfig, NSDictionary *, getManaged) { 57 | return [self getManagedConfig]; 58 | } 59 | 60 | RCT_REMAP_METHOD(exitApp, exit) { 61 | [self exitApp]; 62 | } 63 | 64 | RCT_REMAP_METHOD(setAppGroupId, identifier:(NSString *)identifier) { 65 | [self setAppGroupId:identifier]; 66 | } 67 | 68 | RCT_REMAP_METHOD(setBlurScreen, enabled:(BOOL)enabled) { 69 | [self setBlurScreen:enabled]; 70 | } 71 | 72 | RCT_REMAP_METHOD(applyBlurEffect, radius:(double)radius) { 73 | [self applyBlurEffect:radius]; 74 | } 75 | 76 | RCT_REMAP_METHOD(removeBlurEffect, removeBlur) { 77 | [self removeBlurEffect]; 78 | } 79 | 80 | #ifdef RCT_NEW_ARCH_ENABLED 81 | - (std::shared_ptr)getTurboModule: 82 | (const facebook::react::ObjCTurboModule::InitParams &)params 83 | { 84 | return std::make_shared(params); 85 | } 86 | #endif 87 | 88 | #pragma protocol 89 | 90 | - (void)sendEventWithName:(NSString * _Nonnull)name result:(NSDictionary * _Nullable)result { 91 | if (hasListeners) { 92 | [self sendEventWithName:name body:result]; 93 | } 94 | } 95 | 96 | - (NSArray *)supportedEvents { 97 | return [EmmWrapper supportedEvents]; 98 | } 99 | 100 | #pragma overrides 101 | + (BOOL)requiresMainQueueSetup { 102 | return NO; 103 | } 104 | 105 | #pragma utils 106 | - (NSNumber *)processBooleanValue:(std::optional)optionalBoolValue { 107 | // Use the boolean value 108 | if (optionalBoolValue.has_value()) { 109 | return [NSNumber numberWithBool:optionalBoolValue.value()]; 110 | } 111 | 112 | return 0; 113 | } 114 | 115 | #pragma react methods implementation 116 | 117 | #ifdef RCT_NEW_ARCH_ENABLED 118 | 119 | - (void)authenticate:(JS::NativeEmm::AuthenticateConfig &)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { 120 | id sharedKeySet = [NSDictionary sharedKeySetForKeys:@[@"reason", @"fallback", @"description", @"supressEnterPassword"]]; // returns NSSharedKeySet 121 | NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithSharedKeySet:sharedKeySet]; 122 | dict[@"reason"] = options.reason(); 123 | dict[@"fallback"] = [self processBooleanValue:options.fallback()]; 124 | dict[@"description"] = options.description(); 125 | dict[@"supressEnterPassword"] = [self processBooleanValue:options.supressEnterPassword()]; 126 | [wrapper authenticateWithOptions:dict resolve:resolve reject:reject]; 127 | } 128 | 129 | #endif 130 | 131 | 132 | - (NSDictionary *)getManagedConfig { 133 | return [wrapper getManagedConfig]; 134 | } 135 | 136 | 137 | - (void)deviceSecureWith:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { 138 | [wrapper deviceSecureWithResolve:resolve reject:reject]; 139 | } 140 | 141 | 142 | - (void)exitApp { 143 | [wrapper exitApp]; 144 | } 145 | 146 | 147 | - (void)openSecuritySettings { 148 | NSLog(@"Method not implemented on iOS"); 149 | } 150 | 151 | 152 | - (void)setAppGroupId:(NSString *)identifier { 153 | [wrapper setAppGroupIdWithIdentifier:identifier]; 154 | } 155 | 156 | 157 | - (void)setBlurScreen:(BOOL)enabled { 158 | [wrapper setBlurScreenWithEnabled:enabled]; 159 | } 160 | 161 | -(void)applyBlurEffect:(double)radius { 162 | [[ScreenCaptureManager shared] applyBlurEffectWithRadius:radius]; 163 | } 164 | 165 | -(void)removeBlurEffect { 166 | [[ScreenCaptureManager shared] removeBlurEffectWithForced:true]; 167 | } 168 | 169 | @end 170 | -------------------------------------------------------------------------------- /ios/Emm.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import LocalAuthentication 4 | import React 5 | 6 | @objc public class EmmWrapper: NSObject { 7 | @objc public weak var delegate: EmmDelegate? = nil 8 | 9 | // The Managed app configuration dictionary pushed down from an EMM provider are stored in this key. 10 | internal var hasListeners = false 11 | internal var configurationKey = "com.apple.configuration.managed" 12 | internal var appGroupId:String? 13 | internal var sharedUserDefaults:UserDefaults? 14 | 15 | @objc public func captureEvents() { 16 | NotificationCenter.default.addObserver(self, selector: #selector(managedConfigChaged(notification:)), name: UserDefaults.didChangeNotification, object: nil) 17 | NotificationCenter.default.addObserver(ScreenCaptureManager.shared, selector: #selector(ScreenCaptureManager.applyBlurEffect(notification:)), name: UIApplication.willResignActiveNotification, object: nil) 18 | NotificationCenter.default.addObserver(ScreenCaptureManager.shared, selector: #selector(ScreenCaptureManager.removeBlurEffect), name: UIApplication.didBecomeActiveNotification, object: nil) 19 | } 20 | 21 | @objc public func invalidate() { 22 | ScreenCaptureManager.shared.removeBlurEffect(forced: true) 23 | NotificationCenter.default.removeObserver(self) 24 | } 25 | 26 | @objc public func authenticate(options:Dictionary, resolve:(@escaping RCTPromiseResolveBlock), reject:(@escaping RCTPromiseRejectBlock)) { 27 | DispatchQueue.global(qos: .userInitiated).async { 28 | let reason = options["reason"] as! String 29 | let fallback = options["fallback"] as! Bool 30 | let supressEnterPassword = options["supressEnterPassword"] as! Bool 31 | ScreenCaptureManager.shared.isAuthenticating = true 32 | ScreenCaptureManager.shared.blurOnAuthenticate = options["blurOnAuthenticate"] as? Bool ?? false 33 | ScreenCaptureManager.shared.applyBlurEffect() 34 | self.authenticateWithPolicy(policy: .deviceOwnerAuthenticationWithBiometrics, reason: reason, fallback: fallback, supressEnterPassword: supressEnterPassword, completionHandler: {(success: Bool, error: Error?) in 35 | if success && ScreenCaptureManager.shared.blurOnAuthenticate { 36 | ScreenCaptureManager.shared.isAuthenticating = false 37 | ScreenCaptureManager.shared.removeBlurEffect(forced: true) 38 | } else { 39 | DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 0.5) { 40 | ScreenCaptureManager.shared.isAuthenticating = false 41 | } 42 | } 43 | 44 | if (error != nil) { 45 | let errorReason = self.errorMessageForFails(errorCode: (error! as NSError).code) 46 | reject("error", errorReason, error) 47 | return 48 | } 49 | 50 | resolve(true) 51 | }) 52 | } 53 | } 54 | 55 | @objc public func deviceSecureWith(resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) -> Void { 56 | var result = [ 57 | "face": false, 58 | "fingerprint": false, 59 | "passcode": false 60 | ] 61 | 62 | let context = LAContext() 63 | let hasAuthenticationBiometrics = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) 64 | let hasAuthentication = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) 65 | 66 | if #available(iOS 11.0, *) { 67 | if (hasAuthenticationBiometrics) { 68 | switch context.biometryType { 69 | case .faceID: 70 | result["face"] = true 71 | case .touchID: 72 | result["fingerprint"] = true 73 | default: 74 | print("No Biometrics authentication found") 75 | } 76 | } else if (hasAuthentication) { 77 | result["passcode"] = true 78 | } 79 | } else if (hasAuthenticationBiometrics) { 80 | result["fingerprint"] = true 81 | } else if (hasAuthentication) { 82 | result["passcode"] = true 83 | } 84 | 85 | resolve(result) 86 | } 87 | 88 | @objc public func setBlurScreen(enabled: Bool) { 89 | ScreenCaptureManager.shared.preventScreenCapture = enabled 90 | } 91 | 92 | @objc public func exitApp() -> Void { 93 | exit(0) 94 | } 95 | 96 | @objc public func getManagedConfig() -> Dictionary { 97 | let config = managedConfig() 98 | if ((config) != nil) { 99 | return config! 100 | } else { 101 | return Dictionary() 102 | } 103 | } 104 | 105 | @objc public func setAppGroupId(identifier: String) -> Void { 106 | self.appGroupId = identifier 107 | self.sharedUserDefaults = UserDefaults.init(suiteName: identifier) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /ios/EmmAuthenticate.swift: -------------------------------------------------------------------------------- 1 | import LocalAuthentication 2 | import UIKit 3 | 4 | extension EmmWrapper { 5 | private func showBlockingView() { 6 | DispatchQueue.main.async { 7 | guard let window = ScreenCaptureManager.shared.getLastKeyWindow() else { return } 8 | let blockingView = UIView(frame: window.bounds) 9 | blockingView.backgroundColor = UIColor.black.withAlphaComponent(0.5) // Optional: semi-transparent 10 | blockingView.tag = 999 // Unique tag to identify the view later 11 | window.addSubview(blockingView) 12 | } 13 | } 14 | 15 | private func removeBlockingView() { 16 | DispatchQueue.main.async { 17 | guard let window = ScreenCaptureManager.shared.getLastKeyWindow() else { return } 18 | if let blockingView = window.viewWithTag(999) { 19 | blockingView.removeFromSuperview() 20 | } 21 | } 22 | } 23 | 24 | func authenticateWithPolicy(policy: LAPolicy, reason: String, fallback: Bool, supressEnterPassword: Bool, completionHandler: @escaping (Bool, Error?) -> Void) -> Void { 25 | let context = LAContext() 26 | if (supressEnterPassword) { 27 | context.localizedFallbackTitle = "" 28 | } 29 | 30 | var error: NSError? 31 | 32 | if (!context.canEvaluatePolicy(policy, error: &error)) { 33 | if (policy == LAPolicy.deviceOwnerAuthenticationWithBiometrics) { 34 | if #available(iOS 11.0, *) { 35 | switch error!.code { 36 | case LAError.Code.biometryNotAvailable.rawValue, 37 | LAError.Code.biometryNotEnrolled.rawValue, 38 | LAError.Code.biometryLockout.rawValue: 39 | if (fallback) { 40 | self.authenticateWithPolicy(policy: .deviceOwnerAuthentication, reason: reason, fallback: fallback, supressEnterPassword: supressEnterPassword, completionHandler: completionHandler) 41 | return 42 | } 43 | default: 44 | completionHandler(false, error); 45 | } 46 | } else if (fallback) { 47 | self.authenticateWithPolicy(policy: .deviceOwnerAuthentication, reason: reason, fallback: fallback, supressEnterPassword: supressEnterPassword, completionHandler: completionHandler) 48 | return 49 | } else { 50 | completionHandler(false, error); 51 | } 52 | } 53 | } 54 | 55 | self.showBlockingView(); 56 | 57 | context.evaluatePolicy(policy, localizedReason: reason, reply: {(success: Bool, error: Error?) in 58 | self.removeBlockingView(); 59 | if (error != nil) { 60 | completionHandler(false, error) 61 | return 62 | } 63 | 64 | completionHandler(true, nil) 65 | }) 66 | } 67 | 68 | func errorMessageForFails(errorCode: Int) -> String { 69 | var message = "" 70 | 71 | switch errorCode { 72 | case LAError.authenticationFailed.rawValue: 73 | message = "Authentication was not successful, because user failed to provide valid credentials" 74 | 75 | case LAError.appCancel.rawValue: 76 | message = "Authentication was canceled by application" 77 | 78 | case LAError.invalidContext.rawValue: 79 | message = "LAContext passed to this call has been previously invalidated" 80 | 81 | case LAError.notInteractive.rawValue: 82 | message = "Authentication failed, because it would require showing UI which has been forbidden by using interactionNotAllowed property" 83 | 84 | case LAError.passcodeNotSet.rawValue: 85 | message = "Authentication could not start, because passcode is not set on the device" 86 | 87 | case LAError.systemCancel.rawValue: 88 | message = "Authentication was canceled by system" 89 | 90 | case LAError.userCancel.rawValue: 91 | message = "Authentication was canceled by user" 92 | 93 | case LAError.userFallback.rawValue: 94 | message = "Authentication was canceled, because the user tapped the fallback button" 95 | 96 | default: 97 | message = "Unknown Error" 98 | } 99 | 100 | return message 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /ios/EmmEvents.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @objc public protocol EmmDelegate { 4 | func sendEvent(name: String, result: Dictionary?) 5 | } 6 | 7 | extension EmmWrapper { 8 | enum Event: String, CaseIterable { 9 | case managedConfigChanged 10 | } 11 | 12 | @objc 13 | public static var supportedEvents: [String] { 14 | return Event.allCases.map(\.rawValue) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ios/EmmManagedConfig.swift: -------------------------------------------------------------------------------- 1 | extension EmmWrapper { 2 | func managedConfig() -> Dictionary? { 3 | let defaults = UserDefaults.standard 4 | return defaults.dictionary(forKey: self.configurationKey) 5 | } 6 | 7 | @objc func managedConfigChaged(notification: Notification) -> Void { 8 | let config = managedConfig() 9 | if (config == nil) { 10 | if (self.sharedUserDefaults?.dictionary(forKey: self.configurationKey) != nil) { 11 | self.sharedUserDefaults?.removeObject(forKey: self.configurationKey) 12 | } 13 | return 14 | } 15 | 16 | let initial = self.sharedUserDefaults?.dictionary(forKey: self.configurationKey) ?? Dictionary() 17 | let equal = NSDictionary(dictionary: initial).isEqual(to: config!) 18 | 19 | 20 | if (self.appGroupId != nil && !equal) { 21 | self.sharedUserDefaults?.set(config, forKey: self.configurationKey) 22 | } 23 | 24 | if (hasListeners) { 25 | delegate?.sendEvent(name: "managedConfigChanged", result: config) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /ios/Extensions/UINavigationController+Swizzle.swift: -------------------------------------------------------------------------------- 1 | extension UINavigationController { 2 | private struct AssociatedKeys { 3 | static var didObserveLifecycle: UnsafeRawPointer = UnsafeRawPointer(bitPattern: "didObserveLifecycle".hashValue)! 4 | } 5 | 6 | static var didSwizzle = false 7 | static func swizzleNavigationTracking() { 8 | guard self === UINavigationController.self, !didSwizzle else { return } 9 | didSwizzle = true 10 | 11 | swizzleMethod(#selector(setter: viewControllers), with: #selector(swizzled_setViewControllers(_:animated:))) 12 | swizzleMethod(#selector(pushViewController(_:animated:)), with: #selector(swizzled_pushViewController(_:animated:))) 13 | } 14 | 15 | static func revertNavigationSwizzling() { 16 | guard self === UINavigationController.self else { return } 17 | 18 | swizzleMethod(#selector(swizzled_setViewControllers(_:animated:)), with: #selector(setter: viewControllers)) 19 | swizzleMethod(#selector(swizzled_pushViewController(_:animated:)), with: #selector(pushViewController(_:animated:))) 20 | } 21 | 22 | private static func swizzleMethod(_ originalSelector: Selector, with swizzledSelector: Selector) { 23 | guard let originalMethod = class_getInstanceMethod(self, originalSelector), 24 | let swizzledMethod = class_getInstanceMethod(self, swizzledSelector) else { return } 25 | 26 | method_exchangeImplementations(originalMethod, swizzledMethod) 27 | } 28 | 29 | @objc private func swizzled_setViewControllers(_ viewControllers: [UIViewController], animated: Bool) { 30 | self.swizzled_setViewControllers(viewControllers, animated: animated) 31 | trackViewControllerChanges() 32 | } 33 | 34 | @objc private func swizzled_pushViewController(_ viewController: UIViewController, animated: Bool) { 35 | self.swizzled_pushViewController(viewController, animated: animated) 36 | observeLifecycleIfNeeded(viewController) 37 | } 38 | 39 | /// Track ViewControllers in the stack 40 | private func trackViewControllerChanges() { 41 | #if DEBUG 42 | print("Navigation Stack Updated: \(viewControllers.map { type(of: $0) })") 43 | #endif 44 | 45 | viewControllers.forEach { 46 | observeLifecycleIfNeeded($0) 47 | } 48 | } 49 | 50 | /// Observe lifecycle events 51 | private func observeLifecycleIfNeeded(_ viewController: UIViewController) { 52 | if objc_getAssociatedObject(viewController, &AssociatedKeys.didObserveLifecycle) == nil { 53 | observeLifecycle(of: viewController) 54 | objc_setAssociatedObject(viewController, &AssociatedKeys.didObserveLifecycle, true, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 55 | } 56 | } 57 | 58 | private func observeLifecycle(of viewController: UIViewController) { 59 | if viewController.viewDidAppearHandler == nil { 60 | viewController.viewDidAppearHandler = {[weak viewController] in 61 | #if DEBUG 62 | print("ViewController \(type(of: viewController)) did appear") 63 | #endif 64 | if ScreenCaptureManager.shared.preventScreenCapture { 65 | viewController?.applyScreenCaptureProtection() 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ios/Extensions/UIView+Shield.swift: -------------------------------------------------------------------------------- 1 | extension UIView { 2 | private struct Constants { 3 | static var secureTextFieldTag: Int { 37125 } 4 | static var secureTextFieldKey = UnsafeRawPointer(bitPattern: "secureTextFieldKey".hashValue)! 5 | static var originalSuperlayerKey = UnsafeRawPointer(bitPattern: "originalSuperlayerKey".hashValue)! 6 | } 7 | 8 | func setScreenCaptureProtection(_ isModal: Bool) { 9 | guard let superview = superview else { 10 | return 11 | } 12 | 13 | if let _ = objc_getAssociatedObject(self, &Constants.secureTextFieldKey) as? UITextField { 14 | return // Already protected 15 | } 16 | 17 | // Step 1: Create the secureTextField to obscure screenshots 18 | layer.name = "originalLayer" 19 | let secureTextField = UITextField() 20 | secureTextField.backgroundColor = .clear 21 | secureTextField.translatesAutoresizingMaskIntoConstraints = false 22 | secureTextField.tag = Constants.secureTextFieldTag 23 | secureTextField.isSecureTextEntry = true 24 | secureTextField.isUserInteractionEnabled = false 25 | secureTextField.layer.name = "secureTextFieldLayer" 26 | 27 | // Step 2: Store a reference of the original layout 28 | if let originalSuperlayer = layer.superlayer { 29 | objc_setAssociatedObject(self, &Constants.originalSuperlayerKey, originalSuperlayer, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 30 | } 31 | 32 | // Step 3: Insert the secureTextField in the view hierarchy 33 | // and store its reference 34 | let isBottomSheet = self.nativeID != nil && self.nativeID == "BottomSheetComponent" 35 | let isPermalink = self.nativeID != nil && self.nativeID.lowercased().contains("permalink") 36 | if (isModal && !isBottomSheet && !isPermalink) { 37 | superview.insertSubview(secureTextField, at: 0) 38 | } else { 39 | insertSubview(secureTextField, at: 0) 40 | } 41 | objc_setAssociatedObject(self, &Constants.secureTextFieldKey, secureTextField, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 42 | 43 | // Step 4: Modify the layout 44 | layer.superlayer?.addSublayer(secureTextField.layer) 45 | secureTextField.layer.sublayers?.last?.addSublayer(layer) 46 | } 47 | 48 | public func removeScreenCaptureProtection() { 49 | guard let secureTextField = objc_getAssociatedObject(self, &Constants.secureTextFieldKey) as? UITextField else { 50 | return // Not found, assume already removed 51 | } 52 | 53 | // Step 1: Restore the original superlayer before removing secureTextField 54 | if let originalSuperlayer = objc_getAssociatedObject(self, &Constants.originalSuperlayerKey) as? CALayer { 55 | if layer.superlayer !== originalSuperlayer { 56 | // Temporarily remove the layer 57 | let tempLayer = CALayer() 58 | originalSuperlayer.addSublayer(tempLayer) 59 | 60 | layer.removeFromSuperlayer() 61 | secureTextField.removeFromSuperview() 62 | originalSuperlayer.addSublayer(layer) 63 | 64 | // Remove temp layer after forcing an update 65 | tempLayer.removeFromSuperlayer() 66 | } 67 | } 68 | 69 | // Step 3: Remove stored references 70 | objc_setAssociatedObject(self, &Constants.secureTextFieldKey, nil, .OBJC_ASSOCIATION_ASSIGN) 71 | objc_setAssociatedObject(self, &Constants.originalSuperlayerKey, nil, .OBJC_ASSOCIATION_ASSIGN) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /ios/Extensions/UIViewController+Shield.swift: -------------------------------------------------------------------------------- 1 | extension UIViewController { 2 | private struct ProtectionKeys { 3 | static var protectedViewKey = UnsafeRawPointer(bitPattern: "protectedViewKey".hashValue)! 4 | } 5 | 6 | private func getViewPriority(_ view: UIView) -> Int { 7 | if let nativeID = view.nativeID { 8 | if nativeID == "BottomSheetComponent" { return 4 } // Highest priority 9 | if nativeID.contains("shielded") { return 3 } // Second priority 10 | } 11 | 12 | let className = NSStringFromClass(type(of: view)) 13 | if className.contains("RNCSafeAreaView") || className.contains("RCTSafeAreaView") { 14 | return 2 // Third priority 15 | } 16 | if className.contains("RNCSafeAreaProvider") { 17 | return 1 // Lowest priority 18 | } 19 | 20 | return 0 // If nothing matches 21 | } 22 | 23 | func findProtectionTarget(in target: UIView) -> UIView? { 24 | var fallbackView: UIView? 25 | var safeAreaView: UIView? 26 | var shieldedView: UIView? 27 | var bottomSheetView: UIView? 28 | 29 | func traverse(_ view: UIView) -> Bool { 30 | let priority = getViewPriority(view) 31 | 32 | switch priority { 33 | case 4: // ✅ BottomSheetComponent found, stop immediately 34 | print("✅ Found \(NSStringFromClass(type(of: view))) (BottomSheetComponent), prioritizing it for protection") 35 | bottomSheetView = view 36 | return true 37 | case 3: // ✅ Shielded view, but keep searching for BottomSheetComponent 38 | if shieldedView == nil { 39 | print("✅ Found \(NSStringFromClass(type(of: view))) (Shielded), using as second priority") 40 | shieldedView = view 41 | } 42 | case 2: // ✅ SafeAreaView, but keep searching for higher priorities 43 | if safeAreaView == nil && shieldedView == nil { 44 | print("✅ Found \(NSStringFromClass(type(of: view))) (SafeAreaView), using as third priority") 45 | safeAreaView = view 46 | } 47 | case 1: // ✅ SafeAreaProvider, but lowest priority 48 | if fallbackView == nil && safeAreaView == nil && shieldedView == nil { 49 | fallbackView = view.subviews.first 50 | } 51 | default: 52 | break 53 | } 54 | 55 | // **Optimized DFS Traversal** 56 | let prioritizedSubviews = view.subviews.sorted { a, b in 57 | getViewPriority(a) > getViewPriority(b) // Higher priority first 58 | } 59 | 60 | for subview in prioritizedSubviews { 61 | if traverse(subview) { return true } // Stop if `BottomSheetComponent` is found 62 | } 63 | 64 | return false 65 | } 66 | 67 | let _ = traverse(target) 68 | 69 | return bottomSheetView ?? shieldedView ?? safeAreaView ?? fallbackView 70 | } 71 | 72 | func applyScreenCaptureProtection() { 73 | if self.view.layer.name == "Protected Layer" { 74 | return // already protected 75 | } 76 | 77 | ScreenCaptureManager.shared.logLayerHierarchy(self) 78 | guard let targetView = findProtectionTarget(in: self.view) else { 79 | print("🛑 No valid view found to protect for \(self)") 80 | return 81 | 82 | } 83 | 84 | if let nativeId = targetView.nativeID, 85 | nativeId.contains("skip.shielded") { 86 | print("🛑 >>> Skipping protection for: \(nativeId)") 87 | return 88 | } 89 | 90 | print("✅ Applying screen capture protection to: \(targetView.nativeID ?? "Unknown View")") 91 | var isModalFullScreen = false 92 | if let parentVC = self.parent, 93 | parentVC.modalPresentationStyle == .overFullScreen || parentVC.modalPresentationStyle == .fullScreen 94 | || parentVC.modalPresentationStyle == .overCurrentContext || parentVC.modalPresentationStyle == .currentContext { 95 | // Full-screen modals (e.g., .overFullScreen, .fullScreen) should not be treated as modals 96 | // for screen capture protection but rather as regular screens. 97 | isModalFullScreen = true 98 | } 99 | var isModal = self.isModalInPresentation 100 | if let navigationController = self.navigationController, 101 | let rootViewController = navigationController.viewControllers.first, 102 | rootViewController.presentingViewController != nil { 103 | isModal = !isModalFullScreen 104 | } 105 | 106 | targetView.setScreenCaptureProtection(isModal) 107 | self.view.layer.name = "Protected Layer" 108 | 109 | // Store the protected view in the View Controller 110 | objc_setAssociatedObject(self, &ProtectionKeys.protectedViewKey, targetView, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 111 | } 112 | 113 | func removeScreenCaptureProtection() { 114 | guard let protectedView = objc_getAssociatedObject(self, &ProtectionKeys.protectedViewKey) as? UIView else { 115 | print("⚠️ No stored protected view found, skipping removal") 116 | return 117 | } 118 | 119 | print("✅ Removing screen capture protection from: \(protectedView)") 120 | protectedView.removeScreenCaptureProtection() 121 | objc_setAssociatedObject(self, &ProtectionKeys.protectedViewKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 122 | self.view.layer.name = nil 123 | CATransaction.flush() 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /ios/Extensions/UIViewController+Swizzle.swift: -------------------------------------------------------------------------------- 1 | extension UIViewController { 2 | private struct AssociatedKeys { 3 | static var viewDidAppearHandler: UnsafeRawPointer = UnsafeRawPointer(bitPattern: "viewDidAppearHandler".hashValue)! 4 | } 5 | 6 | var viewDidAppearHandler: (() -> Void)? { 7 | get { return objc_getAssociatedObject(self, AssociatedKeys.viewDidAppearHandler) as? (() -> Void) } 8 | set { objc_setAssociatedObject(self, AssociatedKeys.viewDidAppearHandler, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 9 | } 10 | 11 | static let swizzleViewControllerLifecycleMethods: Void = { 12 | guard let targetClass = NSClassFromString("RNNComponentViewController") as? UIViewController.Type else { 13 | print("⚠️ RNNComponentViewController not found") 14 | return 15 | } 16 | 17 | let methodPairs: [(Selector, Selector)] = [ 18 | (#selector(viewDidAppear(_:)), #selector(swizzled_viewDidAppear(_:))), 19 | ] 20 | 21 | for (originalSelector, swizzledSelector) in methodPairs { 22 | swizzleMethod(for: targetClass, original: originalSelector, swizzled: swizzledSelector) 23 | } 24 | }() 25 | 26 | private static func swizzleMethod(for targetClass: UIViewController.Type, original: Selector, swizzled: Selector) { 27 | guard let originalMethod = class_getInstanceMethod(targetClass, original), 28 | let swizzledMethod = class_getInstanceMethod(UIViewController.self, swizzled) else { 29 | return 30 | } 31 | 32 | let didAddMethod = class_addMethod( 33 | targetClass, 34 | swizzled, 35 | method_getImplementation(swizzledMethod), 36 | method_getTypeEncoding(swizzledMethod) 37 | ) 38 | 39 | if didAddMethod { 40 | guard let newSwizzledMethod = class_getInstanceMethod(targetClass, swizzled) else { return } 41 | method_exchangeImplementations(originalMethod, newSwizzledMethod) 42 | } else { 43 | method_exchangeImplementations(originalMethod, swizzledMethod) 44 | } 45 | } 46 | 47 | @objc private func swizzled_viewDidAppear(_ animated: Bool) { 48 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: { // Check if needed again 49 | self.viewDidAppearHandler?() 50 | self.swizzled_viewDidAppear(animated) 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ios/Extensions/UIWindow+Swizzle.swift: -------------------------------------------------------------------------------- 1 | extension UIWindow { 2 | private static var originalSetWindowLevel: IMP? 3 | 4 | static let swizzleRNNOverlayPresentation: Void = { 5 | guard let targetClass = NSClassFromString("RNNOverlayWindow") as? UIWindow.Type else { 6 | print("⚠️ RNNOverlayWindow not found") 7 | return 8 | } 9 | 10 | let originalSelector = #selector(setter: UIWindow.windowLevel) 11 | let swizzledSelector = #selector(swizzled_setWindowLevel(_:)) 12 | 13 | swizzleMethod(for: targetClass, original: originalSelector, swizzled: swizzledSelector) 14 | }() 15 | 16 | private static func swizzleMethod(for targetClass: UIWindow.Type, original: Selector, swizzled: Selector) { 17 | guard let originalMethod = class_getInstanceMethod(targetClass, original), 18 | let swizzledMethod = class_getInstanceMethod(UIWindow.self, swizzled) else { 19 | return 20 | } 21 | 22 | originalSetWindowLevel = method_getImplementation(originalMethod) 23 | 24 | let didAddMethod = class_addMethod( 25 | targetClass, 26 | swizzled, 27 | method_getImplementation(swizzledMethod), 28 | method_getTypeEncoding(swizzledMethod) 29 | ) 30 | 31 | if didAddMethod { 32 | guard let newSwizzledMethod = class_getInstanceMethod(targetClass, swizzled) else { return } 33 | method_exchangeImplementations(originalMethod, newSwizzledMethod) 34 | } else { 35 | method_exchangeImplementations(originalMethod, swizzledMethod) 36 | } 37 | } 38 | 39 | @objc private func swizzled_setWindowLevel(_ level: UIWindow.Level) { 40 | let windowClassName = String(describing: type(of: self)) 41 | print("Window Class: \(windowClassName)") 42 | 43 | // Ensure the method only runs for RNNOverlayWindow 44 | if let windowClass = NSClassFromString("RNNOverlayWindow"), type(of: self) == windowClass { 45 | print("[RNN Overlay] Overlay visibility changed to: \(level.rawValue) - \(self)") 46 | 47 | if level == .normal && ScreenCaptureManager.shared.preventScreenCapture, 48 | let rootVC = self.rootViewController { 49 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { 50 | ScreenCaptureManager.shared.logLayerHierarchy(rootVC) 51 | rootVC.applyScreenCaptureProtection() 52 | } 53 | } 54 | } 55 | 56 | // Always call the original method at the end 57 | callOriginalSetWindowLevel(level) 58 | } 59 | 60 | 61 | private func callOriginalSetWindowLevel(_ level: UIWindow.Level) { 62 | guard let originalIMP = UIWindow.originalSetWindowLevel else { return } 63 | 64 | typealias Function = @convention(c) (AnyObject, Selector, UIWindow.Level) -> Void 65 | let function = unsafeBitCast(originalIMP, to: Function.self) 66 | 67 | function(self, #selector(setter: UIWindow.windowLevel), level) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /ios/ScreenCaptureManager.swift: -------------------------------------------------------------------------------- 1 | @objc public class ScreenCaptureManager: NSObject { 2 | @objc public static let shared = ScreenCaptureManager() 3 | 4 | // MARK: Properties 5 | internal var blurView: UIImageView? = nil 6 | var isAuthenticating: Bool = false 7 | var blurOnAuthenticate: Bool = false 8 | 9 | var preventScreenCapture: Bool = false { 10 | didSet { 11 | // here we should traverse the viewControllers 12 | // and apply or remove the protection 13 | self.applyScreenCapturePolicy() 14 | } 15 | } 16 | 17 | // MARK: Swizzle for RNN 18 | public static func startTrackingScreens() { 19 | UIWindow.swizzleRNNOverlayPresentation 20 | UIViewController.swizzleViewControllerLifecycleMethods 21 | UINavigationController.swizzleNavigationTracking() 22 | } 23 | 24 | // MARK: Event listener 25 | private func listenForScreenCapture() { 26 | if self.preventScreenCapture { 27 | NotificationCenter.default.addObserver( 28 | self, 29 | selector: #selector(applyBlurEffect(notification:)), 30 | name: UIScreen.capturedDidChangeNotification, 31 | object: nil 32 | ) 33 | return 34 | } 35 | 36 | NotificationCenter.default.removeObserver( 37 | self, 38 | name: UIScreen.capturedDidChangeNotification, 39 | object: nil 40 | ) 41 | } 42 | 43 | func screenCaptureStatusChange() { 44 | if self.preventScreenCapture && UIScreen.main.isCaptured && !self.isAuthenticating { 45 | applyBlurEffect() // Prevent screen recording 46 | } else { 47 | removeBlurEffect() 48 | } 49 | } 50 | 51 | // MARK: Windows and ViewControllers 52 | func getKeyWindow() -> UIWindow? { 53 | if #available(iOS 15.0, *) { 54 | return UIApplication.shared.connectedScenes 55 | .compactMap { $0 as? UIWindowScene } 56 | .flatMap { $0.windows } 57 | .first { $0.isKeyWindow } 58 | } else { 59 | return UIApplication.shared.windows.first { $0.isKeyWindow } 60 | } 61 | } 62 | 63 | func getLastKeyWindow() -> UIWindow? { 64 | if #available(iOS 15.0, *) { 65 | return UIApplication.shared.connectedScenes 66 | .compactMap { $0 as? UIWindowScene } 67 | .flatMap { $0.windows } 68 | .last { $0.isKeyWindow } 69 | } else { 70 | return UIApplication.shared.windows.last { $0.isKeyWindow } 71 | } 72 | } 73 | 74 | func getAllKeyWindows() -> [UIWindow] { 75 | if #available(iOS 15.0, *) { 76 | return UIApplication.shared.connectedScenes 77 | .compactMap { $0 as? UIWindowScene } 78 | .flatMap { $0.windows } 79 | } else { 80 | return UIApplication.shared.windows 81 | } 82 | } 83 | 84 | private func getAllRootViewControllers() -> [UIViewController] { 85 | return getAllKeyWindows().compactMap { $0.rootViewController } 86 | } 87 | 88 | private func getAllPresentedViewControllers(from viewController: UIViewController) -> [UIViewController] { 89 | var presentedVCs: [UIViewController] = [] 90 | 91 | var currentVC: UIViewController? = viewController 92 | while let vc = currentVC { 93 | presentedVCs.append(vc) 94 | currentVC = vc.presentedViewController 95 | } 96 | 97 | return presentedVCs 98 | } 99 | 100 | // MARK: Screenshot protection functions 101 | private func applyPolicyToViewController(_ vc: UIViewController) { 102 | if self.preventScreenCapture { 103 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 104 | vc.applyScreenCaptureProtection() 105 | } 106 | return 107 | } 108 | 109 | vc.removeScreenCaptureProtection() 110 | } 111 | 112 | private func applyScreenCapturePolicy() { 113 | DispatchQueue.main.async { 114 | for rootVC in self.getAllRootViewControllers() { 115 | let c = NSStringFromClass(type(of: rootVC)) 116 | if c.contains("RNNStackController"), let r = rootVC as? UINavigationController { 117 | for vc in r.viewControllers { 118 | print(String(describing: type(of: vc))) 119 | self.applyPolicyToViewController(vc) 120 | } 121 | } 122 | if let modalVC = rootVC.presentedViewController { 123 | let c = NSStringFromClass(type(of: modalVC)) 124 | print(String(describing: type(of: modalVC))) 125 | if c.contains("RNNStackController"), let r = modalVC as? UINavigationController { 126 | for vc in r.viewControllers { 127 | print(String(describing: type(of: vc))) 128 | self.applyPolicyToViewController(vc) 129 | } 130 | } 131 | } 132 | } 133 | } 134 | } 135 | 136 | // MARK: Blur effect functions 137 | func createBlurEffect(window: UIWindow, toImage image: inout UIImage, radius: Double) { 138 | let ciImage = CIImage(image: image) 139 | let filter = CIFilter(name: "CIGaussianBlur") 140 | filter?.setValue(ciImage, forKey: "inputImage") 141 | filter?.setValue(radius, forKey: "inputRadius") 142 | guard let blurredImage = filter?.outputImage else { 143 | image = UIImage() 144 | return 145 | } 146 | let croppedFrame = CGRect( 147 | x: window.frame.origin.x, 148 | y: window.frame.origin.y, 149 | width: window.frame.width * UIScreen.main.scale, 150 | height: window.frame.height * UIScreen.main.scale 151 | ) 152 | let cover = blurredImage.cropped(to: croppedFrame) 153 | image = UIImage.init(ciImage: cover) 154 | } 155 | 156 | func screenShot(window: UIWindow) -> UIImage { 157 | let scale = UIScreen.main.scale 158 | UIGraphicsBeginImageContextWithOptions(window.frame.size, false, scale); 159 | window.layer.render(in: UIGraphicsGetCurrentContext()!) 160 | let screenshot = UIGraphicsGetImageFromCurrentImageContext() 161 | UIGraphicsEndImageContext() 162 | return screenshot ?? UIImage() 163 | } 164 | 165 | @objc func applyBlurEffect(notification: Notification) { 166 | self.applyBlurEffect(radius: 10.0) 167 | } 168 | 169 | 170 | @objc public func applyBlurEffect(radius: Double = 10.0) { 171 | if self.blurView == nil && ( 172 | (self.preventScreenCapture && !self.isAuthenticating) || 173 | (self.isAuthenticating && self.blurOnAuthenticate) 174 | ) { 175 | DispatchQueue.main.async { 176 | if let window = self.getKeyWindow() { 177 | if self.blurView == nil { 178 | self.blurView = UIImageView() 179 | } 180 | guard let blurView = self.blurView else { return } 181 | var cover = self.screenShot(window: window) 182 | blurView.frame = window.frame 183 | blurView.contentMode = .scaleToFill 184 | blurView.backgroundColor = UIColor.gray 185 | window.addSubview(blurView) 186 | self.createBlurEffect(window: window, toImage: &cover, radius: radius) 187 | blurView.image = cover 188 | } 189 | } 190 | } 191 | } 192 | 193 | @objc public func removeBlurEffect(forced: Bool = false) { 194 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 195 | if self.blurView != nil && ((!UIScreen.main.isCaptured && !self.isAuthenticating) || forced) { 196 | self.blurView?.removeFromSuperview() 197 | self.blurView = nil 198 | } 199 | } 200 | } 201 | 202 | // MARK: Print View hierarchy tree for debugging purposes 203 | func logLayerHierarchy(_ view: UIView?, prefix: String = "") { 204 | #if DEBUG 205 | guard let view = view else { return } 206 | let viewClassName: String = String(describing: type(of: view)) 207 | print("\(prefix)└── View: \(viewClassName), Layer: \(view.layer.name ?? "nil"), NativeID: \(String(describing: view.nativeID))") 208 | 209 | if view is UITextField { 210 | print("\(prefix) (FOUND TEXTFIELD)") 211 | } 212 | 213 | let subviewCount = view.subviews.count 214 | for (index, subview) in view.subviews.enumerated() { 215 | let isLast = index == subviewCount - 1 216 | let newPrefix = prefix + (isLast ? " " : "│ ") 217 | logLayerHierarchy(subview, prefix: newPrefix) 218 | } 219 | #endif 220 | } 221 | 222 | func logLayerHierarchy(_ vc: UIViewController?, prefix: String = "") { 223 | guard let vc = vc else { return } 224 | logLayerHierarchy(vc.view, prefix: prefix) 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mattermost/react-native-emm", 3 | "version": "1.6.2", 4 | "description": "React Native package for EMM managed configurations", 5 | "main": "lib/commonjs/index", 6 | "module": "lib/module/index", 7 | "types": "lib/typescript/index.d.ts", 8 | "react-native": "src/index.ts", 9 | "source": "src/index", 10 | "files": [ 11 | "src", 12 | "lib", 13 | "android", 14 | "ios", 15 | "cpp", 16 | "react-native-emm.podspec", 17 | "!lib/typescript/example", 18 | "!**/__tests__", 19 | "!**/__fixtures__", 20 | "!**/__mocks__", 21 | "!example", 22 | "!.vscode", 23 | "!**/.idea", 24 | "!**/.gradle", 25 | "!android/build", 26 | "!ios/Build", 27 | ".circleci" 28 | ], 29 | "scripts": { 30 | "test": "jest", 31 | "typescript": "tsc --noEmit", 32 | "lint": "eslint \"**/*.{js,ts,tsx}\"", 33 | "prepare": "bob build", 34 | "release": "release-it", 35 | "example": "cd example && npm", 36 | "pods": "cd example && RCT_NEW_ARCH_ENABLED=1 pod-install --quiet", 37 | "bootstrap": "npm run example i && npm run pods" 38 | }, 39 | "keywords": [ 40 | "react-native", 41 | "ios", 42 | "android", 43 | "emm" 44 | ], 45 | "repository": "https://github.com/mattermost/react-native-emm", 46 | "author": "Mattermost (https://github.com/mattermost)", 47 | "license": "MIT", 48 | "bugs": { 49 | "url": "https://github.com/mattermost/react-native-emm/issues" 50 | }, 51 | "homepage": "https://github.com/mattermost/react-native-emm#readme", 52 | "devDependencies": { 53 | "@commitlint/config-conventional": "19.6.0", 54 | "@react-native/eslint-config": "0.76.5", 55 | "@react-native/metro-config": "0.76.5", 56 | "@release-it/conventional-changelog": "9.0.3", 57 | "@types/jest": "29.5.14", 58 | "@types/react": "18.3.1", 59 | "@typescript-eslint/eslint-plugin": "7.14.1", 60 | "@typescript-eslint/parser": "7.14.1", 61 | "commitlint": "19.6.1", 62 | "eslint": "8.57.0", 63 | "eslint-config-prettier": "9.1.0", 64 | "eslint-plugin-flowtype": "8.0.3", 65 | "eslint-plugin-prettier": "5.2.1", 66 | "husky": "9.1.7", 67 | "jest": "29.7.0", 68 | "pod-install": "0.3.2", 69 | "prettier": "3.4.2", 70 | "react": "18.3.1", 71 | "react-native": "0.76.5", 72 | "react-native-builder-bob": "0.35.2", 73 | "release-it": "17.10.0", 74 | "typescript": "5.7.2" 75 | }, 76 | "peerDependencies": { 77 | "react": "*", 78 | "react-native": "*" 79 | }, 80 | "codegenConfig": { 81 | "name": "EmmSpec", 82 | "type": "modules", 83 | "jsSrcsDir": "./src", 84 | "android": { 85 | "javaPackageName": "com.mattermost.emm" 86 | } 87 | }, 88 | "jest": { 89 | "preset": "react-native", 90 | "modulePathIgnorePatterns": [ 91 | "/example/node_modules", 92 | "/lib/" 93 | ] 94 | }, 95 | "commitlint": { 96 | "extends": [ 97 | "@commitlint/config-conventional" 98 | ] 99 | }, 100 | "release-it": { 101 | "git": { 102 | "commitMessage": "chore: release ${version}", 103 | "tagName": "v${version}" 104 | }, 105 | "npm": { 106 | "publish": false 107 | }, 108 | "github": { 109 | "release": true 110 | }, 111 | "plugins": { 112 | "@release-it/conventional-changelog": { 113 | "preset": { 114 | "name": "conventionalcommits" 115 | } 116 | } 117 | } 118 | }, 119 | "eslintConfig": { 120 | "parser": "@typescript-eslint/parser", 121 | "extends": [ 122 | "@react-native", 123 | "prettier" 124 | ], 125 | "rules": { 126 | "prettier/prettier": [ 127 | "error", 128 | { 129 | "quoteProps": "consistent", 130 | "singleQuote": true, 131 | "tabWidth": 2, 132 | "trailingComma": "es5", 133 | "useTabs": false 134 | } 135 | ] 136 | } 137 | }, 138 | "eslintIgnore": [ 139 | "node_modules/", 140 | "lib/" 141 | ], 142 | "prettier": { 143 | "quoteProps": "consistent", 144 | "singleQuote": true, 145 | "tabWidth": 4, 146 | "trailingComma": "es5", 147 | "useTabs": false 148 | }, 149 | "react-native-builder-bob": { 150 | "source": "src", 151 | "output": "lib", 152 | "targets": [ 153 | "commonjs", 154 | "module", 155 | [ 156 | "typescript", 157 | { 158 | "project": "tsconfig.build.json" 159 | } 160 | ] 161 | ] 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /react-native-emm.podspec: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | package = JSON.parse(File.read(File.join(__dir__, "package.json"))) 4 | 5 | Pod::Spec.new do |s| 6 | s.name = "react-native-emm" 7 | s.version = package["version"] 8 | s.summary = package["description"] 9 | s.homepage = package["homepage"] 10 | s.license = package["license"] 11 | s.authors = package["author"] 12 | 13 | s.platforms = { :ios => "12.4" } 14 | s.source = { :git => "https://github.com/mattermost/react-native-emm.git", :tag => "#{s.version}" } 15 | s.swift_version = '5.0' 16 | 17 | 18 | s.source_files = "ios/**/*.{h,m,mm,swift}" 19 | 20 | fabric_enabled = ENV["RCT_NEW_ARCH_ENABLED"] == "1" 21 | 22 | if fabric_enabled 23 | s.pod_target_xcconfig = { 24 | "DEFINES_MODULE" => "YES", 25 | "BUILD_LIBRARY_FOR_DISTRIBUTION" => "YES", 26 | "OTHER_CPLUSPLUSFLAGS" => "-DRCT_NEW_ARCH_ENABLED=1", 27 | "OTHER_SWIFT_FLAGS" => "-no-verify-emitted-module-interface" 28 | } 29 | else 30 | s.pod_target_xcconfig = { 31 | "DEFINES_MODULE" => "YES", 32 | "BUILD_LIBRARY_FOR_DISTRIBUTION" => "YES", 33 | "OTHER_SWIFT_FLAGS" => "-no-verify-emitted-module-interface" 34 | } 35 | end 36 | 37 | install_modules_dependencies(s) 38 | 39 | end 40 | -------------------------------------------------------------------------------- /src/NativeEmm.ts: -------------------------------------------------------------------------------- 1 | import {type TurboModule, TurboModuleRegistry} from 'react-native'; 2 | import type { Double, UnsafeObject } from 'react-native/Libraries/Types/CodegenTypes'; 3 | 4 | type AuthenticateConfig = { 5 | reason?: string; 6 | description?: string; 7 | fallback?: boolean; 8 | supressEnterPassword?: boolean; 9 | blurOnAuthenticate?: boolean; 10 | }; 11 | 12 | type AuthenticationMethods = { 13 | readonly face: boolean; 14 | readonly fingerprint: boolean; 15 | readonly passcode: boolean; 16 | }; 17 | 18 | export interface Spec extends TurboModule { 19 | readonly getConstants:() => {}; 20 | 21 | addListener: (eventType: string) => void; 22 | removeListeners: (count: number) => void; 23 | 24 | authenticate(options: AuthenticateConfig): Promise; 25 | deviceSecureWith: () => Promise; 26 | exitApp: () => void; 27 | getManagedConfig: () => UnsafeObject; 28 | openSecuritySettings: () => void; 29 | setAppGroupId: (identifier: string) => void; 30 | setBlurScreen: (enabled: boolean) => void; 31 | applyBlurEffect: (radius: Double) => void; 32 | removeBlurEffect: () => void; 33 | }; 34 | 35 | export default TurboModuleRegistry.get('Emm'); 36 | -------------------------------------------------------------------------------- /src/context.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | createContext, 3 | useEffect, 4 | useState, 5 | useContext, 6 | type ComponentType, 7 | } from 'react'; 8 | import Emm from './emm'; 9 | 10 | interface Props { 11 | children: React.ReactNode; 12 | } 13 | 14 | const initialContext = Emm.getManagedConfig(); 15 | const Context = createContext(initialContext); 16 | 17 | export function useManagedConfig(): T { 18 | return useContext(Context); 19 | } 20 | 21 | export const Provider = ({ children }: Props) => { 22 | const [managed, setManaged] = useState(Emm.getManagedConfig()); 23 | 24 | useEffect(() => { 25 | const listener = Emm.addListener((config: unknown) => { 26 | setManaged(config); 27 | }); 28 | 29 | return () => { 30 | listener.remove(); 31 | }; 32 | }); 33 | 34 | return {children}; 35 | }; 36 | 37 | export function withManagedConfig( 38 | Component: ComponentType

39 | ): ComponentType

{ 40 | return function ManagedConfigComponent(props: P) { 41 | return ( 42 | 43 | {(managedConfig: T) => ( 44 | 45 | )} 46 | 47 | ); 48 | }; 49 | } 50 | 51 | export default Context; 52 | -------------------------------------------------------------------------------- /src/emm-native.ts: -------------------------------------------------------------------------------- 1 | export default require("./NativeEmm").default; -------------------------------------------------------------------------------- /src/emm.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NativeEventEmitter, 3 | Platform, 4 | } from 'react-native'; 5 | 6 | import type { 7 | AuthenticateConfig, 8 | AuthenticationMethods, 9 | } from './types/authenticate'; 10 | import type { 11 | EnterpriseMobilityManager, 12 | ManagedConfigCallBack, 13 | } from './types/managed'; 14 | 15 | import RNEmm from './emm-native'; 16 | 17 | const emitter = new NativeEventEmitter(RNEmm); 18 | 19 | const Emm: EnterpriseMobilityManager = { 20 | addListener: (callback: ManagedConfigCallBack) => { 21 | return emitter.addListener('managedConfigChanged', (config: T) => { 22 | callback(config); 23 | }); 24 | }, 25 | authenticate: async (opts: AuthenticateConfig) => { 26 | try { 27 | const options: AuthenticateConfig = { 28 | reason: opts.reason || '', 29 | description: opts.description || '', 30 | fallback: opts.fallback || true, 31 | supressEnterPassword: opts.supressEnterPassword || false, 32 | blurOnAuthenticate: opts.blurOnAuthenticate || false, 33 | }; 34 | 35 | await RNEmm.authenticate(options); 36 | 37 | return true; 38 | } catch { 39 | return false; 40 | } 41 | }, 42 | getManagedConfig: () => RNEmm.getManagedConfig() as T, 43 | isDeviceSecured: async () => { 44 | try { 45 | const result: AuthenticationMethods = await RNEmm.deviceSecureWith(); 46 | return result.face || result.fingerprint || result.passcode; 47 | } catch { 48 | return false; 49 | } 50 | }, 51 | openSecuritySettings: () => { 52 | if (Platform.OS === 'android') { 53 | RNEmm.openSecuritySettings(); 54 | } 55 | }, 56 | setAppGroupId: (identifier: string) => { 57 | if (Platform.OS === 'ios') { 58 | RNEmm.setAppGroupId(identifier); 59 | } 60 | }, 61 | deviceSecureWith: function (): Promise { 62 | return RNEmm.deviceSecureWith(); 63 | }, 64 | enableBlurScreen: function (enabled: boolean): void { 65 | return RNEmm.setBlurScreen(enabled); 66 | }, 67 | applyBlurEffect: (radius = 8) => RNEmm.applyBlurEffect(radius), 68 | removeBlurEffect: () => RNEmm.removeBlurEffect(), 69 | exitApp: function (): void { 70 | RNEmm.exitApp(); 71 | } 72 | }; 73 | 74 | export default Emm; 75 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './context'; 2 | import Emm from './emm'; 3 | export * from './types/authenticate'; 4 | export * from './types/managed'; 5 | 6 | export default Emm; 7 | -------------------------------------------------------------------------------- /src/types/authenticate.ts: -------------------------------------------------------------------------------- 1 | export type AuthenticateConfig = { 2 | reason?: string; 3 | description?: string; 4 | fallback?: boolean; 5 | supressEnterPassword?: boolean; 6 | blurOnAuthenticate?: boolean; 7 | }; 8 | 9 | export type AuthenticationMethods = { 10 | readonly face: boolean; 11 | readonly fingerprint: boolean; 12 | readonly passcode: boolean; 13 | }; 14 | -------------------------------------------------------------------------------- /src/types/managed.ts: -------------------------------------------------------------------------------- 1 | import type { EmitterSubscription } from 'react-native'; 2 | import type { AuthenticateConfig, AuthenticationMethods } from './authenticate'; 3 | 4 | export type ManagedConfigCallBack = { 5 | (config: T): void; 6 | }; 7 | 8 | export interface EnterpriseMobilityManager { 9 | addListener(callback: ManagedConfigCallBack): EmitterSubscription; 10 | 11 | authenticate(opts: AuthenticateConfig): Promise; 12 | 13 | deviceSecureWith(): Promise; 14 | 15 | enableBlurScreen(enabled: boolean): void; 16 | 17 | applyBlurEffect: (radius: number) => void; 18 | 19 | removeBlurEffect: () => void; 20 | 21 | exitApp(): void; 22 | 23 | getManagedConfig(): T; 24 | 25 | isDeviceSecured(): Promise; 26 | 27 | openSecuritySettings(): void; 28 | 29 | setAppGroupId(identifier: string): void; 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["example"] 4 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@mattermost/react-native-emm": ["./src/index.ts"] 6 | }, 7 | "allowSyntheticDefaultImports": true, 8 | "allowUnreachableCode": false, 9 | "allowUnusedLabels": false, 10 | "esModuleInterop": true, 11 | "verbatimModuleSyntax": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "jsx": "react-native", 14 | "lib": ["esnext"], 15 | "types": ["react-native", "jest"], 16 | "isolatedModules": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "noFallthroughCasesInSwitch": true, 20 | "noImplicitReturns": true, 21 | "noImplicitUseStrict": false, 22 | "noStrictGenericChecks": false, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true, 25 | "resolveJsonModule": true, 26 | "skipLibCheck": true, 27 | "strict": true, 28 | "target": "esnext", 29 | }, 30 | } 31 | --------------------------------------------------------------------------------