expo-modules-core 1.12.2 → 1.12.4
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 +13 -0
- package/android/build.gradle +2 -2
- package/android/src/main/cpp/JSIContext.cpp +53 -8
- package/android/src/main/cpp/JSIContext.h +1 -1
- package/android/src/main/cpp/JavaCallback.cpp +114 -38
- package/android/src/main/cpp/JavaCallback.h +19 -13
- package/android/src/main/cpp/JavaReferencesCache.cpp +1 -1
- package/android/src/main/cpp/MethodMetadata.cpp +18 -17
- package/android/src/main/java/expo/modules/kotlin/Promise.kt +1 -1
- package/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt +52 -3
- package/android/src/main/java/expo/modules/kotlin/devtools/ExpoRequestCdpInterceptor.kt +14 -4
- package/android/src/main/java/expo/modules/kotlin/devtools/cdp/CdpNetworkTypes.kt +9 -4
- package/android/src/main/java/expo/modules/kotlin/jni/JavaCallback.kt +5 -0
- package/android/src/main/java/expo/modules/kotlin/jni/PromiseImpl.kt +9 -58
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,19 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 1.12.4 — 2024-04-29
|
|
14
|
+
|
|
15
|
+
### 🐛 Bug fixes
|
|
16
|
+
|
|
17
|
+
- [Android] Fixed gziped payload does not correctly shown in network inspector. ([#28486](https://github.com/expo/expo/pull/28486) by [@kudo](https://github.com/kudo))
|
|
18
|
+
- [Android] Fixed crashes during the deallocation of shared objects. ([#28491](https://github.com/expo/expo/pull/28491) by [@lukmccall](https://github.com/lukmccall))
|
|
19
|
+
|
|
20
|
+
## 1.12.3 — 2024-04-26
|
|
21
|
+
|
|
22
|
+
### 🐛 Bug fixes
|
|
23
|
+
|
|
24
|
+
- [Android] Fixes SEGFAULTs caused by `std::shared_ptr<JavaCalllback::CallbackContext>::__on_zero_shared`. ([#28483](https://github.com/expo/expo/pull/28483) by [@lukmccall](https://github.com/lukmccall))
|
|
25
|
+
|
|
13
26
|
## 1.12.2 — 2024-04-23
|
|
14
27
|
|
|
15
28
|
_This version does not introduce any user-facing changes._
|
package/android/build.gradle
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
apply plugin: 'com.android.library'
|
|
2
2
|
|
|
3
3
|
group = 'host.exp.exponent'
|
|
4
|
-
version = '1.12.
|
|
4
|
+
version = '1.12.4'
|
|
5
5
|
|
|
6
6
|
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
7
7
|
apply from: expoModulesCorePlugin
|
|
@@ -63,7 +63,7 @@ android {
|
|
|
63
63
|
defaultConfig {
|
|
64
64
|
consumerProguardFiles 'proguard-rules.pro'
|
|
65
65
|
versionCode 1
|
|
66
|
-
versionName "1.12.
|
|
66
|
+
versionName "1.12.4"
|
|
67
67
|
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled.toString()
|
|
68
68
|
|
|
69
69
|
testInstrumentationRunner "expo.modules.TestRunner"
|
|
@@ -29,6 +29,42 @@ namespace expo {
|
|
|
29
29
|
|
|
30
30
|
#endif
|
|
31
31
|
|
|
32
|
+
/*
|
|
33
|
+
* A wrapper for a global reference that can be deallocated on any thread.
|
|
34
|
+
* It should be used with smart pointer. That structure can't be copied or moved.
|
|
35
|
+
*/
|
|
36
|
+
template <typename T>
|
|
37
|
+
class ThreadSafeJNIGlobalRef {
|
|
38
|
+
public:
|
|
39
|
+
ThreadSafeJNIGlobalRef(jobject globalRef) : globalRef(globalRef) {}
|
|
40
|
+
ThreadSafeJNIGlobalRef(const ThreadSafeJNIGlobalRef &other) = delete;
|
|
41
|
+
ThreadSafeJNIGlobalRef(ThreadSafeJNIGlobalRef &&other) = delete;
|
|
42
|
+
ThreadSafeJNIGlobalRef &operator=(const ThreadSafeJNIGlobalRef &other) = delete;
|
|
43
|
+
ThreadSafeJNIGlobalRef &operator=(ThreadSafeJNIGlobalRef &&other) = delete;
|
|
44
|
+
|
|
45
|
+
void use(std::function<void(jni::alias_ref<T> globalRef)> &&action) {
|
|
46
|
+
if (globalRef == nullptr) {
|
|
47
|
+
throw std::runtime_error("ThreadSafeJNIGlobalRef: globalRef is null");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
jni::ThreadScope::WithClassLoader([this, action = std::move(action)]() {
|
|
51
|
+
jni::alias_ref<jobject> aliasRef = jni::wrap_alias(globalRef);
|
|
52
|
+
jni::alias_ref<T> jsiContextRef = jni::static_ref_cast<T>(aliasRef);
|
|
53
|
+
action(jsiContextRef);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
~ThreadSafeJNIGlobalRef() {
|
|
58
|
+
if (globalRef != nullptr) {
|
|
59
|
+
jni::ThreadScope::WithClassLoader([this] {
|
|
60
|
+
jni::Environment::current()->DeleteGlobalRef(this->globalRef);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
jobject globalRef;
|
|
66
|
+
};
|
|
67
|
+
|
|
32
68
|
jni::local_ref<JSIContext::jhybriddata>
|
|
33
69
|
JSIContext::initHybrid(jni::alias_ref<jhybridobject> jThis) {
|
|
34
70
|
return makeCxxInstance(jThis);
|
|
@@ -137,13 +173,17 @@ void JSIContext::prepareRuntime() {
|
|
|
137
173
|
|
|
138
174
|
EventEmitter::installClass(runtime);
|
|
139
175
|
|
|
176
|
+
auto threadSafeRef = std::make_shared<ThreadSafeJNIGlobalRef<JSIContext::javaobject>>(
|
|
177
|
+
jni::Environment::current()->NewGlobalRef(javaPart_.get())
|
|
178
|
+
);
|
|
179
|
+
|
|
140
180
|
SharedObject::installBaseClass(
|
|
141
181
|
runtime,
|
|
142
182
|
// We can't predict the order of deallocation of the JSIContext and the SharedObject.
|
|
143
183
|
// So we need to pass a new ref to retain the JSIContext to make sure it's not deallocated before the SharedObject.
|
|
144
|
-
[
|
|
145
|
-
jni::
|
|
146
|
-
JSIContext::deleteSharedObject(
|
|
184
|
+
[threadSafeRef = std::move(threadSafeRef)](const SharedObject::ObjectId objectId) {
|
|
185
|
+
threadSafeRef->use([objectId](jni::alias_ref<JSIContext::javaobject> globalRef) {
|
|
186
|
+
JSIContext::deleteSharedObject(globalRef, objectId);
|
|
147
187
|
});
|
|
148
188
|
}
|
|
149
189
|
);
|
|
@@ -169,7 +209,8 @@ void JSIContext::prepareRuntime() {
|
|
|
169
209
|
jni::local_ref<JavaScriptModuleObject::javaobject>
|
|
170
210
|
JSIContext::callGetJavaScriptModuleObjectMethod(const std::string &moduleName) const {
|
|
171
211
|
if (javaPart_ == nullptr) {
|
|
172
|
-
throw std::runtime_error(
|
|
212
|
+
throw std::runtime_error(
|
|
213
|
+
"getJavaScriptModuleObject: JSIContext was prepared to be deallocated.");
|
|
173
214
|
}
|
|
174
215
|
|
|
175
216
|
const static auto method = expo::JSIContext::javaClassLocal()
|
|
@@ -270,7 +311,7 @@ void JSIContext::registerSharedObject(
|
|
|
270
311
|
}
|
|
271
312
|
|
|
272
313
|
void JSIContext::deleteSharedObject(
|
|
273
|
-
jni::
|
|
314
|
+
jni::alias_ref<JSIContext::javaobject> javaObject,
|
|
274
315
|
int objectId
|
|
275
316
|
) {
|
|
276
317
|
if (javaObject == nullptr) {
|
|
@@ -324,13 +365,17 @@ void JSIContext::jniSetNativeStateForSharedObject(
|
|
|
324
365
|
int id,
|
|
325
366
|
jni::alias_ref<JavaScriptObject::javaobject> jsObject
|
|
326
367
|
) {
|
|
368
|
+
auto threadSafeRef = std::make_shared<ThreadSafeJNIGlobalRef<JSIContext::javaobject>>(
|
|
369
|
+
jni::Environment::current()->NewGlobalRef(javaPart_.get())
|
|
370
|
+
);
|
|
371
|
+
|
|
327
372
|
auto nativeState = std::make_shared<expo::SharedObject::NativeState>(
|
|
328
373
|
id,
|
|
329
374
|
// We can't predict the order of deallocation of the JSIContext and the SharedObject.
|
|
330
375
|
// So we need to pass a new ref to retain the JSIContext to make sure it's not deallocated before the SharedObject.
|
|
331
|
-
[
|
|
332
|
-
jni::
|
|
333
|
-
JSIContext::deleteSharedObject(
|
|
376
|
+
[threadSafeRef = std::move(threadSafeRef)](const SharedObject::ObjectId objectId) {
|
|
377
|
+
threadSafeRef->use([objectId](jni::alias_ref<JSIContext::javaobject> globalRef) {
|
|
378
|
+
JSIContext::deleteSharedObject(globalRef, objectId);
|
|
334
379
|
});
|
|
335
380
|
}
|
|
336
381
|
);
|
|
@@ -13,6 +13,39 @@
|
|
|
13
13
|
|
|
14
14
|
namespace expo {
|
|
15
15
|
|
|
16
|
+
#if REACT_NATIVE_TARGET_VERSION >= 75
|
|
17
|
+
|
|
18
|
+
JavaCallback::CallbackContext::CallbackContext(
|
|
19
|
+
jsi::Runtime &rt,
|
|
20
|
+
std::weak_ptr<react::CallInvoker> jsCallInvokerHolder,
|
|
21
|
+
std::optional<jsi::Function> resolveHolder,
|
|
22
|
+
std::optional<jsi::Function> rejectHolder
|
|
23
|
+
) : react::LongLivedObject(rt),
|
|
24
|
+
rt(rt),
|
|
25
|
+
jsCallInvokerHolder(std::move(jsCallInvokerHolder)),
|
|
26
|
+
resolveHolder(std::move(resolveHolder)),
|
|
27
|
+
rejectHolder(std::move(rejectHolder)) {}
|
|
28
|
+
|
|
29
|
+
#else
|
|
30
|
+
|
|
31
|
+
JavaCallback::CallbackContext::CallbackContext(
|
|
32
|
+
jsi::Runtime &rt,
|
|
33
|
+
std::weak_ptr<react::CallInvoker> jsCallInvokerHolder,
|
|
34
|
+
std::optional<jsi::Function> resolveHolder,
|
|
35
|
+
std::optional<jsi::Function> rejectHolder
|
|
36
|
+
) : rt(rt),
|
|
37
|
+
jsCallInvokerHolder(std::move(jsCallInvokerHolder)),
|
|
38
|
+
resolveHolder(std::move(resolveHolder)),
|
|
39
|
+
rejectHolder(std::move(rejectHolder)) {}
|
|
40
|
+
|
|
41
|
+
#endif
|
|
42
|
+
|
|
43
|
+
void JavaCallback::CallbackContext::invalidate() {
|
|
44
|
+
resolveHolder.reset();
|
|
45
|
+
rejectHolder.reset();
|
|
46
|
+
allowRelease();
|
|
47
|
+
}
|
|
48
|
+
|
|
16
49
|
JavaCallback::JavaCallback(std::shared_ptr<CallbackContext> callbackContext)
|
|
17
50
|
: callbackContext(std::move(callbackContext)) {}
|
|
18
51
|
|
|
@@ -27,6 +60,7 @@ void JavaCallback::registerNatives() {
|
|
|
27
60
|
makeNativeMethod("invokeNative", JavaCallback::invokeArray),
|
|
28
61
|
makeNativeMethod("invokeNative", JavaCallback::invokeMap),
|
|
29
62
|
makeNativeMethod("invokeNative", JavaCallback::invokeSharedRef),
|
|
63
|
+
makeNativeMethod("invokeNative", JavaCallback::invokeError),
|
|
30
64
|
});
|
|
31
65
|
}
|
|
32
66
|
|
|
@@ -45,24 +79,41 @@ void JavaCallback::invokeJSFunction(
|
|
|
45
79
|
ArgsConverter<T> argsConverter,
|
|
46
80
|
T arg
|
|
47
81
|
) {
|
|
48
|
-
const auto
|
|
82
|
+
const auto strongCallbackContext = this->callbackContext.lock();
|
|
83
|
+
// The context were deallocated before the callback was invoked.
|
|
84
|
+
if (strongCallbackContext == nullptr) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const auto jsInvoker = strongCallbackContext->jsCallInvokerHolder.lock();
|
|
89
|
+
// Call invoker is already released, so we cannot invoke the callback.
|
|
90
|
+
if (jsInvoker == nullptr) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
49
94
|
jsInvoker->invokeAsync(
|
|
50
95
|
[
|
|
51
|
-
context =
|
|
96
|
+
context = callbackContext,
|
|
52
97
|
argsConverter = std::move(argsConverter),
|
|
53
98
|
arg = std::move(arg)
|
|
54
99
|
]() -> void {
|
|
55
|
-
|
|
100
|
+
auto strongContext = context.lock();
|
|
101
|
+
// The context were deallocated before the callback was invoked.
|
|
102
|
+
if (strongContext == nullptr) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!strongContext->resolveHolder.has_value()) {
|
|
56
107
|
throw std::runtime_error(
|
|
57
108
|
"JavaCallback was already settled. Cannot invoke it again"
|
|
58
109
|
);
|
|
59
110
|
}
|
|
60
111
|
|
|
61
|
-
jsi::Function &jsFunction =
|
|
62
|
-
jsi::Runtime &rt =
|
|
112
|
+
jsi::Function &jsFunction = strongContext->resolveHolder.value();
|
|
113
|
+
jsi::Runtime &rt = strongContext->rt;
|
|
63
114
|
|
|
64
|
-
argsConverter(rt, jsFunction, std::move(arg)
|
|
65
|
-
|
|
115
|
+
argsConverter(rt, jsFunction, std::move(arg));
|
|
116
|
+
strongContext->invalidate();
|
|
66
117
|
});
|
|
67
118
|
}
|
|
68
119
|
|
|
@@ -72,8 +123,7 @@ void JavaCallback::invokeJSFunction(T arg) {
|
|
|
72
123
|
[](
|
|
73
124
|
jsi::Runtime &rt,
|
|
74
125
|
jsi::Function &jsFunction,
|
|
75
|
-
T arg
|
|
76
|
-
bool isRejectCallback
|
|
126
|
+
T arg
|
|
77
127
|
) {
|
|
78
128
|
jsFunction.call(rt, {jsi::Value(rt, arg)});
|
|
79
129
|
},
|
|
@@ -86,8 +136,7 @@ void JavaCallback::invoke() {
|
|
|
86
136
|
[](
|
|
87
137
|
jsi::Runtime &rt,
|
|
88
138
|
jsi::Function &jsFunction,
|
|
89
|
-
nullptr_t arg
|
|
90
|
-
bool isRejectCallback
|
|
139
|
+
nullptr_t arg
|
|
91
140
|
) {
|
|
92
141
|
jsFunction.call(rt, {jsi::Value::null()});
|
|
93
142
|
},
|
|
@@ -116,8 +165,7 @@ void JavaCallback::invokeString(jni::alias_ref<jstring> result) {
|
|
|
116
165
|
[](
|
|
117
166
|
jsi::Runtime &rt,
|
|
118
167
|
jsi::Function &jsFunction,
|
|
119
|
-
std::string arg
|
|
120
|
-
bool isRejectCallback
|
|
168
|
+
std::string arg
|
|
121
169
|
) {
|
|
122
170
|
std::optional<jsi::Value> extendedString = convertStringToFollyDynamicIfNeeded(
|
|
123
171
|
rt,
|
|
@@ -145,8 +193,7 @@ void JavaCallback::invokeArray(jni::alias_ref<react::WritableNativeArray::javaob
|
|
|
145
193
|
[](
|
|
146
194
|
jsi::Runtime &rt,
|
|
147
195
|
jsi::Function &jsFunction,
|
|
148
|
-
folly::dynamic arg
|
|
149
|
-
bool isRejectCallback
|
|
196
|
+
folly::dynamic arg
|
|
150
197
|
) {
|
|
151
198
|
jsi::Value convertedArg = jsi::valueFromDynamic(rt, arg);
|
|
152
199
|
auto enhancedArg = decorateValueForDynamicExtension(rt, convertedArg);
|
|
@@ -169,28 +216,8 @@ void JavaCallback::invokeMap(jni::alias_ref<react::WritableNativeMap::javaobject
|
|
|
169
216
|
[](
|
|
170
217
|
jsi::Runtime &rt,
|
|
171
218
|
jsi::Function &jsFunction,
|
|
172
|
-
folly::dynamic arg
|
|
173
|
-
bool isRejectCallback
|
|
219
|
+
folly::dynamic arg
|
|
174
220
|
) {
|
|
175
|
-
if (isRejectCallback) {
|
|
176
|
-
auto errorCode = arg.find("code")->second.asString();
|
|
177
|
-
auto message = arg.find("message")->second.asString();
|
|
178
|
-
|
|
179
|
-
auto codedError = makeCodedError(
|
|
180
|
-
rt,
|
|
181
|
-
jsi::String::createFromUtf8(rt, errorCode),
|
|
182
|
-
jsi::String::createFromUtf8(rt, message)
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
jsFunction.call(
|
|
186
|
-
rt,
|
|
187
|
-
(const jsi::Value *) &codedError,
|
|
188
|
-
(size_t) 1
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
221
|
jsi::Value convertedArg = jsi::valueFromDynamic(rt, arg);
|
|
195
222
|
auto enhancedArg = decorateValueForDynamicExtension(rt, convertedArg);
|
|
196
223
|
if (enhancedArg) {
|
|
@@ -212,8 +239,7 @@ void JavaCallback::invokeSharedRef(jni::alias_ref<SharedRef::javaobject> result)
|
|
|
212
239
|
[](
|
|
213
240
|
jsi::Runtime &rt,
|
|
214
241
|
jsi::Function &jsFunction,
|
|
215
|
-
jni::global_ref<SharedRef::javaobject> arg
|
|
216
|
-
bool isRejectCallback
|
|
242
|
+
jni::global_ref<SharedRef::javaobject> arg
|
|
217
243
|
) {
|
|
218
244
|
const auto jsiContext = getJSIContext(rt);
|
|
219
245
|
auto native = jni::make_local(arg);
|
|
@@ -248,4 +274,54 @@ void JavaCallback::invokeSharedRef(jni::alias_ref<SharedRef::javaobject> result)
|
|
|
248
274
|
jni::make_global(result)
|
|
249
275
|
);
|
|
250
276
|
}
|
|
277
|
+
|
|
278
|
+
void JavaCallback::invokeError(jni::alias_ref<jstring> code, jni::alias_ref<jstring> errorMessage) {
|
|
279
|
+
const auto strongCallbackContext = this->callbackContext.lock();
|
|
280
|
+
// The context were deallocated before the callback was invoked.
|
|
281
|
+
if (strongCallbackContext == nullptr) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const auto jsInvoker = strongCallbackContext->jsCallInvokerHolder.lock();
|
|
286
|
+
// Call invoker is already released, so we cannot invoke the callback.
|
|
287
|
+
if (jsInvoker == nullptr) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
jsInvoker->invokeAsync(
|
|
292
|
+
[
|
|
293
|
+
context = callbackContext,
|
|
294
|
+
code = code->toStdString(),
|
|
295
|
+
errorMessage = errorMessage->toStdString()
|
|
296
|
+
]() -> void {
|
|
297
|
+
auto strongContext = context.lock();
|
|
298
|
+
// The context were deallocated before the callback was invoked.
|
|
299
|
+
if (strongContext == nullptr) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!strongContext->rejectHolder.has_value()) {
|
|
304
|
+
throw std::runtime_error(
|
|
305
|
+
"JavaCallback was already settled. Cannot invoke it again"
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
jsi::Function &jsFunction = strongContext->rejectHolder.value();
|
|
310
|
+
jsi::Runtime &rt = strongContext->rt;
|
|
311
|
+
|
|
312
|
+
auto codedError = makeCodedError(
|
|
313
|
+
rt,
|
|
314
|
+
jsi::String::createFromUtf8(rt, code),
|
|
315
|
+
jsi::String::createFromUtf8(rt, errorMessage)
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
jsFunction.call(
|
|
319
|
+
rt,
|
|
320
|
+
(const jsi::Value *) &codedError,
|
|
321
|
+
(size_t) 1
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
strongContext->invalidate();
|
|
325
|
+
});
|
|
326
|
+
}
|
|
251
327
|
} // namespace expo
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
#include <react/jni/WritableNativeMap.h>
|
|
14
14
|
#include <fbjni/detail/CoreClasses.h>
|
|
15
15
|
#include <ReactCommon/CallInvoker.h>
|
|
16
|
+
#include <ReactCommon/LongLivedObject.h>
|
|
16
17
|
|
|
17
18
|
namespace jni = facebook::jni;
|
|
18
19
|
namespace react = facebook::react;
|
|
@@ -26,19 +27,27 @@ struct SharedRef : public jni::JavaClass<SharedRef> {
|
|
|
26
27
|
|
|
27
28
|
class JSIContext;
|
|
28
29
|
|
|
29
|
-
typedef std::variant<folly::dynamic, jni::global_ref<SharedRef::javaobject>> CallbackArg;
|
|
30
|
-
|
|
31
30
|
class JavaCallback : public jni::HybridClass<JavaCallback, Destructible> {
|
|
32
31
|
public:
|
|
33
32
|
static auto constexpr
|
|
34
33
|
kJavaDescriptor = "Lexpo/modules/kotlin/jni/JavaCallback;";
|
|
35
34
|
static auto constexpr TAG = "JavaCallback";
|
|
36
35
|
|
|
37
|
-
|
|
36
|
+
class CallbackContext : public react::LongLivedObject {
|
|
37
|
+
public:
|
|
38
|
+
CallbackContext(
|
|
39
|
+
jsi::Runtime &rt,
|
|
40
|
+
std::weak_ptr<react::CallInvoker> jsCallInvokerHolder,
|
|
41
|
+
std::optional<jsi::Function> resolveHolder,
|
|
42
|
+
std::optional<jsi::Function> rejectHolder
|
|
43
|
+
);
|
|
44
|
+
|
|
38
45
|
jsi::Runtime &rt;
|
|
39
|
-
std::
|
|
40
|
-
std::optional<jsi::Function>
|
|
41
|
-
|
|
46
|
+
std::weak_ptr<react::CallInvoker> jsCallInvokerHolder;
|
|
47
|
+
std::optional<jsi::Function> resolveHolder;
|
|
48
|
+
std::optional<jsi::Function> rejectHolder;
|
|
49
|
+
|
|
50
|
+
void invalidate();
|
|
42
51
|
};
|
|
43
52
|
|
|
44
53
|
static void registerNatives();
|
|
@@ -49,7 +58,7 @@ public:
|
|
|
49
58
|
);
|
|
50
59
|
|
|
51
60
|
private:
|
|
52
|
-
std::
|
|
61
|
+
std::weak_ptr<CallbackContext> callbackContext;
|
|
53
62
|
|
|
54
63
|
friend HybridBase;
|
|
55
64
|
|
|
@@ -73,13 +82,10 @@ private:
|
|
|
73
82
|
|
|
74
83
|
void invokeSharedRef(jni::alias_ref<SharedRef::javaobject> result);
|
|
75
84
|
|
|
85
|
+
void invokeError(jni::alias_ref<jstring> code, jni::alias_ref<jstring> errorMessage);
|
|
86
|
+
|
|
76
87
|
template<class T>
|
|
77
|
-
using ArgsConverter = std::function<void(
|
|
78
|
-
jsi::Runtime &rt,
|
|
79
|
-
jsi::Function &jsFunction,
|
|
80
|
-
T arg,
|
|
81
|
-
bool isRejectCallback
|
|
82
|
-
)>;
|
|
88
|
+
using ArgsConverter = std::function<void(jsi::Runtime &rt, jsi::Function &jsFunction, T arg)>;
|
|
83
89
|
|
|
84
90
|
template<class T>
|
|
85
91
|
inline void invokeJSFunction(
|
|
@@ -36,7 +36,7 @@ void JavaReferencesCache::loadJClasses(JNIEnv *env) {
|
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
loadJClass(env, "expo/modules/kotlin/jni/PromiseImpl", {
|
|
39
|
-
{"<init>", "(Lexpo/modules/kotlin/jni/JavaCallback;
|
|
39
|
+
{"<init>", "(Lexpo/modules/kotlin/jni/JavaCallback;)V"}
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
loadJClass(env, "java/lang/Object", {});
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
#include "Exceptions.h"
|
|
8
8
|
#include "JavaCallback.h"
|
|
9
9
|
#include "types/JNIToJSIConverter.h"
|
|
10
|
+
#include "JSReferencesCache.h"
|
|
10
11
|
|
|
11
12
|
#include <utility>
|
|
12
13
|
#include <functional>
|
|
13
14
|
#include <unistd.h>
|
|
14
15
|
#include <optional>
|
|
15
16
|
|
|
16
|
-
#include
|
|
17
|
+
#include <ReactCommon/LongLivedObject.h>
|
|
17
18
|
|
|
18
19
|
namespace jni = facebook::jni;
|
|
19
20
|
namespace jsi = facebook::jsi;
|
|
@@ -21,10 +22,10 @@ namespace react = facebook::react;
|
|
|
21
22
|
|
|
22
23
|
namespace expo {
|
|
23
24
|
|
|
24
|
-
jni::local_ref<JavaCallback::JavaPart>
|
|
25
|
-
jsi::Function &&
|
|
26
|
-
jsi::
|
|
27
|
-
|
|
25
|
+
jni::local_ref<JavaCallback::JavaPart> createJavaCallback(
|
|
26
|
+
jsi::Function &&resolveFunction,
|
|
27
|
+
jsi::Function &&rejectFunction,
|
|
28
|
+
jsi::Runtime &rt
|
|
28
29
|
) {
|
|
29
30
|
JSIContext *jsiContext = getJSIContext(rt);
|
|
30
31
|
std::shared_ptr<react::CallInvoker> jsInvoker = jsiContext->runtimeHolder->jsInvoker;
|
|
@@ -32,10 +33,16 @@ jni::local_ref<JavaCallback::JavaPart> createJavaCallbackFromJSIFunction(
|
|
|
32
33
|
std::shared_ptr<JavaCallback::CallbackContext> callbackContext = std::make_shared<JavaCallback::CallbackContext>(
|
|
33
34
|
rt,
|
|
34
35
|
std::move(jsInvoker),
|
|
35
|
-
std::move(
|
|
36
|
-
|
|
36
|
+
std::move(resolveFunction),
|
|
37
|
+
std::move(rejectFunction)
|
|
37
38
|
);
|
|
38
39
|
|
|
40
|
+
#if REACT_NATIVE_TARGET_VERSION >= 75
|
|
41
|
+
facebook::react::LongLivedObjectCollection::get(rt).add(callbackContext);
|
|
42
|
+
#else
|
|
43
|
+
facebook::react::LongLivedObjectCollection::get().add(callbackContext);
|
|
44
|
+
#endif
|
|
45
|
+
|
|
39
46
|
return JavaCallback::newInstance(jsiContext, std::move(callbackContext));
|
|
40
47
|
}
|
|
41
48
|
|
|
@@ -336,15 +343,10 @@ jsi::Function MethodMetadata::createPromiseBody(
|
|
|
336
343
|
jsi::Function resolveJSIFn = promiseConstructorArgs[0].getObject(rt).getFunction(rt);
|
|
337
344
|
jsi::Function rejectJSIFn = promiseConstructorArgs[1].getObject(rt).getFunction(rt);
|
|
338
345
|
|
|
339
|
-
jobject
|
|
346
|
+
jobject javaCallback = createJavaCallback(
|
|
340
347
|
std::move(resolveJSIFn),
|
|
341
|
-
rt
|
|
342
|
-
).release();
|
|
343
|
-
|
|
344
|
-
jobject reject = createJavaCallbackFromJSIFunction(
|
|
345
348
|
std::move(rejectJSIFn),
|
|
346
|
-
rt
|
|
347
|
-
true
|
|
349
|
+
rt
|
|
348
350
|
).release();
|
|
349
351
|
|
|
350
352
|
JNIEnv *env = jni::Environment::current();
|
|
@@ -353,15 +355,14 @@ jsi::Function MethodMetadata::createPromiseBody(
|
|
|
353
355
|
"expo/modules/kotlin/jni/PromiseImpl");
|
|
354
356
|
jmethodID jPromiseConstructor = jPromise.getMethod(
|
|
355
357
|
"<init>",
|
|
356
|
-
"(Lexpo/modules/kotlin/jni/JavaCallback;
|
|
358
|
+
"(Lexpo/modules/kotlin/jni/JavaCallback;)V"
|
|
357
359
|
);
|
|
358
360
|
|
|
359
361
|
// Creates a promise object
|
|
360
362
|
jobject promise = env->NewObject(
|
|
361
363
|
jPromise.clazz,
|
|
362
364
|
jPromiseConstructor,
|
|
363
|
-
|
|
364
|
-
reject
|
|
365
|
+
javaCallback
|
|
365
366
|
);
|
|
366
367
|
|
|
367
368
|
// Cast in this place is safe, cause we know that this function expects promise.
|
|
@@ -33,7 +33,7 @@ interface Promise {
|
|
|
33
33
|
fun Promise.toBridgePromise(): com.facebook.react.bridge.Promise {
|
|
34
34
|
val expoPromise = this
|
|
35
35
|
val resolveMethod: (value: Any?) -> Unit = if (expoPromise is PromiseImpl) {
|
|
36
|
-
expoPromise.
|
|
36
|
+
expoPromise.callback::invoke
|
|
37
37
|
} else {
|
|
38
38
|
expoPromise::resolve
|
|
39
39
|
}
|
package/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
CHANGED
|
@@ -6,6 +6,11 @@ import android.util.Log
|
|
|
6
6
|
import okhttp3.Interceptor
|
|
7
7
|
import okhttp3.Request
|
|
8
8
|
import okhttp3.Response
|
|
9
|
+
import okhttp3.ResponseBody
|
|
10
|
+
import okhttp3.ResponseBody.Companion.asResponseBody
|
|
11
|
+
import okio.GzipSource
|
|
12
|
+
import okio.buffer
|
|
13
|
+
import java.io.IOException
|
|
9
14
|
|
|
10
15
|
private const val TAG = "ExpoNetworkInspector"
|
|
11
16
|
|
|
@@ -31,7 +36,9 @@ class ExpoNetworkInspectOkHttpNetworkInterceptor : Interceptor {
|
|
|
31
36
|
it.priorResponse = response
|
|
32
37
|
}
|
|
33
38
|
} else {
|
|
34
|
-
|
|
39
|
+
val body = peekResponseBody(response)
|
|
40
|
+
delegate.didReceiveResponse(requestId, request, response, body)
|
|
41
|
+
body?.close()
|
|
35
42
|
}
|
|
36
43
|
} catch (e: Exception) {
|
|
37
44
|
Log.e(TAG, "Failed to send network request CDP event", e)
|
|
@@ -62,9 +69,18 @@ class ExpoNetworkInspectOkHttpAppInterceptor : Interceptor {
|
|
|
62
69
|
* The delegate to dispatch network request events
|
|
63
70
|
*/
|
|
64
71
|
internal interface ExpoNetworkInspectOkHttpInterceptorsDelegate {
|
|
65
|
-
fun willSendRequest(
|
|
72
|
+
fun willSendRequest(
|
|
73
|
+
requestId: String,
|
|
74
|
+
request: Request,
|
|
75
|
+
redirectResponse: Response?
|
|
76
|
+
)
|
|
66
77
|
|
|
67
|
-
fun didReceiveResponse(
|
|
78
|
+
fun didReceiveResponse(
|
|
79
|
+
requestId: String,
|
|
80
|
+
request: Request,
|
|
81
|
+
response: Response,
|
|
82
|
+
body: ResponseBody?
|
|
83
|
+
)
|
|
68
84
|
}
|
|
69
85
|
|
|
70
86
|
/**
|
|
@@ -74,3 +90,36 @@ internal class RedirectResponse {
|
|
|
74
90
|
var requestId: String? = null
|
|
75
91
|
var priorResponse: Response? = null
|
|
76
92
|
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Peek response body that could send to CDP delegate.
|
|
96
|
+
* Also uncompress gzip payload if necessary since OkHttp [Interceptor] does not uncompress payload for you.
|
|
97
|
+
* @return null if the response body exceeds [byteCount]
|
|
98
|
+
*/
|
|
99
|
+
internal fun peekResponseBody(
|
|
100
|
+
response: Response,
|
|
101
|
+
byteCount: Long = ExpoNetworkInspectOkHttpNetworkInterceptor.MAX_BODY_SIZE
|
|
102
|
+
): ResponseBody? {
|
|
103
|
+
val body = response.body ?: return null
|
|
104
|
+
val peeked = body.source().peek()
|
|
105
|
+
try {
|
|
106
|
+
if (peeked.request(byteCount + 1)) {
|
|
107
|
+
// When the request() returns true,
|
|
108
|
+
// it means the source have more available bytes then [byteCount].
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
} catch (_: IOException) {}
|
|
112
|
+
|
|
113
|
+
val encoding = response.header("Content-Encoding")
|
|
114
|
+
val source = when {
|
|
115
|
+
encoding.equals("gzip", ignoreCase = true) ->
|
|
116
|
+
GzipSource(peeked).buffer().apply {
|
|
117
|
+
request(byteCount)
|
|
118
|
+
}
|
|
119
|
+
else -> peeked
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
val buffer = okio.Buffer()
|
|
123
|
+
buffer.write(source, minOf(byteCount, source.buffer.size))
|
|
124
|
+
return buffer.asResponseBody(body.contentType(), buffer.size)
|
|
125
|
+
}
|
|
@@ -13,6 +13,7 @@ import kotlinx.coroutines.Dispatchers
|
|
|
13
13
|
import kotlinx.coroutines.launch
|
|
14
14
|
import okhttp3.Request
|
|
15
15
|
import okhttp3.Response
|
|
16
|
+
import okhttp3.ResponseBody
|
|
16
17
|
import java.math.BigDecimal
|
|
17
18
|
import java.math.RoundingMode
|
|
18
19
|
|
|
@@ -38,7 +39,11 @@ object ExpoRequestCdpInterceptor : ExpoNetworkInspectOkHttpInterceptorsDelegate
|
|
|
38
39
|
|
|
39
40
|
//region ExpoNetworkInspectOkHttpInterceptorsDelegate implementations
|
|
40
41
|
|
|
41
|
-
override fun willSendRequest(
|
|
42
|
+
override fun willSendRequest(
|
|
43
|
+
requestId: String,
|
|
44
|
+
request: Request,
|
|
45
|
+
redirectResponse: Response?
|
|
46
|
+
) {
|
|
42
47
|
val now = BigDecimal(System.currentTimeMillis() / 1000.0).setScale(3, RoundingMode.CEILING)
|
|
43
48
|
|
|
44
49
|
val params = RequestWillBeSentParams(now, requestId, request, redirectResponse)
|
|
@@ -48,14 +53,19 @@ object ExpoRequestCdpInterceptor : ExpoNetworkInspectOkHttpInterceptorsDelegate
|
|
|
48
53
|
dispatchEvent(Event("Network.requestWillBeSentExtraInfo", params2))
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
override fun didReceiveResponse(
|
|
56
|
+
override fun didReceiveResponse(
|
|
57
|
+
requestId: String,
|
|
58
|
+
request: Request,
|
|
59
|
+
response: Response,
|
|
60
|
+
body: ResponseBody?
|
|
61
|
+
) {
|
|
52
62
|
val now = BigDecimal(System.currentTimeMillis() / 1000.0).setScale(3, RoundingMode.CEILING)
|
|
53
63
|
|
|
54
64
|
val params = ResponseReceivedParams(now, requestId, request, response)
|
|
55
65
|
dispatchEvent(Event("Network.responseReceived", params))
|
|
56
66
|
|
|
57
|
-
if (
|
|
58
|
-
val params2 = ExpoReceivedResponseBodyParams(now, requestId, request, response)
|
|
67
|
+
if (body != null) {
|
|
68
|
+
val params2 = ExpoReceivedResponseBodyParams(now, requestId, request, response, body)
|
|
59
69
|
dispatchEvent(Event("Expo(Network.receivedResponseBody)", params2))
|
|
60
70
|
}
|
|
61
71
|
|
|
@@ -227,15 +227,20 @@ data class ExpoReceivedResponseBodyParams(
|
|
|
227
227
|
var body: String,
|
|
228
228
|
var base64Encoded: Boolean
|
|
229
229
|
) : JsonSerializable {
|
|
230
|
-
constructor(
|
|
230
|
+
constructor(
|
|
231
|
+
now: BigDecimal,
|
|
232
|
+
requestId: RequestId,
|
|
233
|
+
request: okhttp3.Request,
|
|
234
|
+
response: okhttp3.Response,
|
|
235
|
+
body: okhttp3.ResponseBody
|
|
236
|
+
) : this(
|
|
231
237
|
requestId = requestId,
|
|
232
238
|
body = "",
|
|
233
239
|
base64Encoded = false
|
|
234
240
|
) {
|
|
235
|
-
val
|
|
236
|
-
val contentType = rawBody.contentType()
|
|
241
|
+
val contentType = body.contentType()
|
|
237
242
|
val isText = contentType?.type == "text" || (contentType?.type == "application" && contentType.subtype == "json")
|
|
238
|
-
val bodyString = if (isText)
|
|
243
|
+
val bodyString = if (isText) body.string() else body.source().readByteString().base64()
|
|
239
244
|
|
|
240
245
|
this.body = bodyString
|
|
241
246
|
this.base64Encoded = !isText
|
|
@@ -49,6 +49,10 @@ class JavaCallback @DoNotStrip internal constructor(@DoNotStrip private val mHyb
|
|
|
49
49
|
invokeNative(result)
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
operator fun invoke(code: String, errorMessage: String) = checkIfValid {
|
|
53
|
+
invokeNative(code, errorMessage)
|
|
54
|
+
}
|
|
55
|
+
|
|
52
56
|
private external fun invokeNative()
|
|
53
57
|
private external fun invokeNative(result: Int)
|
|
54
58
|
private external fun invokeNative(result: Boolean)
|
|
@@ -58,6 +62,7 @@ class JavaCallback @DoNotStrip internal constructor(@DoNotStrip private val mHyb
|
|
|
58
62
|
private external fun invokeNative(result: WritableNativeArray)
|
|
59
63
|
private external fun invokeNative(result: WritableNativeMap)
|
|
60
64
|
private external fun invokeNative(result: SharedRef<*>)
|
|
65
|
+
private external fun invokeNative(code: String, errorMessage: String)
|
|
61
66
|
|
|
62
67
|
private inline fun checkIfValid(body: () -> Unit) {
|
|
63
68
|
try {
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
package expo.modules.kotlin.jni
|
|
2
2
|
|
|
3
|
-
import com.facebook.react.bridge.WritableMap
|
|
4
|
-
import com.facebook.react.bridge.WritableNativeArray
|
|
5
|
-
import com.facebook.react.bridge.WritableNativeMap
|
|
6
3
|
import expo.modules.BuildConfig
|
|
7
4
|
import expo.modules.core.interfaces.DoNotStrip
|
|
8
5
|
import expo.modules.kotlin.AppContext
|
|
@@ -30,8 +27,7 @@ private const val STACK_FRAME_KEY_METHOD_NAME = "methodName"
|
|
|
30
27
|
|
|
31
28
|
@DoNotStrip
|
|
32
29
|
class PromiseImpl @DoNotStrip internal constructor(
|
|
33
|
-
@DoNotStrip internal val
|
|
34
|
-
@DoNotStrip internal val rejectBlock: JavaCallback
|
|
30
|
+
@DoNotStrip internal val callback: JavaCallback
|
|
35
31
|
) : Promise {
|
|
36
32
|
internal var wasSettled = false
|
|
37
33
|
private set
|
|
@@ -39,80 +35,35 @@ class PromiseImpl @DoNotStrip internal constructor(
|
|
|
39
35
|
private var fullFunctionName: String? = null
|
|
40
36
|
|
|
41
37
|
override fun resolve(value: Any?) = checkIfWasSettled {
|
|
42
|
-
|
|
38
|
+
callback.invoke(
|
|
43
39
|
JSTypeConverter.convertToJSValue(value)
|
|
44
40
|
)
|
|
45
41
|
}
|
|
46
42
|
|
|
47
43
|
override fun resolve(result: Int) = checkIfWasSettled {
|
|
48
|
-
|
|
44
|
+
callback.invoke(result)
|
|
49
45
|
}
|
|
50
46
|
|
|
51
47
|
override fun resolve(result: Boolean) = checkIfWasSettled {
|
|
52
|
-
|
|
48
|
+
callback.invoke(result)
|
|
53
49
|
}
|
|
54
50
|
|
|
55
51
|
override fun resolve(result: Double) = checkIfWasSettled {
|
|
56
|
-
|
|
52
|
+
callback.invoke(result)
|
|
57
53
|
}
|
|
58
54
|
|
|
59
55
|
override fun resolve(result: Float) = checkIfWasSettled {
|
|
60
|
-
|
|
56
|
+
callback.invoke(result)
|
|
61
57
|
}
|
|
62
58
|
|
|
63
59
|
override fun resolve(result: String) = checkIfWasSettled {
|
|
64
|
-
|
|
60
|
+
callback.invoke(result)
|
|
65
61
|
}
|
|
66
62
|
|
|
67
63
|
// Copy of the reject method from [com.facebook.react.bridge.PromiseImpl]
|
|
68
64
|
override fun reject(code: String, message: String?, cause: Throwable?) = checkIfWasSettled {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
errorInfo.putString(ERROR_MAP_KEY_CODE, code)
|
|
72
|
-
|
|
73
|
-
// Use the custom message if provided otherwise use the throwable message.
|
|
74
|
-
if (message != null) {
|
|
75
|
-
errorInfo.putString(ERROR_MAP_KEY_MESSAGE, message)
|
|
76
|
-
} else if (cause != null) {
|
|
77
|
-
errorInfo.putString(ERROR_MAP_KEY_MESSAGE, cause.message)
|
|
78
|
-
} else {
|
|
79
|
-
// The JavaScript side expects a map with at least an error message.
|
|
80
|
-
// /Libraries/BatchedBridge/NativeModules.js -> createErrorFromErrorData
|
|
81
|
-
// TYPE: (errorData: { message: string })
|
|
82
|
-
errorInfo.putString(ERROR_MAP_KEY_MESSAGE, ERROR_DEFAULT_MESSAGE)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// For consistency with iOS ensure userInfo key exists, even if we null it.
|
|
86
|
-
// iOS: /React/Base/RCTUtils.m -> RCTJSErrorFromCodeMessageAndNSError
|
|
87
|
-
errorInfo.putNull(ERROR_MAP_KEY_USER_INFO)
|
|
88
|
-
|
|
89
|
-
// Attach a nativeStackAndroid array if a throwable was passed
|
|
90
|
-
// this matches iOS behavior - iOS adds a `nativeStackIOS` property
|
|
91
|
-
// iOS: /React/Base/RCTUtils.m -> RCTJSErrorFromCodeMessageAndNSError
|
|
92
|
-
if (cause != null) {
|
|
93
|
-
val stackTrace: Array<StackTraceElement> = cause.stackTrace
|
|
94
|
-
val nativeStackAndroid = WritableNativeArray()
|
|
95
|
-
|
|
96
|
-
// Build an an Array of StackFrames to match JavaScript:
|
|
97
|
-
// iOS: /Libraries/Core/Devtools/parseErrorStack.js -> StackFrame
|
|
98
|
-
var i = 0
|
|
99
|
-
while (i < stackTrace.size && i < ERROR_STACK_FRAME_LIMIT) {
|
|
100
|
-
val frame = stackTrace[i]
|
|
101
|
-
val frameMap: WritableMap = WritableNativeMap()
|
|
102
|
-
// NOTE: no column number exists StackTraceElement
|
|
103
|
-
frameMap.putString(STACK_FRAME_KEY_CLASS, frame.className)
|
|
104
|
-
frameMap.putString(STACK_FRAME_KEY_FILE, frame.fileName)
|
|
105
|
-
frameMap.putInt(STACK_FRAME_KEY_LINE_NUMBER, frame.lineNumber)
|
|
106
|
-
frameMap.putString(STACK_FRAME_KEY_METHOD_NAME, frame.methodName)
|
|
107
|
-
nativeStackAndroid.pushMap(frameMap)
|
|
108
|
-
i++
|
|
109
|
-
}
|
|
110
|
-
errorInfo.putArray(ERROR_MAP_KEY_NATIVE_STACK, nativeStackAndroid)
|
|
111
|
-
} else {
|
|
112
|
-
errorInfo.putArray(ERROR_MAP_KEY_NATIVE_STACK, WritableNativeArray())
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
rejectBlock.invoke(errorInfo)
|
|
65
|
+
// TODO(@lukmccall): Add information about the stack trace to the error message
|
|
66
|
+
callback.invoke(code, message ?: cause?.message ?: "unknown")
|
|
116
67
|
}
|
|
117
68
|
|
|
118
69
|
private inline fun checkIfWasSettled(body: () -> Unit) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-modules-core",
|
|
3
|
-
"version": "1.12.
|
|
3
|
+
"version": "1.12.4",
|
|
4
4
|
"description": "The core of Expo Modules architecture",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -44,5 +44,5 @@
|
|
|
44
44
|
"@testing-library/react-hooks": "^7.0.1",
|
|
45
45
|
"expo-module-scripts": "^3.0.0"
|
|
46
46
|
},
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "4a7cf0d0baf6dfc595d93f604945d2142e705a36"
|
|
48
48
|
}
|