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];
}