expo-dev-launcher 2.1.5 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,24 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 2.2.0 — 2023-04-13
14
+
15
+ ### 🎉 New features
16
+
17
+ - Added experimental network inspector. ([#21265](https://github.com/expo/expo/pull/21265), [#21327](https://github.com/expo/expo/pull/21327) by [@kudo](https://github.com/kudo))
18
+
19
+ ### 🐛 Bug fixes
20
+
21
+ - Add missing `mimeType` when emitting network responses. ([#21676](https://github.com/expo/expo/pull/21676) by [@byCedric](https://github.com/byCedric))
22
+ - Add missing `Network.requestWillBeSentExtraInfo` when emitting network requests. ([#21965](https://github.com/expo/expo/pull/21965) by [@byCedric](https://github.com/byCedric))
23
+ - Don't require legacy manifest signature in dev clients. ([#21970](https://github.com/expo/expo/pull/21970) by [@wschurman](https://github.com/wschurman))
24
+
25
+ ## 2.1.6 — 2023-03-20
26
+
27
+ ### 🐛 Bug fixes
28
+
29
+ - Change arg in gradle `.execute()` call to null to inherit env variables from user's env ([#21712](https://github.com/expo/expo/pull/21712) by [@phoenixiguess](https://github.com/phoenixiguess))
30
+
13
31
  ## 2.1.5 — 2023-03-03
14
32
 
15
33
  ### 💡 Others
@@ -40,7 +40,7 @@ android {
40
40
  minSdkVersion safeExtGet('minSdkVersion', 21)
41
41
  targetSdkVersion safeExtGet("targetSdkVersion", 33)
42
42
  versionCode 9
43
- versionName "2.1.5"
43
+ versionName "2.2.0"
44
44
  }
45
45
 
46
46
  lintOptions {
@@ -180,7 +180,7 @@ def versionToNumber(major, minor, patch) {
180
180
  }
181
181
 
182
182
  def getNodeModulesPackageVersion(packageName, overridePropName) {
183
- def nodeModulesVersion = ["node", "-e", "console.log(require('$packageName/package.json').version);"].execute([], projectDir).text.trim()
183
+ def nodeModulesVersion = ["node", "-e", "console.log(require('$packageName/package.json').version);"].execute(null, projectDir).text.trim()
184
184
  def version = safeExtGet(overridePropName, nodeModulesVersion)
185
185
 
186
186
  def coreVersion = version.split("-")[0]
@@ -59,6 +59,7 @@ fun createUpdatesConfigurationWithUrl(url: Uri, projectUrl: Uri, installationID:
59
59
  "launchWaitMs" to 60000,
60
60
  "checkOnLaunch" to "ALWAYS",
61
61
  "enabled" to true,
62
- "requestHeaders" to requestHeaders
62
+ "requestHeaders" to requestHeaders,
63
+ "expectsSignedManifest" to false,
63
64
  )
64
65
  }
@@ -0,0 +1,232 @@
1
+ package expo.modules.devlauncher.network
2
+
3
+ import androidx.collection.ArrayMap
4
+ import com.facebook.react.ReactInstanceManager
5
+ import com.facebook.react.bridge.Inspector
6
+ import com.facebook.react.common.LifecycleState
7
+ import com.facebook.react.devsupport.DevServerHelper
8
+ import com.facebook.react.devsupport.InspectorPackagerConnection
9
+ import expo.modules.devlauncher.DevLauncherController
10
+ import okhttp3.Headers
11
+ import okhttp3.Request
12
+ import okhttp3.Response
13
+ import okio.Buffer
14
+ import org.json.JSONObject
15
+ import java.lang.ref.WeakReference
16
+ import java.lang.reflect.Field
17
+ import java.lang.reflect.Method
18
+ import java.math.BigDecimal
19
+ import java.math.RoundingMode
20
+
21
+ class DevLauncherNetworkLogger private constructor() {
22
+ private var reactInstanceHashCode: Int = 0
23
+ private var _inspectorPackagerConnection: InspectorPackagerConnectionWrapper? = null
24
+
25
+ private val inspectorPackagerConnection: InspectorPackagerConnectionWrapper
26
+ get() {
27
+ val reactInstanceManager = DevLauncherController.instance.appHost.reactInstanceManager
28
+ if (reactInstanceHashCode != reactInstanceManager.hashCode()) {
29
+ _inspectorPackagerConnection?.clear()
30
+ _inspectorPackagerConnection = null
31
+ reactInstanceHashCode = 0
32
+ }
33
+ if (_inspectorPackagerConnection == null) {
34
+ _inspectorPackagerConnection = InspectorPackagerConnectionWrapper(reactInstanceManager)
35
+ reactInstanceHashCode = reactInstanceManager.hashCode()
36
+ }
37
+ return requireNotNull(_inspectorPackagerConnection)
38
+ }
39
+
40
+ /**
41
+ * Returns true when it is allowed to send CDP events
42
+ */
43
+ fun shouldEmitEvents(): Boolean {
44
+ return DevLauncherController.wasInitialized() && DevLauncherController.instance.appHost.reactInstanceManager.lifecycleState == LifecycleState.RESUMED
45
+ }
46
+
47
+ /**
48
+ * Emits CDP `Network.requestWillBeSent` and `Network.requestWillBeSentExtraInfo` events
49
+ */
50
+ fun emitNetworkWillBeSent(request: Request, requestId: String, redirectResponse: Response?) {
51
+ val now = BigDecimal(System.currentTimeMillis() / 1000.0).setScale(3, RoundingMode.CEILING)
52
+ val requestParams = buildMap<String, Any> {
53
+ put("url", request.url().toString())
54
+ put("method", request.method())
55
+ put("headers", request.headers().toSingleMap())
56
+ val body = request.body()
57
+ if (body != null && body.contentLength() < MAX_BODY_SIZE) {
58
+ val buffer = Buffer()
59
+ body.writeTo(buffer)
60
+ put("postData", buffer.readUtf8(buffer.size.coerceAtMost(MAX_BODY_SIZE)))
61
+ }
62
+ }
63
+ val requestWillBeSentParams = buildMap<String, Any> {
64
+ put("requestId", requestId)
65
+ put("loaderId", "")
66
+ put("documentURL", "mobile")
67
+ put("initiator", mapOf("type" to "script"))
68
+ put("redirectHasExtraInfo", redirectResponse != null)
69
+ put("request", requestParams)
70
+ put("referrerPolicy", "no-referrer")
71
+ put("type", "Fetch")
72
+ put("timestamp", now)
73
+ put("wallTime", now)
74
+ if (redirectResponse != null) {
75
+ put("redirectResponse", mapOf(
76
+ "url" to redirectResponse.request().url().toString(),
77
+ "status" to redirectResponse.code(),
78
+ "statusText" to redirectResponse.message(),
79
+ "headers" to redirectResponse.headers().toSingleMap(),
80
+ ))
81
+ }
82
+ }
83
+ val requestWillBeSentData = JSONObject(mapOf(
84
+ "method" to "Network.requestWillBeSent",
85
+ "params" to requestWillBeSentParams,
86
+ ))
87
+ inspectorPackagerConnection.sendWrappedEventToAllPages(requestWillBeSentData.toString())
88
+
89
+ val extraInfoParams = mapOf(
90
+ "requestId" to requestId,
91
+ "associatedCookies" to emptyList<Void>(),
92
+ "headers" to request.headers().toSingleMap(),
93
+ "connectTiming" to mapOf(
94
+ "requestTime" to now,
95
+ ),
96
+ )
97
+ val extraInfoData = JSONObject(mapOf(
98
+ "method" to "Network.requestWillBeSentExtraInfo",
99
+ "params" to extraInfoParams
100
+ ))
101
+ inspectorPackagerConnection.sendWrappedEventToAllPages(extraInfoData.toString())
102
+ }
103
+
104
+ /**
105
+ * Emits CDP `Network.responseReceived` and `Network.loadingFinished` events
106
+ */
107
+ fun emitNetworkResponse(request: Request, requestId: String, response: Response) {
108
+ val now = BigDecimal(System.currentTimeMillis() / 1000.0).setScale(3, RoundingMode.CEILING)
109
+ val responseReceivedParams = mapOf(
110
+ "requestId" to requestId,
111
+ "loaderId" to "",
112
+ "hasExtraInfo" to false,
113
+ "response" to mapOf(
114
+ "url" to request.url().toString(),
115
+ "status" to response.code(),
116
+ "statusText" to response.message(),
117
+ "headers" to response.headers().toSingleMap(),
118
+ "mimeType" to response.header("Content-Type", ""),
119
+ ),
120
+ "referrerPolicy" to "no-referrer",
121
+ "type" to "Fetch",
122
+ "timestamp" to now,
123
+ )
124
+ val responseReceivedData = JSONObject(mapOf(
125
+ "method" to "Network.responseReceived",
126
+ "params" to responseReceivedParams,
127
+ ))
128
+ inspectorPackagerConnection.sendWrappedEventToAllPages(responseReceivedData.toString())
129
+
130
+ val loadingFinishedParams = mapOf(
131
+ "requestId" to requestId,
132
+ "timestamp" to now,
133
+ "encodedDataLength" to (response.body()?.contentLength() ?: 0),
134
+ )
135
+ val loadingFinishedData = JSONObject(mapOf(
136
+ "method" to "Network.loadingFinished",
137
+ "params" to loadingFinishedParams,
138
+ ))
139
+ inspectorPackagerConnection.sendWrappedEventToAllPages(loadingFinishedData.toString())
140
+ }
141
+
142
+ /**
143
+ * Emits our custom `Expo(Network.receivedResponseBody)` event
144
+ */
145
+ fun emitNetworkDidReceiveBody(requestId: String, response: Response) {
146
+ val contentLength = response.body()?.contentLength() ?: 0
147
+ if (contentLength <= 0 || contentLength > MAX_BODY_SIZE) {
148
+ return
149
+ }
150
+ val body = response.peekBody(MAX_BODY_SIZE)
151
+ val contentType = body.contentType()
152
+ val isText = contentType?.type() == "text" || (contentType?.type() == "application" && contentType?.subtype() == "json")
153
+ val bodyString = if (isText) body.string() else body.source().readByteString().base64()
154
+ val params = mapOf(
155
+ "requestId" to requestId,
156
+ "body" to bodyString,
157
+ "base64Encoded" to !isText,
158
+ )
159
+ val data = JSONObject(mapOf(
160
+ "method" to "Expo(Network.receivedResponseBody)",
161
+ "params" to params,
162
+ ))
163
+ inspectorPackagerConnection.sendWrappedEventToAllPages(data.toString())
164
+ }
165
+
166
+ companion object {
167
+ val instance = DevLauncherNetworkLogger()
168
+ private const val MAX_BODY_SIZE = 1048576L
169
+ }
170
+ }
171
+
172
+ /**
173
+ * A `InspectorPackagerConnection` wrapper to expose private members with reflection
174
+ */
175
+ internal class InspectorPackagerConnectionWrapper constructor(reactInstanceManager: ReactInstanceManager) {
176
+ private var inspectorPackagerConnectionWeak: WeakReference<InspectorPackagerConnection> = WeakReference(null)
177
+ private val devServerHelperWeak: WeakReference<DevServerHelper>
178
+ private val inspectorPackagerConnectionField: Field
179
+ private val sendWrappedEventMethod: Method
180
+
181
+ private val inspectorPackagerConnection: InspectorPackagerConnection?
182
+ get() {
183
+ var inspectorPackagerConnection = inspectorPackagerConnectionWeak.get()
184
+ if (inspectorPackagerConnection == null) {
185
+ val devServerHelper = devServerHelperWeak.get() ?: return null
186
+ inspectorPackagerConnection = inspectorPackagerConnectionField[devServerHelper] as? InspectorPackagerConnection
187
+
188
+ if (inspectorPackagerConnection != null) {
189
+ inspectorPackagerConnectionWeak = WeakReference(inspectorPackagerConnection)
190
+ }
191
+ }
192
+ return inspectorPackagerConnection
193
+ }
194
+
195
+ init {
196
+ val devSupportManager = reactInstanceManager.devSupportManager
197
+ val devSupportManagerBaseClass: Class<*> = devSupportManager.javaClass.superclass
198
+ val mDevServerHelperField = devSupportManagerBaseClass.getDeclaredField("mDevServerHelper")
199
+ mDevServerHelperField.isAccessible = true
200
+ val devServerHelper = mDevServerHelperField[devSupportManager]
201
+ devServerHelperWeak = WeakReference(devServerHelper as DevServerHelper)
202
+
203
+ inspectorPackagerConnectionField = DevServerHelper::class.java.getDeclaredField("mInspectorPackagerConnection")
204
+ inspectorPackagerConnectionField.isAccessible = true
205
+
206
+ sendWrappedEventMethod = InspectorPackagerConnection::class.java.getDeclaredMethod("sendWrappedEvent", String::class.java, String::class.java)
207
+ sendWrappedEventMethod.isAccessible = true
208
+ }
209
+
210
+ fun clear() {
211
+ inspectorPackagerConnectionWeak.clear()
212
+ }
213
+
214
+ fun sendWrappedEventToAllPages(event: String) {
215
+ val inspectorPackagerConnection = this.inspectorPackagerConnection ?: return
216
+ for (page in Inspector.getPages()) {
217
+ sendWrappedEventMethod.invoke(inspectorPackagerConnection, page.id.toString(), event)
218
+ }
219
+ }
220
+ }
221
+
222
+ /**
223
+ * OkHttp `Headers` extension method to generate a simple key-value map
224
+ * which only exposing single value for a key.
225
+ */
226
+ fun Headers.toSingleMap(): Map<String, String> {
227
+ val result = ArrayMap<String, String>()
228
+ for (key in names()) {
229
+ result[key] = get(key)
230
+ }
231
+ return result
232
+ }
@@ -0,0 +1,57 @@
1
+ package expo.modules.devlauncher.network
2
+
3
+ import okhttp3.Interceptor
4
+ import okhttp3.Response
5
+
6
+ /**
7
+ * The OkHttp network interceptor to log requests and the CDP events to [DevLauncherNetworkLogger]
8
+ */
9
+ @Suppress("unused")
10
+ class DevLauncherOkHttpNetworkInterceptor : Interceptor {
11
+ override fun intercept(chain: Interceptor.Chain): Response {
12
+ if (!DevLauncherNetworkLogger.instance.shouldEmitEvents()) {
13
+ return chain.proceed(chain.request());
14
+ }
15
+ val request = chain.request()
16
+ val redirectResponse = request.tag(RedirectResponse::class.java)
17
+ val requestId = redirectResponse?.requestId ?: request.hashCode().toString()
18
+ DevLauncherNetworkLogger.instance.emitNetworkWillBeSent(request, requestId, redirectResponse?.priorResponse)
19
+
20
+ val response = chain.proceed(request)
21
+
22
+ if (response.isRedirect) {
23
+ response.request().tag(RedirectResponse::class.java)?.let {
24
+ it.requestId = requestId
25
+ it.priorResponse = response
26
+ }
27
+ } else {
28
+ DevLauncherNetworkLogger.instance.emitNetworkResponse(request, requestId, response)
29
+ DevLauncherNetworkLogger.instance.emitNetworkDidReceiveBody(requestId, response)
30
+ }
31
+ return response
32
+ }
33
+ }
34
+
35
+ /**
36
+ * The OkHttp app interceptor to add custom tag for [RedirectResponse]
37
+ */
38
+ @Suppress("unused")
39
+ class DevLauncherOkHttpAppInterceptor : Interceptor {
40
+ override fun intercept(chain: Interceptor.Chain): Response {
41
+ if (!DevLauncherNetworkLogger.instance.shouldEmitEvents()) {
42
+ return chain.proceed(chain.request());
43
+ }
44
+ return chain.proceed(chain.request().newBuilder()
45
+ .tag(RedirectResponse::class.java, RedirectResponse())
46
+ .build()
47
+ )
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Custom property for redirect requests
53
+ */
54
+ internal class RedirectResponse {
55
+ var requestId: String? = null
56
+ var priorResponse: Response? = null
57
+ }
@@ -0,0 +1,38 @@
1
+ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
2
+
3
+ plugins {
4
+ kotlin("jvm") version "1.8.10"
5
+ id("java-gradle-plugin")
6
+ }
7
+
8
+ repositories {
9
+ google()
10
+ mavenCentral()
11
+ }
12
+
13
+ dependencies {
14
+ implementation(gradleApi())
15
+ implementation("com.android.tools.build:gradle:7.3.1")
16
+ }
17
+
18
+ java {
19
+ sourceCompatibility = JavaVersion.VERSION_11
20
+ targetCompatibility = JavaVersion.VERSION_11
21
+ }
22
+
23
+ tasks.withType<KotlinCompile> {
24
+ kotlinOptions {
25
+ jvmTarget = JavaVersion.VERSION_11.toString()
26
+ }
27
+ }
28
+
29
+ group = "expo.modules"
30
+
31
+ gradlePlugin {
32
+ plugins {
33
+ register("expoDevLauncherPlugin") {
34
+ id = "expo-dev-launcher-gradle-plugin"
35
+ implementationClass = "expo.modules.devlauncher.DevLauncherPlugin"
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,101 @@
1
+ package expo.modules.devlauncher
2
+
3
+ import com.android.build.api.instrumentation.AsmClassVisitorFactory
4
+ import com.android.build.api.instrumentation.ClassContext
5
+ import com.android.build.api.instrumentation.ClassData
6
+ import com.android.build.api.instrumentation.FramesComputationMode
7
+ import com.android.build.api.instrumentation.InstrumentationParameters
8
+ import com.android.build.api.instrumentation.InstrumentationScope
9
+ import com.android.build.api.variant.AndroidComponentsExtension
10
+ import org.gradle.api.Plugin
11
+ import org.gradle.api.Project
12
+ import org.gradle.api.provider.Property
13
+ import org.gradle.api.tasks.Input
14
+ import org.gradle.api.tasks.Optional
15
+ import org.objectweb.asm.ClassVisitor
16
+ import org.objectweb.asm.MethodVisitor
17
+ import org.objectweb.asm.Opcodes
18
+ import org.slf4j.LoggerFactory
19
+
20
+ abstract class DevLauncherPlugin : Plugin<Project> {
21
+
22
+ override fun apply(project: Project) {
23
+ val enableNetworkInspector = project.properties["EX_DEV_CLIENT_NETWORK_INSPECTOR"]?.toString()?.toBoolean()
24
+ if (enableNetworkInspector != null && enableNetworkInspector) {
25
+ val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
26
+ androidComponents.onVariants(androidComponents.selector().withBuildType("debug")) { variant ->
27
+ variant.instrumentation.transformClassesWith(DevLauncherClassVisitorFactory::class.java, InstrumentationScope.ALL) {
28
+ }
29
+ variant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS)
30
+ }
31
+ }
32
+ }
33
+
34
+ interface DevLauncherPluginParameters : InstrumentationParameters {
35
+ @get:Input
36
+ @get:Optional
37
+ val enabled: Property<Boolean>
38
+ }
39
+
40
+ abstract class DevLauncherClassVisitorFactory : AsmClassVisitorFactory<DevLauncherPluginParameters> {
41
+ override fun createClassVisitor(
42
+ classContext: ClassContext,
43
+ nextClassVisitor: ClassVisitor
44
+ ): ClassVisitor {
45
+ if (parameters.get().enabled.getOrElse(false)) {
46
+ return nextClassVisitor
47
+ }
48
+ return OkHttpClassVisitor(classContext, instrumentationContext.apiVersion.get(), nextClassVisitor)
49
+ }
50
+
51
+ override fun isInstrumentable(classData: ClassData): Boolean {
52
+ if (parameters.get().enabled.getOrElse(false)) {
53
+ return false
54
+ }
55
+ return classData.className in listOf("okhttp3.OkHttpClient\$Builder")
56
+ }
57
+ }
58
+
59
+ class OkHttpClassVisitor(private val classContext: ClassContext, api: Int, classVisitor: ClassVisitor) : ClassVisitor(api, classVisitor) {
60
+ override fun visitMethod(access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor {
61
+ val originalVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
62
+ if (name == "build") {
63
+ return OkHttpClientCustomBuildMethod(api, originalVisitor)
64
+ }
65
+ return originalVisitor
66
+ }
67
+ }
68
+
69
+ class OkHttpClientCustomBuildMethod(api: Int, methodVisitor: MethodVisitor) : MethodVisitor(api, methodVisitor) {
70
+ override fun visitCode() {
71
+ // opcodes for `this.addInterceptor(expo.modules.devlauncher.network.DevLauncherOkHttpAppInterceptor())`
72
+ visitVarInsn(Opcodes.ALOAD, 0)
73
+ visitTypeInsn(Opcodes.NEW, "expo/modules/devlauncher/network/DevLauncherOkHttpAppInterceptor")
74
+ visitInsn(Opcodes.DUP)
75
+ visitMethodInsn(Opcodes.INVOKESPECIAL, "expo/modules/devlauncher/network/DevLauncherOkHttpAppInterceptor", "<init>", "()V", false)
76
+ visitTypeInsn(Opcodes.CHECKCAST, "okhttp3/Interceptor")
77
+ visitMethodInsn(Opcodes.INVOKEVIRTUAL, "okhttp3/OkHttpClient\$Builder", "addInterceptor", "(Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient\$Builder;", false)
78
+
79
+ // opcodes for `this.addNetworkInterceptor(expo.modules.devlauncher.network.DevLauncherOkHttpNetworkInterceptor())`
80
+ visitVarInsn(Opcodes.ALOAD, 0)
81
+ visitTypeInsn(Opcodes.NEW, "expo/modules/devlauncher/network/DevLauncherOkHttpNetworkInterceptor")
82
+ visitInsn(Opcodes.DUP)
83
+ visitMethodInsn(Opcodes.INVOKESPECIAL, "expo/modules/devlauncher/network/DevLauncherOkHttpNetworkInterceptor", "<init>", "()V", false)
84
+ visitTypeInsn(Opcodes.CHECKCAST, "okhttp3/Interceptor")
85
+ visitMethodInsn(Opcodes.INVOKEVIRTUAL, "okhttp3/OkHttpClient\$Builder", "addNetworkInterceptor", "(Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient\$Builder;", false)
86
+
87
+ // opcodes for `return OkHttpClient(this)`
88
+ visitTypeInsn(Opcodes.NEW, "okhttp3/OkHttpClient")
89
+ visitInsn(Opcodes.DUP)
90
+ visitVarInsn(Opcodes.ALOAD, 0)
91
+ visitMethodInsn(Opcodes.INVOKESPECIAL, "okhttp3/OkHttpClient", "<init>", "(Lokhttp3/OkHttpClient\$Builder;)V", false)
92
+ visitInsn(Opcodes.ARETURN)
93
+ }
94
+ }
95
+
96
+ companion object {
97
+ internal val logger by lazy {
98
+ LoggerFactory.getLogger(DevLauncherPlugin::class.java)
99
+ }
100
+ }
101
+ }
@@ -32,17 +32,24 @@ Pod::Spec.new do |s|
32
32
  'GCC_PREPROCESSOR_DEFINITIONS' => "EX_DEV_LAUNCHER_VERSION=#{s.version}"
33
33
  }
34
34
 
35
- # Swift/Objective-C compatibility
36
- s.pod_target_xcconfig = { "DEFINES_MODULE" => "YES" }
35
+ other_c_flags = '$(inherited)'
37
36
  dev_launcher_url = ENV['EX_DEV_LAUNCHER_URL'] || ""
38
37
  if dev_launcher_url != ""
39
38
  escaped_dev_launcher_url = Shellwords.escape(dev_launcher_url).gsub('/','\\/')
40
- s.pod_target_xcconfig = {
41
- 'DEFINES_MODULE' => 'YES',
42
- 'OTHER_CFLAGS[config=Debug]' => "$(inherited) -DEX_DEV_LAUNCHER_URL=\"\\\"" + escaped_dev_launcher_url + "\\\"\""
43
- }
39
+ other_c_flags += " -DEX_DEV_LAUNCHER_URL=\"\\\"" + escaped_dev_launcher_url + "\\\"\""
40
+ end
41
+ other_swift_flags = "$(inherited)"
42
+ if ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] == '1'
43
+ other_swift_flags += ' -DEX_DEV_CLIENT_NETWORK_INSPECTOR'
44
44
  end
45
45
 
46
+ # Swift/Objective-C compatibility
47
+ s.pod_target_xcconfig = {
48
+ 'DEFINES_MODULE' => 'YES',
49
+ 'OTHER_CFLAGS[config=Debug]' => other_c_flags,
50
+ 'OTHER_SWIFT_FLAGS[config=Debug]' => other_swift_flags,
51
+ }
52
+
46
53
  s.user_target_xcconfig = {
47
54
  "HEADER_SEARCH_PATHS" => "\"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-launcher/Swift Compatibility Header\"",
48
55
  }
@@ -7,5 +7,14 @@
7
7
  "appDelegateSubscribers": ["ExpoDevLauncherAppDelegateSubscriber"],
8
8
  "reactDelegateHandlers": ["ExpoDevLauncherReactDelegateHandler"],
9
9
  "debugOnly": true
10
+ },
11
+ "android": {
12
+ "gradlePlugins": [
13
+ {
14
+ "id": "expo-dev-launcher-gradle-plugin",
15
+ "group": "expo.modules",
16
+ "sourceDir": "expo-dev-launcher-gradle-plugin"
17
+ }
18
+ ]
10
19
  }
11
20
  }
@@ -78,6 +78,7 @@
78
78
  self.errorManager = [[EXDevLauncherErrorManager alloc] initWithController:self];
79
79
  self.installationIDHelper = [EXDevLauncherInstallationIDHelper new];
80
80
  self.shouldPreferUpdatesInterfaceSourceUrl = NO;
81
+ [EXDevLauncherNetworkLogger.shared enable];
81
82
  }
82
83
  return self;
83
84
  }
@@ -22,7 +22,8 @@ NS_ASSUME_NONNULL_BEGIN
22
22
  @"EXUpdatesCheckOnLaunch": @"ALWAYS",
23
23
  @"EXUpdatesHasEmbeddedUpdate": @(NO),
24
24
  @"EXUpdatesEnabled": @(YES),
25
- @"EXUpdatesRequestHeaders": requestHeaders
25
+ @"EXUpdatesRequestHeaders": requestHeaders,
26
+ @"EXUpdatesExpectsSignedManifest": @(NO),
26
27
  };
27
28
  }
28
29
 
@@ -23,6 +23,47 @@ class EXDevLauncherUtils {
23
23
  }
24
24
  }
25
25
 
26
+ /**
27
+ Swizzles implementations of given class method selectors.
28
+ This function will backup original selector implementation for `invokeOriginalClassMethod`.
29
+ */
30
+ static func swizzleClassMethod(selector selectorA: Selector, withSelector selectorB: Selector, forClass: AnyClass) {
31
+ if let methodA = class_getClassMethod(forClass, selectorA),
32
+ let methodB = class_getClassMethod(forClass, selectorB) {
33
+ let impA = method_getImplementation(methodA)
34
+ let backupSelectorA = NSSelectorFromString("_" + NSStringFromSelector(selectorA))
35
+ let metaClass = objc_getMetaClass(String(describing: forClass)) as? AnyClass
36
+ class_addMethod(metaClass, backupSelectorA, impA, method_getTypeEncoding(methodA))
37
+ method_setImplementation(methodA, method_getImplementation(methodB))
38
+ }
39
+ }
40
+
41
+ /**
42
+ Invokes the original implementation before swizzling for the given selector
43
+ */
44
+ static func invokeOriginalClassMethod(selector: Selector, forClass: AnyClass) throws -> Any? {
45
+ typealias ClassMethod = @convention(c) (AnyObject, Selector) -> Any
46
+ let imp = try getOriginalClassMethodImp(selector: selector, forClass: forClass)
47
+ return unsafeBitCast(imp, to: ClassMethod.self)(self, selector)
48
+ }
49
+
50
+ /**
51
+ Invokes the original implementation before swizzling for the given selector
52
+ */
53
+ static func invokeOriginalClassMethod(selector: Selector, forClass: AnyClass, A0: Any) throws -> Any? {
54
+ typealias ClassMethod = @convention(c) (AnyObject, Selector, Any) -> Any
55
+ let imp = try getOriginalClassMethodImp(selector: selector, forClass: forClass)
56
+ return unsafeBitCast(imp, to: ClassMethod.self)(self, selector, A0)
57
+ }
58
+
59
+ private static func getOriginalClassMethodImp(selector: Selector, forClass: AnyClass) throws -> IMP {
60
+ let backupSelector = NSSelectorFromString("_" + NSStringFromSelector(selector))
61
+ guard let method = class_getClassMethod(forClass, backupSelector) else {
62
+ fatalError("Backup selector does not exist - forClass[\(forClass)] backupSelector[\(NSStringFromSelector(backupSelector))]")
63
+ }
64
+ return method_getImplementation(method)
65
+ }
66
+
26
67
  static func resourcesBundle() -> Bundle? {
27
68
  let frameworkBundle = Bundle(for: EXDevLauncherUtils.self)
28
69
 
@@ -0,0 +1,234 @@
1
+ import React
2
+
3
+ #if DEBUG && EX_DEV_CLIENT_NETWORK_INSPECTOR
4
+
5
+ /**
6
+ This class intercepts all default `URLSession` requests and send CDP events to the connecting metro-inspector-proxy
7
+ */
8
+ @objc
9
+ public class EXDevLauncherNetworkLogger: NSObject {
10
+ private var enabled: Bool = false
11
+ internal var inspectorPackagerConn: RCTInspectorPackagerConnection?
12
+
13
+ @objc
14
+ public static let shared = EXDevLauncherNetworkLogger()
15
+
16
+ override private init() {}
17
+
18
+ @objc
19
+ public func enable() {
20
+ EXDevLauncherUtils.swizzleClassMethod(
21
+ selector: #selector(RCTInspectorDevServerHelper.connect(withBundleURL:)),
22
+ withSelector: #selector(RCTInspectorDevServerHelper.EXDevLauncher_connect(withBundleURL:)),
23
+ forClass: RCTInspectorDevServerHelper.self
24
+ )
25
+
26
+ EXDevLauncherUtils.swizzleClassMethod(
27
+ selector: #selector(getter: URLSessionConfiguration.default),
28
+ withSelector: #selector(URLSessionConfiguration.EXDevLauncher_urlSessionConfiguration),
29
+ forClass: URLSessionConfiguration.self
30
+ )
31
+ enabled = true
32
+ }
33
+
34
+ /**
35
+ Emits CDP `Network.requestWillBeSent` and `Network.requestWillBeSentExtraInfo` events
36
+ */
37
+ func emitNetworkWillBeSent(request: URLRequest, requestId: String, redirectResponse: HTTPURLResponse?) {
38
+ let now = Date().timeIntervalSince1970
39
+ var requestParams: [String: Any] = [
40
+ "url": request.url?.absoluteString,
41
+ "method": request.httpMethod,
42
+ "headers": request.allHTTPHeaderFields
43
+ ]
44
+ if let httpBody = request.httpBodyData() {
45
+ requestParams["postData"] = String(data: httpBody, encoding: .utf8)
46
+ }
47
+ var params = [
48
+ "requestId": requestId,
49
+ "loaderId": "",
50
+ "documentURL": "mobile",
51
+ "initiator": ["type": "script"],
52
+ "redirectHasExtraInfo": redirectResponse != nil,
53
+ "request": requestParams,
54
+ "referrerPolicy": "no-referrer",
55
+ "type": "Fetch",
56
+ "timestamp": now,
57
+ "wallTime": now
58
+ ] as [String: Any]
59
+ if let redirectResponse {
60
+ params["redirectResponse"] = [
61
+ "url": redirectResponse.url?.absoluteString,
62
+ "status": redirectResponse.statusCode,
63
+ "statusText": "",
64
+ "headers": redirectResponse.allHeaderFields
65
+ ]
66
+ }
67
+ if let data = try? JSONSerialization.data(
68
+ withJSONObject: ["method": "Network.requestWillBeSent", "params": params],
69
+ options: []
70
+ ), let message = String(data: data, encoding: .utf8) {
71
+ inspectorPackagerConn?.sendWrappedEventToAllPages(message)
72
+ }
73
+ params = [
74
+ "requestId": requestId,
75
+ "associatedCookies": [],
76
+ "headers": requestParams["headers"],
77
+ "connectTiming": [
78
+ "requestTime": now
79
+ ]
80
+ ] as [String: Any]
81
+ if let data = try? JSONSerialization.data(
82
+ withJSONObject: ["method": "Network.requestWillBeSentExtraInfo", "params": params],
83
+ options: []
84
+ ), let message = String(data: data, encoding: .utf8) {
85
+ inspectorPackagerConn?.sendWrappedEventToAllPages(message)
86
+ }
87
+ }
88
+
89
+ /**
90
+ Emits CDP `Network.responseReceived` and `Network.loadingFinished` events
91
+ */
92
+ func emitNetworkResponse(request: URLRequest, requestId: String, response: HTTPURLResponse) {
93
+ let now = Date().timeIntervalSince1970
94
+
95
+ var params = [
96
+ "requestId": requestId,
97
+ "loaderId": "",
98
+ "hasExtraInfo": false,
99
+ "response": [
100
+ "url": request.url?.absoluteString,
101
+ "status": response.statusCode,
102
+ "statusText": "",
103
+ "headers": response.allHeaderFields,
104
+ "mimeType": response.value(forHTTPHeaderField: "Content-Type") ?? ""
105
+ ],
106
+ "referrerPolicy": "no-referrer",
107
+ "type": "Fetch",
108
+ "timestamp": now
109
+ ] as [String: Any]
110
+ if let data = try? JSONSerialization.data(
111
+ withJSONObject: ["method": "Network.responseReceived", "params": params],
112
+ options: []
113
+ ), let message = String(data: data, encoding: .utf8) {
114
+ inspectorPackagerConn?.sendWrappedEventToAllPages(message)
115
+ }
116
+
117
+ params = [
118
+ "requestId": requestId,
119
+ "timestamp": now,
120
+ "encodedDataLength": response.expectedContentLength
121
+ ] as [String: Any]
122
+ if let data = try? JSONSerialization.data(
123
+ withJSONObject: [
124
+ "method": "Network.loadingFinished",
125
+ "params": params
126
+ ],
127
+ options: []
128
+ ), let message = String(data: data, encoding: .utf8) {
129
+ inspectorPackagerConn?.sendWrappedEventToAllPages(message)
130
+ }
131
+ }
132
+
133
+ /**
134
+ Emits our custom `Expo(Network.receivedResponseBody)` event
135
+ */
136
+ func emitNetworkDidReceiveBody(requestId: String, responseBody: Data, isText: Bool) {
137
+ let bodyString = isText
138
+ ? String(data: responseBody, encoding: .utf8)
139
+ : responseBody.base64EncodedString()
140
+ let params = [
141
+ "requestId": requestId,
142
+ "body": bodyString,
143
+ "base64Encoded": !isText
144
+ ] as [String: Any]
145
+ if let data = try? JSONSerialization.data(
146
+ withJSONObject: [
147
+ "method": "Expo(Network.receivedResponseBody)",
148
+ "params": params
149
+ ],
150
+ options: []
151
+ ), let message = String(data: data, encoding: .utf8) {
152
+ inspectorPackagerConn?.sendWrappedEventToAllPages(message)
153
+ }
154
+ }
155
+ }
156
+
157
+ extension URLSessionConfiguration {
158
+ private typealias GetterFunc = @convention(c) (AnyObject, Selector) -> URLSessionConfiguration
159
+
160
+ /**
161
+ Swizzled `URLSessionConfiguration.default` for us to add the `EXDevLauncherRequestLoggerProtocol` interceptor
162
+ */
163
+ @objc
164
+ static func EXDevLauncher_urlSessionConfiguration() -> URLSessionConfiguration {
165
+ guard let config = try? EXDevLauncherUtils.invokeOriginalClassMethod(
166
+ selector: #selector(getter: URLSessionConfiguration.default),
167
+ forClass: URLSessionConfiguration.self
168
+ ) as? URLSessionConfiguration else {
169
+ fatalError("Unable to get original URLSessionConfiguration.default")
170
+ }
171
+ var protocolClasses = config.protocolClasses
172
+ protocolClasses?.insert(EXDevLauncherRequestLoggerProtocol.self, at: 0)
173
+ config.protocolClasses = protocolClasses
174
+ return config
175
+ }
176
+ }
177
+
178
+ extension RCTInspectorDevServerHelper {
179
+ private typealias ConnectFunc = @convention(c) (AnyObject, Selector, URL)
180
+ -> RCTInspectorPackagerConnection?
181
+
182
+ /**
183
+ Swizzled `RCTInspectorDevServerHelper.connect(withBundleURL:)` for us to get the `RCTInspectorPackagerConnection` instance
184
+ */
185
+ @objc
186
+ static func EXDevLauncher_connect(withBundleURL bundleURL: URL)
187
+ -> RCTInspectorPackagerConnection? {
188
+ let inspectorPackagerConn = try? EXDevLauncherUtils.invokeOriginalClassMethod(
189
+ selector: #selector(RCTInspectorDevServerHelper.connect(withBundleURL:)),
190
+ forClass: RCTInspectorDevServerHelper.self,
191
+ A0: bundleURL
192
+ ) as? RCTInspectorPackagerConnection
193
+
194
+ EXDevLauncherNetworkLogger.shared.inspectorPackagerConn = inspectorPackagerConn
195
+ return inspectorPackagerConn
196
+ }
197
+ }
198
+
199
+ extension RCTInspectorPackagerConnection {
200
+ /**
201
+ Sends message from native to inspector proxy
202
+ */
203
+ func sendWrappedEventToAllPages(_ event: String) {
204
+ for page in RCTInspector.pages() {
205
+ perform(NSSelectorFromString("sendWrappedEvent:message:"), with: String(page.id), with: event)
206
+ }
207
+ }
208
+ }
209
+
210
+ #else
211
+
212
+ @objc
213
+ public class EXDevLauncherNetworkLogger: NSObject {
214
+ @objc
215
+ public static let shared = EXDevLauncherNetworkLogger()
216
+
217
+ override private init() {}
218
+
219
+ @objc
220
+ public func enable() {
221
+ // no-op when running on release build where RCTInspector classes not exported
222
+ }
223
+
224
+ func emitNetworkWillBeSent(request: URLRequest, requestId: String, redirectResponse: HTTPURLResponse?) {
225
+ }
226
+
227
+ func emitNetworkResponse(request: URLRequest, requestId: String, response: HTTPURLResponse) {
228
+ }
229
+
230
+ func emitNetworkDidReceiveBody(requestId: String, responseBody: Data, isText: Bool) {
231
+ }
232
+ }
233
+
234
+ #endif
@@ -0,0 +1,217 @@
1
+ /**
2
+ A `URLSession` interceptor to log requests and send events to the `EXDevLauncherNetworkLogger`
3
+ */
4
+ @objc
5
+ class EXDevLauncherRequestLoggerProtocol: URLProtocol, URLSessionDataDelegate {
6
+ private static let REQUEST_ID = "EXDevLauncherRequestLoggerProtocol.requestId"
7
+ private static let REDIRECT_RESPONSE = "EXDevLauncherRequestLoggerProtocol.redirectResponse"
8
+ static let MAX_BODY_SIZE = 1_048_576
9
+ private static var requestIdProvider = RequestIdProvider()
10
+ private lazy var urlSession = URLSession(
11
+ configuration: URLSessionConfiguration.default,
12
+ delegate: self,
13
+ delegateQueue: nil
14
+ )
15
+ private var dataTask_: URLSessionDataTask?
16
+ private let responseBody = NSMutableData()
17
+ private var responseIsText = false
18
+ private var responseContentLength: Int64 = 0
19
+
20
+ // MARK: URLProtocol implementations
21
+
22
+ override class func canInit(with request: URLRequest) -> Bool {
23
+ guard let scheme = request.url?.scheme else {
24
+ return false
25
+ }
26
+ if !["http", "https"].contains(scheme) {
27
+ return false
28
+ }
29
+ let isNewRequest = URLProtocol.property(
30
+ forKey: EXDevLauncherRequestLoggerProtocol.REQUEST_ID,
31
+ in: request
32
+ ) == nil
33
+ return isNewRequest
34
+ }
35
+
36
+ override init(
37
+ request: URLRequest,
38
+ cachedResponse: CachedURLResponse?,
39
+ client: URLProtocolClient?
40
+ ) {
41
+ super.init(request: request, cachedResponse: cachedResponse, client: client)
42
+ // swiftlint:disable force_cast
43
+ let mutableRequest = request as! MutableURLRequest
44
+ // swiftlint:enable force_cast
45
+ let redirectResponse = URLProtocol.property(
46
+ forKey: EXDevLauncherRequestLoggerProtocol.REDIRECT_RESPONSE,
47
+ in: request
48
+ ) as? RedirectResponse
49
+ let requestId = redirectResponse?.requestId ?? EXDevLauncherRequestLoggerProtocol.requestIdProvider.create()
50
+ URLProtocol.setProperty(
51
+ requestId,
52
+ forKey: EXDevLauncherRequestLoggerProtocol.REQUEST_ID,
53
+ in: mutableRequest
54
+ )
55
+ EXDevLauncherNetworkLogger.shared.emitNetworkWillBeSent(
56
+ request: mutableRequest as URLRequest,
57
+ requestId: requestId,
58
+ redirectResponse: redirectResponse?.redirectResponse
59
+ )
60
+ dataTask_ = urlSession.dataTask(with: mutableRequest as URLRequest)
61
+ }
62
+
63
+ override class func canonicalRequest(for request: URLRequest) -> URLRequest {
64
+ request
65
+ }
66
+
67
+ override func startLoading() {
68
+ dataTask_?.resume()
69
+ }
70
+
71
+ override func stopLoading() {
72
+ dataTask_?.cancel()
73
+ }
74
+
75
+ // MARK: URLSessionDataDelegate implementations
76
+
77
+ func urlSession(_: URLSession, dataTask _: URLSessionDataTask, didReceive data: Data) {
78
+ client?.urlProtocol(self, didLoad: data)
79
+ if responseBody.length + data.count <= EXDevLauncherRequestLoggerProtocol.MAX_BODY_SIZE {
80
+ responseBody.append(data)
81
+ }
82
+ }
83
+
84
+ func urlSession(
85
+ _: URLSession,
86
+ dataTask: URLSessionDataTask,
87
+ didReceive response: URLResponse,
88
+ completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
89
+ ) {
90
+ if let resp = response as? HTTPURLResponse,
91
+ let currentRequest = dataTask.currentRequest,
92
+ let requestId = URLProtocol.property(
93
+ forKey: EXDevLauncherRequestLoggerProtocol.REQUEST_ID,
94
+ in: currentRequest
95
+ ) as? String {
96
+ EXDevLauncherNetworkLogger.shared.emitNetworkResponse(
97
+ request: request,
98
+ requestId: requestId,
99
+ response: resp
100
+ )
101
+
102
+ let contentType = resp.value(forHTTPHeaderField: "Content-Type")
103
+ responseIsText = (contentType?.starts(with: "text/") ?? false) || contentType == "application/json"
104
+ responseContentLength = resp.expectedContentLength
105
+ }
106
+ completionHandler(.allow)
107
+ client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
108
+ }
109
+
110
+ func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
111
+ if let error = error {
112
+ client?.urlProtocol(self, didFailWithError: error)
113
+ } else {
114
+ client?.urlProtocolDidFinishLoading(self)
115
+ if responseContentLength > 0 && responseContentLength <= EXDevLauncherRequestLoggerProtocol.MAX_BODY_SIZE,
116
+ let currentRequest = task.currentRequest,
117
+ let requestId = URLProtocol.property(
118
+ forKey: EXDevLauncherRequestLoggerProtocol.REQUEST_ID,
119
+ in: currentRequest
120
+ ) as? String {
121
+ EXDevLauncherNetworkLogger.shared.emitNetworkDidReceiveBody(
122
+ requestId: requestId, responseBody: responseBody as Data, isText: responseIsText)
123
+ }
124
+ }
125
+ }
126
+
127
+ func urlSession(
128
+ _: URLSession,
129
+ task _: URLSessionTask,
130
+ willPerformHTTPRedirection response: HTTPURLResponse,
131
+ newRequest request: URLRequest,
132
+ completionHandler: @escaping (URLRequest?) -> Void
133
+ ) {
134
+ let redirectRequest: URLRequest
135
+ if let requestId = URLProtocol.property(forKey: EXDevLauncherRequestLoggerProtocol.REQUEST_ID, in: request) as? String {
136
+ // swiftlint:disable force_cast
137
+ let mutableRequest = request as! MutableURLRequest
138
+ // swiftlint:enable force_cast
139
+ URLProtocol.removeProperty(
140
+ forKey: EXDevLauncherRequestLoggerProtocol.REQUEST_ID,
141
+ in: mutableRequest
142
+ )
143
+ URLProtocol.setProperty(
144
+ RedirectResponse(requestId: requestId, redirectResponse: response),
145
+ forKey: EXDevLauncherRequestLoggerProtocol.REDIRECT_RESPONSE,
146
+ in: mutableRequest
147
+ )
148
+ redirectRequest = mutableRequest as URLRequest
149
+ } else {
150
+ redirectRequest = request
151
+ }
152
+ completionHandler(redirectRequest)
153
+ }
154
+
155
+ /**
156
+ Data structure to save the response for redirection
157
+ */
158
+ private struct RedirectResponse {
159
+ let requestId: String
160
+ let redirectResponse: HTTPURLResponse
161
+ }
162
+
163
+ /**
164
+ A helper class to create a unique request ID
165
+ */
166
+ private struct RequestIdProvider {
167
+ private var value: UInt64 = 0
168
+
169
+ mutating func create() -> String {
170
+ // We could ensure the increment thread safety,
171
+ // because we access this function from the same thread (com.apple.CFNetwork.CustomProtocols).
172
+ value += 1
173
+ return String(value)
174
+ }
175
+ }
176
+ }
177
+
178
+ /**
179
+ `URLRequest.httpBodyData()` extension to read the underlying `httpBodyStream` as Data.
180
+ Only read at maximum `EXDevLauncherRequestLoggerProtocol.MAX_BODY_SIZE` bytes.
181
+ */
182
+ extension URLRequest {
183
+ func httpBodyData() -> Data? {
184
+ if let httpBody = self.httpBody {
185
+ return httpBody
186
+ }
187
+
188
+ if let contentLength = self.allHTTPHeaderFields?["Content-Length"],
189
+ let contentLengthInt = Int(contentLength),
190
+ contentLengthInt > EXDevLauncherRequestLoggerProtocol.MAX_BODY_SIZE {
191
+ return nil
192
+ }
193
+ guard let stream = self.httpBodyStream else {
194
+ return nil
195
+ }
196
+
197
+ let bufferSize: Int = 8192
198
+ let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
199
+
200
+ stream.open()
201
+ defer {
202
+ buffer.deallocate()
203
+ stream.close()
204
+ }
205
+
206
+ var data = Data()
207
+ while stream.hasBytesAvailable {
208
+ let chunkSize = stream.read(buffer, maxLength: bufferSize)
209
+ if data.count + chunkSize > EXDevLauncherRequestLoggerProtocol.MAX_BODY_SIZE {
210
+ return nil
211
+ }
212
+ data.append(buffer, count: chunkSize)
213
+ }
214
+
215
+ return data
216
+ }
217
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "expo-dev-launcher",
3
3
  "title": "Expo Development Launcher",
4
- "version": "2.1.5",
4
+ "version": "2.2.0",
5
5
  "description": "Pre-release version of the Expo development launcher package for testing.",
6
6
  "main": "build/DevLauncher.js",
7
7
  "types": "build/DevLauncher.d.ts",
@@ -29,7 +29,7 @@
29
29
  "license": "MIT",
30
30
  "homepage": "https://docs.expo.dev",
31
31
  "dependencies": {
32
- "expo-dev-menu": "2.1.3",
32
+ "expo-dev-menu": "2.2.0",
33
33
  "resolve-from": "^5.0.0",
34
34
  "semver": "^7.3.5"
35
35
  },
@@ -49,7 +49,7 @@
49
49
  "graphql": "^16.0.1",
50
50
  "graphql-request": "^3.6.1",
51
51
  "react": "18.2.0",
52
- "react-native": "0.71.3",
52
+ "react-native": "0.71.6",
53
53
  "react-query": "^3.34.16",
54
54
  "url": "^0.11.0"
55
55
  },
@@ -63,5 +63,5 @@
63
63
  "./setupTests.ts"
64
64
  ]
65
65
  },
66
- "gitHead": "48970be700d1ab39a92c58dc2eb36e63ad185797"
66
+ "gitHead": "362ed24e78a57e9839afd2925d2af4aff7e28437"
67
67
  }