1--- 2title: Native Modules 3--- 4 5import { CodeBlocksTable } from '~/components/plugins/CodeBlocksTable'; 6import { APIBox } from '~/components/plugins/APIBox'; 7import { PlatformTags } from '~/ui/components/Tag'; 8import { APIMethod } from '~/components/plugins/api/APISectionMethods'; 9 10> **warning** Expo Modules APIs are in beta and subject to breaking changes. 11 12The native modules API is an abstraction layer on top of [JSI](https://reactnative.dev/architecture/glossary#javascript-interfaces-jsi) and other low-level primities that React Native is built upon. It is built with modern languages (Swift and Kotlin) and provides an easy to use and convenient API that is consistent across platforms where possible. 13 14## Definition Components 15 16As you might have noticed in the snippets on the [Get Started](./get-started.mdx) page, each module class must implement the `definition` function. 17The module definition consists of the DSL components that describe the module's functionality and behavior. 18 19<APIBox header="Name"> 20 21Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument. Can be inferred from module's class name, but it's recommended to set it explicitly for clarity. 22 23```swift Swift / Kotlin 24Name("MyModuleName") 25``` 26 27</APIBox> 28<APIBox header="Constants"> 29 30Sets constant properties on the module. Can take a dictionary or a closure that returns a dictionary. 31 32<CodeBlocksTable> 33 34```swift 35// Created from the dictionary 36Constants([ 37 "PI": Double.pi 38]) 39 40// or returned by the closure 41Constants { 42 return [ 43 "PI": Double.pi 44 ] 45} 46``` 47 48```kotlin 49// Passed as arguments 50Constants( 51 "PI" to kotlin.math.PI 52) 53 54// or returned by the closure 55Constants { 56 return@Constants mapOf( 57 "PI" to kotlin.math.PI 58 ) 59} 60``` 61 62</CodeBlocksTable> 63</APIBox> 64<APIBox header="Function"> 65 66Defines a native synchronous function that will be exported to JavaScript. Synchronous means that when the function is executed in JavaScript, its native code is run on the same thread and blocks further execution of the script until the native function returns. 67 68#### Arguments 69 70- **name**: `String` — Name of the function that you'll call from JavaScript. 71- **body**: `(args...) -> ReturnType` — The closure to run when the function is called. 72 73The function can receive up to 8 arguments. This is due to the limitations of generics in both Swift and Kotlin, because this component must be implemented separately for each arity. 74 75See the [Argument Types](#argument-types) section for more details on what types can be used in the function body. 76 77<CodeBlocksTable> 78 79```swift 80Function("syncFunction") { (message: String) in 81 return message 82} 83``` 84 85```kotlin 86Function("syncFunction") { message: String -> 87 return@Function message 88} 89``` 90 91</CodeBlocksTable> 92 93```js JavaScript 94import { requireNativeModule } from 'expo-modules-core'; 95 96// Assume that we have named the module "MyModule" 97const MyModule = requireNativeModule('MyModule'); 98 99function getMessage() { 100 return MyModule.syncFunction('bar'); 101} 102``` 103 104</APIBox> 105<APIBox header="AsyncFunction"> 106 107Defines a JavaScript function that always returns a `Promise` and whose native code is by default dispatched on the different thread than the JavaScript runtime runs on. 108 109#### Arguments 110 111- **name**: `String` — Name of the function that you'll call from JavaScript. 112- **body**: `(args...) -> ReturnType` — The closure to run when the function is called. 113 114If the type of the last argument is `Promise`, the function will wait for the promise to be resolved or rejected before the response is passed back to JavaScript. Otherwise, the function is immediately resolved with the returned value or rejected if it throws an exception. 115The function can receive up to 8 arguments (including the promise). 116 117See the [Argument Types](#argument-types) section for more details on what types can be used in the function body. 118 119It is recommended to use `AsyncFunction` over `Function` when it: 120 121- does I/O bound tasks such as sending network requests or interacting with the file system 122- needs to be run on different thread, e.g. the main UI thread for UI-related tasks 123- is an extensive or long-lasting operation that would block the JavaScript thread which in turn would reduce the responsiveness of the application 124 125<CodeBlocksTable> 126 127```swift 128AsyncFunction("asyncFunction") { (message: String, promise: Promise) in 129 DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { 130 promise.resolve(message) 131 } 132} 133``` 134 135```kotlin 136AsyncFunction("asyncFunction") { message: String, promise: Promise -> 137 launch(Dispatchers.Main) { 138 promise.resolve(message) 139 } 140} 141``` 142 143</CodeBlocksTable> 144 145```js JavaScript 146import { requireNativeModule } from 'expo-modules-core'; 147 148// Assume that we have named the module "MyModule" 149const MyModule = requireNativeModule('MyModule'); 150 151async function getMessageAsync() { 152 return await MyModule.asyncFunction('bar'); 153} 154``` 155 156<hr /> 157 158#### Kotlin coroutines <PlatformTags prefix="" platforms={['android']} /> 159 160`AsyncFunction` can receive a suspendable body on Android. However, it has to be passed in the infix notation after the `Coroutine` block. You can read more about suspendable functions and coroutines on [coroutine overview](https://kotlinlang.org/docs/coroutines-overview.html). 161 162`AsyncFunction` with suspendable body can't receive `Promise` as an argument. It uses a suspension mechanism to execute asynchronous calls. 163The function is immediately resolved with the returned value of the provided suspendable block or rejected if it throws an exception. The function can receive up to 8 arguments. 164 165By default, suspend functions are dispatched on the module's coroutine scope. Moreover, every other suspendable function called from the body block is run within the same scope. 166This scope's lifecycle is bound to the module's lifecycle - all unfinished suspend functions will be canceled when the module is deallocated. 167 168```kotlin Kotlin 169AsyncFunction("suspendFunction") Coroutine { message: String -> 170 launch { 171 return@Coroutine message 172 } 173} 174``` 175 176</APIBox> 177<APIBox header="Events"> 178 179Defines event names that the module can send to JavaScript. 180 181<CodeBlocksTable> 182 183```swift 184Events("onCameraReady", "onPictureSaved", "onBarCodeScanned") 185``` 186 187```kotlin 188Events("onCameraReady", "onPictureSaved", "onBarCodeScanned") 189``` 190 191</CodeBlocksTable> 192 193See [Sending events](#sending-events) to learn how to send events from the native code to JavaScript/TypeScript. 194 195</APIBox> 196<APIBox header="ViewManager"> 197 198> **warning** **Deprecated**: To better integrate with [React Native's new architecture (Fabric)](https://reactnative.dev/architecture/fabric-renderer) and its recycling mechanism, as of SDK 47 the `ViewManager` component is deprecated in favor of [`View`](#view) with a view class passed as the first argument. This component will be removed in SDK 48. 199 200Enables the module to be used as a view manager. The view manager definition is built from the definition components used in the closure passed to `ViewManager`. Definition components that are accepted as part of the view manager definition: [`View`](#view), [`Prop`](#prop). 201 202<CodeBlocksTable> 203 204```swift 205ViewManager { 206 View { 207 MyNativeView() 208 } 209 210 Prop("isHidden") { (view: UIView, hidden: Bool) in 211 view.isHidden = hidden 212 } 213} 214``` 215 216```kotlin 217ViewManager { 218 View { context -> 219 MyNativeView(context) 220 } 221 222 Prop("isHidden") { view: View, hidden: Bool -> 223 view.isVisible = !hidden 224 } 225} 226``` 227 228</CodeBlocksTable> 229</APIBox> 230<APIBox header="View"> 231 232Enables the module to be used as a native view. Definition components that are accepted as part of the view definition: [`Prop`](#prop), [`Events`](#events). 233 234#### Arguments 235 236- **viewType** — The class of the native view that will be rendered. Note: On Android, the provided class must inherit from the [`ExpoView`](#expoview), on iOS it's optional. See [`Extending ExpoView`](#extending--expoview). 237 238<CodeBlocksTable> 239 240```swift 241View(UITextView.self) { 242 Prop("text") { ... } 243} 244``` 245 246```kotlin 247View(TextView::class) { 248 Prop("text") { ... } 249} 250``` 251 252</CodeBlocksTable> 253 254> Support for rendering SwiftUI views is planned. For now, you can use [`UIHostingController`](https://developer.apple.com/documentation/swiftui/uihostingcontroller) and add its content view to your UIKit view. 255 256</APIBox> 257<APIBox header="Prop"> 258 259Defines a setter for the view prop of given name. 260 261#### Arguments 262 263- **name**: `String` — Name of view prop that you want to define a setter. 264- **setter**: `(view: ViewType, value: ValueType) -> ()` — Closure that is invoked when the view rerenders. 265 266This property can only be used within a [`ViewManager`](#viewmanager) closure. 267 268<CodeBlocksTable> 269 270```swift 271Prop("background") { (view: UIView, color: UIColor) in 272 view.backgroundColor = color 273} 274``` 275 276```kotlin 277Prop("background") { view: View, @ColorInt color: Int -> 278 view.setBackgroundColor(color) 279} 280``` 281 282</CodeBlocksTable> 283 284> **Note** Props of function type (callbacks) are not supported yet. 285 286</APIBox> 287<APIBox header="OnCreate"> 288 289Defines module's lifecycle listener that is called right after module initialization. If you need to set up something when the module gets initialized, use this instead of module's class initializer. 290 291</APIBox> 292<APIBox header="OnDestroy"> 293 294Defines module's lifecycle listener that is called when the module is about to be deallocated. Use it instead of module's class destructor. 295 296</APIBox> 297<APIBox header="OnStartObserving"> 298 299Defines the function that is invoked when the first event listener is added. 300 301</APIBox> 302<APIBox header="OnStopObserving"> 303 304Defines the function that is invoked when all event listeners are removed. 305 306</APIBox> 307<APIBox header="OnAppContextDestroys"> 308 309Defines module's lifecycle listener that is called when the app context owning the module is about to be deallocated. 310 311</APIBox> 312<APIBox header="OnAppEntersForeground" platforms={["ios"]}> 313 314Defines the listener that is called when the app is about to enter the foreground mode. 315 316> **Note** This function is not available on Android — you may want to use [`OnActivityEntersForeground`](#onactivityentersforeground) instead. 317 318</APIBox> 319<APIBox header="OnAppEntersBackground" platforms={["ios"]}> 320 321Defines the listener that is called when the app enters the background mode. 322 323> **Note** This function is not available on Android — you may want to use [`OnActivityEntersBackground`](#onactivityentersbackground) instead. 324 325</APIBox> 326<APIBox header="OnAppBecomesActive" platforms={["ios"]}> 327 328Defines the listener that is called when the app becomes active again (after `OnAppEntersForeground`). 329 330> **Note** This function is not available on Android — you may want to use [`OnActivityEntersForeground`](#onactivityentersforeground) instead. 331 332</APIBox> 333<APIBox header="OnActivityEntersForeground" platforms={["android"]}> 334 335Defines the activity lifecycle listener that is called right after the activity is resumed. 336 337> **Note** This function is not available on iOS — you may want to use [`OnAppEntersForeground`](#onappentersforeground) instead. 338 339</APIBox> 340<APIBox header="OnActivityEntersBackground" platforms={["android"]}> 341 342Defines the activity lifecycle listener that is called right after the activity is paused. 343 344> **Note** This function is not available on iOS — you may want to use [`OnAppEntersBackground`](#onappentersbackground) instead. 345 346</APIBox> 347<APIBox header="OnActivityDestroys" platforms={["android"]}> 348 349Defines the activity lifecycle listener that is called when the activity owning the JavaScript context is about to be destroyed. 350 351> **Note** This function is not available on iOS — you may want to use [`OnAppEntersBackground`](#onappentersbackground) instead. 352 353</APIBox> 354 355## Argument Types 356 357Fundamentally, only primitive and serializable data can be passed back and forth between the runtimes. However, usually native modules need to receive custom data structures — more sophisticated than just the dictionary/map where the values are of unknown (`Any`) type and so each value has to be validated and casted on its own. The Expo Modules API provides protocols to make it more convenient to work with data objects, to provide automatic validation, and finally, to ensure native type-safety on each object member. 358 359<APIBox header="Primitives"> 360 361All functions and view prop setters accept all common primitive types in Swift and Kotlin as the arguments. This includes arrays, dictionaries/maps and optionals of these primitive types. 362 363| Language | Supported primitive types | 364| -------- | ------------------------------------------------------------------------------------------------------------------------------ | 365| Swift | `Bool`, `Int`, `Int8`, `Int16`, `Int32`, `Int64`, `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64`, `Float32`, `Double`, `String` | 366| Kotlin | `Boolean`, `Int`, `UInt`, `Float`, `Double`, `String`, `Pair` | 367 368</APIBox> 369<APIBox header="Convertibles"> 370 371_Convertibles_ are native types that can be initialized from certain specific kinds of data received from JavaScript. Such types are allowed to be used as an argument type in `Function`'s body. For example, when the `CGPoint` type is used as a function argument type, its instance can be created from an array of two numbers `(x, y)` or a JavaScript object with numeric `x` and `y` properties. 372 373Some common iOS types from `CoreGraphics` and `UIKit` system frameworks are already made convertible. 374 375| Native iOS Type | TypeScript | 376| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 377| `URL` | `string` with a URL. When scheme is not provided, it's assumed to be a file URL. | 378| `CGFloat` | `number` | 379| `CGPoint` | `{ x: number, y: number }` or `number[]` with _x_ and _y_ coords | 380| `CGSize` | `{ width: number, height: number }` or `number[]` with _width_ and _height_ | 381| `CGVector` | `{ dx: number, dy: number }` or `number[]` with _dx_ and _dy_ vector differentials | 382| `CGRect` | `{ x: number, y: number, width: number, height: number }` or `number[]` with _x_, _y_, _width_ and _height_ values | 383| `CGColor`<br/>`UIColor` | Color hex strings (`#RRGGBB`, `#RRGGBBAA`, `#RGB`, `#RGBA`), named colors following the [CSS3/SVG specification](https://www.w3.org/TR/css-color-3/#svg-color) or `"transparent"` | 384 385Similarly, some common Android types from packages like `java.io`, `java.net`, or `android.graphics` are also made convertible. 386 387| Native Android Type | TypeScript | 388| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 389| `java.net.URL` | `string` with a URL. Note that the scheme has to be provided | 390| `android.net.Uri`<br/>`java.net.URI` | `string` with a URI. Note that the scheme has to be provided | 391| `java.io.File`<br/>`java.nio.file.Path` | `string` with a path to the file | 392| `android.graphics.Color` | Color hex strings (`#RRGGBB`, `#RRGGBBAA`, `#RGB`, `#RGBA`), named colors following the [CSS3/SVG specification](https://www.w3.org/TR/css-color-3/#svg-color) or `"transparent"` | 393| `kotlin.Pair<A, B>` | Array with two values, where the first one is of type _A_ and the second is of type _B_ | 394 395</APIBox> 396<APIBox header="Records"> 397 398_Record_ is a convertible type and an equivalent of the dictionary (Swift) or map (Kotlin), but represented as a struct where each field can have its own type and provide a default value. 399It is a better way to represent a JavaScript object with the native type-safety. 400 401<CodeBlocksTable> 402 403```swift 404struct FileReadOptions: Record { 405 @Field 406 var encoding: String = "utf8" 407 408 @Field 409 var position: Int = 0 410 411 @Field 412 var length: Int? 413} 414 415// Now this record can be used as an argument of the functions or the view prop setters. 416Function("readFile") { (path: String, options: FileReadOptions) -> String in 417 // Read the file using given `options` 418} 419``` 420 421```kotlin 422class FileReadOptions : Record { 423 @Field 424 val encoding: String = "utf8" 425 426 @Field 427 val position: Int = 0 428 429 @Field 430 val length: Int? 431} 432 433// Now this record can be used as an argument of the functions or the view prop setters. 434Function("readFile") { path: String, options: FileReadOptions -> 435 // Read the file using given `options` 436} 437``` 438 439</CodeBlocksTable> 440</APIBox> 441<APIBox header="Enums"> 442 443With enums we can go even further with the above example (with `FileReadOptions` record) and limit supported encodings to `"utf8"` and `"base64"`. To use an enum as an argument or record field, it must represent a primitive value (e.g. `String`, `Int`) and conform to `Enumerable`. 444 445<CodeBlocksTable> 446 447```swift 448enum FileEncoding: String, Enumerable { 449 case utf8 450 case base64 451} 452 453struct FileReadOptions: Record { 454 @Field 455 var encoding: FileEncoding = .utf8 456 // ... 457} 458``` 459 460```kotlin 461// Note: the constructor must have an argument called value. 462enum class FileEncoding(val value: String) : Enumerable { 463 utf8("utf8"), 464 base64("base64") 465} 466 467class FileReadOptions : Record { 468 @Field 469 val encoding: FileEncoding = FileEncoding.utf8 470 // ... 471} 472``` 473 474</CodeBlocksTable> 475</APIBox> 476<APIBox header="Eithers"> 477 478There are some use cases where you want to pass various types for a single function argument. This is where Either types might come in handy. 479They act as a container for a value of one of a couple of types. 480 481<CodeBlocksTable> 482 483```swift 484Function("foo") { (bar: Either<String, Int>) in 485 if let bar: String = bar.get() { 486 // `bar` is a String 487 } 488 if let bar: Int = bar.get() { 489 // `bar` is an Int 490 } 491} 492``` 493 494```kotlin 495Function("foo") { bar: Either<String, Int> -> 496 bar.get(String::class).let { 497 // `it` is a String 498 } 499 bar.get(Int::class).let { 500 // `it` is an Int 501 } 502} 503``` 504 505</CodeBlocksTable> 506 507The implementation for three Either types is currently provided out of the box, allowing you to use up to four different subtypes. 508 509- `Either<FirstType, SecondType>` — A container for one of two types. 510- `EitherOfThree<FirstType, SecondType, ThirdType>` — A container for one of three types. 511- `EitherOfFour<FirstType, SecondType, ThirdType, FourthType>` — A container for one of four types. 512 513> Either types are available as of SDK 47. 514 515</APIBox> 516 517## Native Classes 518 519<APIBox header="Module"> 520 521A base class for a native module. 522 523#### Properties 524 525<APIMethod 526 name="appContext" 527 comment="Provides access to the [`AppContext`](#appcontext)." 528 returnTypeName="AppContext" 529 isProperty={true} 530 isReturnTypeReference={true} 531/> 532 533#### Methods 534 535<APIMethod 536 name="sendEvent" 537 comment="Sends an event with a given name and a payload to JavaScript. See [`Sending events`](#sending-events)" 538 returnTypeName="void" 539 parameters={[ 540 { 541 name: 'eventName', 542 comment: 'The name of the JavaScript event', 543 typeName: 'string', 544 }, 545 { 546 name: 'payload', 547 comment: 'The event payload', 548 typeName: 'Android: Map<String, Any?> | Bundle\niOS: [String: Any?]', 549 }, 550 ]} 551/> 552 553</APIBox> 554 555<APIBox header="AppContext"> 556 557The app context is an interface to a single Expo app. 558 559</APIBox> 560 561<APIBox header="ExpoView"> 562 563A base class that should be used by all exported views. 564 565On iOS, `ExpoView` extends the `RCTView` which handles some styles (e.g. borders) and accessibility. 566 567#### Properties 568 569<APIMethod 570 name="appContext" 571 comment="Provides access to the [`AppContext`](#appcontext)." 572 returnTypeName="AppContext" 573 isProperty={true} 574 isReturnTypeReference={true} 575/> 576 577<hr /> 578 579#### Extending `ExpoView` 580 581To export your view using the [`View`](#view) component, your custom class must inherit from the `ExpoView`. By doing that you will get access to the [`AppContext`](#appcontext) object. It's the only way of communicating with other modules and the JavaScript runtime. Also, you can't change constructor parameters, becuase provided view will be initialized by `expo-modules-core`. 582 583<CodeBlocksTable> 584 585```swift 586class LinearGradientView: ExpoView {} 587 588public class LinearGradientModule: Module { 589 public func definition() -> ModuleDefinition { 590 View(LinearGradientView.self) { 591 // ... 592 } 593 } 594} 595``` 596 597```kotlin 598class LinearGradientView( 599 context: Context, 600 appContext: AppContext, 601) : ExpoView(context, appContext) 602 603class LinearGradientModule : Module() { 604 override fun definition() = ModuleDefinition { 605 View(LinearGradientView::class) { 606 // ... 607 } 608 } 609} 610``` 611 612</CodeBlocksTable> 613 614</APIBox> 615 616## Guides 617 618<APIBox header="Sending events"> 619 620While JavaScript/TypeScript to Native communication is mostly covered by native functions, you might also want to let the JavaScript/TypeScript code know about certain system events, for example, when the clipboard content changes. 621 622To do this, in the module definition, you need to provide the event names that the module can send using the [Events](#events) definition component. After that, you can use the `sendEvent(eventName, payload)` function on the module instance to send the actual event with some payload. For example, a minimal clipboard implementation that sends native events may look like this: 623 624<CodeBlocksTable> 625 626```swift 627let CLIPBOARD_CHANGED_EVENT_NAME = "onClipboardChanged" 628 629public class ClipboardModule: Module { 630 public func definition() -> ModuleDefinition { 631 Events(CLIPBOARD_CHANGED_EVENT_NAME) 632 633 OnStartObserving { 634 NotificationCenter.default.addObserver( 635 self, 636 selector: #selector(self.clipboardChangedListener), 637 name: UIPasteboard.changedNotification, 638 object: nil 639 ) 640 } 641 642 OnStopObserving { 643 NotificationCenter.default.removeObserver( 644 self, 645 name: UIPasteboard.changedNotification, 646 object: nil 647 ) 648 } 649 } 650 651 @objc 652 private func clipboardChangedListener() { 653 sendEvent(CLIPBOARD_CHANGED_EVENT_NAME, [ 654 "contentTypes": availableContentTypes() 655 ]) 656 } 657} 658``` 659 660```kotlin 661const val CLIPBOARD_CHANGED_EVENT_NAME = "onClipboardChanged" 662 663class ClipboardModule : Module() { 664 override fun definition() = ModuleDefinition { 665 Events(CLIPBOARD_CHANGED_EVENT_NAME) 666 667 OnStartObserving { 668 clipboardManager?.addPrimaryClipChangedListener(listener) 669 } 670 671 OnStopObserving { 672 clipboardManager?.removePrimaryClipChangedListener(listener) 673 } 674 } 675 676 private val clipboardManager: ClipboardManager? 677 get() = appContext.reactContext?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager 678 679 private val listener = ClipboardManager.OnPrimaryClipChangedListener { 680 clipboardManager?.primaryClipDescription?.let { clip -> 681 [email protected]( 682 CLIPBOARD_CHANGED_EVENT_NAME, 683 bundleOf( 684 "contentTypes" to availableContentTypes(clip) 685 ) 686 ) 687 } 688 } 689} 690``` 691 692</CodeBlocksTable> 693 694To subscribe to these events in JavaScript/TypeScript, you need to wrap the native module with `EventEmitter` class as shown: 695 696```ts TypeScript 697import { requireNativeModule, EventEmitter, Subscription } from 'expo-modules-core'; 698 699const ClipboardModule = requireNativeModule('Clipboard'); 700const emitter = new EventEmitter(ClipboardModule); 701 702export function addClipboardListener(listener: (event) => void): Subscription { 703 return emitter.addListener('onClipboardChanged', listener); 704} 705``` 706 707</APIBox> 708 709## Examples 710 711<CodeBlocksTable> 712 713```swift 714public class MyModule: Module { 715 public func definition() -> ModuleDefinition { 716 Name("MyFirstExpoModule") 717 718 Function("hello") { (name: String) in 719 return "Hello \(name)!" 720 } 721 } 722} 723``` 724 725```kotlin 726class MyModule : Module() { 727 override fun definition() = ModuleDefinition { 728 Name("MyFirstExpoModule") 729 730 Function("hello") { name: String -> 731 return "Hello $name!" 732 } 733 } 734} 735``` 736 737</CodeBlocksTable> 738 739For more examples from real modules, you can refer to Expo modules that already use this API on GitHub: 740 741- `expo-battery` ([Swift](https://github.com/expo/expo/tree/main/packages/expo-battery/ios)) 742- `expo-cellular` ([Swift](https://github.com/expo/expo/tree/main/packages/expo-cellular/ios), [Kotlin](https://github.com/expo/expo/tree/main/packages/expo-cellular/android/src/main/java/expo/modules/cellular)) 743- `expo-clipboard` ([Swift](https://github.com/expo/expo/tree/main/packages/expo-clipboard/ios), [Kotlin](https://github.com/expo/expo/tree/main/packages/expo-clipboard/android/src/main/java/expo/modules/clipboard)) 744- `expo-crypto` ([Swift](https://github.com/expo/expo/tree/main/packages/expo-crypto/ios), [Kotlin](https://github.com/expo/expo/tree/main/packages/expo-crypto/android/src/main/java/expo/modules/crypto)) 745- `expo-haptics` ([Swift](https://github.com/expo/expo/tree/main/packages/expo-haptics/ios)) 746- `expo-image-manipulator` ([Swift](https://github.com/expo/expo/tree/main/packages/expo-image-manipulator/ios)) 747- `expo-image-picker` ([Swift](https://github.com/expo/expo/tree/main/packages/expo-image-picker/ios), [Kotlin](https://github.com/expo/expo/tree/main/packages/expo-image-picker/android/src/main/java/expo/modules/imagepicker)) 748- `expo-linear-gradient` ([Swift](https://github.com/expo/expo/tree/main/packages/expo-linear-gradient/ios), [Kotlin](https://github.com/expo/expo/tree/main/packages/expo-linear-gradient/android/src/main/java/expo/modules/lineargradient)) 749- `expo-localization` ([Swift](https://github.com/expo/expo/tree/main/packages/expo-localization/ios), [Kotlin](https://github.com/expo/expo/tree/main/packages/expo-localization/android/src/main/java/expo/modules/localization)) 750- `expo-store-review` ([Swift](https://github.com/expo/expo/tree/main/packages/expo-store-review/ios)) 751- `expo-system-ui` ([Swift](https://github.com/expo/expo/tree/main/packages/expo-system-ui/ios/ExpoSystemUI)) 752- `expo-web-browser` ([Swift](https://github.com/expo/expo/blob/main/packages/expo-web-browser/ios), [Kotlin](https://github.com/expo/expo/blob/main/packages/expo-web-browser/android/src/main/java/expo/modules/webbrowser)) 753