react-native-debug-toolkit 3.3.2 → 3.3.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.
@@ -1,87 +1,187 @@
1
+ #import "DebugToolkitDevConnect.h"
2
+
1
3
  #import <Foundation/Foundation.h>
4
+ #import <UIKit/UIKit.h>
2
5
  #import <React/RCTBridge.h>
3
6
  #import <React/RCTBundleManager.h>
4
7
  #import <React/RCTBridgeModule.h>
5
8
  #import <React/RCTBundleURLProvider.h>
6
- #import <React/RCTDefines.h>
7
9
  #import <React/RCTReloadCommand.h>
8
10
  #import <objc/runtime.h>
9
11
  #include <ifaddrs.h>
10
12
  #include <arpa/inet.h>
11
13
  #include <net/if.h>
12
14
 
13
- static NSString *const DebugToolkitBundleRoot = @"index";
14
- static NSString *const kDevConnectMetroHost = @"_devconnect_metro_host";
15
- static NSString *const kDevConnectDiagLog = @"_devconnect_diag_log";
15
+ // Debug-only Metro host switching — zero host-app native changes required.
16
+ //
17
+ // RN/Expo Debug templates call jsBundleURLForBundleRoot:fallbackURLProvider:, which consults
18
+ // -packagerServerHostPort (guessPackagerHost on simulator when Metro is running). We only hook
19
+ // jsBundleURLForBundleRoot:fallbackURLProvider: so no DevConnect host starts from main.jsbundle.
20
+ // Persisted DevConnect hosts still flow through RCTBundleURLProvider, preserving RN's reachability
21
+ // and fallback behavior when a saved Metro host is stale.
22
+ //
23
+ // Optional: host apps may call DebugToolkitMetroBundleURL() from bundleURL() for explicit control.
24
+
25
+ static NSString *const kBundleRoot = @"index";
26
+ // Expo prebuild templates pass this to jsBundleURLForBundleRoot: in Debug (see expo/expo#21643).
27
+ static NSString *const kExpoVirtualMetroEntry = @".expo/.virtual-metro-entry";
28
+ static NSString *const kMetroHostKey = @"_devconnect_metro_host";
29
+
30
+ static BOOL gBundleRootHookInstalled = NO;
31
+
32
+ static NSURL *(*gOrigJsBundleURLForBundleRootWithFallback)(id, SEL, NSString *, NSURL * _Nonnull (^)(void));
16
33
 
17
- // Forward-declared so appendDiag can reference them before the definitions below.
18
- static IMP original_sourceURLForBridge;
19
- static BOOL swizzleInvoked;
34
+ static BOOL DevConnectEmbeddedFirstHooksActive(void)
35
+ {
36
+ return gBundleRootHookInstalled;
37
+ }
20
38
 
21
- static void appendDiag(NSString *stage, NSString *detail)
39
+ static NSURL *DebugToolkitEmbeddedBundleURL(void)
22
40
  {
23
- NSString *msg = [NSString stringWithFormat:@"[%@] %@ | installed=%@ invoked=%@",
24
- stage, detail,
25
- original_sourceURLForBridge ? @"YES" : @"NO",
26
- swizzleInvoked ? @"YES" : @"NO"];
27
- NSMutableArray *log = [[[NSUserDefaults standardUserDefaults] arrayForKey:kDevConnectDiagLog] mutableCopy]
28
- ?: [NSMutableArray new];
29
- [log addObject:msg];
30
- if (log.count > 30) [log removeObjectAtIndex:0];
31
- [[NSUserDefaults standardUserDefaults] setObject:[log copy] forKey:kDevConnectDiagLog];
32
- [[NSUserDefaults standardUserDefaults] synchronize];
33
- NSLog(@"[DevConnect] diag: %@", msg);
41
+ return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
34
42
  }
35
43
 
36
- #pragma mark - AppDelegate Swizzling
44
+ static NSString *DevConnectPersistedMetroHost(void)
45
+ {
46
+ return [[NSUserDefaults standardUserDefaults] stringForKey:kMetroHostKey];
47
+ }
37
48
 
