rnww-plugin-wifi 1.0.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.
Files changed (36) hide show
  1. package/README.md +841 -0
  2. package/lib/bridge/background-bridge.d.ts +22 -0
  3. package/lib/bridge/background-bridge.d.ts.map +1 -0
  4. package/lib/bridge/background-bridge.js +493 -0
  5. package/lib/bridge/index.d.ts +6 -0
  6. package/lib/bridge/index.d.ts.map +1 -0
  7. package/lib/bridge/index.js +23 -0
  8. package/lib/bridge/wifi-bridge.d.ts +16 -0
  9. package/lib/bridge/wifi-bridge.d.ts.map +1 -0
  10. package/lib/bridge/wifi-bridge.js +202 -0
  11. package/lib/modules/index.d.ts +41 -0
  12. package/lib/modules/index.d.ts.map +1 -0
  13. package/lib/modules/index.js +150 -0
  14. package/lib/types/background-module.d.ts +291 -0
  15. package/lib/types/background-module.d.ts.map +1 -0
  16. package/lib/types/background-module.js +5 -0
  17. package/lib/types/bridge.d.ts +22 -0
  18. package/lib/types/bridge.d.ts.map +1 -0
  19. package/lib/types/bridge.js +5 -0
  20. package/lib/types/index.d.ts +7 -0
  21. package/lib/types/index.d.ts.map +1 -0
  22. package/lib/types/index.js +5 -0
  23. package/lib/types/platform.d.ts +10 -0
  24. package/lib/types/platform.d.ts.map +1 -0
  25. package/lib/types/platform.js +2 -0
  26. package/lib/types/wifi-module.d.ts +146 -0
  27. package/lib/types/wifi-module.d.ts.map +1 -0
  28. package/lib/types/wifi-module.js +6 -0
  29. package/package.json +42 -0
  30. package/src/modules/android/build.gradle +64 -0
  31. package/src/modules/android/src/main/AndroidManifest.xml +28 -0
  32. package/src/modules/android/src/main/java/expo/modules/customwifi/WifiModule.kt +517 -0
  33. package/src/modules/android/src/main/java/expo/modules/customwifi/WifiStateReceiver.kt +46 -0
  34. package/src/modules/expo-module.config.json +9 -0
  35. package/src/modules/index.ts +166 -0
  36. package/src/modules/ios/WifiModule.swift +399 -0
