diff --git a/example/app.json b/example/app.json index 898f74e..b0a01e7 100644 --- a/example/app.json +++ b/example/app.json @@ -25,6 +25,7 @@ }, "web": { "favicon": "./assets/favicon.png" - } + }, + "plugins": ["../plugin/build/index.js"] } } diff --git a/example/src/App.tsx b/example/src/App.tsx index 87e2abc..15b8a52 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -12,6 +12,9 @@ import { type CallingCode, type CountryCode, } from 'react-native-phone-entry'; +import { configureIPadPhonePad } from 'react-native-phone-entry'; + +configureIPadPhonePad(); // no-op on iPhone, Android, or web export default function App() { const [phoneNumber, setPhoneNumber] = useState(''); diff --git a/package.json b/package.json index 268ace5..0791492 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,11 @@ } } }, + "plugin": "./plugin/build/index.js", "files": [ "lib", + "plugin/build", + "plugin/ios", "!**/__tests__", "!**/__fixtures__", "!**/__mocks__", @@ -32,8 +35,9 @@ "test:coverage": "jest --collectCoverage --coverageDirectory=\"./coverage\"", "typecheck": "tsc", "lint": "eslint \"**/*.{js,ts,tsx}\"", - "clean": "del-cli lib", - "prepare": "bob build", + "clean": "del-cli lib plugin/build", + "build:plugin": "tsc -p plugin/tsconfig.json", + "prepare": "bob build && yarn build:plugin", "release": "release-it" }, "keywords": [ @@ -57,6 +61,7 @@ "devDependencies": { "@commitlint/config-conventional": "^17.0.2", "@evilmartians/lefthook": "^1.5.0", + "@expo/config-plugins": "^8.0.0", "@react-native/eslint-config": "^0.73.1", "@release-it/conventional-changelog": "latest", "@swc/core": "^1.10.12", @@ -67,6 +72,7 @@ "@types/jest": "^29.5.5", "@types/react": "^18.2.44", "@types/react-test-renderer": "^19.0.0", + "@types/xcode": "^3.0.0", "babel-plugin-module-resolver": "^5.0.2", "commitlint": "^17.0.2", "del-cli": "^5.1.0", @@ -94,9 +100,19 @@ "react-native-country-picker-modal@^2.0.0": "patch:react-native-country-picker-modal@patch%3Areact-native-country-picker-modal@npm%253A2.0.0%23./.yarn/patches/react-native-country-picker-modal-npm-2.0.0-ffda15a759.patch%3A%3Aversion=2.0.0&hash=4dbe83&locator=react-native-phone-entry%2540workspace%253A.#./.yarn/patches/react-native-country-picker-modal-patch-ff4072ee06.patch" }, "peerDependencies": { + "@expo/config-plugins": ">=7.0.0", + "expo": ">=50.0.0", "react": "*", "react-native": "*" }, + "peerDependenciesMeta": { + "@expo/config-plugins": { + "optional": true + }, + "expo": { + "optional": true + } + }, "workspaces": [ "example" ], diff --git a/plugin/ios/PhonePadInputView.swift b/plugin/ios/PhonePadInputView.swift new file mode 100644 index 0000000..685532a --- /dev/null +++ b/plugin/ios/PhonePadInputView.swift @@ -0,0 +1,242 @@ +import UIKit + +/// A custom phone-pad input view designed for iPad. +/// Replaces the system keyboard with a 12-key dial-pad that mirrors the +/// iPhone phone-pad layout, ensuring a consistent experience across devices. +@objc public class PhonePadInputView: UIInputView { + + // MARK: - Types + + private struct Key { + let primary: String + let secondary: String? + let action: Action + + enum Action { + case insert(String) + case delete + case openCountryPicker + case none + } + + init(_ primary: String, _ secondary: String? = nil, action: Action? = nil) { + self.primary = primary + self.secondary = secondary + self.action = action ?? .insert(primary) + } + } + + // MARK: - Layout + + private let rows: [[Key]] = [ + [Key("1"), Key("2", "ABC"), Key("3", "DEF")], + [Key("4", "GHI"), Key("5", "JKL"), Key("6", "MNO")], + [Key("7", "PQRS"), Key("8", "TUV"), Key("9", "WXYZ")], + [Key("🌐", action: .openCountryPicker), Key("0", "+"), Key("⌫", action: .delete)], + ] + + // MARK: - Properties + + private weak var targetField: UITextField? + @objc var onCountryPickerRequest: (() -> Void)? + + // MARK: - Colours (adaptive for dark/light mode) + + private var keyboardBackground: UIColor { + UIColor { traits in + traits.userInterfaceStyle == .dark + ? UIColor(red: 0.17, green: 0.17, blue: 0.18, alpha: 1) + : UIColor(red: 0.82, green: 0.84, blue: 0.87, alpha: 1) + } + } + + private var keyBackground: UIColor { + UIColor { traits in + traits.userInterfaceStyle == .dark + ? UIColor(red: 0.36, green: 0.36, blue: 0.38, alpha: 1) + : UIColor.white + } + } + + private var emptyKeyBackground: UIColor { + UIColor { traits in + traits.userInterfaceStyle == .dark + ? UIColor(red: 0.22, green: 0.22, blue: 0.23, alpha: 1) + : UIColor(red: 0.69, green: 0.71, blue: 0.74, alpha: 1) + } + } + + private var primaryTextColor: UIColor { .label } + private var secondaryTextColor: UIColor { .secondaryLabel } + + // MARK: - Init + + @objc public init(textField: UITextField) { + self.targetField = textField + let screenWidth = UIScreen.main.bounds.width + super.init(frame: CGRect(x: 0, y: 0, width: screenWidth, height: 280), + inputViewStyle: .keyboard) + translatesAutoresizingMaskIntoConstraints = false + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - UI Setup + + private func setupUI() { + backgroundColor = keyboardBackground + + let outerStack = UIStackView() + outerStack.axis = .vertical + outerStack.distribution = .fillEqually + outerStack.spacing = 10 + outerStack.translatesAutoresizingMaskIntoConstraints = false + addSubview(outerStack) + + NSLayoutConstraint.activate([ + outerStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 5), + outerStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -5), + outerStack.topAnchor.constraint(equalTo: topAnchor, constant: 10), + outerStack.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -6), + ]) + + for (rowIndex, row) in rows.enumerated() { + let rowStack = UIStackView() + rowStack.axis = .horizontal + rowStack.distribution = .fillEqually + rowStack.spacing = 10 + outerStack.addArrangedSubview(rowStack) + + for key in row { + rowStack.addArrangedSubview(makeKeyView(key: key, rowIndex: rowIndex)) + } + } + } + + private func makeKeyView(key: Key, rowIndex: Int) -> UIView { + // Empty / spacer slot + if case .none = key.action { + let spacer = UIView() + spacer.backgroundColor = emptyKeyBackground + spacer.layer.cornerRadius = 5 + spacer.layer.shadowColor = UIColor.black.cgColor + spacer.layer.shadowOpacity = 0.3 + spacer.layer.shadowOffset = CGSize(width: 0, height: 1) + spacer.layer.shadowRadius = 0 + return spacer + } + + let button = UIButton(type: .custom) + button.backgroundColor = keyBackground + button.layer.cornerRadius = 5 + button.layer.shadowColor = UIColor.black.cgColor + button.layer.shadowOpacity = 0.3 + button.layer.shadowOffset = CGSize(width: 0, height: 1) + button.layer.shadowRadius = 0 + + // Store action + button.accessibilityLabel = key.primary + switch key.action { + case .delete: + button.addTarget(self, action: #selector(deletePressed(_:)), for: .touchUpInside) + addHighlightBehavior(to: button) + let img = UIImage(systemName: "delete.left") + button.setImage(img, for: .normal) + button.tintColor = primaryTextColor + case .openCountryPicker: + button.addTarget(self, action: #selector(countryPickerPressed), for: .touchUpInside) + addHighlightBehavior(to: button) + let img = UIImage(systemName: "globe") + button.setImage(img, for: .normal) + button.tintColor = primaryTextColor + case .insert(let char): + button.tag = Int(char.unicodeScalars.first?.value ?? 0) + button.addTarget(self, action: #selector(keyPressed(_:)), for: .touchUpInside) + addHighlightBehavior(to: button) + addKeyLabels(to: button, primary: key.primary, secondary: key.secondary) + case .none: + break + } + + return button + } + + private func addKeyLabels(to button: UIButton, primary: String, secondary: String?) { + let primaryLabel = UILabel() + primaryLabel.text = primary + primaryLabel.font = UIFont.systemFont(ofSize: 26, weight: .light) + primaryLabel.textColor = primaryTextColor + primaryLabel.textAlignment = .center + primaryLabel.translatesAutoresizingMaskIntoConstraints = false + button.addSubview(primaryLabel) + + if let sec = secondary { + let secLabel = UILabel() + secLabel.text = sec + secLabel.font = UIFont.systemFont(ofSize: 10, weight: .medium) + secLabel.textColor = secondaryTextColor + secLabel.textAlignment = .center + secLabel.translatesAutoresizingMaskIntoConstraints = false + button.addSubview(secLabel) + + NSLayoutConstraint.activate([ + primaryLabel.centerXAnchor.constraint(equalTo: button.centerXAnchor), + primaryLabel.centerYAnchor.constraint(equalTo: button.centerYAnchor, constant: -7), + secLabel.centerXAnchor.constraint(equalTo: button.centerXAnchor), + secLabel.topAnchor.constraint(equalTo: primaryLabel.bottomAnchor, constant: 1), + ]) + } else { + NSLayoutConstraint.activate([ + primaryLabel.centerXAnchor.constraint(equalTo: button.centerXAnchor), + primaryLabel.centerYAnchor.constraint(equalTo: button.centerYAnchor), + ]) + } + } + + // MARK: - Button Highlight + + private func addHighlightBehavior(to button: UIButton) { + button.addTarget(self, action: #selector(buttonHighlighted(_:)), for: .touchDown) + button.addTarget(self, action: #selector(buttonHighlighted(_:)), for: .touchDragEnter) + button.addTarget(self, action: #selector(buttonUnhighlighted(_:)), for: .touchUpInside) + button.addTarget(self, action: #selector(buttonUnhighlighted(_:)), for: .touchDragExit) + button.addTarget(self, action: #selector(buttonUnhighlighted(_:)), for: .touchCancel) + } + + @objc private func buttonHighlighted(_ sender: UIButton) { + sender.alpha = 0.5 + } + + @objc private func buttonUnhighlighted(_ sender: UIButton) { + UIView.animate(withDuration: 0.1) { sender.alpha = 1.0 } + } + + // MARK: - Actions + + @objc private func keyPressed(_ sender: UIButton) { + guard let scalar = Unicode.Scalar(sender.tag), + let field = targetField else { return } + let char = String(scalar) + // Use insertText so the cursor position and delegate callbacks work correctly + field.insertText(char) + provideFeedback() + } + + @objc private func countryPickerPressed() { + provideFeedback() + onCountryPickerRequest?() + } + + @objc private func deletePressed(_ sender: UIButton) { + targetField?.deleteBackward() + provideFeedback() + } + + private func provideFeedback() { + let generator = UIImpactFeedbackGenerator(style: .light) + generator.impactOccurred() + } +} diff --git a/plugin/ios/RNPhonePadKeyboard.m b/plugin/ios/RNPhonePadKeyboard.m new file mode 100644 index 0000000..3944ac6 --- /dev/null +++ b/plugin/ios/RNPhonePadKeyboard.m @@ -0,0 +1,66 @@ +// NOTE: __RN_PROJECT_NAME__ is substituted by the react-native-phone-entry +// Expo config plugin at prebuild time (e.g. "MyApp-Swift.h"), giving this +// Objective-C file access to PhonePadInputView (a Swift class). +#import +#import +#import +#import "__RN_PROJECT_NAME__-Swift.h" + +@interface RNPhonePadKeyboard : RCTEventEmitter +@end + +@implementation RNPhonePadKeyboard { + id _observer; +} + +RCT_EXPORT_MODULE() + +- (NSArray *)supportedEvents { + return @[@"onCountryPickerRequested"]; +} + ++ (BOOL)requiresMainQueueSetup { + return YES; +} + +- (void)dealloc { + if (_observer) { + [[NSNotificationCenter defaultCenter] removeObserver:_observer]; + } +} + +RCT_EXPORT_METHOD(configure) { + if (_observer != nil) return; + + __weak typeof(self) weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + __strong typeof(self) strongSelf = weakSelf; + if (!strongSelf) return; + strongSelf->_observer = [[NSNotificationCenter defaultCenter] + addObserverForName:UITextFieldTextDidBeginEditingNotification + object:nil + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification *notification) { + [weakSelf handleTextFieldFocus:notification]; + }]; + }); +} + +- (void)handleTextFieldFocus:(NSNotification *)notification { + if ([UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPad) return; + + UITextField *field = notification.object; + if (![field isKindOfClass:[UITextField class]]) return; + if (field.keyboardType != UIKeyboardTypePhonePad) return; + if ([field.inputView isKindOfClass:[PhonePadInputView class]]) return; + + __weak typeof(self) weakSelf = self; + PhonePadInputView *pad = [[PhonePadInputView alloc] initWithTextField:field]; + pad.onCountryPickerRequest = ^{ + [weakSelf sendEventWithName:@"onCountryPickerRequested" body:@{}]; + }; + field.inputView = pad; + [field reloadInputViews]; +} + +@end diff --git a/plugin/src/index.ts b/plugin/src/index.ts new file mode 100644 index 0000000..c287878 --- /dev/null +++ b/plugin/src/index.ts @@ -0,0 +1,119 @@ +import { createRunOncePlugin, withDangerousMod } from '@expo/config-plugins'; +import type { ConfigPlugin } from '@expo/config-plugins'; +import * as fs from 'fs'; +import * as path from 'path'; + +// Swift file copied verbatim; ObjC file is treated as a template. +const IOS_SWIFT_FILES = ['PhonePadInputView.swift']; +const IOS_OBJC_TEMPLATE = 'RNPhonePadKeyboard.m'; + +// Path to our bundled iOS sources relative to the built plugin (plugin/build/index.js). +const IOS_SOURCE_DIR = path.join(__dirname, '..', 'ios'); + +/** + * Expo config plugin — adds the iPhone-style phone-pad keyboard for iPads. + * + * Usage in app.json / app.config.js: + * { "plugins": ["react-native-phone-entry"] } + * + * Then call `configureIPadPhonePad()` once at app startup (e.g. in App.tsx). + */ +const withIPadPhonePad: ConfigPlugin = (config) => { + config = withDangerousMod(config, [ + 'ios', + (modConfig) => { + const projectName = modConfig.modRequest.projectName!; + const platformRoot = modConfig.modRequest.platformProjectRoot; + + // withDangerousMod runs AFTER Expo has generated the native project, + // so the ios// directory is guaranteed to exist here. + const destDir = path.join(platformRoot, projectName); + if (!fs.existsSync(destDir)) { + fs.mkdirSync(destDir, { recursive: true }); + } + + // 1a. Copy Swift files verbatim. + for (const file of IOS_SWIFT_FILES) { + const src = path.join(IOS_SOURCE_DIR, file); + if (!fs.existsSync(src)) { + throw new Error( + `react-native-phone-entry plugin: missing iOS source file: ${src}` + ); + } + fs.copyFileSync(src, path.join(destDir, file)); + } + + // 1b. Copy the ObjC module file, substituting __RN_PROJECT_NAME__ so the + // file can import the Xcode-generated "-Swift.h" header + // which exposes PhonePadInputView to Objective-C. + const objcTemplateSrc = path.join(IOS_SOURCE_DIR, IOS_OBJC_TEMPLATE); + if (!fs.existsSync(objcTemplateSrc)) { + throw new Error( + `react-native-phone-entry plugin: missing iOS source file: ${objcTemplateSrc}` + ); + } + const objcContent = fs + .readFileSync(objcTemplateSrc, 'utf8') + .replace(/__RN_PROJECT_NAME__/g, projectName); + fs.writeFileSync(path.join(destDir, IOS_OBJC_TEMPLATE), objcContent); + + // 2. Register the files in project.pbxproj so Xcode compiles them. + // Done here (not withXcodeProject) to operate on the already-written + // pbxproj, avoiding xcodeproj base-mod timing issues. + const xcodeprojs = fs + .readdirSync(platformRoot) + .filter((name) => name.endsWith('.xcodeproj')); + + if (!xcodeprojs.length) return modConfig; + + const pbxprojPath = path.join( + platformRoot, + xcodeprojs[0]!, + 'project.pbxproj' + ); + if (!fs.existsSync(pbxprojPath)) return modConfig; + + // xcode is a peer dep of @expo/config-plugins and is always present. + + const xcode = require('xcode') as typeof import('xcode'); + const project = xcode.project(pbxprojPath); + project.parseSync(); + + const firstTarget = project.getFirstTarget(); + if (!firstTarget) return modConfig; + + // Passing the group key to addSourceFile makes xcode use the addFile() + // code path instead of addPluginFile(), which crashes on projects that + // have no "Plugins" PBX group (null.path error). + const groupKey = + project.findPBXGroupKey({ name: projectName }) ?? + project.findPBXGroupKey({ path: projectName }); + + const allFiles = [...IOS_SWIFT_FILES, IOS_OBJC_TEMPLATE]; + let changed = false; + for (const file of allFiles) { + const filePath = `${projectName}/${file}`; + if (!project.hasFile(filePath)) { + project.addSourceFile( + filePath, + { target: firstTarget.uuid }, + groupKey + ); + changed = true; + } + } + + if (changed) { + fs.writeFileSync(pbxprojPath, project.writeSync()); + } + + return modConfig; + }, + ]); + + return config; +}; + +const pkg = require('../../package.json'); + +export default createRunOncePlugin(withIPadPhonePad, pkg.name, pkg.version); diff --git a/plugin/tsconfig.json b/plugin/tsconfig.json new file mode 100644 index 0000000..951f71f --- /dev/null +++ b/plugin/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2019", + "module": "commonjs", + "lib": ["ES2019"], + "strict": true, + "outDir": "./build", + "rootDir": "./src", + "declaration": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["src"], + "exclude": ["node_modules", "build"] +} diff --git a/src/PhoneInput/PhoneInput.tsx b/src/PhoneInput/PhoneInput.tsx index 675c862..0ef1dc2 100644 --- a/src/PhoneInput/PhoneInput.tsx +++ b/src/PhoneInput/PhoneInput.tsx @@ -1,5 +1,13 @@ -import React, { useCallback } from 'react'; -import { Appearance, Image, TouchableOpacity, View } from 'react-native'; +import React, { useCallback, useEffect, useRef } from 'react'; +import { + Appearance, + Image, + NativeEventEmitter, + NativeModules, + Platform, + TouchableOpacity, + View, +} from 'react-native'; import CountryPicker, { CountryModalProvider, DARK_THEME, @@ -23,6 +31,21 @@ export const PhoneInput: React.FC = (props) => { forms: { modalVisible, showModal, hideModal }, } = usePhoneInput(props); + // Keep a stable ref so the effect closure never goes stale. + const showModalRef = useRef(showModal); + showModalRef.current = showModal; + + useEffect(() => { + if (Platform.OS !== 'ios') return; + const { RNPhonePadKeyboard } = NativeModules; + if (!RNPhonePadKeyboard) return; + const emitter = new NativeEventEmitter(RNPhonePadKeyboard); + const sub = emitter.addListener('onCountryPickerRequested', () => + showModalRef.current() + ); + return () => sub.remove(); + }, []); + const { theme: { enableDarkTheme = isDarkTheme, @@ -104,7 +127,7 @@ export const PhoneInput: React.FC = (props) => { editable={!disabled} selectionColor="black" keyboardAppearance={enableDarkTheme ? 'dark' : 'default'} - keyboardType="number-pad" + keyboardType="phone-pad" autoFocus={autoFocus} {...maskInputProps} /> diff --git a/src/PhonePadKeyboard.ts b/src/PhonePadKeyboard.ts new file mode 100644 index 0000000..65ca406 --- /dev/null +++ b/src/PhonePadKeyboard.ts @@ -0,0 +1,32 @@ +import { NativeModules, Platform } from 'react-native'; + +const { RNPhonePadKeyboard } = NativeModules; + +/** + * Activates the iPhone-style phone-pad keyboard for iPad. + * + * Call this **once** at app startup (e.g. at the top level of `App.tsx`). + * After calling this, any `TextInput` with `keyboardType="phone-pad"` will + * show a full 12-key dial-pad on iPad instead of the system floating keyboard. + * + * On iPhone this is a no-op — the native phone-pad keyboard is already used. + * + * Requires the Expo config plugin to be added in app.json: + * { "plugins": ["react-native-phone-entry"] } + */ +export function configureIPadPhonePad(): void { + if (Platform.OS !== 'ios') return; + + if (!RNPhonePadKeyboard) { + if (__DEV__) { + console.warn( + '[react-native-phone-entry] configureIPadPhonePad: native module not found. ' + + 'Add "react-native-phone-entry" to the plugins array in app.json and run ' + + 'expo prebuild.' + ); + } + return; + } + + RNPhonePadKeyboard.configure(); +} diff --git a/src/index.ts b/src/index.ts index 8500b4f..20f4a2f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,3 +8,4 @@ export { type CountryCode, type PhoneInputProps, } from './PhoneInput'; +export { configureIPadPhonePad } from './PhonePadKeyboard'; diff --git a/yarn.lock b/yarn.lock index 189d727..6214a8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2070,6 +2070,29 @@ __metadata: languageName: node linkType: hard +"@expo/config-plugins@npm:^8.0.0": + version: 8.0.11 + resolution: "@expo/config-plugins@npm:8.0.11" + dependencies: + "@expo/config-types": ^51.0.3 + "@expo/json-file": ~8.3.0 + "@expo/plist": ^0.1.0 + "@expo/sdk-runtime-versions": ^1.0.0 + chalk: ^4.1.2 + debug: ^4.3.1 + find-up: ~5.0.0 + getenv: ^1.0.0 + glob: 7.1.6 + resolve-from: ^5.0.0 + semver: ^7.5.4 + slash: ^3.0.0 + slugify: ^1.6.6 + xcode: ^3.0.1 + xml2js: 0.6.0 + checksum: 084b20f6254d28644fbfde13cbf4b155c9d965a7ea44c25e67d1ee8d3675570593e8233291948813c0832e03a26ee600676fe39d78f11e388b62555c537d8463 + languageName: node + linkType: hard + "@expo/config-plugins@npm:~9.0.14": version: 9.0.14 resolution: "@expo/config-plugins@npm:9.0.14" @@ -2092,6 +2115,13 @@ __metadata: languageName: node linkType: hard +"@expo/config-types@npm:^51.0.3": + version: 51.0.3 + resolution: "@expo/config-types@npm:51.0.3" + checksum: c46def814a5e0d6c8358b9767a89f51239f4f1c3b4a5305ffcfa1a86e4360ac40de54a65f7c6e787be7656e4144c99a050e98b600a1edd3d6e8e20c83d8e107b + languageName: node + linkType: hard + "@expo/config-types@npm:^52.0.3": version: 52.0.3 resolution: "@expo/config-types@npm:52.0.3" @@ -2202,6 +2232,17 @@ __metadata: languageName: node linkType: hard +"@expo/json-file@npm:~8.3.0": + version: 8.3.3 + resolution: "@expo/json-file@npm:8.3.3" + dependencies: + "@babel/code-frame": ~7.10.4 + json5: ^2.2.2 + write-file-atomic: ^2.3.0 + checksum: 49fcb3581ac21c1c223459f32e9e931149b56a7587318f666303a62e719e3d0f122ff56a60d47ee31fac937c297a66400a00fcee63a17bebbf4b8cd30c5138c1 + languageName: node + linkType: hard + "@expo/metro-config@npm:0.19.9, @expo/metro-config@npm:~0.19.9": version: 0.19.9 resolution: "@expo/metro-config@npm:0.19.9" @@ -2267,6 +2308,17 @@ __metadata: languageName: node linkType: hard +"@expo/plist@npm:^0.1.0": + version: 0.1.3 + resolution: "@expo/plist@npm:0.1.3" + dependencies: + "@xmldom/xmldom": ~0.7.7 + base64-js: ^1.2.3 + xmlbuilder: ^14.0.0 + checksum: 8abe78bed4d1849f2cddddd1a238c6fe5c2549a9dee40158224ff70112f31503db3f17a522b6e21f16eea66b5f4b46cc49d22f2b369067d00a88ef6d301a50cd + languageName: node + linkType: hard + "@expo/plist@npm:^0.2.1": version: 0.2.1 resolution: "@expo/plist@npm:0.2.1" @@ -3677,6 +3729,13 @@ __metadata: languageName: node linkType: hard +"@types/xcode@npm:^3.0.0": + version: 3.0.0 + resolution: "@types/xcode@npm:3.0.0" + checksum: 89391be0d27bd5d38d21a6771100c44b74e6237a12ec3906189bcb697f820878280f713795632baee0f0f439d15bbb320a691efb6b73b08721d2e7bfc5230eeb + languageName: node + linkType: hard + "@types/yargs-parser@npm:*": version: 21.0.3 resolution: "@types/yargs-parser@npm:21.0.3" @@ -7134,7 +7193,7 @@ __metadata: languageName: node linkType: hard -"find-up@npm:^5.0.0": +"find-up@npm:^5.0.0, find-up@npm:~5.0.0": version: 5.0.0 resolution: "find-up@npm:5.0.0" dependencies: @@ -7563,6 +7622,20 @@ __metadata: languageName: node linkType: hard +"glob@npm:7.1.6": + version: 7.1.6 + resolution: "glob@npm:7.1.6" + dependencies: + fs.realpath: ^1.0.0 + inflight: ^1.0.4 + inherits: 2 + minimatch: ^3.0.4 + once: ^1.3.0 + path-is-absolute: ^1.0.0 + checksum: 351d549dd90553b87c2d3f90ce11aed9e1093c74130440e7ae0592e11bbcd2ce7f0ebb8ba6bfe63aaf9b62166a7f4c80cb84490ae5d78408bb2572bf7d4ee0a6 + languageName: node + linkType: hard + "glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.7, glob@npm:^10.4.2": version: 10.4.5 resolution: "glob@npm:10.4.5" @@ -9457,7 +9530,7 @@ __metadata: languageName: node linkType: hard -"json5@npm:^2.2.1, json5@npm:^2.2.3": +"json5@npm:^2.2.1, json5@npm:^2.2.2, json5@npm:^2.2.3": version: 2.2.3 resolution: "json5@npm:2.2.3" bin: @@ -12320,6 +12393,7 @@ __metadata: dependencies: "@commitlint/config-conventional": ^17.0.2 "@evilmartians/lefthook": ^1.5.0 + "@expo/config-plugins": ^8.0.0 "@react-native/eslint-config": ^0.73.1 "@release-it/conventional-changelog": latest "@swc/core": ^1.10.12 @@ -12330,6 +12404,7 @@ __metadata: "@types/jest": ^29.5.5 "@types/react": ^18.2.44 "@types/react-test-renderer": ^19.0.0 + "@types/xcode": ^3.0.0 babel-plugin-module-resolver: ^5.0.2 commitlint: ^17.0.2 del-cli: ^5.1.0 @@ -12349,8 +12424,15 @@ __metadata: release-it: ^17.10.0 typescript: ^5.2.2 peerDependencies: + "@expo/config-plugins": ">=7.0.0" + expo: ">=50.0.0" react: "*" react-native: "*" + peerDependenciesMeta: + "@expo/config-plugins": + optional: true + expo: + optional: true languageName: unknown linkType: soft