diff --git a/README.md b/README.md index 78709c6..686388d 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ WiFi options: - `hiddenSsid`:可选;Android 支持隐藏 SSID;iOS 15+ 会设置 `NEHotspotConfiguration.hidden` - `bssid`:可选,仅 Android 10+ 临时连接使用 - `joinOnce`:可选,仅 iOS;`connectWifi` 默认 `true`,`saveWifi` 默认 `false` +- `iosReferenceFallback`:可选,仅 iOS,默认 `true`;当主实现返回 `NEHotspotConfigurationErrorDomain` code `8` 时,会按 `JGeraldoLima/cordova-wifi-plugin` 的最小 iOS 写法再尝试一次,用于区分本插件参数/分支问题和系统/API 问题 ## Push Notifications @@ -215,6 +216,8 @@ iOS WiFi: - 插件会向 Cordova iOS entitlements 写入 `com.apple.developer.networking.HotspotConfiguration`,但仍必须在 Apple Developer App ID 中开启 `Hotspot Configuration` capability,并使用包含该 capability 的 provisioning profile 重新构建。 - 如果 iOS 返回 `NEHotspotConfigurationErrorDomain` code `8` / `internal error`,优先检查 App ID、entitlements、provisioning profile 是否都包含 `Hotspot Configuration`,并确认是在真机前台运行,不是在模拟器中测试。若签名和参数都正确,常见原因是真实热点使用 WPA2/WPA3 transition mode、WPA3 Personal、iPhone Personal Hotspot 默认安全模式,或其他 `NEHotspotConfiguration` 无法表达的 WiFi 能力。 - iOS 错误结果会包含 `applicationState`、`ssidUtf8Length`、`passwordUtf8Length`、`passwordAsciiPrintable` 等诊断字段;WPA/WPA2 密码建议使用 8-63 位可打印 ASCII 字符。 +- iOS 错误结果会包含 `pluginVersion`,用于确认 IPA 中实际运行的原生插件版本。 +- iOS code `8` 会自动启用 `iosReferenceFallback`。该 fallback 改编自 MIT 许可的 `JGeraldoLima/cordova-wifi-plugin`,只使用 `NEHotspotConfiguration(ssid:passphrase:isWEP:false)`、`joinOnce=false`,不设置 `hidden`,并会返回 `referenceNativeCode` / `referenceNativeError` 供对照。 - iOS 的 `NEHotspotConfiguration` password initializer 公开支持 WEP/WPA/WPA2 Personal,不提供 WPA3 Personal 参数。iOS 传入 `security` 包含 `wpa3` 时会返回 `UNSUPPORTED_WIFI_SECURITY`。WPA2/WPA3 mixed 通常只能按 `security: "wpa2"` 尝试;如果仍失败,需要用户手动加入,或改用配置描述文件/MDM 流程。 - iOS 不提供第三方 App 断开当前 WiFi 的公开 API,因此 `disconnectWifi` 在 iOS 返回错误。 diff --git a/package.json b/package.json index 733d451..16678d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bluememe-utils", - "version": "0.1.9", + "version": "0.1.11", "description": "Cordova utilities for WiFi, Firebase Cloud Messaging, and biometric unlock for OutSystems 11 apps.", "license": "MIT", "files": [ diff --git a/plugin.xml b/plugin.xml index b0a61e0..fa290b8 100644 --- a/plugin.xml +++ b/plugin.xml @@ -2,7 +2,7 @@ + version="0.1.11"> bluememe-utils Cordova utilities for WiFi, Firebase Cloud Messaging, and biometric unlock. MIT @@ -108,6 +108,7 @@ + diff --git a/src/ios/CDVBluememeUtils.m b/src/ios/CDVBluememeUtils.m index 28caeeb..e460907 100644 --- a/src/ios/CDVBluememeUtils.m +++ b/src/ios/CDVBluememeUtils.m @@ -3,10 +3,12 @@ #import #import #import +#import #import #import static NSString * const BluememeDeviceKeyDefaultsKey = @"bluememe_utils_device_key"; +static NSString * const BluememeUtilsPluginVersion = @"0.1.11"; @interface CDVBluememeUtils () @property (nonatomic, copy) NSString *notificationCallbackId; @@ -294,6 +296,7 @@ static NSString * const BluememeDeviceKeyDefaultsKey = @"bluememe_utils_device_k BOOL isWep = [security isEqualToString:@"wep"] || [self boolFromDictionary:options key:@"isWep" defaultValue:NO]; BOOL joinOnce = [self boolFromDictionary:options key:@"joinOnce" defaultValue:defaultJoinOnce]; BOOL hiddenSsid = [self boolFromDictionary:options key:@"hiddenSsid" defaultValue:NO]; + BOOL referenceFallback = [self boolFromDictionary:options key:@"iosReferenceFallback" defaultValue:YES]; NSString *applicationState = [self applicationStateName]; if (ssid.length == 0) { @@ -368,13 +371,25 @@ static NSString * const BluememeDeviceKeyDefaultsKey = @"bluememe_utils_device_k @"ssidUtf8Length": @([ssid lengthOfBytesUsingEncoding:NSUTF8StringEncoding]), @"joinOnce": @(joinOnce), @"hiddenSsid": @(hiddenSsid), + @"iosReferenceFallback": @(referenceFallback), @"minimumIos": @"15.0", @"applicationState": applicationState }; + if (referenceFallback + && [error.domain isEqualToString:NEHotspotConfigurationErrorDomain] + && error.code == 8) { + [self applyReferenceWifiFallbackForCommand:command + ssid:ssid + password:password + primaryError:error + context:context]; + return; + } [self sendWifiApplyErrorForCommand:command error:error context:context]; return; } NSDictionary *result = @{ + @"pluginVersion": BluememeUtilsPluginVersion, @"ssid": ssid, @"platformMode": @"hotspotConfiguration", @"connected": @YES, @@ -396,6 +411,71 @@ static NSString * const BluememeDeviceKeyDefaultsKey = @"bluememe_utils_device_k } } +// Diagnostic fallback adapted from https://github.com/JGeraldoLima/cordova-wifi-plugin. +// Original project copyright (c) 2018 Jose Geraldo de Lima Junior, MIT License. +// It intentionally mirrors that plugin's minimal iOS path: passphrase initializer, +// isWEP=false, joinOnce=false, no hidden SSID setting. +- (void)applyReferenceWifiFallbackForCommand:(CDVInvokedUrlCommand *)command + ssid:(NSString *)ssid + password:(NSString *)password + primaryError:(NSError *)primaryError + context:(NSDictionary *)context API_AVAILABLE(ios(15.0)) +{ + NEHotspotConfiguration *configuration = nil; + if (password.length == 0) { + configuration = [[NEHotspotConfiguration alloc] initWithSSID:ssid]; + } else { + configuration = [[NEHotspotConfiguration alloc] initWithSSID:ssid passphrase:password isWEP:NO]; + } + configuration.joinOnce = NO; + + [[NEHotspotConfigurationManager sharedManager] applyConfiguration:configuration completionHandler:^(NSError *fallbackError) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSString *currentSSID = [self currentWifiSSID]; + if (fallbackError == nil || fallbackError.code == NEHotspotConfigurationErrorAlreadyAssociated) { + NSMutableDictionary *result = [@{ + @"pluginVersion": BluememeUtilsPluginVersion, + @"ssid": ssid, + @"platformMode": @"hotspotConfigurationReferenceFallback", + @"connected": @YES, + @"saved": @YES, + @"joinOnce": @NO, + @"iosReferenceFallback": @YES, + @"referenceFallbackSource": @"JGeraldoLima/cordova-wifi-plugin", + @"alreadyAssociated": @(fallbackError != nil && fallbackError.code == NEHotspotConfigurationErrorAlreadyAssociated) + } mutableCopy]; + if (currentSSID.length > 0) { + result[@"currentSSID"] = currentSSID; + result[@"currentSSIDMatches"] = @([currentSSID isEqualToString:ssid]); + } + if (primaryError != nil) { + [self addWifiErrorDetailsForError:primaryError toDictionary:result prefix:@"primary"]; + } + [self sendSuccessForCommand:command dictionary:result]; + return; + } + + NSMutableDictionary *details = context == nil ? [NSMutableDictionary dictionary] : [context mutableCopy]; + details[@"iosReferenceFallback"] = @YES; + details[@"referenceFallbackSource"] = @"JGeraldoLima/cordova-wifi-plugin"; + details[@"referenceJoinOnce"] = @NO; + if (currentSSID.length > 0) { + details[@"currentSSID"] = currentSSID; + details[@"currentSSIDMatches"] = @([currentSSID isEqualToString:ssid]); + } + if (primaryError != nil) { + [self addWifiErrorDetailsForError:primaryError toDictionary:details prefix:@"primary"]; + } + [self addWifiErrorDetailsForError:fallbackError toDictionary:details prefix:@"reference"]; + + [self sendErrorForCommand:command + code:@"WIFI_CONNECT_FAILED" + message:@"Primary iOS WiFi configuration failed, and the reference cordova-wifi-plugin fallback failed as well." + details:details]; + }); + }]; +} + - (NSString *)applicationStateName { UIApplicationState state = [[UIApplication sharedApplication] applicationState]; @@ -426,6 +506,44 @@ static NSString * const BluememeDeviceKeyDefaultsKey = @"bluememe_utils_device_k return YES; } +- (void)addWifiErrorDetailsForError:(NSError *)error toDictionary:(NSMutableDictionary *)dictionary prefix:(NSString *)prefix +{ + if (error == nil || dictionary == nil || prefix.length == 0) { + return; + } + + NSString *domainKey = [NSString stringWithFormat:@"%@NativeDomain", prefix]; + NSString *codeKey = [NSString stringWithFormat:@"%@NativeCode", prefix]; + NSString *errorKey = [NSString stringWithFormat:@"%@NativeError", prefix]; + NSString *messageKey = [NSString stringWithFormat:@"%@NativeMessage", prefix]; + + if (error.domain.length > 0) { + dictionary[domainKey] = error.domain; + } + dictionary[codeKey] = @(error.code); + + NSString *nativeError = [self hotspotConfigurationErrorNameForCode:error.code]; + if (nativeError.length > 0) { + dictionary[errorKey] = nativeError; + } + if (error.localizedDescription.length > 0) { + dictionary[messageKey] = error.localizedDescription; + } +} + +- (NSString *)currentWifiSSID +{ + NSArray *interfaces = CFBridgingRelease(CNCopySupportedInterfaces()); + for (id interface in interfaces) { + NSDictionary *networkInfo = CFBridgingRelease(CNCopyCurrentNetworkInfo((__bridge CFStringRef)interface)); + NSString *ssid = [networkInfo objectForKey:(__bridge NSString *)kCNNetworkInfoKeySSID]; + if (ssid.length > 0) { + return ssid; + } + } + return @""; +} + - (void)sendWifiApplyErrorForCommand:(CDVInvokedUrlCommand *)command error:(NSError *)error context:(NSDictionary *)context { NSMutableDictionary *details = [NSMutableDictionary dictionary]; @@ -837,6 +955,7 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response - (void)sendErrorForCommand:(CDVInvokedUrlCommand *)command code:(NSString *)code message:(NSString *)message details:(NSDictionary *)details { NSMutableDictionary *error = details == nil ? [NSMutableDictionary dictionary] : [details mutableCopy]; + error[@"pluginVersion"] = BluememeUtilsPluginVersion; error[@"code"] = code ? code : @"ERROR"; error[@"message"] = message ? message : @""; CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:error];