@@ -0,0 +1,166 @@
1
+ /**
2
+ * WiFi Module (Cross-Platform)
3
+ * WiFi 정보 조회 및 연결 관리 기능 제공
4
+ * Supports: Android, iOS
5
+ */
6
+
7
+ import { requireNativeModule } from 'expo-modules-core';
8
+ import { Platform } from 'react-native';
9
+ import type {
10
+ WifiInfoResult,
11
+ WifiListResult,
12
+ WifiStateResult,
13
+ ConnectOptions,
14
+ ConnectResult,
15
+ WifiResult,
16
+ PermissionStatus,
17
+ WifiStateChangeEvent,
18
+ } from '../types/wifi-module';
19
+
20
+ // Lazy 모듈 로드 (크래시 방지)
21
+ let WifiModule: any = null;
22
+
23
+ function getWifiModule() {
24
+ if (Platform.OS !== 'android' && Platform.OS !== 'ios') {
25
+ return null;
26
+ }
27
+
28
+ if (WifiModule === null) {
29
+ try {
30
+ WifiModule = requireNativeModule('CustomWifi');
31
+ } catch (error) {
32
+ console.error('[CustomWifi] Failed to load native module:', error);
33
+ WifiModule = undefined;
34
+ return null;
35
+ }
36
+ }
37
+
38
+ return WifiModule === undefined ? null : WifiModule;
39
+ }
40
+
41
+ /**
42
+ * 현재 연결된 WiFi 정보 조회
43
+ */
44
+ export async function getCurrentWifiInfo(): Promise<WifiInfoResult> {
45
+ const module = getWifiModule();
46
+ if (!module) {
47
+ return {
48
+ success: false,
49
+ error: 'Module not available',
50
+ errorCode: 'MODULE_NOT_AVAILABLE',
51
+ };
52
+ }
53
+ return await module.getCurrentWifiInfo();
54
+ }
55
+
56
+ /**
57
+ * 사용 가능한 WiFi 목록 스캔 (Android only)
58
+ */
59
+ export async function getWifiList(): Promise<WifiListResult> {
60
+ const module = getWifiModule();
61
+ if (!module) {
62
+ return {
63
+ success: false,
64
+ error: 'Module not available',
65
+ errorCode: 'MODULE_NOT_AVAILABLE',
66
+ };
67
+ }
68
+
69
+ if (Platform.OS === 'ios') {
70
+ return {
71
+ success: false,
72
+ error: 'WiFi scanning is not supported on iOS',
73
+ errorCode: 'NOT_SUPPORTED',
74
+ };
75
+ }
76
+
77
+ return await module.getWifiList();
78
+ }
79
+
80
+ /**
81
+ * WiFi 활성화 상태 확인
82
+ */
83
+ export async function isWifiEnabled(): Promise<WifiStateResult> {
84
+ const module = getWifiModule();
85
+ if (!module) {
86
+ return {
87
+ success: false,
88
+ error: 'Module not available',
89
+ errorCode: 'MODULE_NOT_AVAILABLE',
90
+ };
91
+ }
92
+ return await module.isWifiEnabled();
93
+ }
94
+
95
+ /**
96
+ * WiFi 네트워크에 연결
97
+ */
98
+ export async function connectToWifi(options: ConnectOptions): Promise<ConnectResult> {
99
+ const module = getWifiModule();
100
+ if (!module) {
101
+ return {
102
+ success: false,
103
+ error: 'Module not available',
104
+ errorCode: 'MODULE_NOT_AVAILABLE',
105
+ };
106
+ }
107
+ return await module.connectToWifi(options);
108
+ }
109
+
110
+ /**
111
+ * WiFi 연결 해제
112
+ */
113
+ export async function disconnect(): Promise<WifiResult> {
114
+ const module = getWifiModule();
115
+ if (!module) {
116
+ return {
117
+ success: false,
118
+ error: 'Module not available',
119
+ errorCode: 'MODULE_NOT_AVAILABLE',
120
+ };
121
+ }
122
+ return await module.disconnect();
123
+ }
124
+
125
+ /**
126
+ * WiFi 권한 확인
127
+ */
128
+ export async function checkPermission(): Promise<PermissionStatus> {
129
+ const module = getWifiModule();
130
+ if (!module) {
131
+ return {
132
+ locationGranted: false,
133
+ canAccessWifiInfo: false,
134
+ requiredPermissions: [],
135
+ };
136
+ }
137
+ return await module.checkPermission();
138
+ }
139
+
140
+ /**
141
+ * WiFi 권한 요청
142
+ */
143
+ export async function requestPermission(): Promise<PermissionStatus> {
144
+ const module = getWifiModule();
145
+ if (!module) {
146
+ return {
147
+ locationGranted: false,
148
+ canAccessWifiInfo: false,
149
+ requiredPermissions: [],
150
+ };
151
+ }
152
+ return await module.requestPermission();
153
+ }
154
+
155
+ /**
156
+ * WiFi 상태 변경 이벤트 리스너 등록
157
+ */
158
+ export function addWifiStateListener(
159
+ listener: (event: WifiStateChangeEvent) => void
160
+ ): { remove: () => void } {
161
+ const module = getWifiModule();
162
+ if (!module || !module.addListener) {
163
+ return { remove: () => {} };
164
+ }
165
+ return module.addListener('onWifiStateChange', listener);
166
+ }
@@ -0,0 +1,399 @@
1
+ import ExpoModulesCore
2
+ import UIKit
3
+ import NetworkExtension
4
+ import SystemConfiguration.CaptiveNetwork
5
+ import CoreLocation
6
+
7
+ public class WifiModule: Module {
8
+ private var locationManager: CLLocationManager?
9
+ private var permissionPromise: Promise?
10
+ private var connectPromise: Promise?
11
+ private var foregroundObserver: NSObjectProtocol?
12
+ private var reachabilityObserver: NSObjectProtocol?
13
+
14
+ public func definition() -> ModuleDefinition {
15
+ Name("CustomWifi")
16
+
17
+ Events("onWifiStateChange")
18
+
19
+ OnCreate {
20
+ self.setupLocationManager()
21
+ self.setupReachabilityObserver()
22
+ }
23
+
24
+ OnDestroy {
25
+ self.cleanup()
26
+ }
27
+
28
+ // 현재 WiFi 정보 조회
29
+ AsyncFunction("getCurrentWifiInfo") { (promise: Promise) in
30
+ self.getCurrentWifiInfo(promise: promise)
31
+ }
32
+
33
+ // WiFi 목록 스캔 (iOS에서는 지원되지 않음)
34
+ AsyncFunction("getWifiList") { (promise: Promise) in
35
+ promise.resolve([
36
+ "success": false,
37
+ "error": "WiFi scanning is not supported on iOS",
38
+ "errorCode": "NOT_SUPPORTED"
39
+ ])
40
+ }
41
+
42
+ // WiFi 활성화 상태 확인
43
+ AsyncFunction("isWifiEnabled") { (promise: Promise) in
44
+ let isEnabled = self.isWifiEnabled()
45
+ promise.resolve([
46
+ "success": true,
47
+ "isEnabled": isEnabled,
48
+ "wifiState": isEnabled ? "ENABLED" : "UNKNOWN"
49
+ ])
50
+ }
51
+
52
+ // WiFi 연결 (iOS 11+)
53
+ AsyncFunction("connectToWifi") { (options: [String: Any], promise: Promise) in
54
+ self.connectToWifi(options: options, promise: promise)
55
+ }
56
+
57
+ // WiFi 연결 해제
58
+ AsyncFunction("disconnect") { (promise: Promise) in
59
+ self.disconnect(promise: promise)
60
+ }
61
+
62
+ // 권한 확인
63
+ AsyncFunction("checkPermission") { (promise: Promise) in
64
+ self.checkPermission(promise: promise)
65
+ }
66
+
67
+ // 권한 요청
68
+ AsyncFunction("requestPermission") { (promise: Promise) in
69
+ self.requestPermission(promise: promise)
70
+ }
71
+ }
72
+
73
+ // MARK: - Private Methods
74
+
75
+ private func setupLocationManager() {
76
+ locationManager = CLLocationManager()
77
+ locationManager?.delegate = LocationDelegate(module: self)
78
+ }
79
+
80
+ private func setupReachabilityObserver() {
81
+ // 네트워크 상태 변경 감지
82
+ reachabilityObserver = NotificationCenter.default.addObserver(
83
+ forName: NSNotification.Name(rawValue: "kNetworkReachabilityChangedNotification"),
84
+ object: nil,
85
+ queue: .main
86
+ ) { [weak self] _ in
87
+ self?.notifyWifiStateChange()
88
+ }
89
+ }
90
+
91
+ private func cleanup() {
92
+ if let observer = foregroundObserver {
93
+ NotificationCenter.default.removeObserver(observer)
94
+ foregroundObserver = nil
95
+ }
96
+ if let observer = reachabilityObserver {
97
+ NotificationCenter.default.removeObserver(observer)
98
+ reachabilityObserver = nil
99
+ }
100
+ locationManager = nil
101
+ }
102
+
103
+ private func getCurrentWifiInfo(promise: Promise) {
104
+ // 위치 권한 확인
105
+ let status = CLLocationManager.authorizationStatus()
106
+ guard status == .authorizedWhenInUse || status == .authorizedAlways else {
107
+ promise.resolve([
108
+ "success": false,
109
+ "error": "Location permission required for WiFi info",
110
+ "errorCode": "PERMISSION_DENIED"
111
+ ])
112
+ return
113
+ }
114
+
115
+ let wifiInfo = getWifiInfo()
116
+ promise.resolve([
117
+ "success": true,
118
+ "data": wifiInfo
119
+ ])
120
+ }
121
+
122
+ private func getWifiInfo() -> [String: Any?] {
123
+ var ssid: String? = nil
124
+ var bssid: String? = nil
125
+
126
+ // CNCopyCurrentNetworkInfo를 사용하여 SSID 및 BSSID 가져오기
127
+ if let interfaces = CNCopySupportedInterfaces() as? [String] {
128
+ for interface in interfaces {
129
+ if let networkInfo = CNCopyCurrentNetworkInfo(interface as CFString) as? [String: Any] {
130
+ ssid = networkInfo[kCNNetworkInfoKeySSID as String] as? String
131
+ bssid = networkInfo[kCNNetworkInfoKeyBSSID as String] as? String
132
+ break
133
+ }
134
+ }
135
+ }
136
+
137
+ let isConnected = ssid != nil
138
+
139
+ return [
140
+ "ssid": ssid,
141
+ "bssid": bssid,
142
+ "rssi": nil, // iOS에서는 직접 가져올 수 없음
143
+ "signalLevel": nil,
144
+ "frequency": nil,
145
+ "linkSpeed": nil,
146
+ "ipAddress": getWiFiIPAddress(),
147
+ "isConnected": isConnected
148
+ ]
149
+ }
150
+
151
+ private func getWiFiIPAddress() -> String? {
152
+ var address: String?
153
+ var ifaddr: UnsafeMutablePointer<ifaddrs>?
154
+
155
+ guard getifaddrs(&ifaddr) == 0 else { return nil }
156
+ defer { freeifaddrs(ifaddr) }
157
+
158
+ var ptr = ifaddr
159
+ while ptr != nil {
160
+ defer { ptr = ptr?.pointee.ifa_next }
161
+
162
+ let interface = ptr?.pointee
163
+ let addrFamily = interface?.ifa_addr.pointee.sa_family
164
+
165
+ if addrFamily == UInt8(AF_INET) {
166
+ let name = String(cString: (interface?.ifa_name)!)
167
+ if name == "en0" { // WiFi interface
168
+ var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
169
+ getnameinfo(
170
+ interface?.ifa_addr,
171
+ socklen_t((interface?.ifa_addr.pointee.sa_len)!),
172
+ &hostname,
173
+ socklen_t(hostname.count),
174
+ nil,
175
+ socklen_t(0),
176
+ NI_NUMERICHOST
177
+ )
178
+ address = String(cString: hostname)
179
+ }
180
+ }
181
+ }
182
+ return address
183
+ }
184
+
185
+ private func isWifiEnabled() -> Bool {
186
+ // iOS에서는 WiFi 상태를 직접 확인할 수 없으므로 연결 여부로 대체
187
+ let wifiInfo = getWifiInfo()
188
+ return wifiInfo["ssid"] != nil
189
+ }
190
+
191
+ private func connectToWifi(options: [String: Any], promise: Promise) {
192
+ guard let ssid = options["ssid"] as? String, !ssid.isEmpty else {
193
+ promise.resolve([
194
+ "success": false,
195
+ "error": "SSID is required",
196
+ "errorCode": "INVALID_SSID"
197
+ ])
198
+ return
199
+ }
200
+
201
+ let password = options["password"] as? String
202
+ let isHidden = options["isHidden"] as? Bool ?? false
203
+
204
+ if #available(iOS 11.0, *) {
205
+ var hotspotConfig: NEHotspotConfiguration
206
+
207
+ if let pwd = password, !pwd.isEmpty {
208
+ hotspotConfig = NEHotspotConfiguration(ssid: ssid, passphrase: pwd, isWEP: false)
209
+ } else {
210
+ hotspotConfig = NEHotspotConfiguration(ssid: ssid)
211
+ }
212
+
213
+ hotspotConfig.joinOnce = false
214
+
215
+ if isHidden {
216
+ hotspotConfig.hidden = true
217
+ }
218
+
219
+ NEHotspotConfigurationManager.shared.apply(hotspotConfig) { [weak self] error in
220
+ if let error = error as NSError? {
221
+ if error.domain == NEHotspotConfigurationErrorDomain {
222
+ switch error.code {
223
+ case NEHotspotConfigurationError.alreadyAssociated.rawValue:
224
+ // 이미 연결된 경우는 성공으로 처리
225
+ let wifiInfo = self?.getWifiInfo()
226
+ promise.resolve([
227
+ "success": true,
228
+ "wifiInfo": wifiInfo as Any
229
+ ])
230
+ return
231
+ case NEHotspotConfigurationError.userDenied.rawValue:
232
+ promise.resolve([
233
+ "success": false,
234
+ "error": "User denied connection",
235
+ "errorCode": "CONNECTION_FAILED"
236
+ ])
237
+ return
238
+ case NEHotspotConfigurationError.invalidSSID.rawValue:
239
+ promise.resolve([
240
+ "success": false,
241
+ "error": "Invalid SSID",
242
+ "errorCode": "INVALID_SSID"
243
+ ])
244
+ return
245
+ case NEHotspotConfigurationError.invalidWPAPassphrase.rawValue:
246
+ promise.resolve([
247
+ "success": false,
248
+ "error": "Invalid password",
249
+ "errorCode": "INVALID_PASSWORD"
250
+ ])
251
+ return
252
+ default:
253
+ break
254
+ }
255
+ }
256
+ promise.resolve([
257
+ "success": false,
258
+ "error": error.localizedDescription,
259
+ "errorCode": "CONNECTION_FAILED"
260
+ ])
261
+ return
262
+ }
263
+
264
+ // 연결 성공
265
+ let wifiInfo = self?.getWifiInfo()
266
+ promise.resolve([
267
+ "success": true,
268
+ "wifiInfo": wifiInfo as Any
269
+ ])
270
+ }
271
+ } else {
272
+ promise.resolve([
273
+ "success": false,
274
+ "error": "WiFi connection requires iOS 11 or later",
275
+ "errorCode": "NOT_SUPPORTED"
276
+ ])
277
+ }
278
+ }
279
+
280
+ private func disconnect(promise: Promise) {
281
+ if #available(iOS 11.0, *) {
282
+ // 현재 연결된 SSID 가져오기
283
+ let wifiInfo = getWifiInfo()
284
+ if let ssid = wifiInfo["ssid"] as? String {
285
+ NEHotspotConfigurationManager.shared.removeConfiguration(forSSID: ssid)
286
+ }
287
+ promise.resolve(["success": true])
288
+ } else {
289
+ promise.resolve([
290
+ "success": false,
291
+ "error": "WiFi management requires iOS 11 or later",
292
+ "errorCode": "NOT_SUPPORTED"
293
+ ])
294
+ }
295
+ }
296
+
297
+ private func checkPermission(promise: Promise) {
298
+ let status = CLLocationManager.authorizationStatus()
299
+ let locationGranted = status == .authorizedWhenInUse || status == .authorizedAlways
300
+
301
+ var requiredPermissions: [String] = []
302
+ if !locationGranted {
303
+ requiredPermissions.append("Location (When In Use)")
304
+ }
305
+
306
+ promise.resolve([
307
+ "locationGranted": locationGranted,
308
+ "canAccessWifiInfo": locationGranted,
309
+ "requiredPermissions": requiredPermissions,
310
+ "details": [
311
+ "fineLocation": locationGranted,
312
+ "coarseLocation": status == .authorizedAlways
313
+ ]
314
+ ])
315
+ }
316
+
317
+ private func requestPermission(promise: Promise) {
318
+ let status = CLLocationManager.authorizationStatus()
319
+
320
+ if status == .authorizedWhenInUse || status == .authorizedAlways {
321
+ self.checkPermission(promise: promise)
322
+ return
323
+ }
324
+
325
+ if status == .notDetermined {
326
+ // 권한 요청
327
+ permissionPromise = promise
328
+ locationManager?.requestWhenInUseAuthorization()
329
+ } else if status == .denied || status == .restricted {
330
+ // 설정으로 안내
331
+ if let observer = foregroundObserver {
332
+ NotificationCenter.default.removeObserver(observer)
333
+ foregroundObserver = nil
334
+ }
335
+
336
+ guard let url = URL(string: UIApplication.openSettingsURLString) else {
337
+ self.checkPermission(promise: promise)
338
+ return
339
+ }
340
+
341
+ // 앱이 포그라운드로 돌아올 때 상태 확인
342
+ foregroundObserver = NotificationCenter.default.addObserver(
343
+ forName: UIApplication.willEnterForegroundNotification,
344
+ object: nil,
345
+ queue: .main
346
+ ) { [weak self] _ in
347
+ if let observer = self?.foregroundObserver {
348
+ NotificationCenter.default.removeObserver(observer)
349
+ self?.foregroundObserver = nil
350
+ }
351
+
352
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
353
+ self?.checkPermission(promise: promise)
354
+ }
355
+ }
356
+
357
+ DispatchQueue.main.async {
358
+ UIApplication.shared.open(url)
359
+ }
360
+ }
361
+ }
362
+
363
+ func handlePermissionResult() {
364
+ if let promise = permissionPromise {
365
+ checkPermission(promise: promise)
366
+ permissionPromise = nil
367
+ }
368
+ }
369
+
370
+ private func notifyWifiStateChange() {
371
+ let wifiInfo = getWifiInfo()
372
+ let connectionState = (wifiInfo["isConnected"] as? Bool ?? false) ? "CONNECTED" : "DISCONNECTED"
373
+
374
+ sendEvent("onWifiStateChange", [
375
+ "type": "CONNECTION_STATE_CHANGED",
376
+ "connectionState": connectionState,
377
+ "wifiInfo": wifiInfo,
378
+ "timestamp": Int64(Date().timeIntervalSince1970 * 1000)
379
+ ])
380
+ }
381
+ }
382
+
383
+ // MARK: - Location Delegate
384
+ private class LocationDelegate: NSObject, CLLocationManagerDelegate {
385
+ weak var module: WifiModule?
386
+
387
+ init(module: WifiModule) {
388
+ self.module = module
389
+ }
390
+
391
+ func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
392
+ module?.handlePermissionResult()
393
+ }
394
+
395
+ @available(iOS 14.0, *)
396
+ func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
397
+ module?.handlePermissionResult()
398
+ }
399
+ }