diff --git a/.eslintignore b/.eslintignore index 6e1343ef..7aa84596 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,7 @@ .eslintrc.js dist/ example/ +scripts/ fetchWindowsCapabilites.js mock.js node_modules diff --git a/README.md b/README.md index 2afe82db..79b639aa 100644 --- a/README.md +++ b/README.md @@ -26,43 +26,72 @@ $ yarn add react-native-permissions ### iOS -By default no permission handler is linked. To add one, update your `package.json` by adding the permissions used in your app, then run `npx react-native setup-ios-permissions` followed by `pod install` (`reactNativePermissionsIOS.json` is also supported). - -_📌  Note that these commands must be re-executed each time you update this config, delete the `node_modules` directory or update this library. An useful trick to cover a lot of these cases is running them on `postinstall` and just run `yarn` or `npm install` manually when needed._ - -```json -{ - "reactNativePermissionsIOS": [ - "AppTrackingTransparency", - "BluetoothPeripheral", - "Calendars", - "Camera", - "Contacts", - "FaceID", - "LocationAccuracy", - "LocationAlways", - "LocationWhenInUse", - "MediaLibrary", - "Microphone", - "Motion", - "Notifications", - "PhotoLibrary", - "PhotoLibraryAddOnly", - "Reminders", - "Siri", - "SpeechRecognition", - "StoreKit" - ], - "devDependencies": { - "pod-install": "0.1.38" - }, - "scripts": { - "postinstall": "react-native setup-ios-permissions && pod-install" - } -} -``` - -Then update your `Info.plist` with wanted permissions usage descriptions: +1. By default, no permissions are setuped. So first, require the `setup` script in your `Podfile`: + +```diff +# with react-native >= 0.72 +- # Resolve react_native_pods.rb with node to allow for hoisting +- require Pod::Executable.execute_command('node', ['-p', +- 'require.resolve( +- "react-native/scripts/react_native_pods.rb", +- {paths: [process.argv[1]]}, +- )', __dir__]).strip + ++ def node_require(script) ++ # Resolve script with node to allow for hoisting ++ require Pod::Executable.execute_command('node', ['-p', ++ "require.resolve( ++ '#{script}', ++ {paths: [process.argv[1]]}, ++ )", __dir__]).strip ++ end + ++ node_require('react-native/scripts/react_native_pods.rb') ++ node_require('react-native-permissions/scripts/setup.rb') +``` + +```diff +# with react-native < 0.72 +require_relative '../node_modules/react-native/scripts/react_native_pods' +require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' ++ require_relative '../node_modules/react-native-permissions/scripts/setup' +``` + +2. Then in the same file, add a `setup_permissions` call with the wanted permissions: + +```ruby +# … + +platform :ios, min_ios_version_supported +prepare_react_native_project! + +# ⬇️ uncomment wanted permissions (don't forget to remove the last comma) +setup_permissions([ + # 'AppTrackingTransparency', + # 'BluetoothPeripheral', + # 'Calendars', + # 'Camera', + # 'Contacts', + # 'FaceID', + # 'LocationAccuracy', + # 'LocationAlways', + # 'LocationWhenInUse', + # 'MediaLibrary', + # 'Microphone', + # 'Motion', + # 'Notifications', + # 'PhotoLibrary', + # 'PhotoLibraryAddOnly', + # 'Reminders', + # 'SpeechRecognition', + # 'StoreKit' +]) + +# … +``` + +3. Then execute `pod install` _(📌  Note that it must be re-executed each time you update this config)_. +4. Finally, update your `Info.plist` with the wanted permissions usage descriptions: ```xml @@ -181,7 +210,7 @@ Open the project solution file from the `windows` folder. In the app project ope ## 🆘 Manual linking -Because this package targets React Native 0.63.0+, you probably won't need to link it manually. Otherwise if it's not the case, follow these additional instructions. You also need to manual link the module on Windows when using React Native Windows prior to 0.63: +Because this package targets recent React Native versions, you probably don't need to link it manually. But if you have a special case, follow these additional instructions:
👀 See manual linking instructions @@ -707,7 +736,7 @@ check(PERMISSIONS.IOS.LOCATION_ALWAYS) Request one permission. -Note that the `rationale` parameter is only available and used on Android. +The `rationale` is only available and used on Android. It can be a native alert (a `Rationale` object) or a custom implementation (that resolves with a `boolean`). ```ts type Rationale = { @@ -718,7 +747,10 @@ type Rationale = { buttonNeutral?: string; }; -function request(permission: string, rationale?: Rationale): Promise; +function request( + permission: string, + rationale?: Rationale | (() => Promise), +): Promise; ``` ```js diff --git a/example/ios/Podfile b/example/ios/Podfile index 9dd538d3..8ab91407 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,13 +1,39 @@ -# Resolve react_native_pods.rb with node to allow for hoisting -require Pod::Executable.execute_command('node', ['-p', - 'require.resolve( - "react-native/scripts/react_native_pods.rb", - {paths: [process.argv[1]]}, - )', __dir__]).strip +def node_require(script) + # Resolve script with node to allow for hoisting + require Pod::Executable.execute_command('node', ['-p', + "require.resolve( + '#{script}', + {paths: [process.argv[1]]}, + )", __dir__]).strip +end + +node_require('react-native/scripts/react_native_pods.rb') +node_require('react-native-permissions/scripts/setup.rb') platform :ios, min_ios_version_supported prepare_react_native_project! +setup_permissions([ + 'AppTrackingTransparency', + 'BluetoothPeripheral', + 'Calendars', + 'Camera', + 'Contacts', + 'FaceID', + 'LocationAccuracy', + 'LocationAlways', + 'LocationWhenInUse', + 'MediaLibrary', + 'Microphone', + 'Motion', + 'Notifications', + 'PhotoLibrary', + 'PhotoLibraryAddOnly', + 'Reminders', + 'SpeechRecognition', + 'StoreKit' +]) + # If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set. # because `react-native-flipper` depends on (FlipperKit,...) that will be excluded # diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 4f720b97..9ae1be2d 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -487,7 +487,7 @@ PODS: - React-jsi (= 0.72.4) - React-logger (= 0.72.4) - React-perflogger (= 0.72.4) - - RNPermissions (3.8.4): + - RNPermissions (3.9.0): - React-Core - RNVectorIcons (10.0.0): - React-Core @@ -720,12 +720,12 @@ SPEC CHECKSUMS: React-runtimescheduler: 4941cc1b3cf08b792fbf666342c9fc95f1969035 React-utils: b79f2411931f9d3ea5781404dcbb2fa8a837e13a ReactCommon: 4b2bdcb50a3543e1c2b2849ad44533686610826d - RNPermissions: ed00174a2d6efeff72f32af33332b28f64918e7c + RNPermissions: 535ef85ad2e77d65bc90ee2afcb734619cea6f56 RNVectorIcons: 8b5bb0fa61d54cd2020af4f24a51841ce365c7e9 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Yoga: 3efc43e0d48686ce2e8c60f99d4e6bd349aff981 YogaKit: f782866e155069a2cca2517aafea43200b01fd5a -PODFILE CHECKSUM: fff8f587e10e262f18b440ce402f9b16a0fcafda +PODFILE CHECKSUM: d10907374d33d217871dd1ac7944e625d6d03f6c COCOAPODS: 1.12.1 diff --git a/example/package.json b/example/package.json index 5bb49b74..93e42e0a 100644 --- a/example/package.json +++ b/example/package.json @@ -9,30 +9,10 @@ "clean-modules": "rm -rf ./node_modules/react-native-permissions/{example,node_modules}", "clean": "rm -rf ./node_modules ./ios/Pods", "preinstall": "cd .. && yarn prepack && cd example", - "postinstall": "yarn clean-modules && react-native setup-ios-permissions && pod-install", + "postinstall": "yarn clean-modules && pod-install", "start": "react-native start", "reinstall": "yarn clean && yarn install" }, - "reactNativePermissionsIOS": [ - "AppTrackingTransparency", - "BluetoothPeripheral", - "Calendars", - "Camera", - "Contacts", - "FaceID", - "LocationAccuracy", - "LocationAlways", - "LocationWhenInUse", - "MediaLibrary", - "Microphone", - "Motion", - "Notifications", - "PhotoLibrary", - "PhotoLibraryAddOnly", - "Reminders", - "SpeechRecognition", - "StoreKit" - ], "dependencies": { "react": "18.2.0", "react-native": "0.72.4", diff --git a/example/yarn.lock b/example/yarn.lock index 42bd9f16..53c3c88f 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -4621,13 +4621,6 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" -pkg-dir@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760" - integrity sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA== - dependencies: - find-up "^5.0.0" - pod-install@0.1.39: version "0.1.39" resolved "https://registry.yarnpkg.com/pod-install/-/pod-install-0.1.39.tgz#853a0585bafbd332c2ca6543854fd4919958cfb3" @@ -4740,9 +4733,6 @@ react-native-paper@^5.10.3: react-native-permissions@../: version "3.8.4" - dependencies: - picocolors "^1.0.0" - pkg-dir "^5.0.0" react-native-safe-area-context@^4.7.1: version "4.7.1" diff --git a/ios/RNPermissionsModule.mm b/ios/RNPermissionsModule.mm index 69d938e2..278f4426 100644 --- a/ios/RNPermissionsModule.mm +++ b/ios/RNPermissionsModule.mm @@ -201,7 +201,7 @@ - (NSDictionary *)constantsToExport { NSMutableString *message = [NSMutableString new]; [message appendString:@"⚠ No permission handler detected.\n\n"]; - [message appendString:@"• Check that you added at least one permission handler in your package.json reactNativePermissionsIOS config.\n"]; + [message appendString:@"• Check that you are correctly calling setup_permissions in your Podfile.\n"]; [message appendString:@"• Uninstall this app, reinstall your Pods, delete your Xcode DerivedData folder and rebuild it.\n"]; RCTLogError(@"%@", message); diff --git a/package.json b/package.json index 55046958..8f5d1c09 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-permissions", - "version": "3.8.4", + "version": "3.9.0", "license": "MIT", "description": "An unified permissions API for React Native on iOS, Android and Windows", "author": "Mathieu Acthernoene ", @@ -25,6 +25,7 @@ "!/android/build", "/dist", "/ios", + "/scripts", "/windows", "/src", "/*.podspec", @@ -33,7 +34,7 @@ ], "scripts": { "format": "prettier '**/*' -u -w", - "lint": "eslint \"./**/*.{js,ts,tsx}\"", + "lint": "eslint './**/*.{js,ts,tsx}'", "setup-hooks": "git config --local core.hooksPath .hooks", "prepack": "bob build", "typecheck": "tsc --project ./ --noEmit" @@ -69,10 +70,6 @@ "optional": true } }, - "dependencies": { - "picocolors": "^1.0.0", - "pkg-dir": "^5.0.0" - }, "devDependencies": { "@types/react": "^18.2.21", "@typescript-eslint/eslint-plugin": "^6.4.1", diff --git a/react-native.config.js b/react-native.config.js index 6d0c4acf..59a60d39 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -1,14 +1,28 @@ -const {existsSync} = require('fs'); -const fs = require('fs/promises'); const path = require('path'); -const pc = require('picocolors'); -const pkgDir = require('pkg-dir'); +const fs = require('fs'); const CONFIG_KEY = 'reactNativePermissionsIOS'; -const log = { - error: (text) => console.log(pc.red(text)), - warning: (text) => console.log(pc.yellow(text)), +const pkgDir = (dir) => { + const pkgPath = path.join(dir, 'package.json'); + + if (fs.existsSync(pkgPath)) { + return dir; + } + + const parentDir = path.resolve(dir, '..'); + + if (parentDir !== dir) { + return pkgDir(parentDir); + } +}; + +const logError = (message) => { + console.log(`\x1b[31m${message}\x1b[0m`); +}; + +const logWarning = (message) => { + console.log(`\x1b[33m${message}\x1b[0m`); }; module.exports = { @@ -17,57 +31,52 @@ module.exports = { name: 'setup-ios-permissions', description: 'Update react-native-permissions podspec to link additional permission handlers.', - func: async () => { - const rootDir = pkgDir.sync() || process.cwd(); + func: () => { + const rootDir = pkgDir(process.cwd()) || process.cwd(); const pkgPath = path.join(rootDir, 'package.json'); - const pkg = await fs.readFile(pkgPath, 'utf-8'); const jsonPath = path.join(rootDir, `${CONFIG_KEY}.json`); - let config = JSON.parse(pkg)[CONFIG_KEY]; + let config = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))[CONFIG_KEY]; - if (!config && existsSync(jsonPath)) { - const text = await fs.readFile(jsonPath, 'utf-8'); + if (!config && fs.existsSync(jsonPath)) { + const text = fs.readFileSync(jsonPath, 'utf-8'); config = JSON.parse(text); } if (!config) { - log.error( - `No config detected. In order to setup iOS permissions, you first need to add an "${CONFIG_KEY}" array in your package.json.`, - ); - + logError(`No ${CONFIG_KEY} config found`); process.exit(1); } - if (!Array.isArray(config) || config.length === 0) { - log.error(`Invalid "${CONFIG_KEY}" config detected. It must be a non-empty array.`); + if (!Array.isArray(config)) { + logError(`Invalid ${CONFIG_KEY} config`); process.exit(1); } - const iosDirPath = path.join(__dirname, 'ios'); - const podspecPath = path.join(__dirname, 'RNPermissions.podspec'); - const iosDir = await fs.readdir(iosDirPath, {withFileTypes: true}); - const podspec = await fs.readFile(podspecPath, 'utf-8'); + const iosDir = path.join(__dirname, 'ios'); + const iosDirents = fs.readdirSync(iosDir, {withFileTypes: true}); - const directories = iosDir + const directories = iosDirents .filter((dirent) => dirent.isDirectory() || dirent.name.endsWith('.xcodeproj')) .map((dirent) => dirent.name) .filter((name) => config.includes(name)); - const unknownPermissions = config - .filter((name) => !directories.includes(name)) - .map((name) => `"${name}"`); - - if (unknownPermissions.length > 0) { - log.warning(`Unknown iOS permissions: ${unknownPermissions.join(', ')}`); - } - const sourceFiles = [ '"ios/*.{h,m,mm}"', ...directories.map((name) => `"ios/${name}/*.{h,m,mm}"`), ]; + const unknownPermissions = config.filter((name) => !directories.includes(name)); + + if (unknownPermissions.length > 0) { + logWarning(`Unknown permissions: ${unknownPermissions.join(', ')}`); + } + + const podspecPath = path.join(__dirname, 'RNPermissions.podspec'); + const podspec = fs.readFileSync(podspecPath, 'utf-8'); const podspecContent = podspec.replace(/"ios\/\*\.{h,m,mm}".*/, sourceFiles.join(', ')); - return fs.writeFile(podspecPath, podspecContent, 'utf-8'); + + fs.writeFileSync(podspecPath, podspecContent, 'utf-8'); }, }, ], diff --git a/scripts/setup.rb b/scripts/setup.rb new file mode 100644 index 00000000..d1460ce9 --- /dev/null +++ b/scripts/setup.rb @@ -0,0 +1,37 @@ +require 'fileutils' + +def log_warning(message) + puts "[Permissions] #{message}" +end + +def setup_permissions(config) + if config.nil? || !config.is_a?(Array) + return log_warning("Invalid config argument") + end + + module_dir = File.expand_path('..', __dir__) + ios_dir = File.join(module_dir, 'ios') + ios_dirents = Dir.entries(ios_dir).map { |entry| File.join(ios_dir, entry) } + + directories = ios_dirents + .select { |entry| File.directory?(entry) || entry.end_with?('.xcodeproj') } + .map { |entry| File.basename(entry) } + .select { |name| config.include?(name) } + + source_files = [ + '"ios/*.{h,m,mm}"', + *directories.map { |name| "\"ios/#{name}/*.{h,m,mm}\"" } + ] + + unknown_permissions = config.reject { |name| directories.include?(name) } + + unless unknown_permissions.empty? + log_warning("Unknown permissions: #{unknown_permissions.join(', ')}") + end + + podspec_path = File.join(module_dir, 'RNPermissions.podspec') + podspec = File.read(podspec_path) + podspec_content = podspec.gsub(/"ios\/\*\.{h,m,mm}".*/, source_files.join(', ')) + + File.write(podspec_path, podspec_content) +end diff --git a/src/contract.ts b/src/contract.ts index 956202d3..4bb8344d 100644 --- a/src/contract.ts +++ b/src/contract.ts @@ -14,10 +14,12 @@ export type Contract = { checkNotifications(): Promise; openLimitedPhotoLibraryPicker(): Promise; openSettings(): Promise; - request(permission: Permission, rationale?: Rationale): Promise; + request( + permission: Permission, + rationale?: Rationale | (() => Promise), + ): Promise; requestLocationAccuracy(options: LocationAccuracyOptions): Promise; requestNotifications(options: NotificationOption[]): Promise; - checkMultiple

( permissions: P, ): Promise>; diff --git a/src/methods.android.ts b/src/methods.android.ts index 91a898d2..5b2de251 100644 --- a/src/methods.android.ts +++ b/src/methods.android.ts @@ -19,36 +19,36 @@ function check(permission: Permission): Promise { return NativeModule.checkPermission(permission) as Promise; } -async function request(permission: Permission, rationale?: Rationale): Promise { - if (rationale) { - const shouldShowRationale = await NativeModule.shouldShowRequestPermissionRationale(permission); - - if (shouldShowRationale) { - const {title, message, buttonPositive, buttonNegative, buttonNeutral} = rationale; - - return new Promise((resolve) => { - const buttons: AlertButton[] = []; - - if (buttonNegative) { - const onPress = () => - resolve(NativeModule.checkPermission(permission) as Promise); - - buttonNeutral && buttons.push({text: buttonNeutral, onPress}); - buttons.push({text: buttonNegative, onPress}); - } +async function showRationaleAlert(rationale: Rationale): Promise { + return new Promise((resolve) => { + const {title, message, buttonPositive, buttonNegative, buttonNeutral} = rationale; + const buttons: AlertButton[] = []; + + if (buttonNegative) { + const onPress = () => resolve(false); + buttonNeutral && buttons.push({text: buttonNeutral, onPress}); + buttons.push({text: buttonNegative, onPress}); + } - buttons.push({ - text: buttonPositive, - onPress: () => - resolve(NativeModule.requestPermission(permission) as Promise), - }); + buttons.push({text: buttonPositive, onPress: () => resolve(true)}); + Alert.alert(title, message, buttons, {cancelable: false}); + }); +} - Alert.alert(title, message, buttons, {cancelable: false}); - }); - } +async function request( + permission: Permission, + rationale?: Rationale | (() => Promise), +): Promise { + if (rationale == null || !(await NativeModule.shouldShowRequestPermissionRationale(permission))) { + return NativeModule.requestPermission(permission) as Promise; } - return NativeModule.requestPermission(permission) as Promise; + return (typeof rationale === 'function' ? rationale() : showRationaleAlert(rationale)).then( + (shouldRequestPermission) => + (shouldRequestPermission + ? NativeModule.requestPermission(permission) + : NativeModule.checkPermission(permission)) as Promise, + ); } async function checkNotifications(): Promise { diff --git a/yarn.lock b/yarn.lock index 280470a3..9296dc47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4871,13 +4871,6 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" -pkg-dir@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760" - integrity sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA== - dependencies: - find-up "^5.0.0" - prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"