diff --git a/README.md b/README.md index 93d9783..c3f2499 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,8 @@ Android WiFi: iOS WiFi: - 使用 `NEHotspotConfiguration`。 -- 必须在 Apple Developer 账号和 App entitlements 中开启 `Hotspot Configuration` capability。 +- 插件会向 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`,并确认是在真机前台运行,不是在模拟器中测试。 - iOS 不提供第三方 App 断开当前 WiFi 的公开 API,因此 `disconnectWifi` 在 iOS 返回错误。 Firebase: diff --git a/plugin.xml b/plugin.xml index ad3b897..d2cc1a6 100644 --- a/plugin.xml +++ b/plugin.xml @@ -96,6 +96,12 @@ $IOS_FIREBASE_STORAGE_BUCKET + + + + + + diff --git a/src/ios/CDVBluememeUtils.m b/src/ios/CDVBluememeUtils.m index 8156d9c..54c62e1 100644 --- a/src/ios/CDVBluememeUtils.m +++ b/src/ios/CDVBluememeUtils.m @@ -3,6 +3,7 @@ #import #import #import +#import #import static NSString * const BluememeDeviceKeyDefaultsKey = @"bluememe_utils_device_key"; @@ -293,6 +294,13 @@ static NSString * const BluememeDeviceKeyDefaultsKey = @"bluememe_utils_device_k return; } +#if TARGET_OS_SIMULATOR + [self sendErrorForCommand:command + code:@"UNSUPPORTED_IOS_SIMULATOR" + message:@"iOS WiFi configuration requires a physical device."]; + return; +#endif + if (@available(iOS 11.0, *)) { NEHotspotConfiguration *configuration = nil; if (password.length == 0 || [security isEqualToString:@"open"]) { @@ -305,7 +313,7 @@ static NSString * const BluememeDeviceKeyDefaultsKey = @"bluememe_utils_device_k [[NEHotspotConfigurationManager sharedManager] applyConfiguration:configuration completionHandler:^(NSError *error) { dispatch_async(dispatch_get_main_queue(), ^{ if (error != nil && error.code != NEHotspotConfigurationErrorAlreadyAssociated) { - [self sendErrorForCommand:command code:@"WIFI_CONNECT_FAILED" message:(error.localizedDescription ? error.localizedDescription : @"Unable to apply WiFi configuration.")]; + [self sendWifiApplyErrorForCommand:command error:error]; return; } NSDictionary *result = @{ @@ -323,6 +331,111 @@ static NSString * const BluememeDeviceKeyDefaultsKey = @"bluememe_utils_device_k } } +- (void)sendWifiApplyErrorForCommand:(CDVInvokedUrlCommand *)command error:(NSError *)error +{ + NSMutableDictionary *details = [NSMutableDictionary dictionary]; + if (error.domain.length > 0) { + details[@"nativeDomain"] = error.domain; + } + details[@"nativeCode"] = @(error.code); + + NSString *nativeError = [self hotspotConfigurationErrorNameForCode:error.code]; + if (nativeError.length > 0) { + details[@"nativeError"] = nativeError; + } + if (error.localizedFailureReason.length > 0) { + details[@"nativeFailureReason"] = error.localizedFailureReason; + } + if (error.localizedRecoverySuggestion.length > 0) { + details[@"nativeRecoverySuggestion"] = error.localizedRecoverySuggestion; + } + + [self sendErrorForCommand:command + code:@"WIFI_CONNECT_FAILED" + message:[self wifiApplyErrorMessageForError:error] + details:details]; +} + +- (NSString *)wifiApplyErrorMessageForError:(NSError *)error +{ + if (![error.domain isEqualToString:NEHotspotConfigurationErrorDomain]) { + return error.localizedDescription.length > 0 ? error.localizedDescription : @"Unable to apply WiFi configuration."; + } + + switch (error.code) { + case 1: + return @"Invalid WiFi SSID."; + case 2: + return @"Invalid WPA/WPA2/WPA3 passphrase."; + case 3: + return @"Invalid WEP passphrase."; + case 4: + return @"Invalid EAP settings."; + case 5: + return @"Invalid Hotspot 2.0 settings."; + case 6: + return @"Invalid Hotspot 2.0 domain name."; + case 7: + return @"The user denied the iOS WiFi connection prompt."; + case 8: + return @"iOS returned NEHotspotConfigurationErrorInternal. Check that the App ID, entitlements, and provisioning profile include Hotspot Configuration, then test on a physical device."; + case 9: + return @"Another iOS WiFi configuration request is already pending."; + case 10: + return @"iOS system WiFi configuration failed."; + case 11: + return @"iOS returned an unknown WiFi configuration error."; + case 12: + return @"joinOnce is not supported for this WiFi configuration."; + case 14: + return @"The app must be in the foreground to configure WiFi."; + case 15: + return @"Invalid SSID prefix."; + default: + return error.localizedDescription.length > 0 ? error.localizedDescription : @"Unable to apply WiFi configuration."; + } +} + +- (NSString *)hotspotConfigurationErrorNameForCode:(NSInteger)code +{ + switch (code) { + case 0: + return @"invalid"; + case 1: + return @"invalidSSID"; + case 2: + return @"invalidWPAPassphrase"; + case 3: + return @"invalidWEPPassphrase"; + case 4: + return @"invalidEAPSettings"; + case 5: + return @"invalidHS20Settings"; + case 6: + return @"invalidHS20DomainName"; + case 7: + return @"userDenied"; + case 8: + return @"internal"; + case 9: + return @"pending"; + case 10: + return @"systemConfiguration"; + case 11: + return @"unknown"; + case 12: + return @"joinOnceNotSupported"; + case 13: + return @"alreadyAssociated"; + case 14: + return @"applicationIsNotInForeground"; + case 15: + return @"invalidSSIDPrefix"; + default: + return @""; + } +} + - (NSDictionary *)biometricAvailability { LAContext *context = [[LAContext alloc] init]; @@ -620,7 +733,14 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response - (void)sendErrorForCommand:(CDVInvokedUrlCommand *)command code:(NSString *)code message:(NSString *)message { - NSDictionary *error = @{@"code": code ? code : @"ERROR", @"message": message ? message : @""}; + [self sendErrorForCommand:command code:code message:message details:nil]; +} + +- (void)sendErrorForCommand:(CDVInvokedUrlCommand *)command code:(NSString *)code message:(NSString *)message details:(NSDictionary *)details +{ + NSMutableDictionary *error = details == nil ? [NSMutableDictionary dictionary] : [details mutableCopy]; + error[@"code"] = code ? code : @"ERROR"; + error[@"message"] = message ? message : @""; CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:error]; [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; }