38
- static NSURL *devconnect_sourceURLForBridge(id self, SEL _cmd, RCTBridge *bridge)
49
+ static void DevConnectSetPersistedMetroHost(NSString *_Nullable hostPort)
39
50
  {
40
- swizzleInvoked = YES;
41
- NSString *metroHost = [[NSUserDefaults standardUserDefaults] stringForKey:kDevConnectMetroHost];
42
- if (metroHost.length > 0) {
43
- NSString *urlStr = [NSString stringWithFormat:
44
- @"http://%@/%@.bundle?platform=ios&dev=true&minify=false&lazy=true", metroHost, DebugToolkitBundleRoot];
45
- NSURL *url = [NSURL URLWithString:urlStr];
46
- appendDiag(@"swizzle-called", [NSString stringWithFormat:@"host=%@ url=%@", metroHost, url]);
47
- return url;
51
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
52
+ if (hostPort.length > 0) {
53
+ [defaults setObject:hostPort forKey:kMetroHostKey];
54
+ } else {
55
+ [defaults removeObjectForKey:kMetroHostKey];
48
56
  }
49
- if (original_sourceURLForBridge) {
50
- return ((NSURL *(*)(id, SEL, RCTBridge *))original_sourceURLForBridge)(self, _cmd, bridge);
57
+ [defaults synchronize];
58
+ }
59
+
60
+ static void DebugToolkitPrepareBundleSourceIfNeeded(void)
61
+ {
62
+ // Touch sharedSettings so RCTBundleURLProvider is linked before hook install retries.
63
+ RCTBundleURLProvider *settings = [RCTBundleURLProvider sharedSettings];
64
+ if (DevConnectPersistedMetroHost().length > 0) {
65
+ return;
51
66
  }
52
- return nil;
67
+ [settings resetToDefaults];
68
+ }
69
+
70
+ static BOOL DevConnectIsExpoProject(void)
71
+ {
72
+ static dispatch_once_t onceToken;
73
+ static BOOL isExpo = NO;
74
+ dispatch_once(&onceToken, ^{
75
+ // Heuristic: matches common Expo prebuild / dev-client binaries.
76
+ isExpo = NSClassFromString(@"EXAppDelegateWrapper") != nil
77
+ || [[NSBundle mainBundle] objectForInfoDictionaryKey:@"EXUpdatesURL"] != nil
78
+ || [[NSBundle mainBundle] objectForInfoDictionaryKey:@"EXPO_RUNTIME_VERSION"] != nil;
79
+ });
80
+ return isExpo;
81
+ }
82
+
83
+ /// Bundle root Metro expects when DevConnect steers the packager (index for plain RN, virtual entry for Expo).
84
+ static NSString *DevConnectMetroBundleRoot(void)
85
+ {
86
+ return DevConnectIsExpoProject() ? kExpoVirtualMetroEntry : kBundleRoot;
87
+ }
88
+
89
+ static NSURL *DevConnectMetroURLForPersistedHost(void)
90
+ {
91
+ NSString *host = DevConnectPersistedMetroHost();
92
+ if (host.length == 0) {
93
+ return nil;
94
+ }
95
+ RCTBundleURLProvider *settings = [RCTBundleURLProvider sharedSettings];
96
+ settings.jsLocation = host;
97
+ return [settings jsBundleURLForBundleRoot:DevConnectMetroBundleRoot()];
98
+ }
99
+
100
+ /// Primary hook: return embedded main.jsbundle before RN/Expo tries Metro / virtual-metro-entry.
101
+ static NSURL *replacement_jsBundleURLForBundleRoot_fallback(
102
+ id self, SEL _cmd, NSString *bundleRoot, NSURL * _Nonnull (^fallbackURLProvider)(void))
103
+ {
104
+ if (DevConnectPersistedMetroHost().length == 0) {
105
+ NSURL *embedded = DebugToolkitEmbeddedBundleURL();
106
+ if (embedded) {
107
+ NSLog(@"[DevConnect] cold start → embedded bundle (root=%@)", bundleRoot);
108
+ return embedded;
109
+ }
110
+ NSLog(@"[DevConnect] no embedded main.jsbundle — falling back to Metro (root=%@)", bundleRoot);
111
+ }
112
+
113
+ if (gOrigJsBundleURLForBundleRootWithFallback) {
114
+ return gOrigJsBundleURLForBundleRootWithFallback(self, _cmd, bundleRoot, fallbackURLProvider);
115
+ }
116
+ return fallbackURLProvider ? fallbackURLProvider() : nil;
53
117
  }
54
118
 
55
- static void swizzleSourceURLForBridge(Class targetClass, NSString *stage)
119
+ static void DebugToolkitInstallBundleRootHook(Class cls)
56
120
  {
57
- if (!targetClass) {
58
- appendDiag(stage, @"class=nil");
121
+ if (gBundleRootHookInstalled) {
59
122
  return;
60
123
  }
61
- NSString *className = NSStringFromClass(targetClass);
62
- SEL selector = @selector(sourceURLForBridge:);
63
- Method method = class_getInstanceMethod(targetClass, selector);
124
+ SEL selector = @selector(jsBundleURLForBundleRoot:fallbackURLProvider:);
125
+ Method method = class_getInstanceMethod(cls, selector);
64
126
  if (!method) {
65
- appendDiag(stage, [NSString stringWithFormat:@"class=%@ method=NOT_FOUND", className]);
127
+ NSLog(@"[DevConnect] jsBundleURLForBundleRoot:fallbackURLProvider: not found");
128
+ return;
129
+ }
130
+ IMP replacement = (IMP)replacement_jsBundleURLForBundleRoot_fallback;
131
+ gOrigJsBundleURLForBundleRootWithFallback =
132
+ (NSURL * (*)(id, SEL, NSString *, NSURL * _Nonnull (^)(void)))method_getImplementation(method);
133
+ if ((IMP)gOrigJsBundleURLForBundleRootWithFallback == replacement) {
134
+ gBundleRootHookInstalled = YES;
66
135
  return;
67
136
  }
68
- if (original_sourceURLForBridge) {
69
- appendDiag(stage, [NSString stringWithFormat:@"class=%@ already_swizzled", className]);
137
+ method_setImplementation(method, replacement);
138
+ gBundleRootHookInstalled = YES;
139
+ }
140
+
141
+ static void DebugToolkitInstallAllHooks(void)
142
+ {
143
+ if (gBundleRootHookInstalled) {
70
144
  return;
71
145
  }
72
- // class_addMethod ensures the replacement lands on targetClass even when the method
73
- // is inherited — avoids modifying the parent class's IMP for all subclasses.
74
- IMP origIMP = method_getImplementation(method);
75
- const char *types = method_getTypeEncoding(method);
76
- BOOL added = class_addMethod(targetClass, selector, (IMP)devconnect_sourceURLForBridge, types);
77
- if (!added) {
78
- // Method already exists directly on targetClass; replace in place.
79
- method_setImplementation(method, (IMP)devconnect_sourceURLForBridge);
146
+
147
+ Class cls = NSClassFromString(@"RCTBundleURLProvider");
148
+ if (!cls) {
149
+ NSLog(@"[DevConnect] RCTBundleURLProvider not loaded — hooks will retry");
150
+ return;
80
151
  }
81
- original_sourceURLForBridge = origIMP;
82
- appendDiag(stage, [NSString stringWithFormat:@"class=%@ added=%@ SUCCESS", className, added ? @"YES" : @"NO"]);
152
+
153
+ DebugToolkitInstallBundleRootHook(cls);
154
+
155
+ static BOOL didLogOutcome = NO;
156
+ if (!didLogOutcome) {
157
+ didLogOutcome = YES;
158
+ if (DevConnectEmbeddedFirstHooksActive()) {
159
+ NSLog(@"[DevConnect] embedded-first hook active");
160
+ } else {
161
+ NSLog(@"[DevConnect] embedded-first hooks FAILED — rebuild / check React linkage");
162
+ }
163
+ }
164
+ }
165
+
166
+ NSURL *DebugToolkitMetroBundleURL(void)
167
+ {
168
+ return DevConnectMetroURLForPersistedHost();
83
169
  }
84
170
 
171
+ // RCT_EXPORT_MODULE defines +load for module registration — use a separate class for hooks.
172
+ @interface DebugToolkitDevConnectBootstrap : NSObject
173
+ @end
174
+
175
+ @implementation DebugToolkitDevConnectBootstrap
176
+
177
+ + (void)load
178
+ {
179
+ DebugToolkitPrepareBundleSourceIfNeeded();
180
+ DebugToolkitInstallAllHooks();
181
+ }
182
+
183
+ @end
184
+
85
185
  #pragma mark - Module
86
186
 
87
187
  @interface DebugToolkitDevConnect : NSObject <RCTBridgeModule>
@@ -94,63 +194,64 @@ RCT_EXPORT_MODULE(DebugToolkitDevConnect)
94
194
  @synthesize bridge = _bridge;
95
195
  @synthesize bundleManager = _bundleManager;
96
196
 
97
- __attribute__((constructor))
98
- static void devconnect_swizzle_init(void)
99
- {
100
- swizzleSourceURLForBridge(NSClassFromString(@"AppDelegate"), @"constructor");
101
- }
102
-
103
197
  - (instancetype)init
104
198
  {
105
199
  if ((self = [super init])) {
106
- if (!original_sourceURLForBridge) {
107
- Class delegateClass = object_getClass([UIApplication sharedApplication].delegate);
108
- swizzleSourceURLForBridge(delegateClass, @"init");
109
- }
200
+ DebugToolkitInstallAllHooks();
110
201
  }
111
202
  return self;
112
203
  }
113
204
 
114
- + (BOOL)requiresMainQueueSetup
115
- {
116
- return NO;
117
- }
205
+ + (BOOL)requiresMainQueueSetup { return NO; }
206
+ - (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); }
118
207
 
119
- - (dispatch_queue_t)methodQueue
208
+ #pragma mark - Bundle Manager Resolution
209
+
210
+ - (RCTBundleManager *)resolveBundleManager
120
211
  {
121
- return dispatch_get_main_queue();
212
+ if (_bundleManager) return _bundleManager;
213
+ if (_bridge) return [_bridge moduleForClass:[RCTBundleManager class]];
214
+ return nil;
122
215
  }
123
216
 
124
- #pragma mark - Bundle Manager Resolution
217
+ #pragma mark - Host Parsing
125
218
 
126
- - (RCTBundleManager *)resolveBundleManager
219
+ - (NSString *)normalizeHostPort:(NSString *)hostPort
127
220
  {
128
- if (_bundleManager) {
129
- return _bundleManager;
130
- }
131
- if (_bridge) {
132
- return [_bridge moduleForClass:[RCTBundleManager class]];
221
+ NSRange sep = [hostPort rangeOfString:@":" options:NSBackwardsSearch];
222
+ NSString *host = sep.location == NSNotFound ? hostPort : [hostPort substringToIndex:sep.location];
223
+ NSString *portStr = sep.location == NSNotFound ? @"" : [hostPort substringFromIndex:sep.location + 1];
224
+
225
+ NSNumberFormatter *formatter = [NSNumberFormatter new];
226
+ formatter.numberStyle = NSNumberFormatterDecimalStyle;
227
+ NSNumber *parsed = [formatter numberFromString:portStr];
228
+ int port;
229
+ if (parsed && parsed.intValue > 0 && parsed.intValue <= 65535) {
230
+ port = parsed.intValue;
231
+ } else {
232
+ #ifdef RCT_METRO_PORT
233
+ port = RCT_METRO_PORT;
234
+ #else
235
+ port = 8081;
236
+ #endif
133
237
  }
134
- return nil;
238
+ return [NSString stringWithFormat:@"%@:%d", host, port];
135
239
  }
136
240
 
137
- #pragma mark - Exported Methods
241
+ #pragma mark - Reload helper
138
242
 
139
- RCT_EXPORT_METHOD(getMetroHost:(RCTPromiseResolveBlock)resolve
140
- rejecter:(__unused RCTPromiseRejectBlock)reject)
243
+ - (void)reloadWithBundleURL:(NSURL *)bundleURL reason:(NSString *)reason
141
244
  {
142
- @try {
143
- // kDevConnectMetroHost is written by applyMetroHost and survives restart in both Debug+Release.
144
- // jsLocation is only meaningful in Debug (bypassed in Release); use it as a fallback only.
145
- NSString *persisted = [[NSUserDefaults standardUserDefaults] stringForKey:kDevConnectMetroHost];
146
- NSString *jsLocation = [RCTBundleURLProvider sharedSettings].jsLocation;
147
- NSString *host = persisted.length > 0 ? persisted : jsLocation;
148
- resolve(host ?: [NSNull null]);
149
- } @catch (NSException *exception) {
150
- reject(@"native_error", exception.reason, nil);
245
+ if (bundleURL) {
246
+ RCTBundleManager *bm = [self resolveBundleManager];
247
+ if (bm) bm.bundleURL = bundleURL;
248
+ RCTReloadCommandSetBundleURL(bundleURL);
151
249
  }
250
+ RCTTriggerReloadCommandListeners(reason);
152
251
  }
153
252
 
253
+ #pragma mark - Exported Methods
254
+
154
255
  RCT_EXPORT_METHOD(applyMetroHost:(NSString *)hostPort
155
256
  resolver:(RCTPromiseResolveBlock)resolve
156
257
  rejecter:(RCTPromiseRejectBlock)reject)
@@ -160,71 +261,24 @@ RCT_EXPORT_METHOD(applyMetroHost:(NSString *)hostPort
160
261
  reject(@"invalid_host", @"Metro host cannot be empty.", nil);
161
262
  return;
162
263
  }
264
+ NSString *normalized = [self normalizeHostPort:hostPort];
163
265
 
164
- RCTBundleURLProvider *settings = [RCTBundleURLProvider sharedSettings];
165
-
166
- // Parse host and port
167
- NSRange separator = [hostPort rangeOfString:@":" options:NSBackwardsSearch];
168
- NSString *host = separator.location == NSNotFound
169
- ? hostPort
170
- : [hostPort substringToIndex:separator.location];
171
- NSString *port = separator.location == NSNotFound
172
- ? @""
173
- : [hostPort substringFromIndex:separator.location + 1];
174
-
175
- if (host.length == 0 && port.length == 0) {
176
- [self resetMetroHost:resolve rejecter:reject];
177
- return;
178
- }
179
-
180
- NSNumberFormatter *formatter = [NSNumberFormatter new];
181
- formatter.numberStyle = NSNumberFormatterDecimalStyle;
182
- NSNumber *portNumber = [formatter numberFromString:port];
183
- if (portNumber == nil) {
184
- #ifdef RCT_METRO_PORT
185
- portNumber = [NSNumber numberWithInt:RCT_METRO_PORT];
186
- #else
187
- portNumber = [NSNumber numberWithInt:8081];
188
- #endif
189
- }
190
-
191
- NSString *normalizedHostPort = [NSString stringWithFormat:@"%@:%d", host, portNumber.intValue];
192
-
193
- // Persist for AppDelegate swizzle (Release mode + restart)
194
- [[NSUserDefaults standardUserDefaults] setObject:normalizedHostPort forKey:kDevConnectMetroHost];
195
- [[NSUserDefaults standardUserDefaults] synchronize];
196
-
197
- // Also set jsLocation (Debug mode + hot reload)
198
- settings.jsLocation = normalizedHostPort;
266
+ DevConnectSetPersistedMetroHost(normalized);
199
267
 
200
- // Try hot reload via bundleManager (works in Debug)
201
- NSURL *bundleURL = nil;
202
- if (DebugToolkitBundleRoot.length > 0) {
203
- bundleURL = [settings jsBundleURLForBundleRoot:DebugToolkitBundleRoot];
204
- }
205
- RCTBundleManager *bm = [self resolveBundleManager];
206
- if (bm) {
207
- bm.bundleURL = bundleURL;
208
- }
209
- #ifdef RCTReloadCommandSetBundleURL
210
- else if (bundleURL) {
211
- RCTReloadCommandSetBundleURL(bundleURL);
212
- }
213
- #endif
214
-
215
- NSLog(@"[DevConnect] applyMetroHost: %@ | bm=%@ | bridge=%@ | url=%@",
216
- normalizedHostPort, bm ? @"YES" : @"nil", _bridge ? @"YES" : @"nil", bundleURL);
217
-
218
- RCTTriggerReloadCommandListeners(@"Dev menu - apply changes");
219
-
220
- NSMutableDictionary *result = [@{@"hostPort" : normalizedHostPort} mutableCopy];
221
- if (bm && bm.bundleURL.absoluteString) {
222
- result[@"bundleURL"] = bm.bundleURL.absoluteString;
223
- }
224
- resolve(result);
225
- } @catch (NSException *exception) {
226
- NSLog(@"[DevConnect] applyMetroHost EXCEPTION: %@", exception.reason);
227
- reject(@"native_error", exception.reason, nil);
268
+ RCTBundleURLProvider *settings = [RCTBundleURLProvider sharedSettings];
269
+ settings.jsLocation = normalized;
270
+ NSURL *bundleURL = [settings jsBundleURLForBundleRoot:DevConnectMetroBundleRoot()];
271
+
272
+ [self reloadWithBundleURL:bundleURL reason:@"Dev menu - apply changes"];
273
+
274
+ NSLog(@"[DevConnect] applyMetroHost host=%@ url=%@", normalized, bundleURL);
275
+ resolve(@{
276
+ @"hostPort": normalized,
277
+ @"bundleURL": bundleURL.absoluteString ?: [NSNull null],
278
+ });
279
+ } @catch (NSException *e) {
280
+ NSLog(@"[DevConnect] applyMetroHost EXCEPTION: %@", e.reason);
281
+ reject(@"native_error", e.reason ?: @"unknown", nil);
228
282
  }
229
283
  }
230
284
 
@@ -232,48 +286,55 @@ RCT_EXPORT_METHOD(resetMetroHost:(RCTPromiseResolveBlock)resolve
232
286
  rejecter:(__unused RCTPromiseRejectBlock)reject)
233
287
  {
234
288
  @try {
235
- // Clear stored Metro host
236
- [[NSUserDefaults standardUserDefaults] removeObjectForKey:kDevConnectMetroHost];
237
- [[NSUserDefaults standardUserDefaults] synchronize];
289
+ DevConnectSetPersistedMetroHost(nil);
238
290
 
239
- // Reset RN settings
240
291
  RCTBundleURLProvider *settings = [RCTBundleURLProvider sharedSettings];
241
292
  [settings resetToDefaults];
242
- NSURL *bundleURL = [settings jsBundleURLForFallbackExtension:nil];
243
293
 
244
- RCTBundleManager *bm = [self resolveBundleManager];
245
- if (bm) {
246
- bm.bundleURL = bundleURL;
294
+ NSURL *embedded = DebugToolkitEmbeddedBundleURL();
295
+ if (!embedded) {
296
+ embedded = [settings jsBundleURLForFallbackExtension:nil];
247
297
  }
248
298
 
249
- NSLog(@"[DevConnect] resetMetroHost | bm=%@ | url=%@", bm ? @"YES" : @"nil", bundleURL);
250
- RCTTriggerReloadCommandListeners(@"Dev menu - reset to default");
299
+ [self reloadWithBundleURL:embedded reason:@"Dev menu - reset to default"];
300
+
301
+ NSLog(@"[DevConnect] resetMetroHost url=%@", embedded);
251
302
  resolve([NSNull null]);
252
- } @catch (NSException *exception) {
253
- NSLog(@"[DevConnect] resetMetroHost EXCEPTION: %@", exception.reason);
254
- reject(@"native_error", exception.reason, nil);
303
+ } @catch (NSException *e) {
304
+ NSLog(@"[DevConnect] resetMetroHost EXCEPTION: %@", e.reason);
305
+ reject(@"native_error", e.reason ?: @"unknown", nil);
255
306
  }
256
307
  }
257
308
 
258
- RCT_EXPORT_METHOD(getDiagnostics:(RCTPromiseResolveBlock)resolve
309
+ RCT_EXPORT_METHOD(getMetroHost:(RCTPromiseResolveBlock)resolve
259
310
  rejecter:(__unused RCTPromiseRejectBlock)reject)
260
311
  {
261
- NSArray *log = [[NSUserDefaults standardUserDefaults] arrayForKey:kDevConnectDiagLog] ?: @[];
262
- NSString *metroHost = [[NSUserDefaults standardUserDefaults] stringForKey:kDevConnectMetroHost];
263
- resolve(@{
264
- @"log": log,
265
- @"swizzleInstalled": @(original_sourceURLForBridge != NULL),
266
- @"swizzleInvoked": @(swizzleInvoked),
267
- @"persistedMetroHost": metroHost ?: [NSNull null],
268
- });
312
+ NSString *host = DevConnectPersistedMetroHost();
313
+ resolve(host.length > 0 ? host : [NSNull null]);
269
314
  }
270
315
 
271
- RCT_EXPORT_METHOD(clearDiagnostics:(RCTPromiseResolveBlock)resolve
316
+ RCT_EXPORT_METHOD(getDiagnostics:(RCTPromiseResolveBlock)resolve
272
317
  rejecter:(__unused RCTPromiseRejectBlock)reject)
273
318
  {
274
- [[NSUserDefaults standardUserDefaults] removeObjectForKey:kDevConnectDiagLog];
275
- [[NSUserDefaults standardUserDefaults] synchronize];
276
- resolve([NSNull null]);
319
+ Class realDelegate = object_getClass([UIApplication sharedApplication].delegate);
320
+ NSString *delegateName = realDelegate ? NSStringFromClass(realDelegate) : @"unknown";
321
+ NSString *persisted = DevConnectPersistedMetroHost();
322
+
323
+ #if DEBUG
324
+ BOOL isDebug = YES;
325
+ #else
326
+ BOOL isDebug = NO;
327
+ #endif
328
+
329
+ resolve(@{
330
+ @"persistedMetroHost": persisted.length > 0 ? persisted : [NSNull null],
331
+ @"appDelegateClass": delegateName,
332
+ @"isDebugBuild": @(isDebug),
333
+ @"hasEmbeddedBundle": @(DebugToolkitEmbeddedBundleURL() != nil),
334
+ @"embeddedFirstHookInstalled": @(DevConnectEmbeddedFirstHooksActive()),
335
+ @"packagerHookInstalled": @NO,
336
+ @"bundleRootHookInstalled": @(gBundleRootHookInstalled),
337
+ });
277
338
  }
278
339
 
279
340
  RCT_EXPORT_METHOD(getPreference:(NSString *)key
@@ -283,8 +344,8 @@ RCT_EXPORT_METHOD(getPreference:(NSString *)key
283
344
  @try {
284
345
  NSString *value = [[NSUserDefaults standardUserDefaults] stringForKey:key];
285
346
  resolve(value ?: [NSNull null]);
286
- } @catch (NSException *exception) {
287
- reject(@"native_error", exception.reason, nil);
347
+ } @catch (NSException *e) {
348
+ reject(@"native_error", e.reason ?: @"unknown", nil);
288
349
  }
289
350
  }
290
351
 
@@ -297,8 +358,8 @@ RCT_EXPORT_METHOD(setPreference:(NSString *)key
297
358
  [[NSUserDefaults standardUserDefaults] setObject:value forKey:key];
298
359
  [[NSUserDefaults standardUserDefaults] synchronize];
299
360
  resolve([NSNull null]);
300
- } @catch (NSException *exception) {
301
- reject(@"native_error", exception.reason, nil);
361
+ } @catch (NSException *e) {
362
+ reject(@"native_error", e.reason ?: @"unknown", nil);
302
363
  }
303
364
  }
304
365
 
@@ -317,40 +378,27 @@ RCT_EXPORT_METHOD(getLocalIp:(RCTPromiseResolveBlock)resolve
317
378
  {
318
379
  @try {
319
380
  struct ifaddrs *interfaces = NULL;
320
- if (getifaddrs(&interfaces) == 0) {
321
- struct ifaddrs *iface = interfaces;
322
- while (iface != NULL) {
323
- if (iface->ifa_addr != NULL && iface->ifa_addr->sa_family == AF_INET && !(iface->ifa_flags & IFF_LOOPBACK)) {
324
- if (strcmp(iface->ifa_name, "en0") == 0) {
325
- char addrStr[INET_ADDRSTRLEN];
326
- struct sockaddr_in *sin = (struct sockaddr_in *)iface->ifa_addr;
327
- inet_ntop(AF_INET, &sin->sin_addr, addrStr, sizeof(addrStr));
328
- NSString *ip = [NSString stringWithUTF8String:addrStr];
329
- freeifaddrs(interfaces);
330
- resolve(ip);
331
- return;
332
- }
333
- }
334
- iface = iface->ifa_next;
335
- }
336
- iface = interfaces;
337
- while (iface != NULL) {
338
- if (iface->ifa_addr != NULL && iface->ifa_addr->sa_family == AF_INET && !(iface->ifa_flags & IFF_LOOPBACK)) {
339
- char addrStr[INET_ADDRSTRLEN];
340
- struct sockaddr_in *sin = (struct sockaddr_in *)iface->ifa_addr;
341
- inet_ntop(AF_INET, &sin->sin_addr, addrStr, sizeof(addrStr));
342
- NSString *ip = [NSString stringWithUTF8String:addrStr];
343
- freeifaddrs(interfaces);
344
- resolve(ip);
345
- return;
346
- }
347
- iface = iface->ifa_next;
348
- }
381
+ if (getifaddrs(&interfaces) != 0) {
382
+ resolve([NSNull null]);
383
+ return;
384
+ }
385
+
386
+ NSString *preferred = nil;
387
+ NSString *fallback = nil;
388
+ for (struct ifaddrs *iface = interfaces; iface != NULL; iface = iface->ifa_next) {
389
+ if (!iface->ifa_addr || iface->ifa_addr->sa_family != AF_INET) continue;
390
+ if (iface->ifa_flags & IFF_LOOPBACK) continue;
391
+ char addrStr[INET_ADDRSTRLEN];
392
+ struct sockaddr_in *sin = (struct sockaddr_in *)iface->ifa_addr;
393
+ inet_ntop(AF_INET, &sin->sin_addr, addrStr, sizeof(addrStr));
394
+ NSString *ip = [NSString stringWithUTF8String:addrStr];
395
+ if (strcmp(iface->ifa_name, "en0") == 0) { preferred = ip; break; }
396
+ if (!fallback) fallback = ip;
349
397
  }
350
398
  freeifaddrs(interfaces);
351
- resolve([NSNull null]);
352
- } @catch (NSException *exception) {
353
- reject(@"native_error", exception.reason, nil);
399
+ resolve(preferred ?: fallback ?: [NSNull null]);
400
+ } @catch (NSException *e) {
401
+ reject(@"native_error", e.reason ?: @"unknown", nil);
354
402
  }
355
403
  }
356
404